@deeeed/metamask-harness 0.17.5 → 0.19.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
@@ -1,48 +1,206 @@
1
- import { loadActionManifest } from "../manifest.js";
2
- import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveActionManifest } from "../manifest.js";
4
+ import { importRecipeProtocol } from "../paths.js";
5
+ import { color } from "../cli-color.js";
3
6
  import { EXIT } from "./shared.js";
4
- import { optionFlag, optionString, resolveAdapter } from "./parse-args.js";
5
- import { resolveMetaMaskLibrarySources } from "./run-engine.js";
7
+ import { optionFlag, optionString, optionStrings, resolveAdapter, shellQuoteArg } from "./parse-args.js";
8
+ import { describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
9
+ import {
10
+ describeRunnableRecipe,
11
+ listRunnableRecipes,
12
+ resolveMetaMaskLibrarySources
13
+ } from "./run-engine.js";
6
14
  const finalSegment = (name) => name.split(".").pop() ?? name;
7
- async function listFlowIds() {
8
- try {
9
- const sources = await resolveMetaMaskLibrarySources(void 0);
10
- if (!sources || sources.length === 0) return [];
11
- const harness = await importRecipeHarness();
12
- const resolution = await harness.loadRecipeLibraries(sources);
13
- return [...resolution.flows.keys()].sort();
14
- } catch {
15
- return [];
16
- }
15
+ function catalogDescription(description) {
16
+ if (!description) return "";
17
+ const firstSentence = description.match(/^.*?[.!?](?:\s|$)/u)?.[0]?.trim() ?? description;
18
+ return firstSentence.length <= 180 ? firstSentence : `${firstSentence.slice(0, 177).trimEnd()}\u2026`;
17
19
  }
18
20
  async function handleListExecutables(command, options) {
19
21
  const json = optionFlag(options, "json");
20
22
  const { adapter } = resolveAdapter(options);
21
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
23
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
24
+ if (command === "run") {
25
+ const recipes = await listRunnableRecipes(adapter, librarySources);
26
+ if (json) {
27
+ console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, recipes }, null, 2));
28
+ return EXIT.ok;
29
+ }
30
+ const out = (style, text) => color(style, text, { stream: process.stdout });
31
+ console.log(out("bold", `runnable recipes (${adapter})`));
32
+ console.log(out("comment", "Inspect: mm-harness run <recipe> --describe"));
33
+ for (const recipe of recipes) {
34
+ const shadowed = recipe.shadows.length > 0 ? ` shadows=${recipe.shadows.join(",")}` : "";
35
+ const variant = recipe.variant ? ` variant=${recipe.variant}` : " variant=all";
36
+ const parameters = recipe.parameters.length > 0 ? ` params=${recipe.parameters.map((parameter) => parameter.name).join(",")}` : "";
37
+ const summary = catalogDescription(recipe.description);
38
+ const description = summary ? ` \u2014 ${summary}` : "";
39
+ console.log(` ${out("cmd", recipe.name)} ${out("dim", `[${recipe.source}]${variant}${shadowed}${parameters}`)}${description}`);
40
+ }
41
+ return EXIT.ok;
42
+ }
43
+ const { manifest, actionSources } = await resolveActionManifest(
44
+ adapter,
45
+ optionString(options, "actionManifest"),
46
+ librarySources
47
+ );
22
48
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
23
- const names = getRecipeActionManifestActionNames(manifest).slice().sort();
49
+ const protocolNames = new Set(getRecipeActionManifestActionNames(manifest));
50
+ const described = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
24
51
  const shortCounts = /* @__PURE__ */ new Map();
25
- for (const name of names) shortCounts.set(finalSegment(name), (shortCounts.get(finalSegment(name)) ?? 0) + 1);
26
- const actions = names.map((name) => {
27
- const short = finalSegment(name);
28
- return { name, short: shortCounts.get(short) === 1 ? short : null };
52
+ for (const entry of described) {
53
+ const short = finalSegment(entry.name);
54
+ shortCounts.set(short, (shortCounts.get(short) ?? 0) + 1);
55
+ }
56
+ const shortByName = /* @__PURE__ */ new Map();
57
+ const actions = described.map((entry) => {
58
+ const short = finalSegment(entry.name);
59
+ const usableShort = shortCounts.get(short) === 1 ? short : null;
60
+ shortByName.set(entry.name, usableShort);
61
+ return {
62
+ name: entry.name,
63
+ short: usableShort,
64
+ description: entry.description,
65
+ fields: entry.fields,
66
+ source: entry.source,
67
+ sourceTier: entry.sourceTier,
68
+ sourceManifest: entry.sourceManifest,
69
+ shadows: entry.shadows
70
+ };
29
71
  });
30
- const flows = await listFlowIds();
31
72
  if (json) {
32
- console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions, flows }, null, 2));
73
+ console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
74
+ return EXIT.ok;
75
+ }
76
+ console.log(renderHumanActionCatalog(described, {
77
+ title: `invocable actions (${adapter})`,
78
+ guidance: "Use: mm-harness call <name> \xB7 short names shown first where unambiguous",
79
+ displayName: (entry) => {
80
+ const short = shortByName.get(entry.name);
81
+ return short ? `${short} (${entry.name})` : `${entry.name} (full name only \u2014 ambiguous short)`;
82
+ }
83
+ }));
84
+ return EXIT.ok;
85
+ }
86
+ async function handleDescribeRecipe(recipeArg, options) {
87
+ const json = optionFlag(options, "json");
88
+ const { adapter } = resolveAdapter(options);
89
+ const libraryEntries = optionStrings(options, "library");
90
+ const directRecipePath = path.resolve(recipeArg);
91
+ const librarySources = await resolveMetaMaskLibrarySources(
92
+ libraryEntries,
93
+ fs.existsSync(directRecipePath) ? directRecipePath : void 0
94
+ );
95
+ const result = await describeRunnableRecipe(recipeArg, adapter, librarySources);
96
+ const targetEntry = optionString(options, "target");
97
+ const replayTarget = targetEntry ? path.resolve(targetEntry) : void 0;
98
+ const contextFlags = [
99
+ `--adapter ${adapter}`,
100
+ ...libraryEntries?.map((entry) => `--library ${shellQuoteArg(entry)}`) ?? [],
101
+ ...replayTarget ? [`--target ${shellQuoteArg(replayTarget)}`] : []
102
+ ].join(" ");
103
+ const discoveryCommand = `mm-harness run --list ${contextFlags}`;
104
+ if ("notFound" in result || "unreadable" in result) {
105
+ const code = "notFound" in result ? "RECIPE_NOT_FOUND" : "RECIPE_UNPARSEABLE";
106
+ const message = "notFound" in result ? result.notFound : result.unreadable;
107
+ const userAction = "notFound" in result ? discoveryCommand : `fix ${shellQuoteArg(recipeArg)}, then retry: mm-harness run ${shellQuoteArg(recipeArg)} --describe ${contextFlags}`;
108
+ if (json) {
109
+ console.log(JSON.stringify({
110
+ schemaVersion: 1,
111
+ command: "run",
112
+ action: "describe",
113
+ status: "fail",
114
+ error: { code, message, userAction },
115
+ exitCode: EXIT.usage
116
+ }, null, 2));
117
+ } else {
118
+ console.error(`\u2717 run: ${message}
119
+ Next: ${userAction}`);
120
+ }
121
+ return EXIT.usage;
122
+ }
123
+ const recipe = result.recipe;
124
+ const parameterArgs = recipe.parameters.filter((parameter) => parameter.required && !Object.hasOwn(parameter, "default")).map((parameter) => `${parameter.name}=${shellQuoteArg(exampleParameterValue(parameter))}`);
125
+ const runCommand = [
126
+ "mm-harness run",
127
+ shellQuoteArg(recipeArg),
128
+ ...parameterArgs,
129
+ contextFlags
130
+ ].filter(Boolean).join(" ");
131
+ const nextCommand = `${runCommand} --plan`;
132
+ if (json) {
133
+ console.log(JSON.stringify({
134
+ schemaVersion: 1,
135
+ command: "run",
136
+ action: "describe",
137
+ status: "pass",
138
+ recipe,
139
+ runCommand,
140
+ nextCommand
141
+ }, null, 2));
33
142
  return EXIT.ok;
34
143
  }
35
- console.log(`invocable for ${adapter}:`);
36
- console.log(` actions (mm-harness call <name> \u2014 short or full):`);
37
- for (const entry of actions) {
38
- console.log(entry.short ? ` ${entry.short} (${entry.name})` : ` ${entry.name} (full name only \u2014 ambiguous short)`);
144
+ const out = (style, text) => color(style, text, { stream: process.stdout });
145
+ console.log(out("bold", `recipe ${recipe.name}`));
146
+ if (recipe.title) console.log(` ${out("label", "title:")} ${recipe.title}`);
147
+ if (recipe.description) console.log(` ${out("label", "description:")} ${recipe.description}`);
148
+ console.log(` ${out("label", "adapter:")} ${recipe.adapter}`);
149
+ console.log(` ${out("label", "source:")} ${recipe.source} \xB7 ${recipe.file}`);
150
+ console.log(` ${out("label", "path:")} ${out("path", recipe.path)}`);
151
+ if (recipe.$schema) console.log(` ${out("label", "schema:")} ${recipe.$schema}`);
152
+ console.log(` ${out("label", "variant:")} ${recipe.variant ?? "all adapters"}`);
153
+ if (recipe.shadows.length > 0) {
154
+ console.log(` ${out("label", "shadows:")} ${recipe.shadows.join(", ")}`);
39
155
  }
40
- if (flows.length > 0) {
41
- console.log(` flows (mm-harness run <flow>):`);
42
- for (const flow of flows) console.log(` ${flow}`);
156
+ if (recipe.parameters.length === 0) {
157
+ console.log(` ${out("label", "parameters:")} none`);
158
+ } else {
159
+ console.log(` ${out("label", `parameters (${recipe.parameters.length})`)}`);
160
+ for (const parameter of recipe.parameters) {
161
+ const required = parameter.required ? " required" : "";
162
+ const defaultValue = Object.hasOwn(parameter, "default") ? ` default=${compactValue(parameter.default)}` : "";
163
+ const choices = parameter.enum ? ` choices=${compactValue(parameter.enum)}` : "";
164
+ const description = parameter.description ? ` \u2014 ${parameter.description}` : "";
165
+ console.log(
166
+ ` ${out("cmd", parameter.name)}${parameter.type ? ` (${parameter.type})` : ""}${required}${defaultValue}${choices}${description}`
167
+ );
168
+ }
43
169
  }
170
+ renderComposition("actions", recipe.actions, out);
171
+ renderComposition("called recipes", recipe.nestedRecipes, out);
172
+ if (recipe.unresolvedRecipes.length > 0) {
173
+ renderComposition("unresolved recipes", recipe.unresolvedRecipes, out);
174
+ }
175
+ console.log(` ${out("comment", `Run: ${runCommand}`)}`);
176
+ console.log(` ${out("comment", `Next: ${nextCommand}`)}`);
44
177
  return EXIT.ok;
45
178
  }
179
+ function exampleParameterValue(parameter) {
180
+ if (parameter.enum && parameter.enum.length > 0) return stringifyAssignmentValue(parameter.enum[0]);
181
+ if (parameter.type === "boolean") return "false";
182
+ if (parameter.type === "number" || parameter.type === "integer") return "0";
183
+ if (parameter.type === "array") return "[]";
184
+ if (parameter.type === "object") return "{}";
185
+ return "value";
186
+ }
187
+ function stringifyAssignmentValue(value) {
188
+ return typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
189
+ }
190
+ function compactValue(value) {
191
+ const rendered = JSON.stringify(value);
192
+ if (rendered === void 0) return String(value);
193
+ return rendered.length <= 180 ? rendered : `${rendered.slice(0, 177)}\u2026`;
194
+ }
195
+ function renderComposition(label, values, out) {
196
+ if (values.length === 0) {
197
+ console.log(` ${out("label", `${label}:`)} none`);
198
+ return;
199
+ }
200
+ console.log(` ${out("label", `${label} (${values.length})`)}`);
201
+ for (const value of values) console.log(` ${out("cmd", value)}`);
202
+ }
46
203
  export {
204
+ handleDescribeRecipe,
47
205
  handleListExecutables
48
206
  };
@@ -19,9 +19,10 @@ async function handleLogs(argv) {
19
19
  const logSources = surface.logSources(target);
20
20
  const appLogSource = surface.appLogSource(target);
21
21
  const sourceLabels = logSources.map((entry) => entry.label);
22
- const defaultSource = sourceLabels[0];
22
+ const appSourceLabel = appLogSource?.label ?? "app";
23
+ const defaultSource = appLogSource && appSourceLabel !== "app" ? appSourceLabel : sourceLabels[0];
23
24
  const source = str(options, "source") ?? defaultSource;
24
- const validSources = [...sourceLabels, ...appLogSource ? ["app"] : []];
25
+ const validSources = [...sourceLabels, ...appLogSource ? [appSourceLabel] : [], ...appLogSource && appSourceLabel !== "app" ? ["app"] : []];
25
26
  if (!validSources.includes(source)) {
26
27
  return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
27
28
  }
@@ -32,11 +33,12 @@ async function handleLogs(argv) {
32
33
  }
33
34
  process.env.RECIPE_LOG_EVENTS = events;
34
35
  }
35
- const requested = source === "app" ? appLogSource : logSources.find((entry) => entry.label === source);
36
- const ordered = source === "app" ? requested ? [requested] : [] : requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
36
+ const isAppSource = source === "app" || source === appSourceLabel;
37
+ const requested = isAppSource ? appLogSource : logSources.find((entry) => entry.label === source);
38
+ const ordered = isAppSource ? requested ? [requested] : [] : requested ? [requested] : logSources;
37
39
  const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
38
40
  if (!logFile) {
39
- if (source === "app" && appLogSource) {
41
+ if (isAppSource && appLogSource) {
40
42
  const message = `application console unavailable for this checkout (${path.basename(appLogSource.path)} is absent).`;
41
43
  const userAction = surface.hints.launch;
42
44
  if (json) {
@@ -55,7 +57,7 @@ async function handleLogs(argv) {
55
57
  }
56
58
  return EXIT.runtime;
57
59
  }
58
- const names = source === "app" && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
60
+ const names = isAppSource && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
59
61
  return usageOut(
60
62
  json,
61
63
  "logs",
@@ -1,59 +1,242 @@
1
- import { loadActionManifest, validateManifest } from "../manifest.js";
1
+ import { resolveActionManifest } from "../manifest.js";
2
+ import { color } from "../cli-color.js";
2
3
  import { EXIT } from "./shared.js";
3
4
  import {
4
- actionManifestPathOption,
5
5
  isRecord,
6
6
  optionFlag,
7
7
  optionString,
8
- resolveAdapter
8
+ optionStrings,
9
+ resolveAdapter,
10
+ shellQuoteArg
9
11
  } from "./parse-args.js";
10
- async function handleManifest({ options }) {
11
- const { adapter } = resolveAdapter(options);
12
- const actionManifestPath = actionManifestPathOption(options, adapter);
13
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
14
- await validateManifest(manifest);
15
- if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
16
- else console.log(actionManifestPath);
17
- return 0;
18
- }
19
- async function handleActions({ options }) {
20
- const { adapter } = resolveAdapter(options);
21
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
22
- await validateManifest(manifest);
12
+ import { resolveMetaMaskLibrarySources } from "./run-engine.js";
13
+ import {
14
+ officialRecipeActionCapabilities
15
+ } from "@farmslot/protocol";
16
+ import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
17
+ async function handleActions({ options, positional }) {
18
+ const { adapter, target } = resolveAdapter(options);
19
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
20
+ const { manifest, actionSources } = await resolveActionManifest(
21
+ adapter,
22
+ optionString(options, "actionManifest"),
23
+ librarySources
24
+ );
25
+ if (optionFlag(options, "raw")) {
26
+ console.log(JSON.stringify(manifest, null, 2));
27
+ return EXIT.ok;
28
+ }
23
29
  const json = optionFlag(options, "json");
24
30
  const action = optionString(options, "action");
25
- const all = describeManifestActions(manifest);
26
- const actions = action ? fuzzyResolveActions(all, action) : all;
31
+ const query = positional[0]?.trim();
32
+ const category = optionString(options, "category")?.toLowerCase();
33
+ const categoriesOnly = optionFlag(options, "categories");
34
+ const all = describeManifestActions(manifest, actionSources);
35
+ const categories = summarizeActionCategories(all);
36
+ if (categoriesOnly && (action || category)) {
37
+ const message = "--categories cannot be combined with --action or --category.";
38
+ const userAction = `mm-harness actions --adapter ${adapter} --categories`;
39
+ if (json) {
40
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message, userAction } }, null, 2));
41
+ } else {
42
+ console.error(`\u2717 mm-harness actions: ${message}
43
+ Next: ${userAction}`);
44
+ }
45
+ return EXIT.usage;
46
+ }
47
+ if (categoriesOnly) {
48
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, categories }, null, 2));
49
+ else for (const entry of categories) console.log(`${entry.name} (${entry.count})`);
50
+ return EXIT.ok;
51
+ }
52
+ const categoryActions = category ? all.filter((entry) => entry.category === category) : all;
53
+ if (category && categoryActions.length === 0) {
54
+ const message = `no action category matches "${category}" for the ${adapter} adapter.`;
55
+ const userAction = `mm-harness actions --adapter ${adapter} --categories`;
56
+ if (json) {
57
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, category, availableCategories: categories, error: { code: "ACTION_CATEGORY_UNKNOWN", message, userAction } }, null, 2));
58
+ } else {
59
+ console.error(`\u2717 mm-harness actions: ${message}
60
+ Next: ${userAction}`);
61
+ }
62
+ return EXIT.usage;
63
+ }
64
+ const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
27
65
  if (action && actions.length === 0) {
28
66
  const message = `no action matches "${action}" for the ${adapter} adapter.`;
29
- const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
67
+ const userAction = `mm-harness actions --adapter ${adapter}`;
30
68
  if (json) {
31
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
69
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, category, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
32
70
  } else {
33
71
  console.error(`\u2717 mm-harness actions: ${message}
34
72
  Next: ${userAction}`);
35
73
  }
36
74
  return EXIT.usage;
37
75
  }
76
+ if (query && actions.length === 0) {
77
+ const message = `no action matches search "${query}" for the ${adapter} adapter.`;
78
+ const userAction = `mm-harness actions --adapter ${adapter} --categories`;
79
+ if (json) {
80
+ console.log(JSON.stringify({
81
+ schemaVersion: 1,
82
+ command: "actions",
83
+ adapter,
84
+ query,
85
+ category,
86
+ error: { code: "ACTION_SEARCH_EMPTY", message, userAction }
87
+ }, null, 2));
88
+ } else {
89
+ console.error(`\u2717 mm-harness actions: ${message}
90
+ Next: ${userAction}`);
91
+ }
92
+ return EXIT.usage;
93
+ }
94
+ const relatedActions = action && actions.length === 1 ? findRelatedActions(all, actions[0]) : void 0;
38
95
  if (json) {
39
- console.log(JSON.stringify({ adapter, actions }, null, 2));
96
+ const detailed = Boolean(action && actions.length === 1);
97
+ console.log(JSON.stringify({
98
+ schemaVersion: 1,
99
+ command: "actions",
100
+ adapter,
101
+ ...query ? { query } : {},
102
+ category,
103
+ detail: detailed ? "full" : "summary",
104
+ actions: detailed ? actions : actions.map(summarizeDescribedAction),
105
+ ...relatedActions?.length ? { relatedActions } : {}
106
+ }, null, 2));
40
107
  } else {
41
- for (const entry of actions) {
42
- const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
43
- console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
44
- }
108
+ console.log(renderHumanActionCatalog(actions, {
109
+ title: `actions (${adapter})`,
110
+ guidance: "Inspect: mm-harness actions --action <name> \xB7 Run: mm-harness call <name>"
111
+ }));
112
+ const example = action && actions.length === 1 ? renderHumanActionExample(
113
+ actions[0],
114
+ adapter,
115
+ target,
116
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
117
+ ) : void 0;
118
+ if (example) console.log(`
119
+ ${example}`);
120
+ if (relatedActions?.length) console.log(`
121
+ ${color("label", "Related:", { stream: process.stdout })} ${relatedActions.join(", ")}`);
45
122
  }
46
123
  return 0;
47
124
  }
48
125
  function fuzzyResolveActions(entries, query) {
49
126
  const exactFull = entries.filter((e) => e.name === query);
50
127
  if (exactFull.length > 0) return exactFull;
51
- const finalSegment = (name) => name.split(".").pop() ?? name;
52
- const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
128
+ const finalSegment2 = (name) => name.split(".").pop() ?? name;
129
+ const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
53
130
  if (exactSegment.length > 0) return exactSegment;
54
- return entries.filter((e) => finalSegment(e.name).includes(query));
131
+ return entries.filter((e) => finalSegment2(e.name).includes(query));
55
132
  }
56
- function describeManifestActions(manifest) {
133
+ function searchActions(entries, query) {
134
+ const terms = searchTerms(query);
135
+ if (terms.length === 0) return [];
136
+ return entries.map((entry) => {
137
+ const scores = terms.map((term) => actionSearchScore(entry, term));
138
+ return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
139
+ }).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
140
+ }
141
+ function findRelatedActions(entries, selected, limit = 5) {
142
+ const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
143
+ return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
144
+ const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
145
+ const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
146
+ const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
147
+ return { name: entry.name, score };
148
+ }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
149
+ }
150
+ const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
151
+ function summarizeDescribedAction(entry) {
152
+ const { schema: _schema, examples: _examples, ...summary } = entry;
153
+ return summary;
154
+ }
155
+ function renderHumanActionCatalog(actions, options) {
156
+ const out = (style, text) => color(style, text, { stream: process.stdout });
157
+ const groups = /* @__PURE__ */ new Map();
158
+ for (const entry of actions) {
159
+ const group = actionDomainGroup(entry);
160
+ groups.set(group, [...groups.get(group) ?? [], entry]);
161
+ }
162
+ const lines = [out("bold", options.title), out("comment", options.guidance)];
163
+ for (const [group, entries] of groups) {
164
+ lines.push("", `${out(group === "official" ? "label" : "accent", group)} ${out("dim", `(${entries.length})`)}`);
165
+ for (const entry of entries) {
166
+ const fieldNames = humanFieldNames(entry);
167
+ const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
168
+ const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
169
+ const source = ` ${out("dim", `[${entry.source}]`)}`;
170
+ const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
171
+ lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${risk}${description}`);
172
+ }
173
+ }
174
+ return lines.join("\n");
175
+ }
176
+ function renderHumanActionExample(entry, adapter, target, executable) {
177
+ const node = authoredExampleNode(entry.examples);
178
+ if (!node) return void 0;
179
+ const out = (style, text) => color(style, text, { stream: process.stdout });
180
+ const command = actionExampleCommand(entry, adapter, target, executable);
181
+ if (!command) return void 0;
182
+ const lines = [
183
+ out("label", "Example call:"),
184
+ ` ${out("cmd", command)}`,
185
+ out("label", "Recipe node:"),
186
+ ...JSON.stringify(node, null, 2).split("\n").map((line) => ` ${line}`)
187
+ ];
188
+ return lines.join("\n");
189
+ }
190
+ function actionExampleCommand(entry, adapter, target, executable) {
191
+ const node = authoredExampleNode(entry.examples);
192
+ if (!node) return void 0;
193
+ const commandName = executable.endsWith("/mm-harness") ? "mm-harness" : executable;
194
+ const args = entry.fields.flatMap(
195
+ (field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${actionCallValue(node[field])}`)] : []
196
+ );
197
+ return [
198
+ shellQuoteArg(commandName),
199
+ "call",
200
+ shellQuoteArg(entry.name),
201
+ ...args,
202
+ "--adapter",
203
+ adapter,
204
+ "--target",
205
+ shellQuoteArg(target)
206
+ ].join(" ");
207
+ }
208
+ function authoredExampleNode(examples) {
209
+ if (!Array.isArray(examples)) return void 0;
210
+ for (const example of examples) {
211
+ if (isRecord(example) && isRecord(example.node)) return example.node;
212
+ }
213
+ return void 0;
214
+ }
215
+ function actionCallValue(value) {
216
+ return typeof value === "string" ? value : JSON.stringify(value);
217
+ }
218
+ function humanFieldNames(entry) {
219
+ const schema = isRecord(entry.schema) ? entry.schema : void 0;
220
+ const properties = schema && isRecord(schema.properties) ? schema.properties : void 0;
221
+ if (!properties) return entry.fields;
222
+ return entry.fields.map((field) => {
223
+ const property = properties[field];
224
+ if (!isRecord(property) || !Array.isArray(property.enum)) return field;
225
+ const values = property.enum.filter((value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean");
226
+ return values.length > 0 ? `${field}=${values.join("|")}` : field;
227
+ });
228
+ }
229
+ function actionDomainGroup(entry) {
230
+ if (entry.kind === "official") return "official";
231
+ const match = /^metamask[.]([^.]+)[.]/u.exec(entry.name);
232
+ return match ? `metamask \xB7 ${match[1]}` : "custom";
233
+ }
234
+ function summarizeActionCategories(actions) {
235
+ const counts = /* @__PURE__ */ new Map();
236
+ for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
237
+ return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
238
+ }
239
+ function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
57
240
  const manifestRecord = isRecord(manifest) ? manifest : {};
58
241
  const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
59
242
  const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
@@ -68,27 +251,95 @@ function describeManifestActions(manifest) {
68
251
  return [];
69
252
  }) : [];
70
253
  return [
71
- ...official.map((name) => describeManifestAction(name, "official", metadata[name])),
72
- ...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
254
+ ...official.map((name) => describeManifestAction(name, "official", metadata[name], actionSources.get(name))),
255
+ ...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata, actionSources.get(entry.name)))
73
256
  ];
74
257
  }
75
- function describeManifestAction(name, kind, metadata) {
258
+ function describeManifestAction(name, kind, metadata, source) {
76
259
  const record = isRecord(metadata) ? metadata : {};
77
260
  const schema = record.schema;
78
261
  const schemaRecord = isRecord(schema) ? schema : {};
79
- const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).sort() : [];
262
+ const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).filter((field) => field !== "action" && field !== "next").sort() : [];
80
263
  return {
81
264
  name,
82
265
  kind,
266
+ category: actionCategory(name, record.category),
83
267
  description: typeof record.description === "string" ? record.description : "",
84
268
  fields: properties,
85
269
  schema,
86
- examples: record.examples
270
+ examples: record.examples,
271
+ source: source?.name ?? (kind === "official" ? "official" : "metamask"),
272
+ sourceTier: source?.tier ?? (kind === "official" ? "official" : "canonical"),
273
+ sourceManifest: source?.manifestPath ?? "",
274
+ shadows: source?.shadows ?? [],
275
+ capabilities: [.../* @__PURE__ */ new Set([
276
+ ...Array.isArray(record.execution_capabilities) ? record.execution_capabilities.filter(
277
+ (value) => typeof value === "string"
278
+ ) : [],
279
+ ...kind === "official" ? officialRecipeActionCapabilities(name) : metaMaskActionExecutionCapabilities(name)
280
+ ])]
87
281
  };
88
282
  }
283
+ function actionCategory(name, configured) {
284
+ if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
285
+ const segments = name.split(".");
286
+ if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
287
+ if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
288
+ if (segments[0] === "ui") return "ui";
289
+ if (name.startsWith("assert_")) return "assertion";
290
+ if (name === "watch_logs" || name === "index_artifacts") return "evidence";
291
+ if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
292
+ return segments.length > 1 ? segments[0] || "utility" : "utility";
293
+ }
294
+ function actionSearchScore(entry, term) {
295
+ const name = entry.name.toLowerCase();
296
+ const segment = finalSegment(name);
297
+ const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
298
+ const fields = entry.fields.map((field) => field.toLowerCase());
299
+ const description = entry.description.toLowerCase();
300
+ if (name === term) return 120;
301
+ if (segment === term) return 110;
302
+ if (entry.category === term) return 100;
303
+ if (fields.includes(term)) return 90;
304
+ if (nameTerms.includes(term)) return 80;
305
+ if (name.includes(term)) return 70;
306
+ if (fields.some((field) => field.includes(term))) return 60;
307
+ if (description.includes(term)) return 50;
308
+ if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
309
+ return 0;
310
+ }
311
+ function fuzzyTermMatch(term, candidate) {
312
+ if (term.length < 4 || candidate.length < 4) return false;
313
+ const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
314
+ return levenshtein(term, candidate) <= threshold;
315
+ }
316
+ function searchTerms(value) {
317
+ return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
318
+ }
319
+ function finalSegment(name) {
320
+ return name.split(".").pop() ?? name;
321
+ }
322
+ function levenshtein(left, right) {
323
+ const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
324
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
325
+ const current = [leftIndex];
326
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
327
+ const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
328
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
329
+ }
330
+ for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
331
+ }
332
+ return prior[right.length] ?? right.length;
333
+ }
89
334
  export {
335
+ actionExampleCommand,
90
336
  describeManifestActions,
337
+ findRelatedActions,
91
338
  fuzzyResolveActions,
92
339
  handleActions,
93
- handleManifest
340
+ renderHumanActionCatalog,
341
+ renderHumanActionExample,
342
+ searchActions,
343
+ summarizeActionCategories,
344
+ summarizeDescribedAction
94
345
  };