@moda-ai/cli 1.27.0 → 1.28.0
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.
|
@@ -546,14 +546,25 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
|
546
546
|
const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
|
|
547
547
|
const subcommand = positionals[0] || "status";
|
|
548
548
|
switch (subcommand) {
|
|
549
|
-
case "init":
|
|
550
|
-
await initPrompts({ silent: true });
|
|
549
|
+
case "init": {
|
|
550
|
+
const init = await initPrompts({ silent: true });
|
|
551
551
|
context.output.writeData({
|
|
552
552
|
manifest: MANIFEST_PATH,
|
|
553
|
+
manifestPath: init.manifestPath,
|
|
553
554
|
lockfile: LOCK_PATH,
|
|
554
|
-
|
|
555
|
+
lockPath: init.lockPath,
|
|
556
|
+
cwd: init.cwd,
|
|
557
|
+
created: init.created,
|
|
558
|
+
promptPaths: init.promptPaths
|
|
559
|
+
}, {
|
|
560
|
+
artifacts: [{
|
|
561
|
+
kind: "prompt_manifest",
|
|
562
|
+
path: init.manifestPath,
|
|
563
|
+
description: init.created ? "Prompt discovery manifest created by this run." : "Prompt discovery manifest already existed; left unchanged."
|
|
564
|
+
}]
|
|
555
565
|
});
|
|
556
566
|
break;
|
|
567
|
+
}
|
|
557
568
|
case "status":
|
|
558
569
|
context.output.writeData(await getPromptStatus());
|
|
559
570
|
break;
|
|
@@ -577,8 +588,12 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
|
577
588
|
}
|
|
578
589
|
}
|
|
579
590
|
async function initPrompts(options = {}) {
|
|
591
|
+
const cwd = process.cwd();
|
|
592
|
+
const manifestPath = resolve3(cwd, MANIFEST_PATH);
|
|
593
|
+
const lockPath = resolve3(cwd, LOCK_PATH);
|
|
580
594
|
mkdirSync(MODA_DIR, { recursive: true });
|
|
581
|
-
|
|
595
|
+
const existed = existsSync2(MANIFEST_PATH);
|
|
596
|
+
if (!existed) {
|
|
582
597
|
writeFileSync2(MANIFEST_PATH, [
|
|
583
598
|
"version: 1",
|
|
584
599
|
"prompt_paths:",
|
|
@@ -587,13 +602,28 @@ async function initPrompts(options = {}) {
|
|
|
587
602
|
].join(`
|
|
588
603
|
`));
|
|
589
604
|
}
|
|
605
|
+
if (!existsSync2(manifestPath)) {
|
|
606
|
+
throw new Error(`Could not write ${manifestPath}: the file is missing after init. Check directory permissions and available disk space.`);
|
|
607
|
+
}
|
|
608
|
+
const result = {
|
|
609
|
+
manifestPath,
|
|
610
|
+
lockPath,
|
|
611
|
+
cwd,
|
|
612
|
+
created: !existed,
|
|
613
|
+
promptPaths: readManifest().promptPaths
|
|
614
|
+
};
|
|
590
615
|
if (!options.silent) {
|
|
591
616
|
printJson({
|
|
592
617
|
manifest: MANIFEST_PATH,
|
|
618
|
+
manifestPath,
|
|
593
619
|
lockfile: LOCK_PATH,
|
|
594
|
-
|
|
620
|
+
lockPath,
|
|
621
|
+
cwd,
|
|
622
|
+
created: result.created,
|
|
623
|
+
promptPaths: result.promptPaths
|
|
595
624
|
});
|
|
596
625
|
}
|
|
626
|
+
return result;
|
|
597
627
|
}
|
|
598
628
|
async function syncPrompts(flags, profileOptions, context) {
|
|
599
629
|
if (flags.watch === "true") {
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-x1a56ws9.js";
|
|
12
12
|
import {
|
|
13
13
|
ApiError,
|
|
14
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
clearStoredApiKeyCredentials,
|
|
61
61
|
createCliOutput,
|
|
62
62
|
createCommandContext,
|
|
63
|
+
embeddedTenantIdFromApiKey,
|
|
63
64
|
ensureSecretFilePermissions,
|
|
64
65
|
getActiveProfile,
|
|
65
66
|
getModaDir,
|
|
@@ -76,6 +77,7 @@ import {
|
|
|
76
77
|
resolveTenantId,
|
|
77
78
|
safeWriteStderr,
|
|
78
79
|
safeWriteStdout,
|
|
80
|
+
saveCliConfig,
|
|
79
81
|
secretFilePathFromRef,
|
|
80
82
|
setActiveProfile,
|
|
81
83
|
upsertProfile,
|
|
@@ -391,6 +393,7 @@ var FIX_USAGE = [
|
|
|
391
393
|
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
|
|
392
394
|
" moda fix checkout <fix_id>",
|
|
393
395
|
" moda fix submit <fix_id> --pr | --local-ref=<branch>",
|
|
396
|
+
" moda fix mark-applied <fix_id> [--note=TEXT]",
|
|
394
397
|
" moda fix dismiss <fix_id> --reason=TEXT"
|
|
395
398
|
].join(`
|
|
396
399
|
`);
|
|
@@ -454,6 +457,8 @@ async function runFixCommand(context) {
|
|
|
454
457
|
return runFixCheckout(context, profileOptions, tenantId);
|
|
455
458
|
case "submit":
|
|
456
459
|
return runFixSubmit(context, profileOptions, tenantId);
|
|
460
|
+
case "mark-applied":
|
|
461
|
+
return runFixMarkApplied(context, profileOptions, tenantId);
|
|
457
462
|
case "dismiss":
|
|
458
463
|
return runFixDismiss(context, profileOptions, tenantId);
|
|
459
464
|
default:
|
|
@@ -645,6 +650,9 @@ async function runFixCheckout(context, profileOptions, tenantId) {
|
|
|
645
650
|
throw new CliInputError("<fix_id> is required", "Usage: moda fix checkout <fix_id>");
|
|
646
651
|
}
|
|
647
652
|
const fix = await getFix(tenantId, fixId, profileOptions);
|
|
653
|
+
if (isArtifactFix(fix)) {
|
|
654
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} is a ${fix.fixType} fix — its candidate is applied in your own stack, not checked out. Run \`moda fix ${fixId} --packet\` for the candidate (written as local artifacts), apply it, then confirm with \`moda fix mark-applied ${fixId}\`.`);
|
|
655
|
+
}
|
|
648
656
|
if (!fix.targetSourcePath) {
|
|
649
657
|
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no target source path — ${fix.fixType} fixes are packet-only. Run \`moda fix ${fixId} --packet\` for the diagnosis packet.`);
|
|
650
658
|
}
|
|
@@ -752,6 +760,38 @@ async function runFixSubmit(context, profileOptions, tenantId) {
|
|
|
752
760
|
});
|
|
753
761
|
return EXIT_OK;
|
|
754
762
|
}
|
|
763
|
+
async function runFixMarkApplied(context, profileOptions, tenantId) {
|
|
764
|
+
const usage = "Usage: moda fix mark-applied <fix_id> [--note=TEXT]";
|
|
765
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
766
|
+
if (!fixId) {
|
|
767
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
768
|
+
}
|
|
769
|
+
const note = (context.flags.note ?? "").trim();
|
|
770
|
+
if (note.length > 600) {
|
|
771
|
+
throw new CliInputError(`--note must be 600 characters or fewer (got ${note.length})`, usage);
|
|
772
|
+
}
|
|
773
|
+
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/mark-applied`, { method: "POST", body: JSON.stringify(note ? { note } : {}) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
774
|
+
if (context.outputMode === "human") {
|
|
775
|
+
process.stderr.write(fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}) — duplicate no-op; the mark_fixed feedback was re-posted.
|
|
776
|
+
` : `Fix MODA-FIX-${fix.shortRef} marked applied (${fix.status}) — mark_fixed feedback posted, monitoring for recurrence.
|
|
777
|
+
`);
|
|
778
|
+
}
|
|
779
|
+
context.output.writeData(fix, {
|
|
780
|
+
summary: {
|
|
781
|
+
text: fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}); mark-applied was a duplicate no-op.` : `Fix MODA-FIX-${fix.shortRef} marked applied — status ${fix.status}; the problem enters fixed-monitoring.`,
|
|
782
|
+
confidence: "high"
|
|
783
|
+
},
|
|
784
|
+
nextCommands: [
|
|
785
|
+
{
|
|
786
|
+
command: `moda fix ${fix.id}`,
|
|
787
|
+
purpose: "Track confirmation monitoring (HELD after 14 clean days; REGRESSED on reopen).",
|
|
788
|
+
mutability: "read",
|
|
789
|
+
requires_approval: false
|
|
790
|
+
}
|
|
791
|
+
]
|
|
792
|
+
});
|
|
793
|
+
return EXIT_OK;
|
|
794
|
+
}
|
|
755
795
|
async function runFixDismiss(context, profileOptions, tenantId) {
|
|
756
796
|
const usage = "Usage: moda fix dismiss <fix_id> --reason=TEXT";
|
|
757
797
|
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
@@ -778,8 +818,16 @@ async function runFixDismiss(context, profileOptions, tenantId) {
|
|
|
778
818
|
async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
779
819
|
if (context.flags.packet === "true") {
|
|
780
820
|
const packet = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
821
|
+
const extras = materializePacketCandidates(packet, fixId, context.outputMode === "human");
|
|
781
822
|
context.output.writeData(packet, {
|
|
782
|
-
summary: {
|
|
823
|
+
summary: {
|
|
824
|
+
text: `moda.fix_packet.v1 for fix ${fixId}.${extras.summarySuffix}`,
|
|
825
|
+
confidence: "high"
|
|
826
|
+
},
|
|
827
|
+
...extras.findings.length > 0 ? { findings: extras.findings } : {},
|
|
828
|
+
...extras.artifacts.length > 0 ? { artifacts: extras.artifacts } : {},
|
|
829
|
+
...extras.warnings.length > 0 ? { warnings: extras.warnings } : {},
|
|
830
|
+
...extras.nextCommands.length > 0 ? { nextCommands: extras.nextCommands } : {}
|
|
783
831
|
});
|
|
784
832
|
return EXIT_OK;
|
|
785
833
|
}
|
|
@@ -841,6 +889,9 @@ async function driveAdvance(args) {
|
|
|
841
889
|
announce(fix);
|
|
842
890
|
if (!ADVANCING_STATUSES.has(fix.status))
|
|
843
891
|
break;
|
|
892
|
+
if (fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "")) {
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
844
895
|
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
845
896
|
}
|
|
846
897
|
return fix;
|
|
@@ -871,6 +922,112 @@ async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
|
871
922
|
return null;
|
|
872
923
|
}
|
|
873
924
|
}
|
|
925
|
+
function isArtifactFix(fix) {
|
|
926
|
+
return fix.fixType === "TOOL_SCHEMA" || fix.fixType === "SKILL";
|
|
927
|
+
}
|
|
928
|
+
function materializePacketCandidates(packet, fixId, human) {
|
|
929
|
+
const findings = [];
|
|
930
|
+
const artifacts = [];
|
|
931
|
+
const warnings = [];
|
|
932
|
+
const nextCommands = [];
|
|
933
|
+
const summaryParts = [];
|
|
934
|
+
const tool = packet.candidate_tool;
|
|
935
|
+
const skill = packet.candidate_skill;
|
|
936
|
+
const hasTool = typeof tool?.proposed_description === "string" && tool.proposed_description !== "";
|
|
937
|
+
const hasSkill = typeof skill?.skill_md === "string" && skill.skill_md !== "";
|
|
938
|
+
if (!hasTool && !hasSkill) {
|
|
939
|
+
return { findings, artifacts, warnings, nextCommands, summarySuffix: "" };
|
|
940
|
+
}
|
|
941
|
+
const rawRef = typeof packet.short_ref === "string" && packet.short_ref.trim() ? packet.short_ref : fixId;
|
|
942
|
+
const ref = rawRef.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "").slice(0, 64) || "fix";
|
|
943
|
+
const w = (line) => process.stderr.write(`${line}
|
|
944
|
+
`);
|
|
945
|
+
const block = (label, text) => {
|
|
946
|
+
w(` ${label}:`);
|
|
947
|
+
for (const line of (text || "(none)").split(`
|
|
948
|
+
`))
|
|
949
|
+
w(` ${line}`);
|
|
950
|
+
};
|
|
951
|
+
const writeCandidateFile = (name, content, kind, description) => {
|
|
952
|
+
const relPath = safeTargetSourcePath(`.moda/fixes/${ref}/${name}`);
|
|
953
|
+
const absPath = join(process.cwd(), relPath);
|
|
954
|
+
try {
|
|
955
|
+
assertRealpathContained(absPath, relPath);
|
|
956
|
+
writeContainedFile(absPath, relPath, content);
|
|
957
|
+
artifacts.push({ kind, path: relPath, description });
|
|
958
|
+
return relPath;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
warnings.push(`Could not write ${relPath}: ${error instanceof Error ? error.message : String(error)} — the full candidate text is still in the packet payload.`);
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
if (hasTool && tool) {
|
|
965
|
+
const toolName = typeof tool.tool_name === "string" && tool.tool_name ? tool.tool_name : "unknown-tool";
|
|
966
|
+
const source = typeof tool.current_description_source === "string" ? tool.current_description_source : "";
|
|
967
|
+
const paramNotes = (Array.isArray(tool.param_notes) ? tool.param_notes : []).filter((note) => typeof note === "string" && note !== "");
|
|
968
|
+
const rationale = typeof tool.rationale === "string" ? tool.rationale : "";
|
|
969
|
+
const currentPath = writeCandidateFile("tool-description.current.md", typeof tool.current_description === "string" ? tool.current_description : "", "fix_tool_description_current", `Current description of tool '${toolName}'${source ? ` (from ${source})` : ""} — the diff base.`);
|
|
970
|
+
const proposedPath = writeCandidateFile("tool-description.proposed.md", tool.proposed_description ?? "", "fix_tool_description_proposed", `Proposed description for tool '${toolName}' — apply it to your tool definition, then \`moda fix mark-applied ${fixId}\`.`);
|
|
971
|
+
findings.push({
|
|
972
|
+
id: "fix:candidate_tool",
|
|
973
|
+
title: `tool '${toolName}' description candidate${source ? ` (current from ${source})` : ""}`,
|
|
974
|
+
status: "proposed",
|
|
975
|
+
severity: "info",
|
|
976
|
+
...rationale || paramNotes.length > 0 ? {
|
|
977
|
+
description: [rationale, ...paramNotes.map((note) => `param note: ${note}`)].filter(Boolean).join(`
|
|
978
|
+
`)
|
|
979
|
+
} : {}
|
|
980
|
+
});
|
|
981
|
+
summaryParts.push(`Includes a tool-description candidate for '${toolName}'.`);
|
|
982
|
+
if (human) {
|
|
983
|
+
w("");
|
|
984
|
+
w(`Tool-description candidate — '${toolName}'${source ? ` (current description from ${source})` : ""}`);
|
|
985
|
+
block("current ", tool.current_description ?? "");
|
|
986
|
+
block("proposed", tool.proposed_description ?? "");
|
|
987
|
+
if (paramNotes.length > 0)
|
|
988
|
+
block("param notes", paramNotes.join(`
|
|
989
|
+
`));
|
|
990
|
+
if (rationale)
|
|
991
|
+
block("rationale", rationale);
|
|
992
|
+
if (currentPath && proposedPath) {
|
|
993
|
+
w(` diff: diff ${currentPath} ${proposedPath}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (hasSkill && skill) {
|
|
998
|
+
const skillName = typeof skill.skill_name === "string" && skill.skill_name ? skill.skill_name : "moda-fix-skill";
|
|
999
|
+
const skillPath = writeCandidateFile("SKILL.md", skill.skill_md ?? "", "fix_candidate_skill", `Drafted SKILL.md for '${skillName}' — install it (for example .claude/skills/${skillName}/SKILL.md), then \`moda fix mark-applied ${fixId}\`.`);
|
|
1000
|
+
findings.push({
|
|
1001
|
+
id: "fix:candidate_skill",
|
|
1002
|
+
title: `skill '${skillName}' drafted (SKILL.md, ${(skill.skill_md ?? "").length} chars)`,
|
|
1003
|
+
status: "proposed",
|
|
1004
|
+
severity: "info",
|
|
1005
|
+
description: `Install the skill in your agent, then confirm with \`moda fix mark-applied ${fixId}\`. Gating on the replay holdout stays pending until the skill is registered with the skill harness.`
|
|
1006
|
+
});
|
|
1007
|
+
summaryParts.push(`Includes a drafted SKILL.md candidate ('${skillName}').`);
|
|
1008
|
+
if (human) {
|
|
1009
|
+
w("");
|
|
1010
|
+
w(`Skill candidate — '${skillName}'`);
|
|
1011
|
+
if (skillPath)
|
|
1012
|
+
w(` SKILL.md written to ${skillPath}`);
|
|
1013
|
+
w(` install it (for example .claude/skills/${skillName}/SKILL.md), then confirm:`);
|
|
1014
|
+
w(` moda fix mark-applied ${fixId} --note="installed skill ${skillName}"`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
nextCommands.push({
|
|
1018
|
+
command: `moda fix mark-applied ${fixId} --note="where/how you applied it"`,
|
|
1019
|
+
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1020
|
+
mutability: "write",
|
|
1021
|
+
requires_approval: true
|
|
1022
|
+
});
|
|
1023
|
+
return {
|
|
1024
|
+
findings,
|
|
1025
|
+
artifacts,
|
|
1026
|
+
warnings,
|
|
1027
|
+
nextCommands,
|
|
1028
|
+
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
874
1031
|
function gateFindings(fix) {
|
|
875
1032
|
const gateResult = fix.gateResult ?? null;
|
|
876
1033
|
if (!gateResult)
|
|
@@ -918,8 +1075,24 @@ function passLabel(value) {
|
|
|
918
1075
|
return value ? "pass" : "fail";
|
|
919
1076
|
}
|
|
920
1077
|
function nextCommandsFor(fix) {
|
|
1078
|
+
const packetCommand = {
|
|
1079
|
+
command: `moda fix ${fix.id} --packet`,
|
|
1080
|
+
purpose: "Read the packet — the tool/skill candidate is written as local artifacts to apply in your own stack.",
|
|
1081
|
+
mutability: "write",
|
|
1082
|
+
requires_approval: false
|
|
1083
|
+
};
|
|
1084
|
+
const markAppliedCommand = {
|
|
1085
|
+
command: `moda fix mark-applied ${fix.id} --note="where/how you applied it"`,
|
|
1086
|
+
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1087
|
+
mutability: "write",
|
|
1088
|
+
requires_approval: true
|
|
1089
|
+
};
|
|
1090
|
+
const skillGatePending = fix.fixType === "SKILL" && fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId;
|
|
921
1091
|
switch (fix.status) {
|
|
922
1092
|
case "VERIFIED":
|
|
1093
|
+
if (isArtifactFix(fix)) {
|
|
1094
|
+
return [packetCommand, markAppliedCommand];
|
|
1095
|
+
}
|
|
923
1096
|
return [
|
|
924
1097
|
{
|
|
925
1098
|
command: `moda fix submit ${fix.id} --pr`,
|
|
@@ -935,6 +1108,9 @@ function nextCommandsFor(fix) {
|
|
|
935
1108
|
}
|
|
936
1109
|
];
|
|
937
1110
|
case "PROPOSED":
|
|
1111
|
+
if (skillGatePending) {
|
|
1112
|
+
return [packetCommand, markAppliedCommand];
|
|
1113
|
+
}
|
|
938
1114
|
return [
|
|
939
1115
|
{
|
|
940
1116
|
command: `moda fix verify ${fix.id}`,
|
|
@@ -944,6 +1120,17 @@ function nextCommandsFor(fix) {
|
|
|
944
1120
|
}
|
|
945
1121
|
];
|
|
946
1122
|
case "GATE_FAILED":
|
|
1123
|
+
if (isArtifactFix(fix)) {
|
|
1124
|
+
return [
|
|
1125
|
+
{
|
|
1126
|
+
command: `moda fix verify ${fix.id}`,
|
|
1127
|
+
purpose: "Re-gate the stored candidate on the frozen holdout.",
|
|
1128
|
+
mutability: "write",
|
|
1129
|
+
requires_approval: false
|
|
1130
|
+
},
|
|
1131
|
+
packetCommand
|
|
1132
|
+
];
|
|
1133
|
+
}
|
|
947
1134
|
return [
|
|
948
1135
|
{
|
|
949
1136
|
command: `moda fix checkout ${fix.id}`,
|
|
@@ -1843,6 +2030,47 @@ function renderJsonSchemas() {
|
|
|
1843
2030
|
}, null, 2);
|
|
1844
2031
|
}
|
|
1845
2032
|
|
|
2033
|
+
// src/api-key-auth.ts
|
|
2034
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
2035
|
+
function resolveApiKeyCredential(options = {}) {
|
|
2036
|
+
const key = resolveApiKey(options);
|
|
2037
|
+
if (!key)
|
|
2038
|
+
return;
|
|
2039
|
+
const env = options.env ?? process.env;
|
|
2040
|
+
const source = env.MODA_API_KEY === key ? "env" : resolveApiKeyFromActiveProfile(options) === key ? "profile" : "config";
|
|
2041
|
+
return { key, source, tenantId: embeddedTenantIdFromApiKey(key) };
|
|
2042
|
+
}
|
|
2043
|
+
function redactApiKey(key) {
|
|
2044
|
+
if (key.length <= 12)
|
|
2045
|
+
return "<redacted>";
|
|
2046
|
+
return `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
2047
|
+
}
|
|
2048
|
+
async function probeApiKey(key, options = {}) {
|
|
2049
|
+
let response;
|
|
2050
|
+
try {
|
|
2051
|
+
response = await fetch(`${resolveModaBaseUrl(options)}/api/v1/data/overview`, {
|
|
2052
|
+
method: "GET",
|
|
2053
|
+
headers: { "x-api-key": key },
|
|
2054
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
2055
|
+
});
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2058
|
+
return { outcome: "unreachable", reason };
|
|
2059
|
+
}
|
|
2060
|
+
if (response.status === 401 || response.status === 403) {
|
|
2061
|
+
return { outcome: "rejected", status: response.status };
|
|
2062
|
+
}
|
|
2063
|
+
if (response.status !== 200) {
|
|
2064
|
+
return { outcome: "unreachable", reason: `HTTP ${response.status}` };
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
await response.json();
|
|
2068
|
+
} catch {
|
|
2069
|
+
return { outcome: "unreachable", reason: "non-JSON response from the Data API" };
|
|
2070
|
+
}
|
|
2071
|
+
return { outcome: "live" };
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1846
2074
|
// src/keys.ts
|
|
1847
2075
|
import { createHash } from "node:crypto";
|
|
1848
2076
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -2027,13 +2255,75 @@ function serializeProfile(profile, activeProfile) {
|
|
|
2027
2255
|
auth_token_ref: profile.auth_token_ref
|
|
2028
2256
|
};
|
|
2029
2257
|
}
|
|
2258
|
+
async function readStdin() {
|
|
2259
|
+
const chunks = [];
|
|
2260
|
+
for await (const chunk of process.stdin)
|
|
2261
|
+
chunks.push(Buffer.from(chunk));
|
|
2262
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
2263
|
+
}
|
|
2264
|
+
async function apiKeyLoginCandidate(context) {
|
|
2265
|
+
if (boolFlag2(context, "api-key-stdin")) {
|
|
2266
|
+
const piped = await readStdin();
|
|
2267
|
+
if (piped)
|
|
2268
|
+
return { key: piped, fromArgv: false };
|
|
2269
|
+
throw new CliInputError("No API key received on stdin.", 'Pipe the key in, e.g. `printf %s "$KEY" | moda auth login --api-key-stdin`.');
|
|
2270
|
+
}
|
|
2271
|
+
const inline = flag(context, "api-key");
|
|
2272
|
+
if (inline)
|
|
2273
|
+
return { key: inline, fromArgv: true };
|
|
2274
|
+
const env = context.env ?? process.env;
|
|
2275
|
+
if (env.MODA_API_KEY)
|
|
2276
|
+
return { key: env.MODA_API_KEY, fromArgv: false };
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
async function loginWithApiKey(context, candidate) {
|
|
2280
|
+
const { key } = candidate;
|
|
2281
|
+
if (candidate.fromArgv) {
|
|
2282
|
+
console.error('Warning: passing the key as --api-key=<key> exposes it in this process\'s command line and your shell history. Prefer MODA_API_KEY, or `printf %s "$KEY" | moda auth login --api-key-stdin`.');
|
|
2283
|
+
}
|
|
2284
|
+
const authOptions = { profile: context.profile, env: context.env };
|
|
2285
|
+
const probe = await probeApiKey(key, authOptions);
|
|
2286
|
+
if (probe.outcome === "rejected") {
|
|
2287
|
+
throw new CliInputError(`API key rejected by the Moda API (HTTP ${probe.status}).`, 'The key is revoked, malformed, or belongs to another environment. Mint a new one with "moda provision --tenant-id=<org-id>", or check MODA_BASE_URL points at the right environment.');
|
|
2288
|
+
}
|
|
2289
|
+
if (probe.outcome === "unreachable") {
|
|
2290
|
+
throw new CliInputError(`Could not validate the API key: ${probe.reason}.`, "The key was not saved. Check network access to the Moda API and retry; the key itself may still be valid.");
|
|
2291
|
+
}
|
|
2292
|
+
const tenantId = embeddedTenantIdFromApiKey(key);
|
|
2293
|
+
saveCliConfig({ api_key: key, ...tenantId ? { tenant_id: tenantId } : {} }, authOptions);
|
|
2294
|
+
const envKey = (context.env ?? process.env).MODA_API_KEY;
|
|
2295
|
+
const shadowedByEnv = Boolean(envKey && envKey !== key);
|
|
2296
|
+
if (shadowedByEnv) {
|
|
2297
|
+
console.error(`Warning: MODA_API_KEY is set to a different key (${redactApiKey(envKey)}) and takes precedence over the key just saved. Later commands will use the environment key. Unset MODA_API_KEY to use the saved credential.`);
|
|
2298
|
+
}
|
|
2299
|
+
context.output.writeData({
|
|
2300
|
+
status: "authenticated",
|
|
2301
|
+
method: "api_key",
|
|
2302
|
+
profile: context.profile ?? context.env?.MODA_PROFILE ?? process.env.MODA_PROFILE ?? null,
|
|
2303
|
+
apiKeyCredential: { masked: redactApiKey(key) },
|
|
2304
|
+
tenant_id: tenantId ?? null,
|
|
2305
|
+
persisted: true,
|
|
2306
|
+
shadowed_by_env: shadowedByEnv,
|
|
2307
|
+
effective_credential: shadowedByEnv ? "env" : "saved"
|
|
2308
|
+
}, shadowedByEnv ? { status: "degraded" } : undefined);
|
|
2309
|
+
return { exitCode: shadowedByEnv ? EXIT_DEGRADED : EXIT_OK };
|
|
2310
|
+
}
|
|
2030
2311
|
async function runAuthCommand(context) {
|
|
2031
2312
|
switch (subcommand(context)) {
|
|
2032
2313
|
case "login": {
|
|
2314
|
+
const wantsApiKey = "api-key" in context.flags || boolFlag2(context, "api-key-stdin");
|
|
2315
|
+
const nonInteractive = boolFlag2(context, "non-interactive");
|
|
2316
|
+
const candidate = wantsApiKey || nonInteractive ? await apiKeyLoginCandidate(context) : undefined;
|
|
2317
|
+
if (candidate)
|
|
2318
|
+
return loginWithApiKey(context, candidate);
|
|
2319
|
+
if (wantsApiKey || nonInteractive) {
|
|
2320
|
+
throw new CliInputError("No API key available for non-interactive login.", 'Set MODA_API_KEY, pipe the key with "moda auth login --api-key-stdin", or pass "moda auth login --api-key=<key>" (exposes it in argv). Mint a key with "moda provision --tenant-id=<org-id>" (needs an existing browser session), or run "moda init".');
|
|
2321
|
+
}
|
|
2033
2322
|
const screen = flag(context, "screen") === "signup" ? "signup" : "login";
|
|
2034
2323
|
await login(screen, { profile: context.profile, env: context.env });
|
|
2035
2324
|
context.output.writeData({
|
|
2036
2325
|
status: "authenticated",
|
|
2326
|
+
method: "browser",
|
|
2037
2327
|
profile: context.profile ?? process.env.MODA_PROFILE ?? null
|
|
2038
2328
|
});
|
|
2039
2329
|
return { exitCode: 0 };
|
|
@@ -2058,7 +2348,33 @@ async function runAuthCommand(context) {
|
|
|
2058
2348
|
case "whoami": {
|
|
2059
2349
|
const session = loadAuthSession({ profile: context.profile, env: context.env });
|
|
2060
2350
|
if (!session) {
|
|
2061
|
-
|
|
2351
|
+
const credential = resolveApiKeyCredential({
|
|
2352
|
+
profile: context.profile,
|
|
2353
|
+
env: context.env
|
|
2354
|
+
});
|
|
2355
|
+
if (credential) {
|
|
2356
|
+
const probe = await probeApiKey(credential.key, {
|
|
2357
|
+
profile: context.profile,
|
|
2358
|
+
env: context.env
|
|
2359
|
+
});
|
|
2360
|
+
if (probe.outcome === "rejected") {
|
|
2361
|
+
throw new CliInputError(`API key rejected by the Moda API (HTTP ${probe.status}).`, 'The key is revoked or belongs to another environment. Mint a new one with "moda provision --tenant-id=<org-id>", or run "moda auth login".');
|
|
2362
|
+
}
|
|
2363
|
+
context.output.writeData({
|
|
2364
|
+
profile: context.profile ?? process.env.MODA_PROFILE ?? null,
|
|
2365
|
+
credential: "api_key",
|
|
2366
|
+
apiKeyCredential: {
|
|
2367
|
+
source: credential.source,
|
|
2368
|
+
masked: redactApiKey(credential.key),
|
|
2369
|
+
verified: probe.outcome === "live"
|
|
2370
|
+
},
|
|
2371
|
+
tenant_id: credential.tenantId ?? null,
|
|
2372
|
+
session: null,
|
|
2373
|
+
context: null
|
|
2374
|
+
}, probe.outcome === "live" ? undefined : { status: "degraded" });
|
|
2375
|
+
return { exitCode: probe.outcome === "live" ? EXIT_OK : EXIT_DEGRADED };
|
|
2376
|
+
}
|
|
2377
|
+
throw new CliInputError("Not authenticated.", 'Non-interactive: set MODA_API_KEY, or run "moda auth login --api-key=<key>". Interactive: run "moda auth login" or "moda init".');
|
|
2062
2378
|
}
|
|
2063
2379
|
const response = await authFetch("/api/cli/context", {}, { profile: context.profile, env: context.env });
|
|
2064
2380
|
if (!response.ok) {
|
|
@@ -2080,8 +2396,17 @@ async function runAuthCommand(context) {
|
|
|
2080
2396
|
const authOptions = { profile: context.profile, env: context.env };
|
|
2081
2397
|
const session = loadAuthSession({ ...authOptions, includeExpired: true });
|
|
2082
2398
|
const loggedIn = isAuthSessionValid(session);
|
|
2399
|
+
const credential = resolveApiKeyCredential(authOptions);
|
|
2083
2400
|
const data = {
|
|
2084
2401
|
loggedIn,
|
|
2402
|
+
authenticated: loggedIn || Boolean(credential),
|
|
2403
|
+
apiKeyCredential: credential ? {
|
|
2404
|
+
present: true,
|
|
2405
|
+
source: credential.source,
|
|
2406
|
+
masked: redactApiKey(credential.key),
|
|
2407
|
+
tenantId: credential.tenantId ?? null,
|
|
2408
|
+
verified: null
|
|
2409
|
+
} : { present: false },
|
|
2085
2410
|
profile: context.profile ?? context.env.MODA_PROFILE ?? session?.profile ?? null,
|
|
2086
2411
|
expiresAt: session ? new Date(session.expires_at * 1000).toISOString() : null
|
|
2087
2412
|
};
|
|
@@ -2101,9 +2426,16 @@ async function runAuthCommand(context) {
|
|
|
2101
2426
|
}
|
|
2102
2427
|
} catch {}
|
|
2103
2428
|
data.online = online;
|
|
2429
|
+
if (credential && data.apiKeyCredential.present) {
|
|
2430
|
+
const probe = await probeApiKey(credential.key, authOptions);
|
|
2431
|
+
data.apiKeyCredential.verified = probe.outcome === "live";
|
|
2432
|
+
if (probe.outcome === "rejected") {
|
|
2433
|
+
data.authenticated = loggedIn;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2104
2436
|
}
|
|
2105
2437
|
context.output.writeData(data);
|
|
2106
|
-
return { exitCode:
|
|
2438
|
+
return { exitCode: data.authenticated ? EXIT_OK : EXIT_AUTH_REQUIRED };
|
|
2107
2439
|
}
|
|
2108
2440
|
case "token": {
|
|
2109
2441
|
const session = loadAuthSession({ profile: context.profile, env: context.env });
|
|
@@ -4609,7 +4941,7 @@ function buildResumeCommand(command, flags) {
|
|
|
4609
4941
|
];
|
|
4610
4942
|
}
|
|
4611
4943
|
function printHelp() {
|
|
4612
|
-
console.
|
|
4944
|
+
console.log(`Usage: moda <command> [options]
|
|
4613
4945
|
|
|
4614
4946
|
Commands:
|
|
4615
4947
|
init Set up Moda in your project
|
|
@@ -4617,11 +4949,12 @@ Commands:
|
|
|
4617
4949
|
status Show concise local Moda setup status
|
|
4618
4950
|
manifest Describe the CLI machine protocol and commands
|
|
4619
4951
|
doctor Diagnose local Moda setup and data flow
|
|
4620
|
-
auth login Authenticate with Moda
|
|
4621
|
-
auth
|
|
4952
|
+
auth login Authenticate with Moda (browser)
|
|
4953
|
+
auth login --api-key Non-interactive: validate MODA_API_KEY (or --api-key=<key>) and save it
|
|
4954
|
+
auth status Show auth state for both credentials (exit 4 when neither is usable); --online probes the API
|
|
4622
4955
|
auth token Print the raw bearer token for $(moda auth token)
|
|
4623
4956
|
auth logout Clear cached Moda auth
|
|
4624
|
-
auth whoami Show the authenticated Moda
|
|
4957
|
+
auth whoami Show the authenticated Moda identity (session or API key)
|
|
4625
4958
|
profiles list List local Moda profiles
|
|
4626
4959
|
profiles use <name> Switch active Moda profile
|
|
4627
4960
|
profiles create <name> Create or update a local Moda profile
|
|
@@ -4692,7 +5025,8 @@ Commands:
|
|
|
4692
5025
|
|
|
4693
5026
|
Fixes (a fix with proof):
|
|
4694
5027
|
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
4695
|
-
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5028
|
+
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5029
|
+
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
4696
5030
|
--wait drives a still-running pipeline to rest
|
|
4697
5031
|
fix start <problem_id> Draft a Fix from a Problem (--type=auto|prompt); --wait drives
|
|
4698
5032
|
scope -> propose -> gate and prints the packet
|
|
@@ -4700,6 +5034,8 @@ Commands:
|
|
|
4700
5034
|
(exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
|
|
4701
5035
|
fix checkout <fix_id> Write the candidate at its target source path (never runs git)
|
|
4702
5036
|
fix submit <fix_id> Deliver: --pr opens a draft PR | --local-ref=<branch> records your branch
|
|
5037
|
+
fix mark-applied <fix_id> Confirm a candidate you applied in your own stack (--note=TEXT);
|
|
5038
|
+
no PR needed — flips to SHIPPED and starts monitoring
|
|
4703
5039
|
fix dismiss <fix_id> Reject with --reason=TEXT (feeds problem feedback)
|
|
4704
5040
|
|
|
4705
5041
|
Options:
|
|
@@ -4837,6 +5173,8 @@ Provision flags (headless; requires an existing "moda auth login" session):
|
|
|
4837
5173
|
|
|
4838
5174
|
Examples:
|
|
4839
5175
|
moda init
|
|
5176
|
+
moda auth login --api-key=$MODA_API_KEY
|
|
5177
|
+
moda auth whoami
|
|
4840
5178
|
moda provision --tenant-id=<org-id>
|
|
4841
5179
|
moda status
|
|
4842
5180
|
moda manifest --json
|
|
@@ -4900,7 +5238,12 @@ Examples:
|
|
|
4900
5238
|
moda skills pull
|
|
4901
5239
|
moda fixes
|
|
4902
5240
|
moda fix start <problem_id> --wait
|
|
4903
|
-
moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md
|
|
5241
|
+
moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md
|
|
5242
|
+
moda fix mark-applied <fix_id> --note="tool description updated in our agent config"
|
|
5243
|
+
|
|
5244
|
+
Run \`moda <command> --help\` for a command's subcommands, and
|
|
5245
|
+
\`moda <command> <subcommand> --help\` for that subcommand's flags
|
|
5246
|
+
(e.g. \`moda auth login --help\`).`);
|
|
4904
5247
|
}
|
|
4905
5248
|
function printCommandHelp(definition) {
|
|
4906
5249
|
const lines = [];
|
|
@@ -4911,6 +5254,14 @@ function printCommandHelp(definition) {
|
|
|
4911
5254
|
if (definition.mutability === "write") {
|
|
4912
5255
|
lines.push("This command can mutate state.");
|
|
4913
5256
|
}
|
|
5257
|
+
if (definition.subcommands && definition.subcommands.length > 0) {
|
|
5258
|
+
lines.push("", "Subcommands:");
|
|
5259
|
+
const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
|
|
5260
|
+
for (const entry of definition.subcommands) {
|
|
5261
|
+
lines.push(` ${entry.name.padEnd(width)} ${entry.description}`);
|
|
5262
|
+
}
|
|
5263
|
+
lines.push("", `Run 'moda ${definition.name} <subcommand> --help' for that subcommand's flags.`);
|
|
5264
|
+
}
|
|
4914
5265
|
if (definition.examples.length > 0) {
|
|
4915
5266
|
lines.push("", "Examples:");
|
|
4916
5267
|
for (const example of definition.examples) {
|
|
@@ -4918,7 +5269,28 @@ function printCommandHelp(definition) {
|
|
|
4918
5269
|
}
|
|
4919
5270
|
}
|
|
4920
5271
|
lines.push("", "Run 'moda --help' for the full command list and common flags", "(--json, --agent, --profile, --days-back, --limit, --time-range, ...).");
|
|
4921
|
-
console.
|
|
5272
|
+
console.log(lines.join(`
|
|
5273
|
+
`));
|
|
5274
|
+
}
|
|
5275
|
+
function printSubcommandHelp(parent, entry) {
|
|
5276
|
+
const lines = [];
|
|
5277
|
+
lines.push(`moda ${parent.name} ${entry.name} — ${entry.description}`, "");
|
|
5278
|
+
lines.push(`Usage: ${entry.usage}`);
|
|
5279
|
+
if (entry.flags && entry.flags.length > 0) {
|
|
5280
|
+
lines.push("", "Options:");
|
|
5281
|
+
const width = Math.max(...entry.flags.map((flag2) => flag2.name.length));
|
|
5282
|
+
for (const flag2 of entry.flags) {
|
|
5283
|
+
lines.push(` ${flag2.name.padEnd(width)} ${flag2.description}`);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
if (entry.examples && entry.examples.length > 0) {
|
|
5287
|
+
lines.push("", "Examples:");
|
|
5288
|
+
for (const example of entry.examples) {
|
|
5289
|
+
lines.push(` ${example}`);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
lines.push("", `Run 'moda ${parent.name} --help' for the other ${parent.name} subcommands.`);
|
|
5293
|
+
console.log(lines.join(`
|
|
4922
5294
|
`));
|
|
4923
5295
|
}
|
|
4924
5296
|
async function attachWindows(rows, windowSize) {
|
|
@@ -5670,6 +6042,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
5670
6042
|
}
|
|
5671
6043
|
return 0;
|
|
5672
6044
|
}
|
|
6045
|
+
var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff"]);
|
|
5673
6046
|
function legacyCommand(metadata) {
|
|
5674
6047
|
return {
|
|
5675
6048
|
...metadata,
|
|
@@ -5763,7 +6136,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
5763
6136
|
resetApiRequestCountBeforeRun: true,
|
|
5764
6137
|
telemetry: "result",
|
|
5765
6138
|
handler: async (context) => {
|
|
5766
|
-
const { runInit } = await import("./index-
|
|
6139
|
+
const { runInit } = await import("./index-g3gt62rm.js");
|
|
5767
6140
|
if (context.outputMode === "agent-stream") {
|
|
5768
6141
|
context.output.writeEvent({
|
|
5769
6142
|
event: "started",
|
|
@@ -5836,15 +6209,81 @@ var commandRegistry = createCommandRegistry([
|
|
|
5836
6209
|
},
|
|
5837
6210
|
{
|
|
5838
6211
|
name: "auth",
|
|
5839
|
-
description: "Authenticate and inspect
|
|
6212
|
+
description: "Authenticate (browser or API key) and inspect auth state",
|
|
5840
6213
|
examples: [
|
|
5841
6214
|
"moda auth login",
|
|
6215
|
+
"moda auth login --api-key=$MODA_API_KEY",
|
|
5842
6216
|
"moda auth status",
|
|
5843
6217
|
"moda auth status --online",
|
|
5844
6218
|
"moda auth token",
|
|
5845
6219
|
"moda auth whoami",
|
|
5846
6220
|
"moda auth logout"
|
|
5847
6221
|
],
|
|
6222
|
+
subcommands: [
|
|
6223
|
+
{
|
|
6224
|
+
name: "login",
|
|
6225
|
+
description: "Authenticate with Moda (browser by default, or an API key)",
|
|
6226
|
+
usage: "moda auth login [--api-key[=<key>] | --api-key-stdin] [--non-interactive] [--screen=login|signup]",
|
|
6227
|
+
flags: [
|
|
6228
|
+
{
|
|
6229
|
+
name: "--api-key",
|
|
6230
|
+
description: "Authenticate non-interactively with the API key in MODA_API_KEY. Validates it, saves it, and never opens a browser."
|
|
6231
|
+
},
|
|
6232
|
+
{
|
|
6233
|
+
name: "--api-key-stdin",
|
|
6234
|
+
description: "Read the API key from stdin. Safest form: the key never appears in argv or shell history."
|
|
6235
|
+
},
|
|
6236
|
+
{
|
|
6237
|
+
name: "--api-key=<key>",
|
|
6238
|
+
description: "Supply the key inline. Exposes it in this process's command line and shell history; prefer MODA_API_KEY or --api-key-stdin."
|
|
6239
|
+
},
|
|
6240
|
+
{
|
|
6241
|
+
name: "--non-interactive",
|
|
6242
|
+
description: "Never open a browser. Uses MODA_API_KEY if set, otherwise fails with the commands that would mint a key."
|
|
6243
|
+
},
|
|
6244
|
+
{
|
|
6245
|
+
name: "--screen=login|signup",
|
|
6246
|
+
description: "Which browser screen to open (interactive flow only)."
|
|
6247
|
+
}
|
|
6248
|
+
],
|
|
6249
|
+
examples: [
|
|
6250
|
+
"MODA_API_KEY=<key> moda auth login --non-interactive",
|
|
6251
|
+
"moda auth login --api-key",
|
|
6252
|
+
'printf %s "$KEY" | moda auth login --api-key-stdin',
|
|
6253
|
+
"moda auth login"
|
|
6254
|
+
]
|
|
6255
|
+
},
|
|
6256
|
+
{
|
|
6257
|
+
name: "status",
|
|
6258
|
+
description: "Show local auth state for both credentials; exit 4 when neither is usable",
|
|
6259
|
+
usage: "moda auth status [--online]",
|
|
6260
|
+
flags: [
|
|
6261
|
+
{
|
|
6262
|
+
name: "--online",
|
|
6263
|
+
description: "Also probe the API to confirm the browser session is accepted."
|
|
6264
|
+
}
|
|
6265
|
+
],
|
|
6266
|
+
examples: ["moda auth status", "moda auth status --online"]
|
|
6267
|
+
},
|
|
6268
|
+
{
|
|
6269
|
+
name: "whoami",
|
|
6270
|
+
description: "Show the authenticated identity (browser session, or API-key credential)",
|
|
6271
|
+
usage: "moda auth whoami",
|
|
6272
|
+
examples: ["moda auth whoami"]
|
|
6273
|
+
},
|
|
6274
|
+
{
|
|
6275
|
+
name: "token",
|
|
6276
|
+
description: "Print the raw browser session bearer token for $(moda auth token)",
|
|
6277
|
+
usage: "moda auth token",
|
|
6278
|
+
examples: ["moda auth token", 'curl -H "Authorization: Bearer $(moda auth token)" ...']
|
|
6279
|
+
},
|
|
6280
|
+
{
|
|
6281
|
+
name: "logout",
|
|
6282
|
+
description: "Revoke and clear the browser session and any stored API key",
|
|
6283
|
+
usage: "moda auth logout",
|
|
6284
|
+
examples: ["moda auth logout"]
|
|
6285
|
+
}
|
|
6286
|
+
],
|
|
5848
6287
|
auth: "none",
|
|
5849
6288
|
outputModes: ["human", "json", "agent"],
|
|
5850
6289
|
mutability: "write",
|
|
@@ -6147,11 +6586,75 @@ var commandRegistry = createCommandRegistry([
|
|
|
6147
6586
|
legacyCommand({
|
|
6148
6587
|
name: "prompts",
|
|
6149
6588
|
description: "Manage prompt versions and labels",
|
|
6150
|
-
examples: ["moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
|
|
6589
|
+
examples: ["moda prompts init", "moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
|
|
6590
|
+
subcommands: [
|
|
6591
|
+
{
|
|
6592
|
+
name: "init",
|
|
6593
|
+
description: "Create .moda/prompts.yml in the current directory (never overwrites)",
|
|
6594
|
+
usage: "moda prompts init",
|
|
6595
|
+
flags: [],
|
|
6596
|
+
examples: ["moda prompts init", "cd /path/to/project && moda prompts init"]
|
|
6597
|
+
},
|
|
6598
|
+
{
|
|
6599
|
+
name: "status",
|
|
6600
|
+
description: "Show prompt files that changed since the last sync",
|
|
6601
|
+
usage: "moda prompts status",
|
|
6602
|
+
examples: ["moda prompts status"]
|
|
6603
|
+
},
|
|
6604
|
+
{
|
|
6605
|
+
name: "diff",
|
|
6606
|
+
description: "Alias of status: show local prompt changes",
|
|
6607
|
+
usage: "moda prompts diff",
|
|
6608
|
+
examples: ["moda prompts diff"]
|
|
6609
|
+
},
|
|
6610
|
+
{
|
|
6611
|
+
name: "sync",
|
|
6612
|
+
description: "Upload changed prompt versions to Moda Cloud",
|
|
6613
|
+
usage: "moda prompts sync [--watch] [--dry-run]",
|
|
6614
|
+
flags: [
|
|
6615
|
+
{ name: "--watch", description: "Re-upload whenever a prompt file changes." },
|
|
6616
|
+
{ name: "--dry-run", description: "Show what would be uploaded without writing." }
|
|
6617
|
+
],
|
|
6618
|
+
examples: ["moda prompts sync", "moda prompts sync --dry-run"]
|
|
6619
|
+
},
|
|
6620
|
+
{
|
|
6621
|
+
name: "promote",
|
|
6622
|
+
description: "Move a dev/staging/prod label to a prompt version",
|
|
6623
|
+
usage: "moda prompts promote <key> --label=<dev|staging|prod> --version=<version_id>",
|
|
6624
|
+
flags: [
|
|
6625
|
+
{ name: "--label=<label>", description: "Release label to move (dev, staging, prod)." },
|
|
6626
|
+
{ name: "--version=<id>", description: "Prompt version id to point the label at." }
|
|
6627
|
+
],
|
|
6628
|
+
examples: ["moda prompts promote support.triage --label=prod --version=pver_abc123"]
|
|
6629
|
+
},
|
|
6630
|
+
{
|
|
6631
|
+
name: "ab",
|
|
6632
|
+
description: "Run a judged prompt A/B replay experiment",
|
|
6633
|
+
usage: "moda prompts ab --baseline=<path> --candidate=<path> --conversations=<ids>",
|
|
6634
|
+
flags: [
|
|
6635
|
+
{ name: "--baseline=<path>", description: "Prompt file to treat as the control." },
|
|
6636
|
+
{ name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
|
|
6637
|
+
{ name: "--conversations=<ids>", description: "Comma-separated conversation ids to replay." }
|
|
6638
|
+
],
|
|
6639
|
+
examples: [
|
|
6640
|
+
"moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2"
|
|
6641
|
+
]
|
|
6642
|
+
},
|
|
6643
|
+
{
|
|
6644
|
+
name: "propose",
|
|
6645
|
+
description: "Revise a prompt from a run's failures",
|
|
6646
|
+
usage: "moda prompts propose [--gate] [--promote-on-win]",
|
|
6647
|
+
flags: [
|
|
6648
|
+
{ name: "--gate", description: "Automatically A/B the proposed revision." },
|
|
6649
|
+
{ name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
|
|
6650
|
+
],
|
|
6651
|
+
examples: ["moda prompts propose --gate --promote-on-win"]
|
|
6652
|
+
}
|
|
6653
|
+
],
|
|
6151
6654
|
auth: "api-key",
|
|
6152
6655
|
outputModes: ["human", "json", "agent"],
|
|
6153
6656
|
mutability: "write",
|
|
6154
|
-
validateConfigBeforeRun:
|
|
6657
|
+
validateConfigBeforeRun: (parsed) => !OFFLINE_PROMPTS_SUBCOMMANDS.has(parsed.positional ?? "status"),
|
|
6155
6658
|
resetApiRequestCountBeforeRun: true,
|
|
6156
6659
|
telemetry: "result-and-error"
|
|
6157
6660
|
}),
|
|
@@ -6192,6 +6695,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
6192
6695
|
"moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md",
|
|
6193
6696
|
"moda fix checkout <fix_id>",
|
|
6194
6697
|
"moda fix submit <fix_id> --pr",
|
|
6698
|
+
'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
|
|
6195
6699
|
'moda fix dismiss <fix_id> --reason="not actionable"'
|
|
6196
6700
|
],
|
|
6197
6701
|
auth: "none",
|
|
@@ -6284,7 +6788,13 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
6284
6788
|
if (command === "--help" || command === "-h" || flags.help === "true") {
|
|
6285
6789
|
const helpTarget = command && command !== "--help" && command !== "-h" ? commandRegistry.find(command) : undefined;
|
|
6286
6790
|
if (helpTarget) {
|
|
6287
|
-
|
|
6791
|
+
const subcommandName = parsed.positional;
|
|
6792
|
+
const subcommandEntry = subcommandName ? helpTarget.subcommands?.find((entry) => entry.name === subcommandName) : undefined;
|
|
6793
|
+
if (subcommandEntry) {
|
|
6794
|
+
printSubcommandHelp(helpTarget, subcommandEntry);
|
|
6795
|
+
} else {
|
|
6796
|
+
printCommandHelp(helpTarget);
|
|
6797
|
+
}
|
|
6288
6798
|
} else {
|
|
6289
6799
|
printHelp();
|
|
6290
6800
|
}
|
|
@@ -6329,7 +6839,8 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
6329
6839
|
})
|
|
6330
6840
|
});
|
|
6331
6841
|
setActiveContext(context);
|
|
6332
|
-
|
|
6842
|
+
const needsConfig = typeof definition.validateConfigBeforeRun === "function" ? definition.validateConfigBeforeRun(parsed) : definition.validateConfigBeforeRun;
|
|
6843
|
+
if (needsConfig) {
|
|
6333
6844
|
try {
|
|
6334
6845
|
validateConfig();
|
|
6335
6846
|
} catch (error) {
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: moda-cli
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.4.0
|
|
4
4
|
description: Query Moda's AI agent conversation analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -55,9 +55,9 @@ Optional:
|
|
|
55
55
|
|
|
56
56
|
- `MODA_BASE_URL` defaults to `https://moda.dev`; override only for
|
|
57
57
|
self-hosted or staging.
|
|
58
|
-
- `MODA_SKILL_VERSION` — export to `2.
|
|
58
|
+
- `MODA_SKILL_VERSION` — export to `2.4.0` so search-adoption telemetry can
|
|
59
59
|
attribute usage to this skill version. Set it once per session:
|
|
60
|
-
`export MODA_SKILL_VERSION=2.
|
|
60
|
+
`export MODA_SKILL_VERSION=2.4.0`.
|
|
61
61
|
|
|
62
62
|
## Setup
|
|
63
63
|
|
|
@@ -106,10 +106,12 @@ Output shape:
|
|
|
106
106
|
|
|
107
107
|
Rules and limitations:
|
|
108
108
|
|
|
109
|
-
- **Requires an existing CLI session.**
|
|
110
|
-
|
|
109
|
+
- **Requires an existing CLI session.** Minting a *new* key is control-plane
|
|
110
|
+
work: with no stored session it exits `4` (`auth_required`) telling you to
|
|
111
111
|
run `moda auth login` on a machine with a browser first, or to skip
|
|
112
|
-
provisioning and export `MODA_API_KEY` directly.
|
|
112
|
+
provisioning and export `MODA_API_KEY` directly. If you already hold a key,
|
|
113
|
+
you do not need this command at all — `moda auth login --api-key` gets you
|
|
114
|
+
to an authenticated state with no browser.
|
|
113
115
|
- **Never prompts, even on a TTY.** A single-tenant account auto-selects;
|
|
114
116
|
with multiple tenants you must pass `--tenant-id=<id>` (or set
|
|
115
117
|
`MODA_TENANT_ID`), otherwise it exits `5` (`needs_input`) with per-tenant
|
|
@@ -134,13 +136,34 @@ just need credentials.
|
|
|
134
136
|
|
|
135
137
|
```bash
|
|
136
138
|
moda auth login # browser PKCE sign-in; stores a moda_ct_ session token
|
|
139
|
+
moda auth login --api-key # NON-INTERACTIVE: validate MODA_API_KEY and save it
|
|
140
|
+
moda auth login --non-interactive # never open a browser; fail if no key is available
|
|
141
|
+
printf %s "$KEY" | moda auth login --api-key-stdin # key never touches argv
|
|
142
|
+
moda auth login --api-key=<key> # inline; WARNS, key is visible in argv + shell history
|
|
137
143
|
moda auth logout # revoke the session server-side, then delete it locally
|
|
138
|
-
moda auth status #
|
|
144
|
+
moda auth status # both credentials; exit 0 if either is usable, else 4
|
|
139
145
|
moda auth status --online # additionally probe GET /api/cli/context
|
|
146
|
+
moda auth whoami # identity from the session, or from the API key
|
|
140
147
|
moda auth token # print the raw bearer token + newline, nothing else
|
|
141
148
|
curl -H "Authorization: Bearer $(moda auth token)" https://moda.dev/api/cli/context
|
|
142
149
|
```
|
|
143
150
|
|
|
151
|
+
**If you are an agent with `MODA_API_KEY` and no browser, this is the whole
|
|
152
|
+
flow:**
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
moda auth whoami # exit 0 + "verified": true means you are authenticated
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The API key alone is sufficient for every data command; `moda auth login` is
|
|
159
|
+
only needed for control-plane work (`moda provision`, `moda init`). There are
|
|
160
|
+
two independent credentials and `auth` reports both:
|
|
161
|
+
|
|
162
|
+
| Credential | Obtained by | Used for |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `moda_ct_` session token | `moda auth login` (browser PKCE) | control plane: `provision`, `init`, `auth token` |
|
|
165
|
+
| `moda_sk_` API key | `MODA_API_KEY`, `moda provision`, `moda init` | every data command |
|
|
166
|
+
|
|
144
167
|
- `moda auth login` opens the browser consent flow at `{MODA_BASE_URL}/auth/cli`
|
|
145
168
|
with a localhost callback (ports 8432-8435, first free). Approving in the
|
|
146
169
|
browser hands the CLI a one-time code that it exchanges for a `moda_ct_...`
|
|
@@ -151,13 +174,45 @@ curl -H "Authorization: Bearer $(moda auth token)" https://moda.dev/api/cli/cont
|
|
|
151
174
|
- `moda auth logout` revokes the stored `moda_ct_` session server-side
|
|
152
175
|
(best-effort) and deletes the local credential; legacy sessions are just
|
|
153
176
|
deleted locally.
|
|
177
|
+
- `moda auth login --api-key` authenticates **without a browser**. Prefer
|
|
178
|
+
`MODA_API_KEY` (bare `--api-key`) or `--api-key-stdin`; `--api-key=<key>`
|
|
179
|
+
works but puts the secret in argv and shell history, so it warns. The key is
|
|
180
|
+
validated against the Data API before anything is written, then saved to
|
|
181
|
+
`~/.moda/config.json`. A rejected key (401/403) or an unreachable API both
|
|
182
|
+
fail without persisting. `--non-interactive` is the same path plus a
|
|
183
|
+
guarantee that no browser opens even when no key is found — it exits with
|
|
184
|
+
the commands that would mint one.
|
|
185
|
+
- If `MODA_API_KEY` holds a **different** key than the one being saved, the
|
|
186
|
+
environment still wins afterwards. Login says so instead of reporting a
|
|
187
|
+
clean success: `"shadowed_by_env": true`, `"effective_credential": "env"`,
|
|
188
|
+
exit `3`. Unset `MODA_API_KEY` to use the saved credential.
|
|
189
|
+
- `moda auth whoami` reports the browser session when there is one. With no
|
|
190
|
+
session but a usable API key it reports
|
|
191
|
+
`{ "credential": "api_key", "apiKeyCredential": { "source": <"env"|"profile"|"config">,
|
|
192
|
+
"masked": <string>, "verified": <bool> }, "tenant_id": <string|null> }`.
|
|
193
|
+
`verified: true` means the key was accepted by the API just now. If the API
|
|
194
|
+
is unreachable the command exits `3` (degraded) with `verified: false`
|
|
195
|
+
rather than claiming an identity it could not confirm; a rejected key is an
|
|
196
|
+
error, not a "logged out" state.
|
|
154
197
|
- `moda auth status` reports
|
|
155
|
-
`{ "loggedIn": <bool>, "
|
|
198
|
+
`{ "loggedIn": <bool>, "authenticated": <bool>, "apiKeyCredential": {...},
|
|
199
|
+
"profile": <string|null>, "expiresAt": <ISO|null> }`
|
|
156
200
|
plus `"user": { "email": <string> }` when the stored session carries an
|
|
157
|
-
email (PKCE `moda_ct_` sessions do; legacy sessions omit it)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
201
|
+
email (PKCE `moda_ct_` sessions do; legacy sessions omit it). `loggedIn` is
|
|
202
|
+
**browser-session only**; `authenticated` is true when *either* credential is
|
|
203
|
+
usable, and the exit code follows `authenticated` (`0` usable, `4` neither).
|
|
204
|
+
`apiKeyCredential` is `{ "present": false }` or
|
|
205
|
+
`{ "present": true, "source": <"env"|"profile"|"config">, "masked": <string>,
|
|
206
|
+
"tenantId": <string|null> }` — `source: "env"` tells you a `MODA_API_KEY` is
|
|
207
|
+
shadowing anything on disk. The raw key is never emitted. Exit `4` is an
|
|
208
|
+
**expected state signal, not an error**: the payload is clean data (no error
|
|
209
|
+
block, no stderr noise), and no browser is ever opened.
|
|
210
|
+
Without `--online` there is no network I/O, so `authenticated` means the
|
|
211
|
+
credential is **present**, not that it still works, and
|
|
212
|
+
`apiKeyCredential.verified` is `null`. With `--online` the key is probed too:
|
|
213
|
+
a rejected key gets `verified: false`, stops counting toward `authenticated`,
|
|
214
|
+
and exits `4`; an unreachable API leaves it counting, so a blip never kills a
|
|
215
|
+
good credential. With `--online` the payload adds
|
|
161
216
|
`{ "online": { "reachable": <bool>, "ok": <bool>, "tenantCount": <n> } }`:
|
|
162
217
|
`reachable: true` means any HTTP response arrived (even a 401/500), `ok`
|
|
163
218
|
reflects HTTP success, and `tenantCount` is present only when the response
|
|
@@ -621,23 +676,35 @@ replay **gate** that proves it on held-out production evidence. Use this loop
|
|
|
621
676
|
whenever you are asked to fix a detected Problem, or before hand-editing
|
|
622
677
|
prompt/skill files in a repo that talks to Moda.
|
|
623
678
|
|
|
679
|
+
Fixes are typed by where the evidence points: `PROMPT` (a managed prompt
|
|
680
|
+
revision — the checkout/verify/submit loop below), `TOOL_SCHEMA` (a proposed
|
|
681
|
+
rewrite of one tool's description), and `SKILL` (a drafted SKILL.md for an
|
|
682
|
+
agent-behavior problem). Tool and skill candidates are not repo files: read
|
|
683
|
+
them from the packet (they are also written locally under
|
|
684
|
+
`.moda/fixes/<SHORTREF>/`), apply them in your own stack, then confirm with
|
|
685
|
+
`moda fix mark-applied` — no GitHub integration required.
|
|
686
|
+
|
|
624
687
|
The fail-to-pass loop:
|
|
625
688
|
|
|
626
689
|
```bash
|
|
627
690
|
moda fixes # ranked queue of fixes with proof
|
|
628
691
|
moda fix <fix_id> --packet # moda.fix_packet.v1: cause, target
|
|
629
692
|
# file, evidence links, verify cmd,
|
|
630
|
-
# handback + magic word
|
|
693
|
+
# handback + magic word; tool/skill
|
|
694
|
+
# candidates land in .moda/fixes/
|
|
631
695
|
moda fix start <problem_id> --wait # draft from a Problem and drive
|
|
632
696
|
# scope -> propose -> gate
|
|
633
|
-
moda fix checkout <fix_id> # write the candidate
|
|
634
|
-
#
|
|
697
|
+
moda fix checkout <fix_id> # PROMPT fixes: write the candidate
|
|
698
|
+
# at its target source path
|
|
699
|
+
# (NEVER runs git)
|
|
635
700
|
# ...edit the target file...
|
|
636
701
|
moda fix verify <fix_id> --prompt-file=<path> # re-gate local content on the
|
|
637
|
-
# frozen holdout
|
|
702
|
+
# frozen holdout (PROMPT only)
|
|
638
703
|
# iterate edit -> verify until exit 0, then hand back:
|
|
639
704
|
moda fix submit <fix_id> --pr # draft PR via the Moda GitHub App
|
|
640
705
|
moda fix submit <fix_id> --local-ref=<branch> # or record your own branch
|
|
706
|
+
moda fix mark-applied <fix_id> --note="…" # tool/skill/no-PR path: confirm a
|
|
707
|
+
# candidate you applied yourself
|
|
641
708
|
moda fix dismiss <fix_id> --reason="why" # reject (feeds problem feedback)
|
|
642
709
|
```
|
|
643
710
|
|
|
@@ -660,6 +727,15 @@ Rules that keep the proof honest:
|
|
|
660
727
|
- Fix PRs end with the magic word `Fixes MODA-FIX-<SHORTREF>` in the body —
|
|
661
728
|
Moda-authored PRs carry it automatically; hand-authored PRs must include it
|
|
662
729
|
so the merge confirms the fix and starts production monitoring.
|
|
730
|
+
- `mark-applied` is the confirmation for changes shipped without a PR (tool
|
|
731
|
+
descriptions, installed skills, hand-applied prompts): it flips the fix to
|
|
732
|
+
`SHIPPED`, records your `--note`, and starts the same monitoring the merge
|
|
733
|
+
webhook would. It is idempotent — repeating it is a duplicate no-op.
|
|
734
|
+
- `TOOL_SCHEMA` fixes gate like prompt fixes (both arms pin the same current
|
|
735
|
+
prompt; the proposed description is the only delta). `SKILL` fixes rest at
|
|
736
|
+
`PROPOSED` until the skill is registered with the Moda skill harness — an
|
|
737
|
+
inline drafted SKILL.md cannot enter the gate, so deliver it via the packet
|
|
738
|
+
and `mark-applied`.
|
|
663
739
|
- The pipeline is advance-on-poll: `--wait` (on `start`, `verify`, or
|
|
664
740
|
`moda fix <fix_id> --wait`) drives it; a fix left `GATING` will not finish
|
|
665
741
|
on its own.
|
|
@@ -813,8 +889,10 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
813
889
|
| `moda init` | Browser auth + provision API key + install agent rules |
|
|
814
890
|
| `moda provision` | Headless tenant + API key provisioning; single JSON credentials doc on stdout (exit `4` without a session, `5` when `--tenant-id` is needed) |
|
|
815
891
|
| `moda auth login` | Browser PKCE sign-in; stores a revocable `moda_ct_` session token |
|
|
892
|
+
| `moda auth login --api-key` | Non-interactive: validate `MODA_API_KEY` (or `--api-key=<key>`) and save it; never opens a browser |
|
|
893
|
+
| `moda auth whoami` | Authenticated identity from the session, or from the API key when there is no session |
|
|
816
894
|
| `moda auth logout` | Revoke the CLI session server-side, then delete it locally |
|
|
817
|
-
| `moda auth status` |
|
|
895
|
+
| `moda auth status` | Both credentials; exit `0` if either is usable, `4` if neither (expected, not an error) |
|
|
818
896
|
| `moda auth token` | Print the raw bearer token for `$(moda auth token)` composition |
|
|
819
897
|
| `moda search "<query>"` | **Message-grain semantic/keyword/hybrid search (incl. tool calls); primary content-discovery path** |
|
|
820
898
|
| `moda overview` | Dashboard KPIs, top clusters, recent activity |
|
|
@@ -842,11 +920,12 @@ npx: `npx -p @moda-ai/cli moda <command>`.
|
|
|
842
920
|
| `moda prompts sync` | Upload changed prompt versions and update lockfile |
|
|
843
921
|
| `moda prompts promote <key>` | Move a remote prompt label |
|
|
844
922
|
| `moda fixes` | Ranked queue of Fixes ("a fix with proof"); `--status=S`, `--limit=N`, `--cursor=TOKEN` |
|
|
845
|
-
| `moda fix <fix_id>` | One Fix: status + gate result; `--packet` prints `moda.fix_packet.v1
|
|
923
|
+
| `moda fix <fix_id>` | One Fix: status + gate result; `--packet` prints `moda.fix_packet.v1` and writes tool/skill candidates under `.moda/fixes/<ref>/`; `--wait` drives a running pipeline to rest |
|
|
846
924
|
| `moda fix start <problem_id>` | Draft a Fix from a Problem (`--type=auto\|prompt`); `--wait` drives scope → propose → gate |
|
|
847
|
-
| `moda fix verify <fix_id>` | Re-gate on the frozen holdout; `--prompt-file=P` gates local content (exit `0` pass / `1` fail with per-case findings / `3` degraded — never a pass) |
|
|
848
|
-
| `moda fix checkout <fix_id>` | Write the candidate at its target source path (never runs git) |
|
|
925
|
+
| `moda fix verify <fix_id>` | Re-gate on the frozen holdout; `--prompt-file=P` gates local content, PROMPT fixes only (exit `0` pass / `1` fail with per-case findings / `3` degraded — never a pass) |
|
|
926
|
+
| `moda fix checkout <fix_id>` | Write the candidate at its target source path (never runs git; PROMPT fixes only) |
|
|
849
927
|
| `moda fix submit <fix_id>` | Deliver: `--pr` opens a draft PR \| `--local-ref=<branch>` records your branch |
|
|
928
|
+
| `moda fix mark-applied <fix_id>` | Confirm a candidate you applied in your own stack (`--note=TEXT`, ≤600 chars); no PR needed — flips to SHIPPED and starts monitoring |
|
|
850
929
|
| `moda fix dismiss <fix_id>` | Reject with `--reason=TEXT` (feeds problem feedback) |
|
|
851
930
|
|
|
852
931
|
`moda search` flags: `--mode=keyword|semantic|hybrid` (default `hybrid`),
|