markform 0.0.1 → 0.1.1
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/LICENSE +664 -0
- package/README.md +216 -44
- package/dist/ai-sdk.d.mts +2 -3
- package/dist/ai-sdk.mjs +2 -3
- package/dist/{apply-C0vjijlP.mjs → apply-BQdd-fdx.mjs} +381 -38
- package/dist/bin.d.mts +0 -0
- package/dist/bin.mjs +4 -5
- package/dist/{cli-9fvFySww.mjs → cli-pjOiHgCW.mjs} +506 -94
- package/dist/cli.d.mts +1 -2
- package/dist/cli.mjs +3 -3
- package/dist/{coreTypes-T7dAuewt.d.mts → coreTypes--6etkcwb.d.mts} +1088 -131
- package/dist/index.d.mts +90 -10
- package/dist/index.mjs +2 -2
- package/dist/{src-DBD3Dt4f.mjs → src-Cs4_9lWP.mjs} +461 -96
- package/examples/simple/simple-mock-filled.form.md +36 -0
- package/examples/simple/simple-skipped-filled.form.md +147 -0
- package/examples/simple/simple-with-skips.session.yaml +230 -0
- package/examples/simple/simple.form.md +22 -0
- package/examples/simple/simple.session.yaml +60 -28
- package/examples/startup-deep-research/startup-deep-research.form.md +404 -0
- package/examples/startup-research/startup-research-mock-filled.form.md +307 -0
- package/examples/startup-research/startup-research.form.md +211 -0
- package/package.json +17 -16
- package/dist/ai-sdk.mjs.map +0 -1
- package/dist/apply-C0vjijlP.mjs.map +0 -1
- package/dist/bin.mjs.map +0 -1
- package/dist/cli-9fvFySww.mjs.map +0 -1
- package/dist/src-DBD3Dt4f.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as parseForm, a as resolveModel, c as createMockAgent, h as serializeSession, i as getProviderNames, o as createLiveAgent, r as getProviderInfo, t as VERSION, u as createHarness } from "./src-
|
|
1
|
+
import { C as parseRolesFlag, b as USER_ROLE, d as serializeRawMarkdown, et as PatchSchema, f as AGENT_ROLE, g as DEFAULT_PORT, h as DEFAULT_MAX_TURNS, m as DEFAULT_MAX_PATCHES_PER_TURN, p as DEFAULT_MAX_ISSUES, r as inspect, t as applyPatches, u as serialize, x as formatSuggestedLlms, y as SUGGESTED_LLMS } from "./apply-BQdd-fdx.mjs";
|
|
2
|
+
import { _ as parseForm, a as resolveModel, c as createMockAgent, h as serializeSession, i as getProviderNames, o as createLiveAgent, r as getProviderInfo, t as VERSION, u as createHarness } from "./src-Cs4_9lWP.mjs";
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import pc from "picocolors";
|
|
@@ -453,6 +453,52 @@ function exportMultiFormat(form, basePath) {
|
|
|
453
453
|
return paths;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/cli/lib/patchFormat.ts
|
|
458
|
+
/** Maximum characters for a patch value display before truncation */
|
|
459
|
+
const PATCH_VALUE_MAX_LENGTH = 1e3;
|
|
460
|
+
/**
|
|
461
|
+
* Truncate a string to max length with ellipsis if needed.
|
|
462
|
+
*/
|
|
463
|
+
function truncate(value, maxLength = PATCH_VALUE_MAX_LENGTH) {
|
|
464
|
+
if (value.length <= maxLength) return value;
|
|
465
|
+
return value.slice(0, maxLength) + "…";
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Format a patch value for display with truncation.
|
|
469
|
+
*/
|
|
470
|
+
function formatPatchValue(patch) {
|
|
471
|
+
switch (patch.op) {
|
|
472
|
+
case "set_string": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
473
|
+
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
474
|
+
case "set_string_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
475
|
+
case "set_single_select": return patch.selected ?? "(none)";
|
|
476
|
+
case "set_multi_select": return patch.selected.length > 0 ? truncate(`[${patch.selected.join(", ")}]`) : "(none)";
|
|
477
|
+
case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
|
|
478
|
+
case "clear_field": return "(cleared)";
|
|
479
|
+
case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
|
|
480
|
+
case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
|
|
481
|
+
case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get a short display name for the patch operation type.
|
|
486
|
+
*/
|
|
487
|
+
function formatPatchType(patch) {
|
|
488
|
+
switch (patch.op) {
|
|
489
|
+
case "set_string": return "string";
|
|
490
|
+
case "set_number": return "number";
|
|
491
|
+
case "set_string_list": return "string_list";
|
|
492
|
+
case "set_single_select": return "select";
|
|
493
|
+
case "set_multi_select": return "multi_select";
|
|
494
|
+
case "set_checkboxes": return "checkboxes";
|
|
495
|
+
case "clear_field": return "clear";
|
|
496
|
+
case "skip_field": return "skip";
|
|
497
|
+
case "set_url": return "url";
|
|
498
|
+
case "set_url_list": return "url_list";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
456
502
|
//#endregion
|
|
457
503
|
//#region src/cli/examples/exampleRegistry.ts
|
|
458
504
|
/**
|
|
@@ -481,6 +527,13 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
481
527
|
description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
|
|
482
528
|
filename: "earnings-analysis.form.md",
|
|
483
529
|
path: "earnings-analysis/earnings-analysis.form.md"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "startup-deep-research",
|
|
533
|
+
title: "Startup Deep Research",
|
|
534
|
+
description: "Comprehensive startup intelligence gathering with company info, founders, funding, competitors, social media, and community presence.",
|
|
535
|
+
filename: "startup-deep-research.form.md",
|
|
536
|
+
path: "startup-deep-research/startup-deep-research.form.md"
|
|
484
537
|
}
|
|
485
538
|
];
|
|
486
539
|
/**
|
|
@@ -514,6 +567,17 @@ function loadExampleContent(exampleId) {
|
|
|
514
567
|
function getExampleById(id) {
|
|
515
568
|
return EXAMPLE_DEFINITIONS.find((e) => e.id === id);
|
|
516
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Get the absolute path to an example's source file.
|
|
572
|
+
* @param exampleId - The example ID (e.g., 'simple', 'political-research')
|
|
573
|
+
* @returns The absolute path to the example form file
|
|
574
|
+
* @throws Error if the example is not found
|
|
575
|
+
*/
|
|
576
|
+
function getExamplePath(exampleId) {
|
|
577
|
+
const example = EXAMPLE_DEFINITIONS.find((e) => e.id === exampleId);
|
|
578
|
+
if (!example) throw new Error(`Unknown example: ${exampleId}`);
|
|
579
|
+
return join(getExamplesDir(), example.path);
|
|
580
|
+
}
|
|
517
581
|
|
|
518
582
|
//#endregion
|
|
519
583
|
//#region src/cli/lib/versioning.ts
|
|
@@ -623,6 +687,38 @@ function formatFieldLabel(ctx) {
|
|
|
623
687
|
return `${ctx.field.label}${required} ${progress}`;
|
|
624
688
|
}
|
|
625
689
|
/**
|
|
690
|
+
* Create a skip_field patch for the given field.
|
|
691
|
+
*/
|
|
692
|
+
function createSkipPatch(field) {
|
|
693
|
+
return {
|
|
694
|
+
op: "skip_field",
|
|
695
|
+
fieldId: field.id,
|
|
696
|
+
reason: "User skipped in console"
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* For optional fields, prompt user to choose between filling or skipping.
|
|
701
|
+
* Returns "fill" if user wants to enter a value, or a skip_field patch if skipping.
|
|
702
|
+
* Returns null if user cancelled.
|
|
703
|
+
*/
|
|
704
|
+
async function promptSkipOrFill(ctx) {
|
|
705
|
+
const field = ctx.field;
|
|
706
|
+
if (field.required) return "fill";
|
|
707
|
+
const result = await p.select({
|
|
708
|
+
message: `${formatFieldLabel(ctx)} ${pc.dim("(optional)")}`,
|
|
709
|
+
options: [{
|
|
710
|
+
value: "fill",
|
|
711
|
+
label: "Enter value"
|
|
712
|
+
}, {
|
|
713
|
+
value: "skip",
|
|
714
|
+
label: "Skip this field"
|
|
715
|
+
}]
|
|
716
|
+
});
|
|
717
|
+
if (p.isCancel(result)) return null;
|
|
718
|
+
if (result === "skip") return createSkipPatch(field);
|
|
719
|
+
return "fill";
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
626
722
|
* Prompt for a string field value.
|
|
627
723
|
*/
|
|
628
724
|
async function promptForString(ctx) {
|
|
@@ -850,11 +946,76 @@ async function promptForCheckboxes(ctx) {
|
|
|
850
946
|
};
|
|
851
947
|
}
|
|
852
948
|
/**
|
|
949
|
+
* Prompt for a URL field value.
|
|
950
|
+
*/
|
|
951
|
+
async function promptForUrl(ctx) {
|
|
952
|
+
const field = ctx.field;
|
|
953
|
+
const currentVal = ctx.currentValue?.kind === "url" ? ctx.currentValue.value : null;
|
|
954
|
+
const result = await p.text({
|
|
955
|
+
message: formatFieldLabel(ctx),
|
|
956
|
+
placeholder: currentVal ?? "https://example.com",
|
|
957
|
+
initialValue: currentVal ?? "",
|
|
958
|
+
validate: (value) => {
|
|
959
|
+
if (field.required && !value.trim()) return "This field is required";
|
|
960
|
+
if (!value.trim()) return;
|
|
961
|
+
try {
|
|
962
|
+
new URL(value);
|
|
963
|
+
} catch {
|
|
964
|
+
return "Please enter a valid URL (e.g., https://example.com)";
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
if (p.isCancel(result)) return null;
|
|
969
|
+
if (!result && !field.required) return null;
|
|
970
|
+
return {
|
|
971
|
+
op: "set_url",
|
|
972
|
+
fieldId: field.id,
|
|
973
|
+
value: result || null
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Prompt for a URL list field value.
|
|
978
|
+
*/
|
|
979
|
+
async function promptForUrlList(ctx) {
|
|
980
|
+
const field = ctx.field;
|
|
981
|
+
const currentItems = ctx.currentValue?.kind === "url_list" ? ctx.currentValue.items : [];
|
|
982
|
+
const hint = ctx.description ? `${ctx.description.slice(0, 50)}... (one URL per line)` : "Enter URLs, one per line. Press Enter twice when done.";
|
|
983
|
+
const result = await p.text({
|
|
984
|
+
message: formatFieldLabel(ctx),
|
|
985
|
+
placeholder: hint,
|
|
986
|
+
initialValue: currentItems.join("\n"),
|
|
987
|
+
validate: (value) => {
|
|
988
|
+
const items$1 = value.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
989
|
+
if (field.required && items$1.length === 0) return "At least one URL is required";
|
|
990
|
+
if (field.minItems && items$1.length < field.minItems) return `Minimum ${field.minItems} URLs required`;
|
|
991
|
+
if (field.maxItems && items$1.length > field.maxItems) return `Maximum ${field.maxItems} URLs allowed`;
|
|
992
|
+
for (const item of items$1) try {
|
|
993
|
+
new URL(item);
|
|
994
|
+
} catch {
|
|
995
|
+
return `Invalid URL: ${item}`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
if (p.isCancel(result)) return null;
|
|
1000
|
+
const items = result.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1001
|
+
if (items.length === 0 && !field.required) return null;
|
|
1002
|
+
return {
|
|
1003
|
+
op: "set_url_list",
|
|
1004
|
+
fieldId: field.id,
|
|
1005
|
+
items
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
853
1009
|
* Prompt user for a single field value based on field type.
|
|
854
1010
|
* Returns a Patch to set the value, or null if skipped/cancelled.
|
|
1011
|
+
*
|
|
1012
|
+
* For optional fields, first offers a choice to skip or fill.
|
|
855
1013
|
*/
|
|
856
1014
|
async function promptForField(ctx) {
|
|
857
1015
|
if (ctx.description) p.note(ctx.description, pc.dim("Instructions"));
|
|
1016
|
+
const skipOrFillResult = await promptSkipOrFill(ctx);
|
|
1017
|
+
if (skipOrFillResult === null) return null;
|
|
1018
|
+
if (typeof skipOrFillResult !== "string") return skipOrFillResult;
|
|
858
1019
|
switch (ctx.field.kind) {
|
|
859
1020
|
case "string": return promptForString(ctx);
|
|
860
1021
|
case "number": return promptForNumber(ctx);
|
|
@@ -862,6 +1023,8 @@ async function promptForField(ctx) {
|
|
|
862
1023
|
case "single_select": return promptForSingleSelect(ctx);
|
|
863
1024
|
case "multi_select": return promptForMultiSelect(ctx);
|
|
864
1025
|
case "checkboxes": return promptForCheckboxes(ctx);
|
|
1026
|
+
case "url": return promptForUrl(ctx);
|
|
1027
|
+
case "url_list": return promptForUrlList(ctx);
|
|
865
1028
|
default: return null;
|
|
866
1029
|
}
|
|
867
1030
|
}
|
|
@@ -935,7 +1098,7 @@ function showInteractiveIntro(formTitle, role, fieldCount) {
|
|
|
935
1098
|
/**
|
|
936
1099
|
* Show outro message after interactive fill.
|
|
937
1100
|
*/
|
|
938
|
-
function showInteractiveOutro(patchCount,
|
|
1101
|
+
function showInteractiveOutro(patchCount, cancelled) {
|
|
939
1102
|
if (cancelled) {
|
|
940
1103
|
p.cancel("Interactive fill cancelled.");
|
|
941
1104
|
return;
|
|
@@ -944,7 +1107,7 @@ function showInteractiveOutro(patchCount, outputPath, cancelled) {
|
|
|
944
1107
|
p.outro(pc.yellow("No changes made."));
|
|
945
1108
|
return;
|
|
946
1109
|
}
|
|
947
|
-
p.outro(`✓ ${patchCount} field(s) updated
|
|
1110
|
+
p.outro(`✓ ${patchCount} field(s) updated.`);
|
|
948
1111
|
}
|
|
949
1112
|
|
|
950
1113
|
//#endregion
|
|
@@ -958,7 +1121,7 @@ function printExamplesList() {
|
|
|
958
1121
|
console.log(` ${pc.cyan(example.id)}`);
|
|
959
1122
|
console.log(` ${pc.bold(example.title)}`);
|
|
960
1123
|
console.log(` ${pc.dim(example.description)}`);
|
|
961
|
-
console.log(`
|
|
1124
|
+
console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
|
|
962
1125
|
console.log("");
|
|
963
1126
|
}
|
|
964
1127
|
}
|
|
@@ -984,7 +1147,7 @@ function buildModelOptions() {
|
|
|
984
1147
|
for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
|
|
985
1148
|
const info = getProviderInfo(provider);
|
|
986
1149
|
const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : pc.dim("○");
|
|
987
|
-
for (const model of models
|
|
1150
|
+
for (const model of models) options.push({
|
|
988
1151
|
value: `${provider}/${model}`,
|
|
989
1152
|
label: `${provider}/${model}`,
|
|
990
1153
|
hint: `${keyStatus} ${info.envVar}`
|
|
@@ -1046,14 +1209,15 @@ async function runAgentFill(form, modelId, _outputPath) {
|
|
|
1046
1209
|
let stepResult = harness.step();
|
|
1047
1210
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1048
1211
|
console.log(pc.dim(` Turn ${stepResult.turnNumber}: ${stepResult.issues.length} issue(s) to address`));
|
|
1049
|
-
const patches = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1212
|
+
const { patches } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1050
1213
|
for (const patch of patches) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1214
|
+
const typeName = formatPatchType(patch);
|
|
1215
|
+
const value = formatPatchValue(patch);
|
|
1216
|
+
console.log(pc.dim(` ${pc.cyan(patch.fieldId)} (${typeName}) = ${pc.green(value)}`));
|
|
1053
1217
|
}
|
|
1054
1218
|
stepResult = harness.apply(patches, stepResult.issues);
|
|
1055
1219
|
console.log(pc.dim(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining`));
|
|
1056
|
-
if (!stepResult.isComplete) stepResult = harness.step();
|
|
1220
|
+
if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
|
|
1057
1221
|
}
|
|
1058
1222
|
if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
|
|
1059
1223
|
else p.log.warn(pc.yellow(`Max turns reached (${harnessConfig.maxTurns})`));
|
|
@@ -1151,12 +1315,12 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1151
1315
|
showInteractiveIntro(form.schema.title ?? form.schema.id, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1152
1316
|
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1153
1317
|
if (cancelled) {
|
|
1154
|
-
showInteractiveOutro(0,
|
|
1318
|
+
showInteractiveOutro(0, true);
|
|
1155
1319
|
process.exit(1);
|
|
1156
1320
|
}
|
|
1157
1321
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1158
1322
|
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath);
|
|
1159
|
-
showInteractiveOutro(patches.length,
|
|
1323
|
+
showInteractiveOutro(patches.length, false);
|
|
1160
1324
|
console.log("");
|
|
1161
1325
|
p.log.success("Outputs:");
|
|
1162
1326
|
console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
|
|
@@ -1180,7 +1344,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1180
1344
|
if (p.isCancel(runAgent) || !runAgent) {
|
|
1181
1345
|
console.log("");
|
|
1182
1346
|
console.log(pc.dim("You can run agent fill later with:"));
|
|
1183
|
-
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --
|
|
1347
|
+
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --model=<provider/model>`));
|
|
1184
1348
|
p.outro(pc.dim("Happy form filling!"));
|
|
1185
1349
|
return;
|
|
1186
1350
|
}
|
|
@@ -1223,7 +1387,7 @@ async function runInteractiveFlow(preselectedId) {
|
|
|
1223
1387
|
p.log.error(`Agent fill failed: ${message}`);
|
|
1224
1388
|
console.log("");
|
|
1225
1389
|
console.log(pc.dim("You can try again with:"));
|
|
1226
|
-
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --
|
|
1390
|
+
console.log(pc.dim(` markform fill ${formatPath(outputPath)} --model=${modelId}`));
|
|
1227
1391
|
}
|
|
1228
1392
|
}
|
|
1229
1393
|
p.outro(pc.dim("Happy form filling!"));
|
|
@@ -1315,22 +1479,6 @@ function registerExportCommand(program) {
|
|
|
1315
1479
|
|
|
1316
1480
|
//#endregion
|
|
1317
1481
|
//#region src/cli/commands/fill.ts
|
|
1318
|
-
/** Supported agent types */
|
|
1319
|
-
const AGENT_TYPES = ["mock", "live"];
|
|
1320
|
-
/**
|
|
1321
|
-
* Format a patch value for display.
|
|
1322
|
-
*/
|
|
1323
|
-
function formatPatchValue(patch) {
|
|
1324
|
-
switch (patch.op) {
|
|
1325
|
-
case "set_string": return patch.value ? `"${patch.value}"` : "(empty)";
|
|
1326
|
-
case "set_number": return patch.value !== null ? String(patch.value) : "(empty)";
|
|
1327
|
-
case "set_string_list": return patch.items.length > 0 ? `[${patch.items.join(", ")}]` : "(empty)";
|
|
1328
|
-
case "set_single_select": return patch.selected ?? "(none)";
|
|
1329
|
-
case "set_multi_select": return patch.selected.length > 0 ? `[${patch.selected.join(", ")}]` : "(none)";
|
|
1330
|
-
case "set_checkboxes": return Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
1331
|
-
case "clear_field": return "(cleared)";
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
1482
|
/**
|
|
1335
1483
|
* Format session transcript for console output.
|
|
1336
1484
|
*/
|
|
@@ -1369,7 +1517,7 @@ function formatConsoleSession(transcript, useColors) {
|
|
|
1369
1517
|
* Register the fill command.
|
|
1370
1518
|
*/
|
|
1371
1519
|
function registerFillCommand(program) {
|
|
1372
|
-
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--
|
|
1520
|
+
program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-4o)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES})`, String(DEFAULT_MAX_ISSUES)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
|
|
1373
1521
|
const ctx = getCommandContext(cmd);
|
|
1374
1522
|
const filePath = resolve(file);
|
|
1375
1523
|
try {
|
|
@@ -1395,8 +1543,8 @@ function registerFillCommand(program) {
|
|
|
1395
1543
|
logVerbose(ctx, "Parsing form...");
|
|
1396
1544
|
const form = parseForm(formContent);
|
|
1397
1545
|
if (options.interactive) {
|
|
1398
|
-
if (options.
|
|
1399
|
-
logError("--interactive cannot be used with --
|
|
1546
|
+
if (options.mock) {
|
|
1547
|
+
logError("--interactive cannot be used with --mock");
|
|
1400
1548
|
process.exit(1);
|
|
1401
1549
|
}
|
|
1402
1550
|
if (options.model) {
|
|
@@ -1414,35 +1562,38 @@ function registerFillCommand(program) {
|
|
|
1414
1562
|
showInteractiveIntro(formTitle, targetRoles.join(", "), uniqueFieldIds.size);
|
|
1415
1563
|
const { patches, cancelled } = await runInteractiveFill(form, inspectResult.issues);
|
|
1416
1564
|
if (cancelled) {
|
|
1417
|
-
showInteractiveOutro(0,
|
|
1565
|
+
showInteractiveOutro(0, true);
|
|
1418
1566
|
process.exit(1);
|
|
1419
1567
|
}
|
|
1420
1568
|
if (patches.length > 0) applyPatches(form, patches);
|
|
1421
1569
|
const durationMs$1 = Date.now() - startTime;
|
|
1422
1570
|
const outputPath$1 = options.output ? resolve(options.output) : generateVersionedPath(filePath);
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1571
|
+
if (ctx.dryRun) {
|
|
1572
|
+
logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
|
|
1573
|
+
showInteractiveOutro(patches.length, false);
|
|
1574
|
+
} else {
|
|
1575
|
+
const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath$1);
|
|
1576
|
+
showInteractiveOutro(patches.length, false);
|
|
1577
|
+
console.log("");
|
|
1578
|
+
p.log.success("Outputs:");
|
|
1579
|
+
console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
|
|
1580
|
+
console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
|
|
1581
|
+
console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
|
|
1582
|
+
}
|
|
1427
1583
|
logTiming(ctx, "Fill time", durationMs$1);
|
|
1428
1584
|
if (patches.length > 0) {
|
|
1429
1585
|
console.log("");
|
|
1430
1586
|
console.log(pc.dim("Next step: fill remaining fields with agent"));
|
|
1431
|
-
console.log(pc.dim(` markform fill ${outputPath$1} --
|
|
1587
|
+
console.log(pc.dim(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`));
|
|
1432
1588
|
}
|
|
1433
1589
|
process.exit(0);
|
|
1434
1590
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
logError(`Invalid agent type '${options.agent}'. Valid types: ${AGENT_TYPES.join(", ")}`);
|
|
1591
|
+
if (options.mock && !options.mockSource) {
|
|
1592
|
+
logError("--mock requires --mock-source <file>");
|
|
1438
1593
|
process.exit(1);
|
|
1439
1594
|
}
|
|
1440
|
-
if (
|
|
1441
|
-
logError("
|
|
1442
|
-
process.exit(1);
|
|
1443
|
-
}
|
|
1444
|
-
if (agentType === "live" && !options.model) {
|
|
1445
|
-
logError("--agent=live requires --model <provider/model-id>");
|
|
1595
|
+
if (!options.mock && !options.model) {
|
|
1596
|
+
logError("Live agent requires --model <provider/model-id>");
|
|
1446
1597
|
console.log("");
|
|
1447
1598
|
console.log(formatSuggestedLlms());
|
|
1448
1599
|
process.exit(1);
|
|
@@ -1460,14 +1611,14 @@ function registerFillCommand(program) {
|
|
|
1460
1611
|
const harness = createHarness(form, harnessConfig);
|
|
1461
1612
|
let agent;
|
|
1462
1613
|
let mockPath;
|
|
1463
|
-
if (
|
|
1614
|
+
if (options.mock) {
|
|
1464
1615
|
mockPath = resolve(options.mockSource);
|
|
1465
1616
|
logVerbose(ctx, `Reading mock source: ${mockPath}`);
|
|
1466
1617
|
agent = createMockAgent(parseForm(await readFile(mockPath)));
|
|
1467
1618
|
} else {
|
|
1468
1619
|
const modelId = options.model;
|
|
1469
1620
|
logVerbose(ctx, `Resolving model: ${modelId}`);
|
|
1470
|
-
const { model } = await resolveModel(modelId);
|
|
1621
|
+
const { model, provider } = await resolveModel(modelId);
|
|
1471
1622
|
let systemPrompt;
|
|
1472
1623
|
if (options.instructions) {
|
|
1473
1624
|
systemPrompt = options.instructions;
|
|
@@ -1478,15 +1629,18 @@ function registerFillCommand(program) {
|
|
|
1478
1629
|
systemPrompt = await readFile(promptPath);
|
|
1479
1630
|
}
|
|
1480
1631
|
const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
|
|
1481
|
-
|
|
1632
|
+
const liveAgent = createLiveAgent({
|
|
1482
1633
|
model,
|
|
1634
|
+
provider,
|
|
1483
1635
|
systemPromptAddition: systemPrompt,
|
|
1484
1636
|
targetRole: primaryRole
|
|
1485
1637
|
});
|
|
1638
|
+
agent = liveAgent;
|
|
1639
|
+
logInfo(ctx, `Available tools: ${liveAgent.getAvailableToolNames().join(", ")}`);
|
|
1486
1640
|
logVerbose(ctx, `Using live agent with model: ${modelId}`);
|
|
1487
1641
|
}
|
|
1488
1642
|
logInfo(ctx, pc.cyan(`Filling form: ${filePath}`));
|
|
1489
|
-
logInfo(ctx, `Agent: ${
|
|
1643
|
+
logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
|
|
1490
1644
|
logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
|
|
1491
1645
|
logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
|
|
1492
1646
|
logVerbose(ctx, `Max issues per step: ${harnessConfig.maxIssues}`);
|
|
@@ -1495,15 +1649,35 @@ function registerFillCommand(program) {
|
|
|
1495
1649
|
let stepResult = harness.step();
|
|
1496
1650
|
logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
|
|
1497
1651
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
1498
|
-
const patches = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1499
|
-
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches
|
|
1652
|
+
const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
1653
|
+
logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches:`);
|
|
1500
1654
|
for (const patch of patches) {
|
|
1655
|
+
const typeName = formatPatchType(patch);
|
|
1501
1656
|
const value = formatPatchValue(patch);
|
|
1502
|
-
|
|
1657
|
+
logInfo(ctx, ` ${pc.cyan(patch.fieldId)} ${pc.dim(`(${typeName})`)} ${pc.dim("=")} ${pc.green(value)}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (stats) {
|
|
1660
|
+
logVerbose(ctx, ` Stats: ${stats.inputTokens ?? 0} in / ${stats.outputTokens ?? 0} out tokens`);
|
|
1661
|
+
if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
|
|
1662
|
+
if (stats.prompts) {
|
|
1663
|
+
logVerbose(ctx, ``);
|
|
1664
|
+
logVerbose(ctx, pc.dim(` ─── System Prompt ───`));
|
|
1665
|
+
for (const line of stats.prompts.system.split("\n")) logVerbose(ctx, pc.dim(` ${line}`));
|
|
1666
|
+
logVerbose(ctx, ``);
|
|
1667
|
+
logVerbose(ctx, pc.dim(` ─── Context Prompt ───`));
|
|
1668
|
+
for (const line of stats.prompts.context.split("\n")) logVerbose(ctx, pc.dim(` ${line}`));
|
|
1669
|
+
logVerbose(ctx, ``);
|
|
1670
|
+
}
|
|
1503
1671
|
}
|
|
1504
|
-
|
|
1672
|
+
let llmStats;
|
|
1673
|
+
if (stats) llmStats = {
|
|
1674
|
+
inputTokens: stats.inputTokens,
|
|
1675
|
+
outputTokens: stats.outputTokens,
|
|
1676
|
+
toolCalls: stats.toolCalls.length > 0 ? stats.toolCalls : void 0
|
|
1677
|
+
};
|
|
1678
|
+
stepResult = harness.apply(patches, stepResult.issues, llmStats);
|
|
1505
1679
|
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
1506
|
-
else {
|
|
1680
|
+
else if (!harness.hasReachedMaxTurns()) {
|
|
1507
1681
|
stepResult = harness.step();
|
|
1508
1682
|
logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
|
|
1509
1683
|
}
|
|
@@ -1519,7 +1693,7 @@ function registerFillCommand(program) {
|
|
|
1519
1693
|
await writeFile(outputPath, formMarkdown);
|
|
1520
1694
|
logSuccess(ctx, `Form written to: ${outputPath}`);
|
|
1521
1695
|
}
|
|
1522
|
-
const transcript = buildSessionTranscript(filePath,
|
|
1696
|
+
const transcript = buildSessionTranscript(filePath, options.mock ? "mock" : "live", mockPath, options.model, harnessConfig, harness.getTurns(), stepResult.isComplete, outputPath);
|
|
1523
1697
|
if (options.record) {
|
|
1524
1698
|
const recordPath = resolve(options.record);
|
|
1525
1699
|
const yaml = serializeSession(transcript);
|
|
@@ -1544,20 +1718,20 @@ function registerFillCommand(program) {
|
|
|
1544
1718
|
/**
|
|
1545
1719
|
* Build a session transcript from harness execution.
|
|
1546
1720
|
*/
|
|
1547
|
-
function buildSessionTranscript(formPath,
|
|
1721
|
+
function buildSessionTranscript(formPath, mockMode, mockPath, modelId, harnessConfig, turns, expectComplete, outputPath) {
|
|
1548
1722
|
const transcript = {
|
|
1549
1723
|
sessionVersion: "0.1.0",
|
|
1550
|
-
mode:
|
|
1724
|
+
mode: mockMode,
|
|
1551
1725
|
form: { path: formPath },
|
|
1552
1726
|
harness: harnessConfig,
|
|
1553
1727
|
turns,
|
|
1554
1728
|
final: {
|
|
1555
1729
|
expectComplete,
|
|
1556
|
-
expectedCompletedForm:
|
|
1730
|
+
expectedCompletedForm: mockMode === "mock" ? mockPath ?? outputPath : outputPath
|
|
1557
1731
|
}
|
|
1558
1732
|
};
|
|
1559
|
-
if (
|
|
1560
|
-
else if (
|
|
1733
|
+
if (mockMode === "mock" && mockPath) transcript.mock = { completedMock: mockPath };
|
|
1734
|
+
else if (mockMode === "live" && modelId) transcript.live = { modelId };
|
|
1561
1735
|
return transcript;
|
|
1562
1736
|
}
|
|
1563
1737
|
|
|
@@ -1649,7 +1823,8 @@ function formatConsoleReport$1(report, useColors) {
|
|
|
1649
1823
|
lines.push(bold("Progress:"));
|
|
1650
1824
|
lines.push(` Total fields: ${progress.counts.totalFields}`);
|
|
1651
1825
|
lines.push(` Required: ${progress.counts.requiredFields}`);
|
|
1652
|
-
lines.push(`
|
|
1826
|
+
lines.push(` Answered: ${progress.counts.answeredFields}`);
|
|
1827
|
+
lines.push(` Skipped: ${progress.counts.skippedFields}`);
|
|
1653
1828
|
lines.push(` Complete: ${progress.counts.completeFields}`);
|
|
1654
1829
|
lines.push(` Incomplete: ${progress.counts.incompleteFields}`);
|
|
1655
1830
|
lines.push(` Invalid: ${progress.counts.invalidFields}`);
|
|
@@ -1736,6 +1911,100 @@ function registerInspectCommand(program) {
|
|
|
1736
1911
|
});
|
|
1737
1912
|
}
|
|
1738
1913
|
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/cli/commands/instructions.ts
|
|
1916
|
+
/**
|
|
1917
|
+
* Get the path to the README.md file.
|
|
1918
|
+
* Works both during development and when installed as a package.
|
|
1919
|
+
*/
|
|
1920
|
+
function getReadmePath() {
|
|
1921
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
1922
|
+
if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "README.md");
|
|
1923
|
+
return join(dirname(dirname(dirname(thisDir))), "README.md");
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Load the README content.
|
|
1927
|
+
*/
|
|
1928
|
+
function loadReadme() {
|
|
1929
|
+
const readmePath = getReadmePath();
|
|
1930
|
+
try {
|
|
1931
|
+
return readFileSync(readmePath, "utf-8");
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1934
|
+
throw new Error(`Failed to load README from ${readmePath}: ${message}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Apply basic terminal formatting to markdown content.
|
|
1939
|
+
* Colorizes headers, code blocks, and other elements for better readability.
|
|
1940
|
+
*/
|
|
1941
|
+
function formatMarkdown(content, useColors) {
|
|
1942
|
+
if (!useColors) return content;
|
|
1943
|
+
const lines = content.split("\n");
|
|
1944
|
+
const formatted = [];
|
|
1945
|
+
let inCodeBlock = false;
|
|
1946
|
+
for (const line of lines) {
|
|
1947
|
+
if (line.startsWith("```")) {
|
|
1948
|
+
inCodeBlock = !inCodeBlock;
|
|
1949
|
+
formatted.push(pc.dim(line));
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
if (inCodeBlock) {
|
|
1953
|
+
formatted.push(pc.dim(line));
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (line.startsWith("# ")) {
|
|
1957
|
+
formatted.push(pc.bold(pc.cyan(line)));
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
if (line.startsWith("## ")) {
|
|
1961
|
+
formatted.push(pc.bold(pc.blue(line)));
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
if (line.startsWith("### ")) {
|
|
1965
|
+
formatted.push(pc.bold(line));
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1969
|
+
return pc.yellow(code);
|
|
1970
|
+
});
|
|
1971
|
+
formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
|
|
1972
|
+
return pc.bold(text);
|
|
1973
|
+
});
|
|
1974
|
+
formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
1975
|
+
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
1976
|
+
});
|
|
1977
|
+
formatted.push(formattedLine);
|
|
1978
|
+
}
|
|
1979
|
+
return formatted.join("\n");
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Check if stdout is an interactive terminal.
|
|
1983
|
+
*/
|
|
1984
|
+
function isInteractive() {
|
|
1985
|
+
return process.stdout.isTTY === true;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Display content. In a future enhancement, could pipe to a pager for long output.
|
|
1989
|
+
*/
|
|
1990
|
+
function displayContent(content) {
|
|
1991
|
+
console.log(content);
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Register the instructions command.
|
|
1995
|
+
*/
|
|
1996
|
+
function registerInstructionsCommand(program) {
|
|
1997
|
+
program.command("instructions").alias("readme").alias("docs").description("Display usage instructions and documentation").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
|
|
1998
|
+
const ctx = getCommandContext(cmd);
|
|
1999
|
+
try {
|
|
2000
|
+
displayContent(formatMarkdown(loadReadme(), !options.raw && isInteractive() && !ctx.quiet));
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
logError(error instanceof Error ? error.message : String(error));
|
|
2003
|
+
process.exit(1);
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1739
2008
|
//#endregion
|
|
1740
2009
|
//#region src/cli/commands/models.ts
|
|
1741
2010
|
/**
|
|
@@ -1881,6 +2150,13 @@ function formDataToPatches(formData, form) {
|
|
|
1881
2150
|
const fields = form.schema.groups.flatMap((g) => g.children);
|
|
1882
2151
|
for (const field of fields) {
|
|
1883
2152
|
const fieldId = field.id;
|
|
2153
|
+
if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
|
|
2154
|
+
patches.push({
|
|
2155
|
+
op: "skip_field",
|
|
2156
|
+
fieldId
|
|
2157
|
+
});
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
1884
2160
|
switch (field.kind) {
|
|
1885
2161
|
case "string": {
|
|
1886
2162
|
const value = formData[fieldId];
|
|
@@ -1980,6 +2256,38 @@ function formDataToPatches(formData, form) {
|
|
|
1980
2256
|
});
|
|
1981
2257
|
}
|
|
1982
2258
|
break;
|
|
2259
|
+
case "url": {
|
|
2260
|
+
const value = formData[fieldId];
|
|
2261
|
+
if (typeof value === "string" && value.trim() !== "") patches.push({
|
|
2262
|
+
op: "set_url",
|
|
2263
|
+
fieldId,
|
|
2264
|
+
value: value.trim()
|
|
2265
|
+
});
|
|
2266
|
+
else patches.push({
|
|
2267
|
+
op: "clear_field",
|
|
2268
|
+
fieldId
|
|
2269
|
+
});
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
case "url_list": {
|
|
2273
|
+
const value = formData[fieldId];
|
|
2274
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2275
|
+
const items = value.split("\n").map((s) => s.trim()).filter((s) => s !== "");
|
|
2276
|
+
if (items.length > 0) patches.push({
|
|
2277
|
+
op: "set_url_list",
|
|
2278
|
+
fieldId,
|
|
2279
|
+
items
|
|
2280
|
+
});
|
|
2281
|
+
else patches.push({
|
|
2282
|
+
op: "clear_field",
|
|
2283
|
+
fieldId
|
|
2284
|
+
});
|
|
2285
|
+
} else patches.push({
|
|
2286
|
+
op: "clear_field",
|
|
2287
|
+
fieldId
|
|
2288
|
+
});
|
|
2289
|
+
break;
|
|
2290
|
+
}
|
|
1983
2291
|
}
|
|
1984
2292
|
}
|
|
1985
2293
|
return patches;
|
|
@@ -2026,9 +2334,9 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
|
2026
2334
|
* @public Exported for testing.
|
|
2027
2335
|
*/
|
|
2028
2336
|
function renderFormHtml(form) {
|
|
2029
|
-
const { schema, valuesByFieldId } = form;
|
|
2337
|
+
const { schema, valuesByFieldId, skipsByFieldId } = form;
|
|
2030
2338
|
const formTitle = schema.title ?? schema.id;
|
|
2031
|
-
const groupsHtml = schema.groups.map((group) => renderGroup(group, valuesByFieldId)).join("\n");
|
|
2339
|
+
const groupsHtml = schema.groups.map((group) => renderGroup(group, valuesByFieldId, skipsByFieldId ?? {})).join("\n");
|
|
2032
2340
|
return `<!DOCTYPE html>
|
|
2033
2341
|
<html lang="en">
|
|
2034
2342
|
<head>
|
|
@@ -2047,7 +2355,7 @@ function renderFormHtml(form) {
|
|
|
2047
2355
|
color: #212529;
|
|
2048
2356
|
}
|
|
2049
2357
|
h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; }
|
|
2050
|
-
h2 { color: #6c757d;
|
|
2358
|
+
h2 { color: #6c757d; font-size: 1.25rem; }
|
|
2051
2359
|
.group {
|
|
2052
2360
|
background: white;
|
|
2053
2361
|
border-radius: 8px;
|
|
@@ -2159,6 +2467,40 @@ function renderFormHtml(form) {
|
|
|
2159
2467
|
color: #6c757d;
|
|
2160
2468
|
margin-top: 0.25rem;
|
|
2161
2469
|
}
|
|
2470
|
+
.field-actions {
|
|
2471
|
+
display: flex;
|
|
2472
|
+
gap: 0.5rem;
|
|
2473
|
+
margin-top: 0.5rem;
|
|
2474
|
+
}
|
|
2475
|
+
.btn-skip {
|
|
2476
|
+
padding: 0.25rem 0.75rem;
|
|
2477
|
+
font-size: 0.85rem;
|
|
2478
|
+
background: #f8f9fa;
|
|
2479
|
+
border: 1px solid #ced4da;
|
|
2480
|
+
border-radius: 4px;
|
|
2481
|
+
color: #6c757d;
|
|
2482
|
+
cursor: pointer;
|
|
2483
|
+
}
|
|
2484
|
+
.btn-skip:hover {
|
|
2485
|
+
background: #e9ecef;
|
|
2486
|
+
color: #495057;
|
|
2487
|
+
}
|
|
2488
|
+
.field-skipped {
|
|
2489
|
+
opacity: 0.6;
|
|
2490
|
+
}
|
|
2491
|
+
.field-skipped input,
|
|
2492
|
+
.field-skipped textarea,
|
|
2493
|
+
.field-skipped select {
|
|
2494
|
+
background: #f8f9fa;
|
|
2495
|
+
}
|
|
2496
|
+
.skipped-badge {
|
|
2497
|
+
font-size: 0.75rem;
|
|
2498
|
+
padding: 0.15rem 0.4rem;
|
|
2499
|
+
background: #6c757d;
|
|
2500
|
+
color: white;
|
|
2501
|
+
border-radius: 3px;
|
|
2502
|
+
margin-left: 0.5rem;
|
|
2503
|
+
}
|
|
2162
2504
|
</style>
|
|
2163
2505
|
</head>
|
|
2164
2506
|
<body>
|
|
@@ -2170,10 +2512,50 @@ function renderFormHtml(form) {
|
|
|
2170
2512
|
</div>
|
|
2171
2513
|
</form>
|
|
2172
2514
|
<script>
|
|
2515
|
+
// Track fields marked for skip
|
|
2516
|
+
const skippedFields = new Set();
|
|
2517
|
+
|
|
2518
|
+
// Handle skip button clicks
|
|
2519
|
+
document.querySelectorAll('.btn-skip').forEach(btn => {
|
|
2520
|
+
btn.addEventListener('click', (e) => {
|
|
2521
|
+
const fieldId = e.target.dataset.skipField;
|
|
2522
|
+
if (!fieldId) return;
|
|
2523
|
+
|
|
2524
|
+
// Toggle skip state
|
|
2525
|
+
if (skippedFields.has(fieldId)) {
|
|
2526
|
+
skippedFields.delete(fieldId);
|
|
2527
|
+
e.target.textContent = 'Skip';
|
|
2528
|
+
e.target.classList.remove('btn-skip-active');
|
|
2529
|
+
// Re-enable the field input
|
|
2530
|
+
const fieldDiv = e.target.closest('.field');
|
|
2531
|
+
fieldDiv.classList.remove('field-skipped');
|
|
2532
|
+
fieldDiv.querySelectorAll('input, select, textarea').forEach(input => {
|
|
2533
|
+
input.disabled = false;
|
|
2534
|
+
});
|
|
2535
|
+
} else {
|
|
2536
|
+
skippedFields.add(fieldId);
|
|
2537
|
+
e.target.textContent = 'Unskip';
|
|
2538
|
+
e.target.classList.add('btn-skip-active');
|
|
2539
|
+
// Disable the field input to show it's skipped
|
|
2540
|
+
const fieldDiv = e.target.closest('.field');
|
|
2541
|
+
fieldDiv.classList.add('field-skipped');
|
|
2542
|
+
fieldDiv.querySelectorAll('input, select, textarea').forEach(input => {
|
|
2543
|
+
input.disabled = true;
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2173
2549
|
document.getElementById('markform').addEventListener('submit', async (e) => {
|
|
2174
2550
|
e.preventDefault();
|
|
2175
2551
|
const formData = new FormData(e.target);
|
|
2176
2552
|
const params = new URLSearchParams();
|
|
2553
|
+
|
|
2554
|
+
// Add skip markers for skipped fields
|
|
2555
|
+
for (const fieldId of skippedFields) {
|
|
2556
|
+
params.append('__skip__' + fieldId, '1');
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2177
2559
|
for (const [key, value] of formData) {
|
|
2178
2560
|
params.append(key, value);
|
|
2179
2561
|
}
|
|
@@ -2201,9 +2583,9 @@ function renderFormHtml(form) {
|
|
|
2201
2583
|
/**
|
|
2202
2584
|
* Render a field group as HTML.
|
|
2203
2585
|
*/
|
|
2204
|
-
function renderGroup(group, values) {
|
|
2586
|
+
function renderGroup(group, values, skips) {
|
|
2205
2587
|
const groupTitle = group.title ?? group.id;
|
|
2206
|
-
const fieldsHtml = group.children.map((field) => renderFieldHtml(field, values[field.id])).join("\n");
|
|
2588
|
+
const fieldsHtml = group.children.map((field) => renderFieldHtml(field, values[field.id], skips[field.id])).join("\n");
|
|
2207
2589
|
return `
|
|
2208
2590
|
<div class="group">
|
|
2209
2591
|
<h2>${escapeHtml(groupTitle)}</h2>
|
|
@@ -2214,79 +2596,109 @@ function renderGroup(group, values) {
|
|
|
2214
2596
|
* Render a field as HTML.
|
|
2215
2597
|
* @public Exported for testing.
|
|
2216
2598
|
*/
|
|
2217
|
-
function renderFieldHtml(field, value) {
|
|
2599
|
+
function renderFieldHtml(field, value, skipInfo) {
|
|
2600
|
+
const isSkipped = skipInfo?.skipped === true;
|
|
2218
2601
|
const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
|
|
2219
2602
|
const typeLabel = `<span class="type-badge">${field.kind}</span>`;
|
|
2603
|
+
const skippedBadge = isSkipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
|
|
2604
|
+
const fieldClass = isSkipped ? "field field-skipped" : "field";
|
|
2605
|
+
const disabledAttr = isSkipped ? " disabled" : "";
|
|
2220
2606
|
let inputHtml;
|
|
2221
2607
|
switch (field.kind) {
|
|
2222
2608
|
case "string":
|
|
2223
|
-
inputHtml = renderStringInput(field, value);
|
|
2609
|
+
inputHtml = renderStringInput(field, value, disabledAttr);
|
|
2224
2610
|
break;
|
|
2225
2611
|
case "number":
|
|
2226
|
-
inputHtml = renderNumberInput(field, value);
|
|
2612
|
+
inputHtml = renderNumberInput(field, value, disabledAttr);
|
|
2227
2613
|
break;
|
|
2228
2614
|
case "string_list":
|
|
2229
|
-
inputHtml = renderStringListInput(field, value);
|
|
2615
|
+
inputHtml = renderStringListInput(field, value, disabledAttr);
|
|
2230
2616
|
break;
|
|
2231
2617
|
case "single_select":
|
|
2232
|
-
inputHtml = renderSingleSelectInput(field, value);
|
|
2618
|
+
inputHtml = renderSingleSelectInput(field, value, disabledAttr);
|
|
2233
2619
|
break;
|
|
2234
2620
|
case "multi_select":
|
|
2235
|
-
inputHtml = renderMultiSelectInput(field, value);
|
|
2621
|
+
inputHtml = renderMultiSelectInput(field, value, disabledAttr);
|
|
2236
2622
|
break;
|
|
2237
2623
|
case "checkboxes":
|
|
2238
|
-
inputHtml = renderCheckboxesInput(field, value);
|
|
2624
|
+
inputHtml = renderCheckboxesInput(field, value, disabledAttr);
|
|
2625
|
+
break;
|
|
2626
|
+
case "url":
|
|
2627
|
+
inputHtml = renderUrlInput(field, value, disabledAttr);
|
|
2628
|
+
break;
|
|
2629
|
+
case "url_list":
|
|
2630
|
+
inputHtml = renderUrlListInput(field, value, disabledAttr);
|
|
2239
2631
|
break;
|
|
2240
2632
|
default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
|
|
2241
2633
|
}
|
|
2634
|
+
const skipButton = !field.required && !isSkipped ? `<div class="field-actions">
|
|
2635
|
+
<button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
|
|
2636
|
+
</div>` : "";
|
|
2242
2637
|
return `
|
|
2243
|
-
<div class="
|
|
2638
|
+
<div class="${fieldClass}">
|
|
2244
2639
|
<label class="field-label" for="field-${field.id}">
|
|
2245
|
-
${escapeHtml(field.label)} ${requiredMark} ${typeLabel}
|
|
2640
|
+
${escapeHtml(field.label)} ${requiredMark} ${typeLabel} ${skippedBadge}
|
|
2246
2641
|
</label>
|
|
2247
2642
|
${inputHtml}
|
|
2643
|
+
${skipButton}
|
|
2248
2644
|
</div>`;
|
|
2249
2645
|
}
|
|
2250
2646
|
/**
|
|
2251
2647
|
* Render a string field as text input.
|
|
2252
2648
|
*/
|
|
2253
|
-
function renderStringInput(field, value) {
|
|
2649
|
+
function renderStringInput(field, value, disabledAttr) {
|
|
2254
2650
|
const currentValue = value?.kind === "string" && value.value !== null ? value.value : "";
|
|
2255
2651
|
const requiredAttr = field.required ? " required" : "";
|
|
2256
2652
|
const minLengthAttr = field.minLength !== void 0 ? ` minlength="${field.minLength}"` : "";
|
|
2257
2653
|
const maxLengthAttr = field.maxLength !== void 0 ? ` maxlength="${field.maxLength}"` : "";
|
|
2258
|
-
return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}>`;
|
|
2654
|
+
return `<input type="text" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minLengthAttr}${maxLengthAttr}${disabledAttr}>`;
|
|
2259
2655
|
}
|
|
2260
2656
|
/**
|
|
2261
2657
|
* Render a number field as number input.
|
|
2262
2658
|
*/
|
|
2263
|
-
function renderNumberInput(field, value) {
|
|
2659
|
+
function renderNumberInput(field, value, disabledAttr) {
|
|
2264
2660
|
const currentValue = value?.kind === "number" && value.value !== null ? String(value.value) : "";
|
|
2265
2661
|
const requiredAttr = field.required ? " required" : "";
|
|
2266
2662
|
const minAttr = field.min !== void 0 ? ` min="${field.min}"` : "";
|
|
2267
2663
|
const maxAttr = field.max !== void 0 ? ` max="${field.max}"` : "";
|
|
2268
2664
|
const stepAttr = field.integer ? " step=\"1\"" : "";
|
|
2269
|
-
return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}>`;
|
|
2665
|
+
return `<input type="number" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}"${requiredAttr}${minAttr}${maxAttr}${stepAttr}${disabledAttr}>`;
|
|
2270
2666
|
}
|
|
2271
2667
|
/**
|
|
2272
2668
|
* Render a string list field as textarea.
|
|
2273
2669
|
*/
|
|
2274
|
-
function renderStringListInput(field, value) {
|
|
2670
|
+
function renderStringListInput(field, value, disabledAttr) {
|
|
2275
2671
|
const currentValue = (value?.kind === "string_list" ? value.items : []).join("\n");
|
|
2276
2672
|
const requiredAttr = field.required ? " required" : "";
|
|
2277
|
-
return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one item per line"${requiredAttr}>${escapeHtml(currentValue)}</textarea>`;
|
|
2673
|
+
return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one item per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Render a URL field as url input.
|
|
2677
|
+
*/
|
|
2678
|
+
function renderUrlInput(field, value, disabledAttr) {
|
|
2679
|
+
const currentValue = value?.kind === "url" && value.value !== null ? value.value : "";
|
|
2680
|
+
const requiredAttr = field.required ? " required" : "";
|
|
2681
|
+
return `<input type="url" id="field-${field.id}" name="${field.id}" value="${escapeHtml(currentValue)}" placeholder="https://example.com"${requiredAttr}${disabledAttr}>`;
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Render a URL list field as textarea.
|
|
2685
|
+
*/
|
|
2686
|
+
function renderUrlListInput(field, value, disabledAttr) {
|
|
2687
|
+
const currentValue = (value?.kind === "url_list" ? value.items : []).join("\n");
|
|
2688
|
+
const requiredAttr = field.required ? " required" : "";
|
|
2689
|
+
return `<textarea id="field-${field.id}" name="${field.id}" placeholder="Enter one URL per line"${requiredAttr}${disabledAttr}>${escapeHtml(currentValue)}</textarea>`;
|
|
2278
2690
|
}
|
|
2279
2691
|
/**
|
|
2280
2692
|
* Render a single-select field as select element.
|
|
2281
2693
|
*/
|
|
2282
|
-
function renderSingleSelectInput(field, value) {
|
|
2694
|
+
function renderSingleSelectInput(field, value, disabledAttr) {
|
|
2283
2695
|
const selected = value?.selected ?? null;
|
|
2284
2696
|
const requiredAttr = field.required ? " required" : "";
|
|
2285
2697
|
const options = field.options.map((opt) => {
|
|
2286
2698
|
const selectedAttr = selected === opt.id ? " selected" : "";
|
|
2287
2699
|
return `<option value="${escapeHtml(opt.id)}"${selectedAttr}>${escapeHtml(opt.label)}</option>`;
|
|
2288
2700
|
}).join("\n ");
|
|
2289
|
-
return `<select id="field-${field.id}" name="${field.id}"${requiredAttr}>
|
|
2701
|
+
return `<select id="field-${field.id}" name="${field.id}"${requiredAttr}${disabledAttr}>
|
|
2290
2702
|
<option value="">-- Select --</option>
|
|
2291
2703
|
${options}
|
|
2292
2704
|
</select>`;
|
|
@@ -2294,14 +2706,14 @@ function renderSingleSelectInput(field, value) {
|
|
|
2294
2706
|
/**
|
|
2295
2707
|
* Render a multi-select field as checkboxes.
|
|
2296
2708
|
*/
|
|
2297
|
-
function renderMultiSelectInput(field, value) {
|
|
2709
|
+
function renderMultiSelectInput(field, value, disabledAttr) {
|
|
2298
2710
|
const selected = value?.selected ?? [];
|
|
2299
2711
|
return `<div class="checkbox-group">
|
|
2300
2712
|
${field.options.map((opt) => {
|
|
2301
2713
|
const checkedAttr = selected.includes(opt.id) ? " checked" : "";
|
|
2302
2714
|
const checkboxId = `field-${field.id}-${opt.id}`;
|
|
2303
2715
|
return `<div class="checkbox-item">
|
|
2304
|
-
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}>
|
|
2716
|
+
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}${disabledAttr}>
|
|
2305
2717
|
<label for="${checkboxId}">${escapeHtml(opt.label)}</label>
|
|
2306
2718
|
</div>`;
|
|
2307
2719
|
}).join("\n ")}
|
|
@@ -2310,7 +2722,7 @@ function renderMultiSelectInput(field, value) {
|
|
|
2310
2722
|
/**
|
|
2311
2723
|
* Render checkboxes field based on mode.
|
|
2312
2724
|
*/
|
|
2313
|
-
function renderCheckboxesInput(field, value) {
|
|
2725
|
+
function renderCheckboxesInput(field, value, disabledAttr) {
|
|
2314
2726
|
const checkboxValues = value?.values ?? {};
|
|
2315
2727
|
const mode = field.checkboxMode ?? "multi";
|
|
2316
2728
|
if (mode === "simple") return `<div class="checkbox-group">
|
|
@@ -2318,7 +2730,7 @@ function renderCheckboxesInput(field, value) {
|
|
|
2318
2730
|
const checkedAttr = checkboxValues[opt.id] === "done" ? " checked" : "";
|
|
2319
2731
|
const checkboxId = `field-${field.id}-${opt.id}`;
|
|
2320
2732
|
return `<div class="checkbox-item">
|
|
2321
|
-
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}>
|
|
2733
|
+
<input type="checkbox" id="${checkboxId}" name="${field.id}" value="${escapeHtml(opt.id)}"${checkedAttr}${disabledAttr}>
|
|
2322
2734
|
<label for="${checkboxId}">${escapeHtml(opt.label)}</label>
|
|
2323
2735
|
</div>`;
|
|
2324
2736
|
}).join("\n ")}
|
|
@@ -2330,7 +2742,7 @@ function renderCheckboxesInput(field, value) {
|
|
|
2330
2742
|
const selectName = `${field.id}.${opt.id}`;
|
|
2331
2743
|
return `<div class="option-row">
|
|
2332
2744
|
<span class="option-label">${escapeHtml(opt.label)}</span>
|
|
2333
|
-
<select id="${selectId}" name="${selectName}">
|
|
2745
|
+
<select id="${selectId}" name="${selectName}"${disabledAttr}>
|
|
2334
2746
|
<option value="unfilled"${state === "unfilled" ? " selected" : ""}>-- Select --</option>
|
|
2335
2747
|
<option value="yes"${state === "yes" ? " selected" : ""}>Yes</option>
|
|
2336
2748
|
<option value="no"${state === "no" ? " selected" : ""}>No</option>
|
|
@@ -2345,7 +2757,7 @@ function renderCheckboxesInput(field, value) {
|
|
|
2345
2757
|
const selectName = `${field.id}.${opt.id}`;
|
|
2346
2758
|
return `<div class="option-row">
|
|
2347
2759
|
<span class="option-label">${escapeHtml(opt.label)}</span>
|
|
2348
|
-
<select id="${selectId}" name="${selectName}">
|
|
2760
|
+
<select id="${selectId}" name="${selectName}"${disabledAttr}>
|
|
2349
2761
|
<option value="todo"${state === "todo" ? " selected" : ""}>To Do</option>
|
|
2350
2762
|
<option value="active"${state === "active" ? " selected" : ""}>Active</option>
|
|
2351
2763
|
<option value="done"${state === "done" ? " selected" : ""}>Done</option>
|
|
@@ -2551,6 +2963,7 @@ function createProgram() {
|
|
|
2551
2963
|
registerFillCommand(program);
|
|
2552
2964
|
registerModelsCommand(program);
|
|
2553
2965
|
registerExamplesCommand(program);
|
|
2966
|
+
registerInstructionsCommand(program);
|
|
2554
2967
|
return program;
|
|
2555
2968
|
}
|
|
2556
2969
|
/**
|
|
@@ -2561,5 +2974,4 @@ async function runCli() {
|
|
|
2561
2974
|
}
|
|
2562
2975
|
|
|
2563
2976
|
//#endregion
|
|
2564
|
-
export { runCli as t };
|
|
2565
|
-
//# sourceMappingURL=cli-9fvFySww.mjs.map
|
|
2977
|
+
export { runCli as t };
|