@deeeed/metamask-harness 0.2.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 (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-undef, import-x/no-extraneous-dependencies */
3
+ /**
4
+ * CDP Bridge — Connect to the running MetaMask app via Hermes Chrome DevTools Protocol.
5
+ *
6
+ * Usage:
7
+ * node library/actions/mobile/bridge-runtime/cdp-bridge.cjs navigate PerpsMarketListView
8
+ * node library/actions/mobile/bridge-runtime/cdp-bridge.cjs get-route
9
+ * node library/actions/mobile/bridge-runtime/cdp-bridge.cjs get-state "engine.backgroundState.NetworkController"
10
+ * node library/actions/mobile/bridge-runtime/cdp-bridge.cjs eval "1+1"
11
+ *
12
+ * Environment:
13
+ * WATCHER_PORT Metro port (default: 8081, read from .js.env if present)
14
+ * CDP_TIMEOUT Connection timeout in ms (default: 5000)
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const { loadPort } = require('./lib/config.cjs');
22
+ const { discoverTarget } = require('./lib/target-discovery.cjs');
23
+ const { createWSClient } = require('./lib/ws-client.cjs');
24
+ const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
25
+ const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Commands
29
+ // ---------------------------------------------------------------------------
30
+
31
+ // Map of nested route names to their parent navigator.
32
+ // Friendly aliases → actual route names (for developer convenience)
33
+ const ROUTE_ALIASES = {
34
+ PerpsHomeView: 'PerpsMarketListView',
35
+ };
36
+
37
+ // When navigating to a nested route, we need: navigate('Parent', { screen: 'Child', params })
38
+ // Routes not in this map are assumed to be root-level and navigated to directly.
39
+ const NESTED_ROUTE_PARENTS = {
40
+ // Perps
41
+ PerpsMarketListView: 'Perps', PerpsMarketDetails: 'Perps', PerpsPositions: 'Perps',
42
+ PerpsTrendingView: 'Perps', PerpsWithdraw: 'Perps', PerpsTutorial: 'Perps', PerpsOrderRedirect: 'Perps',
43
+ PerpsClosePosition: 'Perps', PerpsTPSL: 'Perps', PerpsAdjustMargin: 'Perps',
44
+ PerpsOrderDetailsView: 'Perps', PerpsActivity: 'Perps', PerpsOrderBook: 'Perps',
45
+ PerpsPnlHeroCard: 'Perps', PerpsHIP3Debug: 'Perps',
46
+ PerpsSelectModifyAction: 'Perps', PerpsSelectAdjustMarginAction: 'Perps',
47
+ PerpsSelectOrderType: 'Perps', PerpsTradingView: 'Perps',
48
+ // Predict
49
+ PredictMarketList: 'Predict', PredictMarketDetails: 'Predict',
50
+ PredictActivityDetail: 'Predict',
51
+ // Bridge
52
+ BridgeView: 'Bridge', BridgeTokenSelector: 'Bridge',
53
+ BridgeTransactionDetails: 'Bridge',
54
+ // Staking
55
+ Stake: 'Staking', StakeConfirmation: 'Staking', Unstake: 'Staking',
56
+ UnstakeConfirmation: 'Staking', EarningsHistory: 'Staking', Claim: 'Staking',
57
+ // Earn
58
+ EarnLendingDepositConfirmation: 'EarnScreens',
59
+ EarnLendingWithdrawalConfirmation: 'EarnScreens',
60
+ // Card
61
+ CardHome: 'CardScreens', CardWelcome: 'CardScreens', CardAuthentication: 'CardScreens',
62
+ // Settings (nested under Settings root)
63
+ ContactsSettings: 'Settings', SecuritySettings: 'Settings',
64
+ AdvancedSettings: 'Settings', ExperimentalSettings: 'Settings',
65
+ DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
66
+ };
67
+
68
+ const COMMANDS = {
69
+ async navigate(client, args, { deviceName, platform } = {}) {
70
+ const routeName = ROUTE_ALIASES[args[0]] || args[0];
71
+ if (!routeName) {
72
+ throw new Error('Usage: navigate <RouteName> [params-json]');
73
+ }
74
+ let params = {};
75
+ if (args[1]) {
76
+ try {
77
+ params = JSON.parse(args[1]);
78
+ } catch (e) {
79
+ throw new Error(`Invalid params JSON: ${e.message}`);
80
+ }
81
+ }
82
+
83
+ // Capture route before navigation
84
+ const previousRoute = await cdpEval(client, 'globalThis.__AGENTIC__?.getRoute()');
85
+
86
+ let expr;
87
+ const parent = NESTED_ROUTE_PARENTS[routeName];
88
+ if (parent) {
89
+ // Nested route: navigate('Parent', { screen: 'Child', params: {...} })
90
+ const navParams = Object.keys(params).length > 0
91
+ ? JSON.stringify({ screen: routeName, params })
92
+ : JSON.stringify({ screen: routeName });
93
+ expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(parent)}, ${navParams})`;
94
+ } else {
95
+ // Root-level route: navigate('Route', params)
96
+ const paramsStr = JSON.stringify(params);
97
+ expr = `globalThis.__AGENTIC__?.navigate(${JSON.stringify(routeName)}, ${paramsStr})`;
98
+ }
99
+
100
+ await cdpEval(client, expr);
101
+ // Small delay for navigation to settle, then return current route
102
+ await new Promise((r) => setTimeout(r, 500));
103
+ const currentRoute = await cdpEval(
104
+ client,
105
+ 'globalThis.__AGENTIC__?.getRoute()',
106
+ );
107
+ return { navigated: routeName, params, previousRoute, currentRoute, deviceName, platform };
108
+ },
109
+
110
+ async 'get-route'(client) {
111
+ const route = await cdpEval(
112
+ client,
113
+ 'globalThis.__AGENTIC__?.getRoute()',
114
+ );
115
+ return route;
116
+ },
117
+
118
+ async 'get-state'(client, args) {
119
+ const path = args[0] || '';
120
+ if (!path) {
121
+ // Return navigation state
122
+ const state = await cdpEval(
123
+ client,
124
+ 'globalThis?.__AGENTIC__?.getState()',
125
+ );
126
+ return state;
127
+ }
128
+ // Access Redux store at the given dot-path
129
+ const expr = `(() => {
130
+ const store = globalThis?.store;
131
+ if (!store) return { error: 'Redux store not found on globalThis.store' };
132
+ const state = store.getState();
133
+ const parts = ${JSON.stringify(path)}.split('.');
134
+ let current = state;
135
+ for (const p of parts) {
136
+ if (current == null) return undefined;
137
+ current = current[p];
138
+ }
139
+ return current;
140
+ })()`;
141
+ return await cdpEval(client, expr);
142
+ },
143
+
144
+ async eval(client, args) {
145
+ const expression = args.join(' ');
146
+ if (!expression) {
147
+ throw new Error('Usage: eval <expression>');
148
+ }
149
+ return await cdpEval(client, expression);
150
+ },
151
+
152
+ async 'eval-async'(client, args) {
153
+ const expression = args.join(' ');
154
+ if (!expression) {
155
+ throw new Error('Usage: eval-async <expression>');
156
+ }
157
+ return await cdpEvalAsync(client, expression);
158
+ },
159
+
160
+ async 'can-go-back'(client) {
161
+ return await cdpEval(client, 'globalThis.__AGENTIC__?.canGoBack()');
162
+ },
163
+
164
+ async 'go-back'(client, _args, { deviceName, platform } = {}) {
165
+ await cdpEval(client, 'globalThis.__AGENTIC__?.goBack()');
166
+ await new Promise((r) => setTimeout(r, 300));
167
+ const route = await cdpEval(
168
+ client,
169
+ 'globalThis.__AGENTIC__?.getRoute()',
170
+ );
171
+ return { currentRoute: route, deviceName, platform };
172
+ },
173
+
174
+ async status(client, _args, { deviceName, platform } = {}) {
175
+ const expr = `(function() {
176
+ var route = globalThis.__AGENTIC__?.getRoute() || null;
177
+ var account = null;
178
+ try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
179
+ return { route: route, account: account };
180
+ })()`;
181
+ const snapshot = await cdpEval(client, expr);
182
+ return { ...snapshot, deviceName: deviceName || '', platform: platform || '' };
183
+ },
184
+
185
+ async 'list-accounts'(client) {
186
+ return await cdpEval(client, 'globalThis.__AGENTIC__?.listAccounts()');
187
+ },
188
+
189
+ async 'get-selected-account'(client) {
190
+ return await cdpEval(client, 'globalThis.__AGENTIC__?.getSelectedAccount()');
191
+ },
192
+
193
+ async 'switch-account'(client, args) {
194
+ const address = args[0];
195
+ if (!address) {
196
+ throw new Error('Usage: switch-account <address>');
197
+ }
198
+ return await cdpEval(client, `globalThis.__AGENTIC__?.switchAccount(${JSON.stringify(address)})`);
199
+ },
200
+
201
+ async 'press-test-id'(client, args, { deviceName } = {}) {
202
+ const testId = args[0];
203
+ if (!testId) {
204
+ throw new Error('Usage: press-test-id <testId>');
205
+ }
206
+ // Try __AGENTIC__ bridge first, fall back to inline fiber walking
207
+ const expr = `(function() {
208
+ if (globalThis.__AGENTIC__?.pressTestId) return globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)});
209
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
210
+ if (!hook) return { ok: false, error: 'No React DevTools hook' };
211
+ var renderers = hook.renderers;
212
+ if (!renderers) return { ok: false, error: 'No renderers' };
213
+ var getFiberRoots = hook.getFiberRoots;
214
+ function walk(fiber) {
215
+ if (!fiber) return false;
216
+ var props = fiber.memoizedProps;
217
+ if (props && props.testID === ${JSON.stringify(testId)}) {
218
+ if (typeof props.onPress === 'function') { props.onPress(); return true; }
219
+ }
220
+ return walk(fiber.child) || walk(fiber.sibling);
221
+ }
222
+ for (var [id] of renderers) {
223
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
224
+ if (!roots) continue;
225
+ var found = false;
226
+ roots.forEach(function(r) { if (!found) found = walk(r.current); });
227
+ if (found) return { ok: true, testId: ${JSON.stringify(testId)} };
228
+ }
229
+ return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onPress' };
230
+ })()`;
231
+ const result = await cdpEval(client, expr);
232
+ return { ...result, testId, deviceName };
233
+ },
234
+
235
+ async 'scroll-view'(client, args, { deviceName } = {}) {
236
+ let testId;
237
+ let offset = 300;
238
+ let animated = false;
239
+ for (let i = 0; i < args.length; i++) {
240
+ if (args[i] === '--test-id' && i + 1 < args.length) {
241
+ testId = args[++i];
242
+ } else if (args[i] === '--offset' && i + 1 < args.length) {
243
+ offset = Number(args[++i]);
244
+ } else if (args[i] === '--animated') {
245
+ animated = true;
246
+ } else if (args[i] === '--no-animated') {
247
+ animated = false;
248
+ }
249
+ }
250
+ const optsJson = JSON.stringify({ testId, offset, animated });
251
+ // Try __AGENTIC__ bridge first, fall back to inline fiber walking
252
+ const expr = `(function() {
253
+ if (globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
254
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
255
+ if (!hook) return { ok: false, error: 'No React DevTools hook' };
256
+ var renderers = hook.renderers;
257
+ if (!renderers) return { ok: false, error: 'No renderers' };
258
+ var getFiberRoots = hook.getFiberRoots;
259
+ var opts = ${optsJson};
260
+ function tryScroll(fiber, walkSiblings) {
261
+ if (walkSiblings === undefined) walkSiblings = true;
262
+ var current = fiber;
263
+ while (current) {
264
+ var sn = current.stateNode;
265
+ if (sn) {
266
+ if (typeof sn.scrollTo === 'function') { sn.scrollTo({ y: opts.offset, animated: opts.animated }); return true; }
267
+ if (typeof sn.scrollToOffset === 'function') { sn.scrollToOffset({ offset: opts.offset, animated: opts.animated }); return true; }
268
+ }
269
+ if (tryScroll(current.child)) return true;
270
+ current = walkSiblings ? current.sibling : null;
271
+ }
272
+ return false;
273
+ }
274
+ function findTestId(fiber) {
275
+ if (!fiber) return null;
276
+ var props = fiber.memoizedProps;
277
+ if (props && props.testID === opts.testId) return fiber;
278
+ return findTestId(fiber.child) || findTestId(fiber.sibling);
279
+ }
280
+ for (var [id] of renderers) {
281
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
282
+ if (!roots) continue;
283
+ var scrolled = false;
284
+ roots.forEach(function(r) {
285
+ if (scrolled) return;
286
+ if (opts.testId) {
287
+ var anchor = findTestId(r.current);
288
+ if (anchor) scrolled = tryScroll(anchor, false);
289
+ } else {
290
+ scrolled = tryScroll(r.current);
291
+ }
292
+ });
293
+ if (scrolled) return { ok: true, testId: opts.testId, offset: opts.offset, animated: opts.animated };
294
+ }
295
+ return { ok: false, error: opts.testId ? 'No scrollable near testID=' + opts.testId : 'No scrollable found' };
296
+ })()`;
297
+ const result = await cdpEval(client, expr);
298
+ return { ...result, deviceName };
299
+ },
300
+
301
+ async 'set-input'(client, args, { deviceName } = {}) {
302
+ const testId = args[0];
303
+ const value = args.slice(1).join(' ');
304
+ if (!testId) {
305
+ throw new Error('Usage: set-input <testId> <value>');
306
+ }
307
+ // Try __AGENTIC__ bridge first, fall back to inline fiber walking
308
+ const expr = `(function() {
309
+ if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
310
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
311
+ if (!hook) return { ok: false, error: 'No React DevTools hook' };
312
+ var renderers = hook.renderers;
313
+ if (!renderers) return { ok: false, error: 'No renderers' };
314
+ var getFiberRoots = hook.getFiberRoots;
315
+ function findByTestId(fiber) {
316
+ if (!fiber) return null;
317
+ var props = fiber.memoizedProps;
318
+ if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
319
+ return findByTestId(fiber.child) || findByTestId(fiber.sibling);
320
+ }
321
+ for (var [id] of renderers) {
322
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
323
+ if (!roots) continue;
324
+ var result = null;
325
+ roots.forEach(function(r) {
326
+ if (result) return;
327
+ var fiber = findByTestId(r.current);
328
+ if (!fiber) return;
329
+ var cur = fiber;
330
+ while (cur) {
331
+ if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
332
+ cur.memoizedProps.onChangeText(${JSON.stringify(value)});
333
+ result = { ok: true, testId: ${JSON.stringify(testId)}, value: ${JSON.stringify(value)} };
334
+ return;
335
+ }
336
+ cur = cur.return || null;
337
+ }
338
+ });
339
+ if (result) return result;
340
+ }
341
+ return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
342
+ })()`;
343
+ const result = await cdpEval(client, expr);
344
+ return { ...result, testId, value, deviceName };
345
+ },
346
+
347
+ async 'sentry-debug'(client, args, { deviceName } = {}) {
348
+ const action = args[0] || 'enable';
349
+ if (action === 'enable') {
350
+ const expr = `(function() {
351
+ try {
352
+ var sentryHub = globalThis.__SENTRY__;
353
+ if (!sentryHub) return { ok: false, error: 'globalThis.__SENTRY__ not found' };
354
+ var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
355
+ if (!ver) return { ok: false, error: 'No Sentry version key found in __SENTRY__' };
356
+ var hub = sentryHub[ver];
357
+ var scope = hub.defaultCurrentScope;
358
+ if (!scope) return { ok: false, error: 'No defaultCurrentScope in Sentry hub' };
359
+ var sentryClient = scope.getClient();
360
+ if (!sentryClient) return { ok: false, error: 'No Sentry client found via scope.getClient()' };
361
+
362
+ if (sentryClient.__agenticPatched) return { ok: true, alreadyPatched: true, version: ver };
363
+
364
+ var origException = sentryClient.captureException.bind(sentryClient);
365
+ var origMessage = sentryClient.captureMessage.bind(sentryClient);
366
+
367
+ sentryClient.captureException = function(err, hint, currentScope) {
368
+ var msg = err && err.message ? err.message : String(err);
369
+ console.warn('[SENTRY-DEBUG] captureException:', msg);
370
+ if (err && err.stack) console.warn('[SENTRY-DEBUG] stack:', err.stack);
371
+ return origException(err, hint, currentScope);
372
+ };
373
+ sentryClient.captureMessage = function(msg, level, hint, currentScope) {
374
+ console.warn('[SENTRY-DEBUG] captureMessage:', msg);
375
+ return origMessage(msg, level, hint, currentScope);
376
+ };
377
+ sentryClient.__agenticPatched = true;
378
+ sentryClient.__agenticOrigException = origException;
379
+ sentryClient.__agenticOrigMessage = origMessage;
380
+ return { ok: true, patched: true, version: ver };
381
+ } catch(e) {
382
+ return { ok: false, error: String(e) };
383
+ }
384
+ })()`;
385
+ return await cdpEval(client, expr);
386
+ } else if (action === 'disable') {
387
+ const expr = `(function() {
388
+ try {
389
+ var sentryHub = globalThis.__SENTRY__;
390
+ if (!sentryHub) return { ok: true, wasNotPatched: true };
391
+ var ver = Object.keys(sentryHub).filter(function(k) { return /^[0-9]/.test(k); })[0];
392
+ if (!ver) return { ok: true, wasNotPatched: true };
393
+ var hub = sentryHub[ver];
394
+ var scope = hub.defaultCurrentScope;
395
+ if (!scope) return { ok: true, wasNotPatched: true };
396
+ var sentryClient = scope.getClient();
397
+ if (!sentryClient || !sentryClient.__agenticPatched) return { ok: true, wasNotPatched: true };
398
+ sentryClient.captureException = sentryClient.__agenticOrigException;
399
+ sentryClient.captureMessage = sentryClient.__agenticOrigMessage;
400
+ delete sentryClient.__agenticPatched;
401
+ delete sentryClient.__agenticOrigException;
402
+ delete sentryClient.__agenticOrigMessage;
403
+ return { ok: true, unpatched: true, version: ver };
404
+ } catch(e) {
405
+ return { ok: false, error: String(e) };
406
+ }
407
+ })()`;
408
+ return await cdpEval(client, expr);
409
+ }
410
+ throw new Error('Usage: sentry-debug [enable|disable]');
411
+ },
412
+
413
+ async unlock(client, args, { deviceName } = {}) {
414
+ const password = args[0];
415
+ if (!password) {
416
+ throw new Error('Usage: unlock <password>');
417
+ }
418
+ const escapedPw = JSON.stringify(password);
419
+
420
+ // Fiber-tree walker snippet reused in both phases
421
+ const fiberWalker = `
422
+ var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
423
+ if (!hook) return { ok: false, error: 'No React DevTools hook' };
424
+ var renderers = hook.renderers;
425
+ if (!renderers) return { ok: false, error: 'No renderers' };
426
+ var getFiberRoots = hook.getFiberRoots;
427
+ function walkFiber(fiber, testId) {
428
+ if (!fiber) return null;
429
+ var props = fiber.memoizedProps;
430
+ if (props && props.testID === testId) return fiber;
431
+ var found = walkFiber(fiber.child, testId);
432
+ if (found) return found;
433
+ return walkFiber(fiber.sibling, testId);
434
+ }`;
435
+
436
+ // Phase 1: inject password via onChangeText
437
+ const injectExpr = `(function() {
438
+ ${fiberWalker}
439
+ var password = ${escapedPw};
440
+ for (var [id] of renderers) {
441
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
442
+ if (!roots) continue;
443
+ var injected = false;
444
+ roots.forEach(function(root) {
445
+ if (injected) return;
446
+ var input = walkFiber(root.current, 'login-password-input');
447
+ if (input && input.memoizedProps && typeof input.memoizedProps.onChangeText === 'function') {
448
+ input.memoizedProps.onChangeText(password);
449
+ injected = true;
450
+ }
451
+ });
452
+ if (injected) return { ok: true, phase: 'inject' };
453
+ }
454
+ return { ok: false, error: 'login-password-input not found' };
455
+ })()`;
456
+
457
+ const injectResult = await cdpEval(client, injectExpr);
458
+ if (!injectResult?.ok) {
459
+ return { ok: false, error: injectResult?.error || 'Password injection failed', deviceName };
460
+ }
461
+
462
+ // Wait for React to re-render so useCallback picks up the new password
463
+ await new Promise((r) => setTimeout(r, 600));
464
+
465
+ // Phase 2: walk the fiber tree AGAIN to get the updated onPress callback
466
+ const pressExpr = `(function() {
467
+ ${fiberWalker}
468
+ for (var [id] of renderers) {
469
+ var roots = getFiberRoots ? getFiberRoots(id) : undefined;
470
+ if (!roots) continue;
471
+ var pressed = false;
472
+ roots.forEach(function(root) {
473
+ if (pressed) return;
474
+ var btn = walkFiber(root.current, 'log-in-button');
475
+ if (btn && btn.memoizedProps && typeof btn.memoizedProps.onPress === 'function') {
476
+ btn.memoizedProps.onPress();
477
+ pressed = true;
478
+ }
479
+ });
480
+ if (pressed) return { ok: true, phase: 'press' };
481
+ }
482
+ return { ok: false, error: 'log-in-button not found' };
483
+ })()`;
484
+
485
+ const pressResult = await cdpEval(client, pressExpr);
486
+ return {
487
+ ok: pressResult?.ok ?? false,
488
+ injected: true,
489
+ pressed: pressResult?.ok ?? false,
490
+ error: pressResult?.error,
491
+ deviceName,
492
+ };
493
+ },
494
+
495
+ async 'show-step-json'(client, args) {
496
+ const raw = args.join(' ');
497
+ let step;
498
+ try {
499
+ step = JSON.parse(raw);
500
+ } catch (error) {
501
+ throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
502
+ }
503
+ if (!step || typeof step !== 'object' || Array.isArray(step)) {
504
+ throw new Error('show-step-json requires a JSON object step payload');
505
+ }
506
+ if (typeof step.intent !== 'string' || !step.intent.trim()) {
507
+ throw new Error('show-step-json requires step.intent');
508
+ }
509
+ // HUD derives status/progress from step.id when status is omitted; a
510
+ // non-string id makes statusForStep throw during render. Reject at the boundary.
511
+ if (step.id !== undefined && typeof step.id !== 'string') {
512
+ throw new Error('show-step-json step.id must be a string when provided');
513
+ }
514
+ const payload = JSON.stringify(step);
515
+ await cdpEval(client, `globalThis.__AGENTIC__?.showStep && globalThis.__AGENTIC__.showStep(${payload})`);
516
+ return { ok: true };
517
+ },
518
+
519
+ async 'hide-step'(client) {
520
+ await cdpEval(client, `globalThis.__AGENTIC__?.hideStep && globalThis.__AGENTIC__.hideStep()`);
521
+ return { ok: true };
522
+ },
523
+
524
+ async 'profiler-start'(client) {
525
+ // Hermes CDP exposes the sampling profiler via the Profiler domain.
526
+ // Output of Profiler.stop is a Chrome-compatible .cpuprofile object.
527
+ // Note: Hermes does NOT implement Profiler.enable (returns -32601 Unsupported);
528
+ // Profiler.start/stop work without it.
529
+ await client.send('Profiler.start');
530
+ return { ok: true, started: true };
531
+ },
532
+
533
+ async 'profiler-stop'(client, args) {
534
+ let outPath = '';
535
+ let label = 'trace';
536
+ for (let i = 0; i < args.length; i++) {
537
+ if (args[i] === '--out' && i + 1 < args.length) {
538
+ outPath = args[++i];
539
+ } else if (args[i] === '--label' && i + 1 < args.length) {
540
+ label = args[++i];
541
+ }
542
+ }
543
+ const res = await client.send('Profiler.stop', {}, 60000);
544
+ const profile = res?.profile;
545
+ if (!profile) {
546
+ return { ok: false, error: 'Profiler.stop returned no profile' };
547
+ }
548
+ const serialized = JSON.stringify(profile);
549
+ if (!outPath) {
550
+ const tracesDir = path.resolve(process.env.APP_ROOT || process.cwd(), 'temp/agentic/traces');
551
+ fs.mkdirSync(tracesDir, { recursive: true });
552
+ outPath = path.join(tracesDir, `trace-${label}.cpuprofile`);
553
+ } else {
554
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
555
+ }
556
+ fs.writeFileSync(outPath, serialized);
557
+ const nodesCount = Array.isArray(profile.nodes) ? profile.nodes.length : 0;
558
+ const samplesCount = Array.isArray(profile.samples) ? profile.samples.length : 0;
559
+ return {
560
+ ok: true,
561
+ path: outPath,
562
+ label,
563
+ sizeBytes: Buffer.byteLength(serialized, 'utf8'),
564
+ nodesCount,
565
+ samplesCount,
566
+ startTime: profile.startTime ?? null,
567
+ endTime: profile.endTime ?? null,
568
+ };
569
+ },
570
+
571
+ async 'issues-arm'(client) {
572
+ // Installs console.warn/error and global error/unhandledrejection hooks
573
+ // that push into globalThis.__AGENTIC_ISSUES__ (capped at 500 entries).
574
+ // Idempotent — subsequent calls return { installed: true, reason: 'already-installed' }.
575
+ const result = await cdpEval(client, buildArmSnippet());
576
+ return result || { installed: false };
577
+ },
578
+
579
+ async 'issues-collect'(client) {
580
+ // Snapshots and clears globalThis.__AGENTIC_ISSUES__.
581
+ // Returns { count, entries } where entries are { t, level, text }.
582
+ const result = await cdpEval(client, buildCollectSnippet());
583
+ return result || { count: 0, entries: [] };
584
+ },
585
+
586
+ };
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // Main
590
+ // ---------------------------------------------------------------------------
591
+
592
+ async function main() {
593
+ const args = process.argv.slice(2);
594
+ const command = args[0];
595
+
596
+ if (!command || command === '--help' || command === '-h') {
597
+ console.log(`CDP Bridge — interact with the running MetaMask app via Hermes CDP
598
+
599
+ Usage:
600
+ node library/actions/mobile/bridge-runtime/cdp-bridge.cjs <command> [args...]
601
+
602
+ Commands:
603
+ navigate <RouteName> [params-json] Navigate to a screen
604
+ status Route + selected account snapshot
605
+ get-route Get current route name and params
606
+ get-state [dot.path] Get Redux state (or nav state if no path)
607
+ eval <expression> Evaluate arbitrary JS in app context
608
+ eval-async <expression> Evaluate async/Promise expression
609
+ can-go-back Check if navigation can go back
610
+ go-back Navigate back
611
+ list-accounts List all accounts
612
+ get-selected-account Get the currently selected account
613
+ switch-account <address> Switch to account by address
614
+ press-test-id <testId> Press a component by its testID prop
615
+ scroll-view [--test-id <id>] [--offset <n>] [--animated]
616
+ Scroll a ScrollView/FlatList
617
+ set-input <testId> <value> Set text input value by testID (calls onChangeText)
618
+ sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
619
+ unlock <password> Unlock wallet (inject password + press login button via fiber tree)
620
+ profiler-start Start Hermes sampling profiler
621
+ profiler-stop [--out <path>] [--label <name>]
622
+ Stop profiler, dump Chrome-compatible
623
+ .cpuprofile to <path> (default:
624
+ temp/agentic/traces/trace-<label>.cpuprofile)
625
+ issues-arm Install console/exception hooks that
626
+ populate globalThis.__AGENTIC_ISSUES__
627
+ issues-collect Snapshot + clear the in-app issue buffer
628
+
629
+ Environment:
630
+ WATCHER_PORT Metro port (default: 8081)
631
+ CDP_TIMEOUT Connection timeout in ms (default: 5000)
632
+ IOS_SIMULATOR Filter targets by iOS simulator name
633
+ ANDROID_DEVICE Filter targets by Android device/emulator serial`);
634
+ process.exit(0);
635
+ }
636
+
637
+ const handler = COMMANDS[command];
638
+ if (!handler) {
639
+ console.error(`Unknown command: ${command}`);
640
+ console.error(`Available: ${Object.keys(COMMANDS).join(', ')}`);
641
+ process.exit(1);
642
+ }
643
+
644
+
645
+ const port = loadPort();
646
+ const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
647
+
648
+ // `status` probes ALL connected targets so both platforms are visible.
649
+ if (command === 'status') {
650
+ const { discoverAllTargets } = require('./lib/target-discovery.cjs');
651
+ const allTargets = await discoverAllTargets(port);
652
+ const results = [];
653
+ for (const target of allTargets) {
654
+ let client;
655
+ try {
656
+ client = await createWSClient(target.wsUrl, timeout);
657
+ const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
658
+ const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
659
+ results.push(result);
660
+ } catch {
661
+ // Target not responsive — skip
662
+ } finally {
663
+ if (client) client.close();
664
+ }
665
+ }
666
+ console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
667
+ return;
668
+ }
669
+
670
+ const { wsUrl, deviceName } = await discoverTarget(port);
671
+ const client = await createWSClient(wsUrl, timeout);
672
+
673
+ try {
674
+ // Detect platform from the running app (exposed by __AGENTIC__ bridge as Platform.OS)
675
+ const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
676
+ const result = await handler(client, args.slice(1), { deviceName, platform });
677
+ console.log(JSON.stringify(result, null, 2));
678
+ } finally {
679
+ client.close();
680
+ }
681
+ }
682
+
683
+ main().catch((err) => {
684
+ console.error(`ERROR: ${err.message}`);
685
+ process.exit(1);
686
+ });