@deeeed/metamask-harness 0.16.0 → 0.17.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.17.0 - 2026-07-14
6
+
7
+ ### Added
8
+
9
+ - Command-specific JSON failures now preserve a structured `userAction` across device targeting, action calls, recipe validation/execution, checks, and dev-server stop. Successful install/verify/cleanup, provision, and stop transitions add `next` only when one truthful follow-up exists; discovery and terminal evidence results remain quiet.
10
+ - `run` and `launch` now implement the documented `--json-stream` contract: line-flushed JSONL phase/progress events, per-node recipe status, compact evidence paths, and one terminal `complete` event. Stream mode keeps stdout machine-clean and takes precedence when combined with `--json`; existing `--json` output is unchanged.
11
+ - Interactive Mobile launch, Runway provisioning, and recipe execution now print contextual intent before their first potentially slow readiness or validation probe. The new lines are suppressed for `--json` and non-TTY stdout.
12
+ - `last --json` reads an atomic, mode-`0600`, per-checkout resumability journal for significant runtime/proof commands, including redacted args, verdict, evidence paths, and timestamps. Interrupted commands remain `running`; discovery commands do not overwrite the record.
13
+ - `actions <query>` now searches action names, categories, fields, and descriptions with typo tolerance, while single-action detail includes compact related-action names.
14
+
15
+ ### Changed
16
+
17
+ - Action categories now group protocol primitives into useful `assertion`, `control`, `evidence`, and `runtime` domains instead of exposing one category per unnamespaced action.
18
+ - Public commands now reject unknown commands/options, missing values and positionals, invalid enums, and excess positionals before dispatch. Human errors include valid command-local options, a closest match when available, and a runnable example; `--json` returns stable `CLI_*` codes with `error.message` and `error.userAction`.
19
+ - `docs/CLI-ERGONOMICS-AUDIT.md` is generated from the public command contract so every command remains covered by the shared bad-flag and JSON usage-error policy as the surface evolves.
20
+
5
21
  ## 0.16.0 - 2026-07-14
6
22
 
7
23
  ### Added
package/README.md CHANGED
@@ -29,6 +29,7 @@ Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
29
29
  (`@farmslot/recipe-harness`); the files it installs into a checkout are the
30
30
  **runtime overlay**. Read [MENTAL-MODEL.md](docs/MENTAL-MODEL.md) first (the
31
31
  before→after map), then [CLI-SPEC.md](docs/CLI-SPEC.md) for the full contract.
32
+ Release candidates have a short [human QA checklist](docs/CLI-ERGONOMICS-HUMAN-QA.md).
32
33
 
33
34
  One package, two responsibilities:
34
35
 
@@ -76,16 +77,18 @@ windows, wallet state, health, ports, parallel).
76
77
 
77
78
  ```bash
78
79
  # Capabilities (compose recipes from the vocabulary + flow library)
79
- mm-harness actions --adapter mobile --categories --json # compact discovery
80
+ mm-harness actions positions --adapter mobile --json # focused discovery
80
81
  mm-harness actions --adapter mobile --category ui --json # bounded vocabulary
81
82
  mm-harness actions --adapter mobile --raw # raw action-manifest dump
82
83
  mm-harness call unlock --adapter extension # run one action via the real engine path
84
+ mm-harness last --json # resume from the last significant command
83
85
  mm-harness flows --json # reusable library flows, with provenance
84
86
 
85
87
  # Readiness + proof
86
88
  mm-harness doctor # read-only readiness check (no launch)
87
89
  mm-harness doctor --fix # heal the overlay without launching
88
90
  mm-harness run recipe.json --plan --adapter mobile # validate + plan, touch nothing
91
+ mm-harness run recipe.json --json-stream # line-flushed JSONL progress for agents
89
92
 
90
93
  # Daily loop (run from inside a checkout)
91
94
  mm-harness launch ios # launch (mobile: ios | android)
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ import { handleRecipeQuality } from "./commands/recipe-quality.js";
21
21
  import { handleStatus } from "./commands/status.js";
22
22
  import { handleCheck } from "./commands/check.js";
23
23
  import { handleChecklist } from "./commands/checklist.js";
24
+ import { handleLast } from "./commands/last.js";
24
25
  import { parseArgs, targetPath } from "./commands/parse-args.js";
25
26
  import { runOneNode } from "./commands/run-engine.js";
26
27
  const COMMANDS = {
@@ -133,6 +134,7 @@ async function main(argv) {
133
134
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
134
135
  if (command === "check") return handleCheck(argv.slice(1));
135
136
  if (command === "checklist") return handleChecklist(argv.slice(1));
137
+ if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
136
138
  const handler = COMMANDS[command];
137
139
  if (!handler) throw new Error(`Unknown command: ${command}`);
138
140
  return handler(parseArgs(argv.slice(1), command));
@@ -0,0 +1,441 @@
1
+ const bool = () => ({ kind: "boolean" });
2
+ const value = (choices) => ({ kind: "value", ...choices ? { choices } : {} });
3
+ const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { choices } : {} });
4
+ const options = (...groups) => Object.assign({}, ...groups);
5
+ const HELP = { "--help": bool(), "-h": bool() };
6
+ const JSON = { "--json": bool() };
7
+ const JSON_STREAM = { "--json-stream": bool() };
8
+ const TARGET = { "--target": value(), "--project-root": value() };
9
+ const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
10
+ const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
11
+ const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "ios", "android"]) };
12
+ const DEVICE = { "--device": value() };
13
+ const RUNTIME_PORTS = {
14
+ "--cdp-port": value(),
15
+ "--watcher-port": value(),
16
+ "--metro-port": value(),
17
+ "--runtime-dir": value()
18
+ };
19
+ const RECIPE_RUNTIME = {
20
+ "--artifacts-dir": value(),
21
+ "--action-manifest": value(),
22
+ "--library": value(),
23
+ "--heal": value(["off", "infra-only", "auto"]),
24
+ "--slot": value(),
25
+ "--validation-runtime-dir": value(),
26
+ "--launch-existing-dist": bool(),
27
+ "--record-video": optionalValue(["full-run", "off"]),
28
+ "--record": bool()
29
+ };
30
+ const RUNWAY = {
31
+ "--branch": value(),
32
+ "--default-branch": value(),
33
+ "--run": value(),
34
+ "--cache-root": value(),
35
+ "--simulator": value(),
36
+ "--runtime": value(),
37
+ "--device-type": value(),
38
+ "--slot": value(),
39
+ "--watcher-port": value(),
40
+ "--runtime-dir": value(),
41
+ "--force": bool(),
42
+ "--resolve-only": bool()
43
+ };
44
+ const PUBLIC_COMMAND_CONTRACTS = {
45
+ status: {
46
+ aliases: ["health", "home"],
47
+ options: options(HELP, JSON, TARGET, DEVICE, {
48
+ "--fast": bool(),
49
+ "--all-devices": bool()
50
+ })
51
+ },
52
+ checklist: {
53
+ usage: "mm-harness checklist mark <task-dir> <step> [options]",
54
+ options: options(HELP, {
55
+ "--mark-last": bool(),
56
+ "--already-fixed": bool(),
57
+ "--no-self-review": bool(),
58
+ "--skip-learnings": bool(),
59
+ "--skip-checklist": bool(),
60
+ "--reason": value(),
61
+ "--checklist": value(),
62
+ "--signal": value()
63
+ }),
64
+ positionals: [
65
+ { label: "action", choices: ["mark"] },
66
+ { label: "task-dir" },
67
+ {
68
+ label: "step",
69
+ validate: (step) => {
70
+ if (["start", "complete", "no-change", "blocked"].includes(step)) return true;
71
+ const numeric = Number(step);
72
+ return Number.isInteger(numeric) && numeric >= 1;
73
+ },
74
+ validDescription: "start|complete|no-change|blocked|a positive numeric step"
75
+ }
76
+ ],
77
+ minimumPositionals: 3
78
+ },
79
+ actions: {
80
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
81
+ "--action": value(),
82
+ "--categories": bool(),
83
+ "--category": value(),
84
+ "--action-manifest": value(),
85
+ "--raw": bool()
86
+ }),
87
+ positionals: [{ label: "query" }],
88
+ noPositionalsWith: ["--action", "--categories", "--raw"]
89
+ },
90
+ stop: {
91
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
92
+ "--port": value(),
93
+ "--watcher-port": value()
94
+ })
95
+ },
96
+ call: {
97
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
98
+ "--list": bool(),
99
+ "--arg": value()
100
+ }),
101
+ positionals: [{ label: "action" }],
102
+ minimumPositionals: 0,
103
+ requiredUnless: ["--list"],
104
+ noPositionalsWith: ["--list"],
105
+ leadingPositionals: 1,
106
+ variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
107
+ },
108
+ flows: {
109
+ options: options(HELP, JSON, TARGET, {
110
+ "--library": value(),
111
+ "--from": value(),
112
+ "--flow": value(),
113
+ "--to": value(),
114
+ "--domain": value(),
115
+ "--run": value(),
116
+ "--force": bool()
117
+ }),
118
+ positionals: [{ label: "action", choices: ["list", "promote"] }]
119
+ },
120
+ run: {
121
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
122
+ "--list": bool(),
123
+ "--plan": bool()
124
+ }),
125
+ positionals: [{ label: "recipe" }],
126
+ minimumPositionals: 1,
127
+ requiredUnless: ["--list"],
128
+ noPositionalsWith: ["--list"]
129
+ },
130
+ last: {
131
+ options: options(HELP, JSON, TARGET, {
132
+ "--runtime-dir": value()
133
+ })
134
+ },
135
+ doctor: {
136
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
137
+ "--fix": bool(),
138
+ "--expect-live": bool(),
139
+ "--print-ready": bool(),
140
+ "--all-devices": bool(),
141
+ "--action-manifest": value(),
142
+ "--port": value()
143
+ })
144
+ },
145
+ check: {
146
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
147
+ "--base": value(),
148
+ "--profile": value(["fast", "full"]),
149
+ "--fix": bool(),
150
+ "--artifacts-dir": value()
151
+ }),
152
+ positionals: [{ label: "action", choices: ["diff"] }],
153
+ minimumPositionals: 1
154
+ },
155
+ "recipe-quality": {
156
+ options: options(HELP, JSON, {
157
+ "--input": value(),
158
+ "--output": value()
159
+ }),
160
+ positionals: [{ label: "action", choices: ["build"] }]
161
+ },
162
+ provision: {
163
+ options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
164
+ "--platform": value(["ios"]),
165
+ "--device-platform": value(["ios"])
166
+ }),
167
+ positionals: [
168
+ { label: "source", choices: ["runway", "ios"] },
169
+ { label: "platform", choices: ["ios"] }
170
+ ]
171
+ },
172
+ install: {
173
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNWAY, {
174
+ "--runway": bool()
175
+ }),
176
+ allowPassthrough: true
177
+ },
178
+ verify: {
179
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, RUNTIME_PORTS),
180
+ allowPassthrough: true
181
+ },
182
+ cleanup: {
183
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM),
184
+ allowPassthrough: true
185
+ },
186
+ launch: {
187
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
188
+ "--build": bool(),
189
+ "--verify": bool(),
190
+ "--runway": bool(),
191
+ "--watch": bool(),
192
+ "--sidepanel": bool(),
193
+ "--fullscreen": bool(),
194
+ "--url": value(),
195
+ "--heal": value(["off", "infra-only", "auto"])
196
+ }),
197
+ positionals: [{ label: "platform", choices: ["ios", "android"] }]
198
+ },
199
+ logs: {
200
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
201
+ "--full": bool(),
202
+ "--window": bool(),
203
+ "--events": value(),
204
+ "--source": value()
205
+ })
206
+ },
207
+ debug: {
208
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
209
+ "--worker": bool(),
210
+ "--dev-menu": bool(),
211
+ "--no-open": bool()
212
+ })
213
+ },
214
+ update: {
215
+ options: options(HELP, JSON, { "--check": bool() })
216
+ },
217
+ fixtures: {
218
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
219
+ "--from": value(),
220
+ "--dev": bool(),
221
+ "--force": bool(),
222
+ "--fixture": value(),
223
+ "--out": value(),
224
+ "--state": value(),
225
+ "--cdp-port": value(),
226
+ "--extension-dir": value(),
227
+ "--extension-id-file": value(),
228
+ "--action-manifest": value()
229
+ }),
230
+ positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
231
+ minimumPositionals: 1
232
+ }
233
+ };
234
+ function validatePublicInvocation(argv, examples = {}) {
235
+ const token = argv[0];
236
+ if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
237
+ const resolved = resolveCommand(token);
238
+ if (!resolved) {
239
+ const names = publicCommandTokens();
240
+ const suggestion = closest(token, names);
241
+ if (token.startsWith("-")) {
242
+ return {
243
+ code: "CLI_UNKNOWN_OPTION",
244
+ command: "mm-harness",
245
+ message: `unknown top-level option '${token}'. Valid top-level options: --help, --version.`,
246
+ userAction: actionFor(token, closest(token, ["--help", "--version"]), "mm-harness --help")
247
+ };
248
+ }
249
+ return {
250
+ code: "CLI_UNKNOWN_COMMAND",
251
+ command: token,
252
+ message: `unknown command '${token}'. Valid commands: ${Object.keys(PUBLIC_COMMAND_CONTRACTS).join(", ")}.`,
253
+ userAction: actionFor(token, suggestion, suggestion ? exampleFor(resolveCommand(suggestion)?.name, examples) : "mm-harness --help")
254
+ };
255
+ }
256
+ const { name, contract } = resolved;
257
+ const tokens = argv.slice(1);
258
+ const positionals = [];
259
+ const seenOptions = /* @__PURE__ */ new Set();
260
+ const validOptions = Object.keys(contract.options);
261
+ for (let index = 0; index < tokens.length; index += 1) {
262
+ const argument = tokens[index] ?? "";
263
+ if (argument === "--") {
264
+ if (!contract.allowPassthrough) {
265
+ return usageFailure("CLI_UNEXPECTED_PASSTHROUGH", name, "this command does not accept `--` passthrough.", contract, examples);
266
+ }
267
+ break;
268
+ }
269
+ if (!argument.startsWith("-") || argument === "-") {
270
+ positionals.push(argument);
271
+ continue;
272
+ }
273
+ const equals = argument.indexOf("=");
274
+ const optionName = equals === -1 ? argument : argument.slice(0, equals);
275
+ const inlineValue = equals === -1 ? void 0 : argument.slice(equals + 1);
276
+ const spec = contract.options[optionName];
277
+ if (!spec) {
278
+ const suggestion = closest(optionName, validOptions);
279
+ return {
280
+ code: "CLI_UNKNOWN_OPTION",
281
+ command: name,
282
+ message: `unknown option '${optionName}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
283
+ userAction: actionFor(optionName, suggestion, exampleFor(name, examples))
284
+ };
285
+ }
286
+ seenOptions.add(optionName);
287
+ if (spec.kind === "boolean") {
288
+ if (inlineValue !== void 0) {
289
+ return usageFailure("CLI_INVALID_OPTION_VALUE", name, `${optionName} is a flag and does not take a value.`, contract, examples);
290
+ }
291
+ continue;
292
+ }
293
+ if (spec.kind === "optional-value" && inlineValue === void 0) continue;
294
+ const optionValue = inlineValue ?? tokens[index + 1];
295
+ const nextTokenIsOption = inlineValue === void 0 && (optionValue === "-h" || optionValue === "--" || optionValue?.startsWith("--"));
296
+ if (optionValue === void 0 || spec.kind === "value" && optionValue === "" || nextTokenIsOption) {
297
+ return usageFailure("CLI_MISSING_OPTION_VALUE", name, `${optionName} requires a value.`, contract, examples);
298
+ }
299
+ if (inlineValue === void 0) index += 1;
300
+ if (spec.choices && !spec.choices.includes(optionValue)) {
301
+ const suggestion = closest(optionValue, spec.choices);
302
+ return {
303
+ code: "CLI_INVALID_OPTION_VALUE",
304
+ command: name,
305
+ message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
306
+ userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
307
+ };
308
+ }
309
+ }
310
+ if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
311
+ if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
312
+ const missing = contract.positionals?.[0]?.label ?? "argument";
313
+ return usageFailure("CLI_MISSING_POSITIONAL", name, `${name} requires <${missing}> first.`, contract, examples);
314
+ }
315
+ const forbiddingOption = contract.noPositionalsWith?.find((option) => seenOptions.has(option));
316
+ if (forbiddingOption && positionals.length > 0) {
317
+ return usageFailure(
318
+ "CLI_EXCESS_POSITIONAL",
319
+ name,
320
+ `${forbiddingOption} does not accept positional '${positionals[0]}'.`,
321
+ contract,
322
+ examples
323
+ );
324
+ }
325
+ const minimum = contract.requiredUnless?.some((option) => seenOptions.has(option)) ? 0 : contract.minimumPositionals ?? 0;
326
+ if (positionals.length < minimum) {
327
+ const missingSpec = contract.positionals?.[positionals.length];
328
+ const missing = missingSpec?.label ?? "argument";
329
+ const valid = missingSpec?.choices?.length ? ` Choose one: ${missingSpec.choices.join("|")}.` : "";
330
+ const usage = contract.usage ? ` Usage: ${contract.usage}.` : "";
331
+ return usageFailure("CLI_MISSING_POSITIONAL", name, `missing required <${missing}>.${valid}${usage}`, contract, examples);
332
+ }
333
+ const fixed = contract.positionals ?? [];
334
+ for (let index = 0; index < Math.min(positionals.length, fixed.length); index += 1) {
335
+ const invalid = invalidPositional(positionals[index] ?? "", fixed[index]);
336
+ if (invalid) return positionalFailure(name, positionals[index] ?? "", fixed[index], contract, examples);
337
+ }
338
+ if (positionals.length > fixed.length && !contract.variadic) {
339
+ const unexpected = positionals[fixed.length] ?? "";
340
+ return usageFailure("CLI_EXCESS_POSITIONAL", name, `unexpected positional '${unexpected}'; this command accepts ${formatPositionals(contract)}.`, contract, examples);
341
+ }
342
+ if (contract.variadic) {
343
+ for (const positional of positionals.slice(fixed.length)) {
344
+ if (invalidPositional(positional, contract.variadic)) {
345
+ return positionalFailure(name, positional, contract.variadic, contract, examples);
346
+ }
347
+ }
348
+ }
349
+ return null;
350
+ }
351
+ function publicCommandTokens() {
352
+ const tokens = [];
353
+ for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
354
+ tokens.push(name, ...contract.aliases ?? []);
355
+ }
356
+ return tokens;
357
+ }
358
+ function publicCommandNames() {
359
+ return Object.keys(PUBLIC_COMMAND_CONTRACTS);
360
+ }
361
+ function resolveCommand(token) {
362
+ const direct = PUBLIC_COMMAND_CONTRACTS[token];
363
+ if (direct) return { name: token, contract: direct };
364
+ for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
365
+ if (contract.aliases?.includes(token)) return { name, contract };
366
+ }
367
+ return null;
368
+ }
369
+ function usageFailure(code, command, message, contract, examples) {
370
+ return {
371
+ code,
372
+ command,
373
+ message: `${message} Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
374
+ userAction: `Try: ${exampleFor(command, examples)}. Inspect options: mm-harness ${command} --help`
375
+ };
376
+ }
377
+ function positionalFailure(command, received, spec, contract, examples) {
378
+ const valid = spec.validDescription ?? (spec.choices ? spec.choices.join(", ") : spec.pattern || spec.validate ? `a valid <${spec.label}>` : `<${spec.label}>`);
379
+ const suggestion = spec.choices ? closest(received, spec.choices) : void 0;
380
+ return {
381
+ code: "CLI_INVALID_POSITIONAL",
382
+ command,
383
+ message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}. Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
384
+ userAction: actionFor(received, suggestion, exampleFor(command, examples))
385
+ };
386
+ }
387
+ function invalidPositional(value2, spec) {
388
+ if (!spec) return true;
389
+ if (spec.choices && !spec.choices.includes(value2)) return true;
390
+ if (spec.validate && !spec.validate(value2)) return true;
391
+ return Boolean(spec.pattern && !spec.pattern.test(value2));
392
+ }
393
+ function formatOptions(contract) {
394
+ return Object.entries(contract.options).map(([name, spec]) => {
395
+ if (spec.kind === "boolean") return name;
396
+ const valueLabel = spec.choices?.join("|") ?? "value";
397
+ return spec.kind === "optional-value" ? `${name}[=${valueLabel}]` : `${name} <${valueLabel}>`;
398
+ }).join(", ");
399
+ }
400
+ function formatChoiceList(choices) {
401
+ if (choices.length === 1) return choices[0] ?? "";
402
+ if (choices.length === 2) return `${choices[0]} or ${choices[1]}`;
403
+ return `${choices.slice(0, -1).join(", ")}, or ${choices[choices.length - 1]}`;
404
+ }
405
+ function formatPositionals(contract) {
406
+ const fixed = (contract.positionals ?? []).map((spec) => `<${spec.label}>`);
407
+ if (contract.variadic) fixed.push(`[<${contract.variadic.label}> ...]`);
408
+ return fixed.length > 0 ? fixed.join(" ") : "no positionals";
409
+ }
410
+ function actionFor(received, suggestion, example) {
411
+ return `${suggestion ? `Did you mean '${suggestion}' instead of '${received}'? ` : ""}Try: ${example}`;
412
+ }
413
+ function exampleFor(command, examples) {
414
+ return command ? examples[command] ?? `mm-harness ${command} --help` : "mm-harness --help";
415
+ }
416
+ function closest(input, candidates) {
417
+ if (candidates.length === 0) return void 0;
418
+ const ranked = candidates.map((candidate) => ({ candidate, distance: levenshtein(input, candidate) })).sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate));
419
+ const best = ranked[0];
420
+ if (!best) return void 0;
421
+ const threshold = Math.max(2, Math.floor(Math.max(input.length, best.candidate.length) / 3));
422
+ return best.distance <= threshold ? best.candidate : void 0;
423
+ }
424
+ function levenshtein(left, right) {
425
+ const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
426
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
427
+ const current = [leftIndex];
428
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
429
+ const substitution = prior[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
430
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
431
+ }
432
+ for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
433
+ }
434
+ return prior[right.length] ?? right.length;
435
+ }
436
+ export {
437
+ PUBLIC_COMMAND_CONTRACTS,
438
+ publicCommandNames,
439
+ publicCommandTokens,
440
+ validatePublicInvocation
441
+ };