@martinloop/mcp 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/server-validation.js +2 -0
- package/dist/server.js +30 -3
- package/dist/tools/run-loop.d.ts +1 -0
- package/dist/tools/run-loop.js +25 -4
- package/dist/tools/tool-response.d.ts +9 -1
- package/dist/tools/tool-response.js +7 -5
- package/dist/tools/tool-support.js +62 -271
- package/dist/vendor/adapters/claude-cli.d.ts +9 -0
- package/dist/vendor/adapters/claude-cli.js +19 -2
- package/dist/vendor/adapters/codex-capabilities.d.ts +12 -3
- package/dist/vendor/adapters/codex-capabilities.js +46 -18
- package/dist/vendor/adapters/codex-cli.d.ts +2 -1
- package/dist/vendor/adapters/codex-cli.js +12 -2
- package/dist/vendor/adapters/codex-host.d.ts +4 -1
- package/dist/vendor/adapters/codex-host.js +34 -16
- package/dist/vendor/adapters/codex-launcher.d.ts +1 -1
- package/dist/vendor/adapters/codex-launcher.js +1 -1
- package/dist/vendor/adapters/index.d.ts +2 -1
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/contracts/agent-execution.d.ts +9 -0
- package/dist/vendor/contracts/agent-execution.js +14 -0
- package/dist/vendor/contracts/index.d.ts +9 -0
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/grounding.js +42 -7
- package/dist/vendor/core/index.d.ts +5 -2
- package/dist/vendor/core/index.js +12 -3
- package/dist/vendor/presentation/governed-plan.d.ts +33 -0
- package/dist/vendor/presentation/governed-plan.js +146 -0
- package/dist/vendor/presentation/index.d.ts +7 -0
- package/dist/vendor/presentation/index.js +7 -0
- package/dist/vendor/presentation/motion.d.ts +12 -0
- package/dist/vendor/presentation/motion.js +123 -0
- package/dist/vendor/presentation/status.d.ts +8 -0
- package/dist/vendor/presentation/status.js +11 -0
- package/dist/vendor/presentation/table.d.ts +11 -0
- package/dist/vendor/presentation/table.js +47 -0
- package/dist/vendor/presentation/text.d.ts +6 -0
- package/dist/vendor/presentation/text.js +30 -0
- package/dist/vendor/presentation/theme.d.ts +31 -0
- package/dist/vendor/presentation/theme.js +82 -0
- package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
- package/dist/vendor/presentation/verified-handoff.js +151 -0
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
import { appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
3
|
+
import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, appendLoopEvent, createLoopRecord } from "../contracts/index.js";
|
|
4
4
|
import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
|
|
5
5
|
import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
|
|
6
6
|
import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
|
|
@@ -15,6 +15,7 @@ import { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
15
15
|
import { decideCircuitBreak } from "./trajectory.js";
|
|
16
16
|
import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
|
|
17
17
|
import { verifierActuallyPassed, } from "./verified-handoff.js";
|
|
18
|
+
export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
|
|
18
19
|
export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
|
|
19
20
|
export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
|
|
20
21
|
export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
|
|
@@ -160,12 +161,16 @@ export function distillContext(loop, options = {}) {
|
|
|
160
161
|
export async function runMartin(input) {
|
|
161
162
|
const now = input.now ?? (() => new Date().toISOString());
|
|
162
163
|
const idFactory = input.idFactory;
|
|
164
|
+
const providerExecutionTimeoutMs = normalizeProviderExecutionTimeoutMs(input.task.providerExecutionTimeoutMs);
|
|
165
|
+
const agentExecutionIntent = input.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT;
|
|
163
166
|
let loop = createLoopRecord({
|
|
164
167
|
workspaceId: input.workspaceId,
|
|
165
168
|
projectId: input.projectId,
|
|
166
|
-
task: input.task,
|
|
169
|
+
task: { ...input.task, agentExecutionIntent, providerExecutionTimeoutMs },
|
|
167
170
|
budget: input.budget,
|
|
168
|
-
...(input.receiptScope
|
|
171
|
+
...(input.receiptScope
|
|
172
|
+
? { receiptScope: { ...input.receiptScope, agentExecutionIntent, providerExecutionTimeoutMs } }
|
|
173
|
+
: {}),
|
|
169
174
|
...(input.teamId ? { teamId: input.teamId } : {}),
|
|
170
175
|
...(input.metadata ? { metadata: input.metadata } : {})
|
|
171
176
|
}, { now: now(), idFactory });
|
|
@@ -655,6 +660,8 @@ export async function runMartin(input) {
|
|
|
655
660
|
? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
|
|
656
661
|
: {}),
|
|
657
662
|
...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
|
|
663
|
+
agentExecutionIntent: loop.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT,
|
|
664
|
+
providerExecutionTimeoutMs: normalizeProviderExecutionTimeoutMs(loop.task.providerExecutionTimeoutMs),
|
|
658
665
|
focus: distilled.focus,
|
|
659
666
|
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
660
667
|
remainingIterations: distilled.constraints.remainingIterations,
|
|
@@ -745,6 +752,8 @@ export async function runMartin(input) {
|
|
|
745
752
|
? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
|
|
746
753
|
: {}),
|
|
747
754
|
...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
|
|
755
|
+
agentExecutionIntent: loop.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT,
|
|
756
|
+
providerExecutionTimeoutMs: normalizeProviderExecutionTimeoutMs(loop.task.providerExecutionTimeoutMs),
|
|
748
757
|
focus: distilled.focus,
|
|
749
758
|
remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
|
|
750
759
|
remainingIterations: distilled.constraints.remainingIterations,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { LoopBudget, ReceiptScope } from "../contracts/index.js";
|
|
2
|
+
import { type RenderEnvironment } from "./theme.js";
|
|
3
|
+
export type PlanStageState = "READY" | "BLOCKED" | "NEXT" | "CONDITIONAL";
|
|
4
|
+
export interface GovernedPlanStage {
|
|
5
|
+
label: string;
|
|
6
|
+
state: PlanStageState;
|
|
7
|
+
gate: string;
|
|
8
|
+
purpose: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GovernedRunPlanView {
|
|
11
|
+
ready: boolean;
|
|
12
|
+
task: string;
|
|
13
|
+
engine: string;
|
|
14
|
+
mode: "live" | "proof";
|
|
15
|
+
budget: LoopBudget;
|
|
16
|
+
verifier: string[];
|
|
17
|
+
receiptScope: ReceiptScope;
|
|
18
|
+
policyProfile: string;
|
|
19
|
+
blockingIssues: string[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
stages: GovernedPlanStage[];
|
|
22
|
+
}
|
|
23
|
+
export declare function buildGovernedPlanStages(input: Pick<GovernedRunPlanView, "ready" | "engine" | "verifier" | "blockingIssues">): GovernedPlanStage[];
|
|
24
|
+
export declare function renderGovernedRunPlan(input: GovernedRunPlanView, options?: {
|
|
25
|
+
width?: number;
|
|
26
|
+
environment?: RenderEnvironment;
|
|
27
|
+
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Renders a GovernedRunPlanView as plain Markdown (no ANSI).
|
|
30
|
+
*/
|
|
31
|
+
export declare function renderGovernedRunPlanMarkdown(input: GovernedRunPlanView, options?: {
|
|
32
|
+
includeStatus?: boolean;
|
|
33
|
+
}): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { renderTable } from "./table.js";
|
|
2
|
+
import { horizontalRule, terminalWidth, truncateVisible } from "./text.js";
|
|
3
|
+
import { bold, paint, } from "./theme.js";
|
|
4
|
+
function stageTone(state) {
|
|
5
|
+
switch (state) {
|
|
6
|
+
case "READY":
|
|
7
|
+
return "success";
|
|
8
|
+
case "BLOCKED":
|
|
9
|
+
return "danger";
|
|
10
|
+
case "NEXT":
|
|
11
|
+
return "info";
|
|
12
|
+
case "CONDITIONAL":
|
|
13
|
+
return "warning";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function buildGovernedPlanStages(input) {
|
|
17
|
+
const blocked = !input.ready || input.blockingIssues.length > 0;
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
label: "Contract & scope",
|
|
21
|
+
state: blocked ? "BLOCKED" : "READY",
|
|
22
|
+
gate: "request + paths",
|
|
23
|
+
purpose: "Bind the task to its governed workspace",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: "Budget & policy",
|
|
27
|
+
state: blocked ? "BLOCKED" : "READY",
|
|
28
|
+
gate: "hard cap + policy",
|
|
29
|
+
purpose: "Arm execution limits before work starts",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "Agent execution",
|
|
33
|
+
state: blocked ? "BLOCKED" : "NEXT",
|
|
34
|
+
gate: input.engine,
|
|
35
|
+
purpose: "Run the selected coding agent",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "Verification",
|
|
39
|
+
state: input.verifier.length > 0 && !blocked ? "NEXT" : "BLOCKED",
|
|
40
|
+
gate: input.verifier.length > 0 ? input.verifier.join(", ") : "no verifier",
|
|
41
|
+
purpose: "Run configured completion checks",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "Recovery",
|
|
45
|
+
state: "CONDITIONAL",
|
|
46
|
+
gate: "stop / fail",
|
|
47
|
+
purpose: "Preserve rollback and recovery evidence",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Proof",
|
|
51
|
+
state: blocked ? "BLOCKED" : "NEXT",
|
|
52
|
+
gate: "receipt",
|
|
53
|
+
purpose: "Produce the final governed record",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function bounded(line, width) {
|
|
58
|
+
return truncateVisible(line, width);
|
|
59
|
+
}
|
|
60
|
+
export function renderGovernedRunPlan(input, options = {}) {
|
|
61
|
+
const environment = options.environment ?? {};
|
|
62
|
+
const width = terminalWidth(options.width);
|
|
63
|
+
const rows = input.stages.map((stage) => ({
|
|
64
|
+
...stage,
|
|
65
|
+
renderedState: paint(stage.state, stageTone(stage.state), environment),
|
|
66
|
+
}));
|
|
67
|
+
const lines = [
|
|
68
|
+
horizontalRule(width, "━"),
|
|
69
|
+
bounded(bold("MARTINLOOP", environment) +
|
|
70
|
+
" ▸ " +
|
|
71
|
+
paint("GOVERNED RUN PLAN", input.ready ? "success" : "danger", environment), width),
|
|
72
|
+
horizontalRule(width, "━"),
|
|
73
|
+
"",
|
|
74
|
+
bounded(paint("Task".padEnd(18), "info", environment) + input.task, width),
|
|
75
|
+
bounded(paint("Engine".padEnd(18), "info", environment) + input.engine, width),
|
|
76
|
+
bounded(paint("Mode".padEnd(18), "info", environment) + input.mode, width),
|
|
77
|
+
"",
|
|
78
|
+
renderTable(rows, [
|
|
79
|
+
{ header: "STAGE", value: (row) => row.label, minWidth: 18, maxWidth: 24 },
|
|
80
|
+
{
|
|
81
|
+
header: "STATUS",
|
|
82
|
+
value: (row) => row.renderedState,
|
|
83
|
+
minWidth: 12,
|
|
84
|
+
maxWidth: 14,
|
|
85
|
+
},
|
|
86
|
+
{ header: "GATE", value: (row) => row.gate, minWidth: 18, maxWidth: 36 },
|
|
87
|
+
{ header: "PURPOSE", value: (row) => row.purpose, minWidth: 24 },
|
|
88
|
+
], width),
|
|
89
|
+
"",
|
|
90
|
+
bounded(bold("Budget cap", environment) +
|
|
91
|
+
" " +
|
|
92
|
+
paint("$" + input.budget.maxUsd.toFixed(2), "success", environment), width),
|
|
93
|
+
bounded(bold("Soft limit", environment) +
|
|
94
|
+
" $" +
|
|
95
|
+
input.budget.softLimitUsd.toFixed(2), width),
|
|
96
|
+
bounded(bold("Attempts", environment) + " " + input.budget.maxIterations, width),
|
|
97
|
+
bounded(bold("Verifier", environment) +
|
|
98
|
+
" " +
|
|
99
|
+
(input.verifier.join(", ") || "NOT CONFIGURED"), width),
|
|
100
|
+
bounded(bold("Policy", environment) + " " + input.policyProfile, width),
|
|
101
|
+
];
|
|
102
|
+
if (input.blockingIssues.length > 0) {
|
|
103
|
+
lines.push("", bold("BLOCKING", environment), ...input.blockingIssues.map((issue) => bounded(" " + paint("✗", "danger", environment) + " " + issue, width)));
|
|
104
|
+
}
|
|
105
|
+
if (input.warnings.length > 0) {
|
|
106
|
+
lines.push("", bold("WARNINGS", environment), ...input.warnings.map((warning) => bounded(" " + paint("!", "warning", environment) + " " + warning, width)));
|
|
107
|
+
}
|
|
108
|
+
lines.push("", input.ready
|
|
109
|
+
? paint("✓ READY TO EXECUTE", "success", environment)
|
|
110
|
+
: paint("✗ PREFLIGHT BLOCKED", "danger", environment), horizontalRule(width, "━"));
|
|
111
|
+
return lines.map((line) => bounded(line, width)).join("\n");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Renders a GovernedRunPlanView as plain Markdown (no ANSI).
|
|
115
|
+
*/
|
|
116
|
+
export function renderGovernedRunPlanMarkdown(input, options = {}) {
|
|
117
|
+
void options;
|
|
118
|
+
const lines = [
|
|
119
|
+
`## MartinLoop Governed Run Plan — ${input.ready ? "READY" : "BLOCKED"}`,
|
|
120
|
+
"",
|
|
121
|
+
"| Field | Value |",
|
|
122
|
+
"|-------|-------|",
|
|
123
|
+
`| Task | ${input.task} |`,
|
|
124
|
+
`| Engine | ${input.engine} |`,
|
|
125
|
+
`| Mode | ${input.mode} |`,
|
|
126
|
+
`| Budget cap | $${input.budget.maxUsd.toFixed(2)} |`,
|
|
127
|
+
`| Soft limit | $${input.budget.softLimitUsd.toFixed(2)} |`,
|
|
128
|
+
`| Max attempts | ${input.budget.maxIterations} |`,
|
|
129
|
+
`| Verifier | ${input.verifier.join(", ") || "NOT CONFIGURED"} |`,
|
|
130
|
+
`| Policy | ${input.policyProfile} |`,
|
|
131
|
+
"",
|
|
132
|
+
"### Stages",
|
|
133
|
+
"",
|
|
134
|
+
"| Stage | Status | Gate | Purpose |",
|
|
135
|
+
"|-------|--------|------|---------|",
|
|
136
|
+
...input.stages.map((s) => `| ${s.label} | ${s.state} | ${s.gate} | ${s.purpose} |`),
|
|
137
|
+
];
|
|
138
|
+
if (input.blockingIssues.length > 0) {
|
|
139
|
+
lines.push("", "### Blocking Issues", "", ...input.blockingIssues.map((issue) => `- ✗ ${issue}`));
|
|
140
|
+
}
|
|
141
|
+
if (input.warnings.length > 0) {
|
|
142
|
+
lines.push("", "### Warnings", "", ...input.warnings.map((w) => `- ${w}`));
|
|
143
|
+
}
|
|
144
|
+
lines.push("", input.ready ? "**✓ READY TO EXECUTE**" : "**✗ PREFLIGHT BLOCKED**");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RenderEnvironment } from "./theme.js";
|
|
2
|
+
export interface MotionEnvironment extends RenderEnvironment {
|
|
3
|
+
readonly outputMode?: "human" | "json" | "quiet";
|
|
4
|
+
readonly stdinIsTty?: boolean;
|
|
5
|
+
readonly stdoutIsTty?: boolean;
|
|
6
|
+
readonly ci?: boolean;
|
|
7
|
+
readonly noMotion?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function canAnimate(env?: MotionEnvironment): boolean;
|
|
10
|
+
export declare function animateBannerShine(input: string, env?: MotionEnvironment): Promise<void>;
|
|
11
|
+
export declare function withSpinner<T>(label: string, task: () => Promise<T>, env?: MotionEnvironment): Promise<T>;
|
|
12
|
+
export declare function writeHumanOutputWithShine(text: string, env?: MotionEnvironment): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { paint, terminalAnsi } from "./theme.js";
|
|
3
|
+
import { stripAnsi } from "./text.js";
|
|
4
|
+
function envFlag(name) {
|
|
5
|
+
const value = process.env[name];
|
|
6
|
+
return value === "1" || value === "true" || value === "yes";
|
|
7
|
+
}
|
|
8
|
+
export function canAnimate(env = {}) {
|
|
9
|
+
const mode = env.outputMode ?? "human";
|
|
10
|
+
if (mode !== "human") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const noColor = env.noColor ?? Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
14
|
+
if (noColor) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const stdoutTty = env.stdoutIsTty ?? process.stdout.isTTY === true;
|
|
18
|
+
if (!stdoutTty) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const ci = env.ci ?? Boolean(process.env.CI);
|
|
22
|
+
if (ci) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const noMotion = env.noMotion ??
|
|
26
|
+
(envFlag("MARTIN_NO_MOTION") || envFlag("MARTIN_REDUCED_MOTION"));
|
|
27
|
+
if (noMotion) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const term = env.term ?? process.env.TERM;
|
|
31
|
+
return term !== "dumb";
|
|
32
|
+
}
|
|
33
|
+
function shineCharacter(character, distance, env) {
|
|
34
|
+
if (character === " ") {
|
|
35
|
+
return character;
|
|
36
|
+
}
|
|
37
|
+
if (distance === 0) {
|
|
38
|
+
return paint(character, "plain", { ...env, color: "always" });
|
|
39
|
+
}
|
|
40
|
+
if (distance === 1) {
|
|
41
|
+
return paint(character, "info", { ...env, color: "always" });
|
|
42
|
+
}
|
|
43
|
+
return paint(character, "brand", { ...env, color: "always" });
|
|
44
|
+
}
|
|
45
|
+
export async function animateBannerShine(input, env = {}) {
|
|
46
|
+
if (!canAnimate(env)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const target = [...stripAnsi(input)].slice(0, 100);
|
|
50
|
+
if (target.length === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
process.stdout.write(terminalAnsi.hideCursor);
|
|
55
|
+
for (let center = -2; center < target.length + 4; center += 1) {
|
|
56
|
+
const frame = target
|
|
57
|
+
.map((character, index) => shineCharacter(character, Math.abs(index - center), env))
|
|
58
|
+
.join("");
|
|
59
|
+
process.stdout.write(terminalAnsi.cursorHome +
|
|
60
|
+
terminalAnsi.eraseLine +
|
|
61
|
+
frame +
|
|
62
|
+
terminalAnsi.reset);
|
|
63
|
+
await sleep(18);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
process.stdout.write(terminalAnsi.cursorHome +
|
|
68
|
+
terminalAnsi.eraseLine +
|
|
69
|
+
terminalAnsi.reset +
|
|
70
|
+
terminalAnsi.showCursor);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
74
|
+
export async function withSpinner(label, task, env = {}) {
|
|
75
|
+
if (!canAnimate(env)) {
|
|
76
|
+
return task();
|
|
77
|
+
}
|
|
78
|
+
let frame = 0;
|
|
79
|
+
process.stdout.write(terminalAnsi.hideCursor);
|
|
80
|
+
const timer = setInterval(() => {
|
|
81
|
+
const glyph = spinnerFrames[frame % spinnerFrames.length] ?? "•";
|
|
82
|
+
frame += 1;
|
|
83
|
+
process.stdout.write(terminalAnsi.cursorHome +
|
|
84
|
+
terminalAnsi.eraseLine +
|
|
85
|
+
paint(glyph, "brand", { ...env, color: "always" }) +
|
|
86
|
+
" " +
|
|
87
|
+
label);
|
|
88
|
+
}, 70);
|
|
89
|
+
try {
|
|
90
|
+
return await task();
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearInterval(timer);
|
|
94
|
+
process.stdout.write(terminalAnsi.cursorHome +
|
|
95
|
+
terminalAnsi.eraseLine +
|
|
96
|
+
terminalAnsi.reset +
|
|
97
|
+
terminalAnsi.showCursor);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function writeHumanOutputWithShine(text, env = {}) {
|
|
101
|
+
if (!text) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!canAnimate(env)) {
|
|
105
|
+
process.stdout.write(text + "\n");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const lines = text.split("\n");
|
|
109
|
+
const headingIndex = lines.findIndex((line) => stripAnsi(line).toUpperCase().includes("MARTINLOOP"));
|
|
110
|
+
if (headingIndex < 0) {
|
|
111
|
+
process.stdout.write(text + "\n");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (headingIndex > 0) {
|
|
115
|
+
process.stdout.write(lines.slice(0, headingIndex).join("\n") + "\n");
|
|
116
|
+
}
|
|
117
|
+
const heading = lines[headingIndex] ?? "";
|
|
118
|
+
await animateBannerShine(heading, env);
|
|
119
|
+
process.stdout.write(heading + "\n");
|
|
120
|
+
if (headingIndex < lines.length - 1) {
|
|
121
|
+
process.stdout.write(lines.slice(headingIndex + 1).join("\n") + "\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MartinTone } from "./theme.js";
|
|
2
|
+
export type ProofStatus = "VERIFIED" | "NEEDS_REVIEW" | "STOPPED" | "FAILED" | "RUNNING" | "PENDING";
|
|
3
|
+
export interface StatusPresentation {
|
|
4
|
+
readonly glyph: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly tone: MartinTone;
|
|
7
|
+
}
|
|
8
|
+
export declare function statusPresentation(status: ProofStatus): StatusPresentation;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const presentations = {
|
|
2
|
+
VERIFIED: { glyph: "✓", label: "VERIFIED", tone: "success" },
|
|
3
|
+
NEEDS_REVIEW: { glyph: "!", label: "NEEDS REVIEW", tone: "warning" },
|
|
4
|
+
STOPPED: { glyph: "■", label: "STOPPED", tone: "danger" },
|
|
5
|
+
FAILED: { glyph: "×", label: "FAILED", tone: "danger" },
|
|
6
|
+
RUNNING: { glyph: "◆", label: "RUNNING", tone: "info" },
|
|
7
|
+
PENDING: { glyph: "○", label: "PENDING", tone: "muted" },
|
|
8
|
+
};
|
|
9
|
+
export function statusPresentation(status) {
|
|
10
|
+
return presentations[status];
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TableColumn<Row> {
|
|
2
|
+
readonly header: string;
|
|
3
|
+
readonly value: (row: Row) => string;
|
|
4
|
+
readonly minWidth?: number;
|
|
5
|
+
readonly maxWidth?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface RenderTableOptions {
|
|
8
|
+
readonly separator?: string;
|
|
9
|
+
readonly width?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function renderTable<Row>(rows: readonly Row[], columns: readonly TableColumn<Row>[], options?: RenderTableOptions | number): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { padRightVisible, truncateVisible, visibleWidth } from "./text.js";
|
|
2
|
+
function columnWidth(rows, column) {
|
|
3
|
+
const contentWidth = Math.max(visibleWidth(column.header), ...rows.map((row) => visibleWidth(column.value(row))), column.minWidth ?? 0);
|
|
4
|
+
return Math.min(contentWidth, column.maxWidth ?? Number.POSITIVE_INFINITY);
|
|
5
|
+
}
|
|
6
|
+
function fitWidths(naturalWidths, columns, availableWidth) {
|
|
7
|
+
const widths = [...naturalWidths];
|
|
8
|
+
const floors = columns.map((column, index) => Math.min(widths[index] ?? 0, Math.max(4, visibleWidth(column.header))));
|
|
9
|
+
while (widths.reduce((sum, width) => sum + width, 0) > availableWidth) {
|
|
10
|
+
let candidate = -1;
|
|
11
|
+
let largestSlack = 0;
|
|
12
|
+
for (let index = 0; index < widths.length; index += 1) {
|
|
13
|
+
const slack = (widths[index] ?? 0) - (floors[index] ?? 0);
|
|
14
|
+
if (slack > largestSlack) {
|
|
15
|
+
candidate = index;
|
|
16
|
+
largestSlack = slack;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (candidate < 0) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
widths[candidate] = (widths[candidate] ?? 0) - 1;
|
|
23
|
+
}
|
|
24
|
+
return widths;
|
|
25
|
+
}
|
|
26
|
+
export function renderTable(rows, columns, options = {}) {
|
|
27
|
+
if (columns.length === 0) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
const normalizedOptions = typeof options === "number" ? { width: options } : options;
|
|
31
|
+
const separator = normalizedOptions.separator ?? " │ ";
|
|
32
|
+
const naturalWidths = columns.map((column) => columnWidth(rows, column));
|
|
33
|
+
const separatorWidth = visibleWidth(separator) * (columns.length - 1);
|
|
34
|
+
const availableWidth = normalizedOptions.width === undefined
|
|
35
|
+
? Number.POSITIVE_INFINITY
|
|
36
|
+
: Math.max(columns.length * 4, normalizedOptions.width - separatorWidth);
|
|
37
|
+
const widths = Number.isFinite(availableWidth)
|
|
38
|
+
? fitWidths(naturalWidths, columns, availableWidth)
|
|
39
|
+
: naturalWidths;
|
|
40
|
+
const renderCells = (cells) => cells
|
|
41
|
+
.map((cell, index) => padRightVisible(truncateVisible(cell, widths[index] ?? 0), widths[index] ?? 0))
|
|
42
|
+
.join(separator);
|
|
43
|
+
const header = renderCells(columns.map((column) => column.header));
|
|
44
|
+
const divider = widths.map((width) => "─".repeat(width)).join("─┼─");
|
|
45
|
+
const body = rows.map((row) => renderCells(columns.map((column) => column.value(row))));
|
|
46
|
+
return [header, divider, ...body].join("\n");
|
|
47
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function stripAnsi(value: string): string;
|
|
2
|
+
export declare function visibleWidth(value: string): number;
|
|
3
|
+
export declare function padRightVisible(value: string, width: number): string;
|
|
4
|
+
export declare function truncateVisible(value: string, width: number): string;
|
|
5
|
+
export declare function terminalWidth(width?: number): number;
|
|
6
|
+
export declare function horizontalRule(width: number, character?: string): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const csiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
+
const oscPattern = /\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g;
|
|
3
|
+
export function stripAnsi(value) {
|
|
4
|
+
return value.replace(oscPattern, "").replace(csiPattern, "");
|
|
5
|
+
}
|
|
6
|
+
export function visibleWidth(value) {
|
|
7
|
+
return Array.from(stripAnsi(value)).length;
|
|
8
|
+
}
|
|
9
|
+
export function padRightVisible(value, width) {
|
|
10
|
+
return value + " ".repeat(Math.max(0, width - visibleWidth(value)));
|
|
11
|
+
}
|
|
12
|
+
export function truncateVisible(value, width) {
|
|
13
|
+
if (width <= 0) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
if (visibleWidth(value) <= width) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (width === 1) {
|
|
20
|
+
return "…";
|
|
21
|
+
}
|
|
22
|
+
return Array.from(stripAnsi(value)).slice(0, width - 1).join("") + "…";
|
|
23
|
+
}
|
|
24
|
+
export function terminalWidth(width) {
|
|
25
|
+
const resolved = width ?? process.stdout.columns ?? 80;
|
|
26
|
+
return Math.max(40, Math.min(160, Math.floor(resolved)));
|
|
27
|
+
}
|
|
28
|
+
export function horizontalRule(width, character = "─") {
|
|
29
|
+
return character.repeat(Math.max(0, width));
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type MartinTone = "brand" | "success" | "danger" | "warning" | "info" | "muted" | "plain";
|
|
2
|
+
export interface RenderEnvironment {
|
|
3
|
+
readonly color?: "auto" | "always" | "never";
|
|
4
|
+
readonly isTty?: boolean;
|
|
5
|
+
readonly noColor?: boolean;
|
|
6
|
+
readonly term?: string;
|
|
7
|
+
readonly colorDepth?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const palette: {
|
|
10
|
+
readonly brand: readonly [107, 85, 198];
|
|
11
|
+
readonly success: readonly [94, 225, 115];
|
|
12
|
+
readonly danger: readonly [255, 93, 93];
|
|
13
|
+
readonly warning: readonly [244, 200, 74];
|
|
14
|
+
readonly info: readonly [85, 199, 243];
|
|
15
|
+
readonly muted: readonly [148, 163, 184];
|
|
16
|
+
readonly plain: readonly [248, 250, 252];
|
|
17
|
+
};
|
|
18
|
+
declare function rgb(red: number, green: number, blue: number): string;
|
|
19
|
+
export declare function resolveColorEnabled(env?: RenderEnvironment): boolean;
|
|
20
|
+
export declare function paint(value: string, tone: MartinTone, env?: RenderEnvironment): string;
|
|
21
|
+
export declare function bold(value: string, env?: RenderEnvironment): string;
|
|
22
|
+
export declare function dim(value: string, env?: RenderEnvironment): string;
|
|
23
|
+
export declare const terminalAnsi: {
|
|
24
|
+
readonly reset: string;
|
|
25
|
+
readonly hideCursor: string;
|
|
26
|
+
readonly showCursor: string;
|
|
27
|
+
readonly eraseLine: string;
|
|
28
|
+
readonly cursorHome: "\r";
|
|
29
|
+
readonly rgb: typeof rgb;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const escape = "\u001B[";
|
|
2
|
+
const basic = {
|
|
3
|
+
reset: escape + "0m",
|
|
4
|
+
bold: escape + "1m",
|
|
5
|
+
dim: escape + "2m",
|
|
6
|
+
brand: escape + "35m",
|
|
7
|
+
success: escape + "32m",
|
|
8
|
+
danger: escape + "31m",
|
|
9
|
+
warning: escape + "33m",
|
|
10
|
+
info: escape + "36m",
|
|
11
|
+
muted: escape + "90m",
|
|
12
|
+
plain: "",
|
|
13
|
+
};
|
|
14
|
+
export const palette = {
|
|
15
|
+
brand: [107, 85, 198],
|
|
16
|
+
success: [94, 225, 115],
|
|
17
|
+
danger: [255, 93, 93],
|
|
18
|
+
warning: [244, 200, 74],
|
|
19
|
+
info: [85, 199, 243],
|
|
20
|
+
muted: [148, 163, 184],
|
|
21
|
+
plain: [248, 250, 252],
|
|
22
|
+
};
|
|
23
|
+
function rgb(red, green, blue) {
|
|
24
|
+
return escape + "38;2;" + red + ";" + green + ";" + blue + "m";
|
|
25
|
+
}
|
|
26
|
+
function resolveColorDepth() {
|
|
27
|
+
const stream = process.stdout;
|
|
28
|
+
try {
|
|
29
|
+
return stream.getColorDepth?.() ?? 1;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function resolveColorEnabled(env = {}) {
|
|
36
|
+
if (env.color === "always") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (env.color === "never") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const noColor = env.noColor ?? Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
|
|
43
|
+
if (noColor) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const term = env.term ?? process.env.TERM;
|
|
47
|
+
if (term === "dumb") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return env.isTty ?? process.stdout.isTTY === true;
|
|
51
|
+
}
|
|
52
|
+
function toneCode(tone, env) {
|
|
53
|
+
if (tone === "plain") {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
const depth = env.colorDepth ?? resolveColorDepth();
|
|
57
|
+
if (depth >= 24) {
|
|
58
|
+
const [red, green, blue] = palette[tone];
|
|
59
|
+
return rgb(red, green, blue);
|
|
60
|
+
}
|
|
61
|
+
return basic[tone];
|
|
62
|
+
}
|
|
63
|
+
export function paint(value, tone, env = {}) {
|
|
64
|
+
if (!resolveColorEnabled(env) || tone === "plain") {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
return toneCode(tone, env) + value + basic.reset;
|
|
68
|
+
}
|
|
69
|
+
export function bold(value, env = {}) {
|
|
70
|
+
return resolveColorEnabled(env) ? basic.bold + value + basic.reset : value;
|
|
71
|
+
}
|
|
72
|
+
export function dim(value, env = {}) {
|
|
73
|
+
return resolveColorEnabled(env) ? basic.dim + value + basic.reset : value;
|
|
74
|
+
}
|
|
75
|
+
export const terminalAnsi = {
|
|
76
|
+
reset: basic.reset,
|
|
77
|
+
hideCursor: escape + "?25l",
|
|
78
|
+
showCursor: escape + "?25h",
|
|
79
|
+
eraseLine: escape + "2K",
|
|
80
|
+
cursorHome: "\r",
|
|
81
|
+
rgb,
|
|
82
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VerifiedHandoffV1 } from "../contracts/index.js";
|
|
2
|
+
import { type RenderEnvironment } from "./theme.js";
|
|
3
|
+
export interface VerifiedHandoffRenderOptions {
|
|
4
|
+
readonly width?: number;
|
|
5
|
+
readonly environment?: RenderEnvironment;
|
|
6
|
+
}
|
|
7
|
+
export declare function renderVerifiedHandoff(handoff: VerifiedHandoffV1, options?: VerifiedHandoffRenderOptions): string;
|
|
8
|
+
/**
|
|
9
|
+
* Renders a VerifiedHandoffV1 as plain Markdown (no ANSI).
|
|
10
|
+
*
|
|
11
|
+
* CRITICAL: replicates the exact effectiveOutcome trust-authority check from
|
|
12
|
+
* renderVerifiedHandoff so that CLI and MCP surfaces always agree on
|
|
13
|
+
* VERIFIED / NEEDS REVIEW / STOPPED for the same input.
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderVerifiedHandoffMarkdown(handoff: VerifiedHandoffV1, options?: VerifiedHandoffRenderOptions): string;
|