@martinloop/mcp 0.5.3 → 0.5.6

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.
Files changed (65) hide show
  1. package/README.md +175 -175
  2. package/dist/arcade/capabilities.d.ts +36 -0
  3. package/dist/arcade/capabilities.js +37 -0
  4. package/dist/arcade/resource.d.ts +31 -0
  5. package/dist/arcade/resource.js +52 -0
  6. package/dist/arcade/status.d.ts +23 -0
  7. package/dist/arcade/status.js +25 -0
  8. package/dist/package-version.d.ts +1 -1
  9. package/dist/package-version.js +3 -3
  10. package/dist/resources.js +147 -147
  11. package/dist/server-validation.js +2 -0
  12. package/dist/server.js +79 -35
  13. package/dist/tools/run-loop.d.ts +2 -2
  14. package/dist/tools/run-loop.js +26 -6
  15. package/dist/tools/tool-response.d.ts +9 -1
  16. package/dist/tools/tool-response.js +7 -5
  17. package/dist/tools/tool-support.js +62 -271
  18. package/dist/vendor/adapters/claude-cli.d.ts +9 -0
  19. package/dist/vendor/adapters/claude-cli.js +21 -3
  20. package/dist/vendor/adapters/cli-bridge.js +7 -1
  21. package/dist/vendor/adapters/codex-capabilities.d.ts +12 -3
  22. package/dist/vendor/adapters/codex-capabilities.js +46 -18
  23. package/dist/vendor/adapters/codex-cli.d.ts +2 -1
  24. package/dist/vendor/adapters/codex-cli.js +12 -2
  25. package/dist/vendor/adapters/codex-host.d.ts +4 -1
  26. package/dist/vendor/adapters/codex-host.js +34 -16
  27. package/dist/vendor/adapters/codex-launcher.d.ts +1 -1
  28. package/dist/vendor/adapters/codex-launcher.js +1 -1
  29. package/dist/vendor/adapters/index.d.ts +3 -1
  30. package/dist/vendor/adapters/index.js +2 -0
  31. package/dist/vendor/adapters/openai-compatible.js +12 -9
  32. package/dist/vendor/adapters/stub-agent-cli.d.ts +9 -0
  33. package/dist/vendor/adapters/stub-agent-cli.js +43 -0
  34. package/dist/vendor/adapters/stub-direct-provider.d.ts +8 -0
  35. package/dist/vendor/adapters/stub-direct-provider.js +16 -0
  36. package/dist/vendor/contracts/agent-execution.d.ts +9 -0
  37. package/dist/vendor/contracts/agent-execution.js +14 -0
  38. package/dist/vendor/contracts/index.d.ts +9 -0
  39. package/dist/vendor/contracts/index.js +1 -0
  40. package/dist/vendor/core/grounding.js +42 -7
  41. package/dist/vendor/core/index.d.ts +5 -2
  42. package/dist/vendor/core/index.js +20 -66
  43. package/dist/vendor/core/policy.d.ts +1 -0
  44. package/dist/vendor/core/policy.js +8 -0
  45. package/dist/vendor/core/rollback.d.ts +3 -0
  46. package/dist/vendor/core/rollback.js +33 -20
  47. package/dist/vendor/core/verified-handoff.js +25 -6
  48. package/dist/vendor/presentation/governed-plan.d.ts +33 -0
  49. package/dist/vendor/presentation/governed-plan.js +146 -0
  50. package/dist/vendor/presentation/index.d.ts +7 -0
  51. package/dist/vendor/presentation/index.js +7 -0
  52. package/dist/vendor/presentation/motion.d.ts +12 -0
  53. package/dist/vendor/presentation/motion.js +123 -0
  54. package/dist/vendor/presentation/status.d.ts +8 -0
  55. package/dist/vendor/presentation/status.js +11 -0
  56. package/dist/vendor/presentation/table.d.ts +11 -0
  57. package/dist/vendor/presentation/table.js +47 -0
  58. package/dist/vendor/presentation/text.d.ts +6 -0
  59. package/dist/vendor/presentation/text.js +30 -0
  60. package/dist/vendor/presentation/theme.d.ts +31 -0
  61. package/dist/vendor/presentation/theme.js +82 -0
  62. package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
  63. package/dist/vendor/presentation/verified-handoff.js +148 -0
  64. package/package.json +81 -80
  65. package/server.json +21 -21
@@ -33,6 +33,9 @@ export function verifierActuallyPassed(evidence, expected) {
33
33
  if (!evidence?.passed || !evidence.binding || !evidence.steps) {
34
34
  return false;
35
35
  }
36
+ if (expected.commands.length === 0) {
37
+ return false;
38
+ }
36
39
  if (evidence.binding.runId !== expected.runId ||
37
40
  evidence.binding.workspaceId !== expected.workspaceId ||
38
41
  evidence.binding.cwd !== expected.cwd ||
@@ -93,7 +96,6 @@ export function deriveVerifiedHandoffExecutionBoundary(loop) {
93
96
  const persistedMode = isExecutionMode(loop.metadata["executionMode"])
94
97
  ? loop.metadata["executionMode"]
95
98
  : undefined;
96
- const groundingEvidenceAvailable = loop.metadata["groundingEvidenceStatus"] !== "unavailable";
97
99
  const executionMode = hasSimulatedAdapter
98
100
  ? "simulated"
99
101
  : hasVerifierOnlyAdapter
@@ -101,7 +103,7 @@ export function deriveVerifiedHandoffExecutionBoundary(loop) {
101
103
  : persistedMode ?? (adapterIds.length > 0 ? "governed" : "simulated");
102
104
  return {
103
105
  executionMode,
104
- governanceClaimEligible: executionMode === "governed" && adapterIds.length > 0 && groundingEvidenceAvailable,
106
+ governanceClaimEligible: executionMode === "governed" && adapterIds.length > 0,
105
107
  };
106
108
  }
107
109
  function toEvidenceStatus(status) {
@@ -157,17 +159,34 @@ export function buildVerifiedHandoff(input) {
157
159
  "Test integrity was not evaluated for this run.",
158
160
  };
159
161
  const executionBoundary = deriveVerifiedHandoffExecutionBoundary(input.loop);
162
+ const verifierStepsActuallyPassed = input.verification.steps.length > 0 &&
163
+ input.verification.steps.every((step) => step.launched === true &&
164
+ step.completed === true &&
165
+ step.crashed === false &&
166
+ step.timedOut !== true &&
167
+ step.exitCode === 0);
168
+ const effectiveVerificationStatus = input.verification.status === "passed" && !verifierStepsActuallyPassed
169
+ ? input.verification.steps.length === 0
170
+ ? "not_run"
171
+ : "contradicted"
172
+ : input.verification.status;
173
+ const verificationWarnings = [
174
+ ...input.verification.warnings,
175
+ ...(effectiveVerificationStatus !== input.verification.status
176
+ ? ["A VERIFIED claim requires at least one launched, completed, exit-zero verifier step."]
177
+ : []),
178
+ ];
160
179
  const outcome = resolveVerifiedHandoffOutcome({
161
180
  lifecycleState: input.loop.lifecycleState,
162
181
  executionStatus: input.executionStatus,
163
- verificationStatus: input.verification.status,
182
+ verificationStatus: effectiveVerificationStatus,
164
183
  receiptIntegrity: input.receiptIntegrity.state,
165
184
  scopeStatus: scope.status,
166
185
  testIntegrityStatus: testIntegrity.status,
167
186
  mutationRequired: input.mutationRequired ?? input.loop.task.mutationMode === "edit",
168
187
  changedFileCount: scope.changedFiles.length,
169
188
  definitionOfDonePreSatisfied: input.definitionOfDonePreSatisfied,
170
- evidenceContradicted: input.evidenceContradicted ?? input.verification.status === "contradicted",
189
+ evidenceContradicted: input.evidenceContradicted ?? effectiveVerificationStatus === "contradicted",
171
190
  governanceClaimEligible: executionBoundary.governanceClaimEligible,
172
191
  unresolvedWorkCount: unresolvedWork.length,
173
192
  });
@@ -196,10 +215,10 @@ export function buildVerifiedHandoff(input) {
196
215
  lifecycleState: input.loop.lifecycleState,
197
216
  },
198
217
  verification: {
199
- status: toEvidenceStatus(input.verification.status),
218
+ status: toEvidenceStatus(effectiveVerificationStatus),
200
219
  summary: input.verification.summary,
201
220
  checks: input.verification.steps.map(toCheck),
202
- warnings: input.verification.warnings,
221
+ warnings: verificationWarnings,
203
222
  },
204
223
  requirements: input.requirements ?? [],
205
224
  scope,
@@ -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,7 @@
1
+ export * from "./governed-plan.js";
2
+ export * from "./motion.js";
3
+ export * from "./status.js";
4
+ export * from "./table.js";
5
+ export * from "./text.js";
6
+ export * from "./theme.js";
7
+ export * from "./verified-handoff.js";
@@ -0,0 +1,7 @@
1
+ export * from "./governed-plan.js";
2
+ export * from "./motion.js";
3
+ export * from "./status.js";
4
+ export * from "./table.js";
5
+ export * from "./text.js";
6
+ export * from "./theme.js";
7
+ export * from "./verified-handoff.js";
@@ -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;