@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.
- package/CHANGELOG.md +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- package/library/flows/perps.flows.json +0 -64
|
@@ -1,48 +1,206 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
38
|
-
|
|
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 (
|
|
41
|
-
console.log(`
|
|
42
|
-
|
|
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
|
};
|
package/dist/commands/logs.js
CHANGED
|
@@ -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
|
|
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
|
|
36
|
-
const
|
|
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 (
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
8
|
+
optionStrings,
|
|
9
|
+
resolveAdapter,
|
|
10
|
+
shellQuoteArg
|
|
9
11
|
} from "./parse-args.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
26
|
-
const
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
52
|
-
const exactSegment = entries.filter((e) =>
|
|
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) =>
|
|
131
|
+
return entries.filter((e) => finalSegment2(e.name).includes(query));
|
|
55
132
|
}
|
|
56
|
-
function
|
|
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
|
-
|
|
340
|
+
renderHumanActionCatalog,
|
|
341
|
+
renderHumanActionExample,
|
|
342
|
+
searchActions,
|
|
343
|
+
summarizeActionCategories,
|
|
344
|
+
summarizeDescribedAction
|
|
94
345
|
};
|