react-native-mcp-kit 3.0.1 → 4.1.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 (195) hide show
  1. package/README.md +25 -13
  2. package/dist/babel/testIdPlugin.d.ts.map +1 -1
  3. package/dist/babel/testIdPlugin.js +22 -13
  4. package/dist/babel/testIdPlugin.js.map +1 -1
  5. package/dist/bin/ios-hid +0 -0
  6. package/dist/client/contexts/McpContext/McpProvider.d.ts.map +1 -1
  7. package/dist/client/contexts/McpContext/McpProvider.js +5 -5
  8. package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
  9. package/dist/client/core/McpClient.d.ts.map +1 -1
  10. package/dist/client/core/McpClient.js +42 -34
  11. package/dist/client/core/McpClient.js.map +1 -1
  12. package/dist/modules/alert/alert.d.ts.map +1 -1
  13. package/dist/modules/alert/alert.js +2 -5
  14. package/dist/modules/alert/alert.js.map +1 -1
  15. package/dist/modules/console/console.d.ts.map +1 -1
  16. package/dist/modules/console/console.js +32 -107
  17. package/dist/modules/console/console.js.map +1 -1
  18. package/dist/modules/console/types.d.ts +1 -0
  19. package/dist/modules/console/types.d.ts.map +1 -1
  20. package/dist/modules/device/device.d.ts.map +1 -1
  21. package/dist/modules/device/device.js +224 -133
  22. package/dist/modules/device/device.js.map +1 -1
  23. package/dist/modules/errors/errors.d.ts.map +1 -1
  24. package/dist/modules/errors/errors.js +19 -36
  25. package/dist/modules/errors/errors.js.map +1 -1
  26. package/dist/modules/fiberTree/children.d.ts +49 -0
  27. package/dist/modules/fiberTree/children.d.ts.map +1 -0
  28. package/dist/modules/fiberTree/children.js +182 -0
  29. package/dist/modules/fiberTree/children.js.map +1 -0
  30. package/dist/modules/fiberTree/constants.d.ts +13 -0
  31. package/dist/modules/fiberTree/constants.d.ts.map +1 -0
  32. package/dist/modules/fiberTree/constants.js +24 -0
  33. package/dist/modules/fiberTree/constants.js.map +1 -0
  34. package/dist/modules/fiberTree/fiberTree.d.ts +3 -6
  35. package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
  36. package/dist/modules/fiberTree/fiberTree.js +219 -1080
  37. package/dist/modules/fiberTree/fiberTree.js.map +1 -1
  38. package/dist/modules/fiberTree/finder.d.ts +60 -0
  39. package/dist/modules/fiberTree/finder.d.ts.map +1 -0
  40. package/dist/modules/fiberTree/finder.js +107 -0
  41. package/dist/modules/fiberTree/finder.js.map +1 -0
  42. package/dist/modules/fiberTree/hooks.d.ts +103 -0
  43. package/dist/modules/fiberTree/hooks.d.ts.map +1 -0
  44. package/dist/modules/fiberTree/hooks.js +532 -0
  45. package/dist/modules/fiberTree/hooks.js.map +1 -0
  46. package/dist/modules/fiberTree/projection.d.ts +49 -0
  47. package/dist/modules/fiberTree/projection.d.ts.map +1 -0
  48. package/dist/modules/fiberTree/projection.js +82 -0
  49. package/dist/modules/fiberTree/projection.js.map +1 -0
  50. package/dist/modules/fiberTree/query.d.ts +56 -0
  51. package/dist/modules/fiberTree/query.d.ts.map +1 -0
  52. package/dist/modules/fiberTree/query.js +151 -0
  53. package/dist/modules/fiberTree/query.js.map +1 -0
  54. package/dist/modules/fiberTree/redact.d.ts +24 -0
  55. package/dist/modules/fiberTree/redact.d.ts.map +1 -0
  56. package/dist/modules/fiberTree/redact.js +51 -0
  57. package/dist/modules/fiberTree/redact.js.map +1 -0
  58. package/dist/modules/fiberTree/types.d.ts +7 -0
  59. package/dist/modules/fiberTree/types.d.ts.map +1 -1
  60. package/dist/modules/fiberTree/utils.d.ts +8 -2
  61. package/dist/modules/fiberTree/utils.d.ts.map +1 -1
  62. package/dist/modules/fiberTree/utils.js +79 -78
  63. package/dist/modules/fiberTree/utils.js.map +1 -1
  64. package/dist/modules/fiberTree/viewport.d.ts +28 -0
  65. package/dist/modules/fiberTree/viewport.d.ts.map +1 -0
  66. package/dist/modules/fiberTree/viewport.js +50 -0
  67. package/dist/modules/fiberTree/viewport.js.map +1 -0
  68. package/dist/modules/fiberTree/waitFor.d.ts +52 -0
  69. package/dist/modules/fiberTree/waitFor.d.ts.map +1 -0
  70. package/dist/modules/fiberTree/waitFor.js +98 -0
  71. package/dist/modules/fiberTree/waitFor.js.map +1 -0
  72. package/dist/modules/logBox/logBox.d.ts.map +1 -1
  73. package/dist/modules/logBox/logBox.js +59 -66
  74. package/dist/modules/logBox/logBox.js.map +1 -1
  75. package/dist/modules/navigation/navigation.d.ts.map +1 -1
  76. package/dist/modules/navigation/navigation.js +115 -114
  77. package/dist/modules/navigation/navigation.js.map +1 -1
  78. package/dist/modules/network/network.d.ts.map +1 -1
  79. package/dist/modules/network/network.js +78 -197
  80. package/dist/modules/network/network.js.map +1 -1
  81. package/dist/modules/network/types.d.ts +23 -27
  82. package/dist/modules/network/types.d.ts.map +1 -1
  83. package/dist/modules/reactQuery/reactQuery.d.ts.map +1 -1
  84. package/dist/modules/reactQuery/reactQuery.js +46 -52
  85. package/dist/modules/reactQuery/reactQuery.js.map +1 -1
  86. package/dist/modules/storage/storage.d.ts.map +1 -1
  87. package/dist/modules/storage/storage.js +20 -3
  88. package/dist/modules/storage/storage.js.map +1 -1
  89. package/dist/server/bridge.d.ts +1 -0
  90. package/dist/server/bridge.d.ts.map +1 -1
  91. package/dist/server/bridge.js +1 -0
  92. package/dist/server/bridge.js.map +1 -1
  93. package/dist/server/host/coredevice/dtx.d.ts +79 -0
  94. package/dist/server/host/coredevice/dtx.d.ts.map +1 -0
  95. package/dist/server/host/coredevice/dtx.js +453 -0
  96. package/dist/server/host/coredevice/dtx.js.map +1 -0
  97. package/dist/server/host/coredevice/nska.d.ts +6 -0
  98. package/dist/server/host/coredevice/nska.d.ts.map +1 -0
  99. package/dist/server/host/coredevice/nska.js +277 -0
  100. package/dist/server/host/coredevice/nska.js.map +1 -0
  101. package/dist/server/host/coredevice/rsd.d.ts +20 -0
  102. package/dist/server/host/coredevice/rsd.d.ts.map +1 -0
  103. package/dist/server/host/coredevice/rsd.js +244 -0
  104. package/dist/server/host/coredevice/rsd.js.map +1 -0
  105. package/dist/server/host/coredevice/screenshot.d.ts +11 -0
  106. package/dist/server/host/coredevice/screenshot.d.ts.map +1 -0
  107. package/dist/server/host/coredevice/screenshot.js +84 -0
  108. package/dist/server/host/coredevice/screenshot.js.map +1 -0
  109. package/dist/server/host/coredevice/tunnel.d.ts +29 -0
  110. package/dist/server/host/coredevice/tunnel.d.ts.map +1 -0
  111. package/dist/server/host/coredevice/tunnel.js +222 -0
  112. package/dist/server/host/coredevice/tunnel.js.map +1 -0
  113. package/dist/server/host/coredevice/xpc.d.ts +21 -0
  114. package/dist/server/host/coredevice/xpc.d.ts.map +1 -0
  115. package/dist/server/host/coredevice/xpc.js +318 -0
  116. package/dist/server/host/coredevice/xpc.js.map +1 -0
  117. package/dist/server/host/deviceResolver.d.ts +8 -0
  118. package/dist/server/host/deviceResolver.d.ts.map +1 -1
  119. package/dist/server/host/deviceResolver.js +128 -62
  120. package/dist/server/host/deviceResolver.js.map +1 -1
  121. package/dist/server/host/tools/capture.d.ts.map +1 -1
  122. package/dist/server/host/tools/capture.js +26 -0
  123. package/dist/server/host/tools/capture.js.map +1 -1
  124. package/dist/server/host/tools/input.js +2 -2
  125. package/dist/server/host/tools/input.js.map +1 -1
  126. package/dist/server/mcpServer.d.ts.map +1 -1
  127. package/dist/server/mcpServer.js +44 -3
  128. package/dist/server/mcpServer.js.map +1 -1
  129. package/dist/server/metro/eventCapture.d.ts +0 -2
  130. package/dist/server/metro/eventCapture.d.ts.map +1 -1
  131. package/dist/server/metro/eventCapture.js +1 -2
  132. package/dist/server/metro/eventCapture.js.map +1 -1
  133. package/dist/server/metro/tools/events.d.ts.map +1 -1
  134. package/dist/server/metro/tools/events.js +11 -11
  135. package/dist/server/metro/tools/events.js.map +1 -1
  136. package/dist/shared/projection/projectValue.d.ts +90 -0
  137. package/dist/shared/projection/projectValue.d.ts.map +1 -0
  138. package/dist/shared/projection/projectValue.js +322 -0
  139. package/dist/shared/projection/projectValue.js.map +1 -0
  140. package/dist/shared/projection/redact.d.ts +31 -0
  141. package/dist/shared/projection/redact.d.ts.map +1 -0
  142. package/dist/shared/projection/redact.js +78 -0
  143. package/dist/shared/projection/redact.js.map +1 -0
  144. package/dist/shared/projection/resolvePath.d.ts +45 -0
  145. package/dist/shared/projection/resolvePath.d.ts.map +1 -0
  146. package/dist/shared/projection/resolvePath.js +211 -0
  147. package/dist/shared/projection/resolvePath.js.map +1 -0
  148. package/dist/shared/protocol.d.ts +2 -1
  149. package/dist/shared/protocol.d.ts.map +1 -1
  150. package/dist/shared/protocol.js +1 -1
  151. package/dist/shared/rn/core.d.ts +48 -0
  152. package/dist/shared/rn/core.d.ts.map +1 -0
  153. package/dist/shared/rn/core.js +100 -0
  154. package/dist/shared/rn/core.js.map +1 -0
  155. package/dist/shared/rn/deviceInfo.d.ts +40 -0
  156. package/dist/shared/rn/deviceInfo.d.ts.map +1 -0
  157. package/dist/shared/rn/deviceInfo.js +78 -0
  158. package/dist/shared/rn/deviceInfo.js.map +1 -0
  159. package/package.json +4 -2
  160. package/dist/client/hooks/useMcpState.d.ts +0 -3
  161. package/dist/client/hooks/useMcpState.d.ts.map +0 -1
  162. package/dist/client/hooks/useMcpState.js +0 -20
  163. package/dist/client/hooks/useMcpState.js.map +0 -1
  164. package/dist/server/canonicalize.d.ts +0 -8
  165. package/dist/server/canonicalize.d.ts.map +0 -1
  166. package/dist/server/canonicalize.js +0 -23
  167. package/dist/server/canonicalize.js.map +0 -1
  168. package/dist/server/host/modules/screenshot.d.ts +0 -4
  169. package/dist/server/host/modules/screenshot.d.ts.map +0 -1
  170. package/dist/server/host/modules/screenshot.js +0 -615
  171. package/dist/server/host/modules/screenshot.js.map +0 -1
  172. package/dist/server/host/tools/connectionStatus.d.ts +0 -9
  173. package/dist/server/host/tools/connectionStatus.d.ts.map +0 -1
  174. package/dist/server/host/tools/connectionStatus.js +0 -39
  175. package/dist/server/host/tools/connectionStatus.js.map +0 -1
  176. package/dist/server/host/tools/symbolicate.d.ts +0 -3
  177. package/dist/server/host/tools/symbolicate.d.ts.map +0 -1
  178. package/dist/server/host/tools/symbolicate.js +0 -209
  179. package/dist/server/host/tools/symbolicate.js.map +0 -1
  180. package/dist/server/host/wda.d.ts +0 -15
  181. package/dist/server/host/wda.d.ts.map +0 -1
  182. package/dist/server/host/wda.js +0 -100
  183. package/dist/server/host/wda.js.map +0 -1
  184. package/dist/server/inputSchemaToZod.d.ts +0 -19
  185. package/dist/server/inputSchemaToZod.d.ts.map +0 -1
  186. package/dist/server/inputSchemaToZod.js +0 -89
  187. package/dist/server/inputSchemaToZod.js.map +0 -1
  188. package/dist/server/metro/tools/openUrl.d.ts +0 -3
  189. package/dist/server/metro/tools/openUrl.d.ts.map +0 -1
  190. package/dist/server/metro/tools/openUrl.js +0 -71
  191. package/dist/server/metro/tools/openUrl.js.map +0 -1
  192. package/dist/shared/slice.d.ts +0 -16
  193. package/dist/shared/slice.d.ts.map +0 -1
  194. package/dist/shared/slice.js +0 -29
  195. package/dist/shared/slice.js.map +0 -1
@@ -1,643 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fiberTreeModule = void 0;
4
+ const projectValue_1 = require("../../shared/projection/projectValue");
5
+ const children_1 = require("./children");
6
+ const constants_1 = require("./constants");
7
+ const finder_1 = require("./finder");
8
+ const hooks_1 = require("./hooks");
9
+ const projection_1 = require("./projection");
10
+ const query_1 = require("./query");
11
+ const redact_1 = require("./redact");
4
12
  const utils_1 = require("./utils");
5
- const DEFAULT_DEPTH = 10;
6
- const QUERY_LIMIT_DEFAULT = 50;
7
- const QUERY_LIMIT_MAX = 500;
8
- const QUERY_DEFAULT_FIELDS = ['mcpId', 'name', 'testID'];
9
- const WAIT_TIMEOUT_DEFAULT = 10_000;
10
- const WAIT_TIMEOUT_MAX = 60_000;
11
- const WAIT_INTERVAL_DEFAULT = 300;
12
- const WAIT_INTERVAL_MIN = 100;
13
- // Parse a name pattern: `/regex/flags` → RegExp matcher; anything else →
14
- // exact-string matcher. Same convention as log_box__ignore.
15
- const parseNamePattern = (raw) => {
16
- const m = raw.match(/^\/(.+)\/([gimsuy]*)$/);
17
- if (m && m[1] !== undefined) {
18
- try {
19
- const rx = new RegExp(m[1], m[2] ?? '');
20
- return (n) => {
21
- return rx.test(n);
22
- };
23
- }
24
- catch {
25
- return (n) => {
26
- return n === raw;
27
- };
28
- }
29
- }
30
- return (n) => {
31
- return n === raw;
32
- };
33
- };
34
- const HOOK_DEFAULT_MAX_DEPTH = 3;
35
- // Try to treat `current` as a React component / native view instance and
36
- // collapse it to a compact identifier. Native views expose
37
- // `_internalFiberInstanceHandleDEV` / `_reactInternals`; class instances
38
- // expose `_reactInternals`. If we find a fiber, we surface its mcpId /
39
- // testID / component name so the agent can follow up via `query` if needed.
40
- // If it doesn't look like a component, return null to signal "serialize
41
- // normally".
42
- const resolveComponentRef = (current) => {
43
- if (current === null || current === undefined)
44
- return null;
45
- if (typeof current !== 'object')
46
- return null;
47
- const obj = current;
48
- // Pick up a fiber from the typical internal fields used by RN / React.
49
- const fiber = obj._reactInternals ??
50
- obj._reactInternalFiber ??
51
- obj._internalFiberInstanceHandleDEV ??
52
- obj._internalInstanceHandle;
53
- const hasNativeTag = '_nativeTag' in obj;
54
- if (!fiber && !hasNativeTag)
55
- return null;
56
- const out = { __componentRef: true };
57
- if (fiber) {
58
- const props = fiber.memoizedProps;
59
- const mcpId = props?.['data-mcp-id'];
60
- const testID = props?.testID;
61
- if (mcpId)
62
- out.mcpId = mcpId;
63
- if (testID)
64
- out.testID = testID;
65
- const typeName = fiber.type?.displayName ??
66
- fiber.type?.name;
67
- if (typeName)
68
- out.componentName = typeName;
69
- }
70
- if (hasNativeTag) {
71
- out.nativeTag = obj._nativeTag;
72
- const viewConfig = obj.viewConfig;
73
- if (viewConfig?.uiViewClassName)
74
- out.viewClass = viewConfig.uiViewClassName;
75
- }
76
- return out;
77
- };
78
- // Recognise React's effect-record shape: `{ tag: number, create: function,
79
- // deps: null | unknown[] }` with optional `inst` / `destroy` / `next`.
80
- // useState / useReducer / useContext memoizedState values of this exact
81
- // shape in real user code are astronomically unlikely, so we treat this as
82
- // a reliable "definitely not a state slot" signal.
83
- const looksLikeEffectRecord = (raw) => {
84
- if (!raw || typeof raw !== 'object')
85
- return false;
86
- const r = raw;
87
- return (typeof r.tag === 'number' &&
88
- typeof r.create === 'function' &&
89
- (r.deps === null || r.deps === undefined || Array.isArray(r.deps)));
90
- };
91
- // Recognise the useRef shape: `{ current: X }` with NO other keys. A useState
92
- // value that is literally an object whose sole own-key is "current" is so
93
- // improbable in real code that we treat it as a reliable "this slot is a
94
- // ref, not a state" signal — lets State/Custom skip ref slots that leaked in
95
- // through custom-hook internals.
96
- const looksLikeRefShape = (raw) => {
97
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
98
- return false;
99
- const keys = Object.keys(raw);
100
- return keys.length === 1 && keys[0] === 'current';
101
- };
102
- // Shape-verify a hook slot's memoizedState against its expected kind. When
103
- // a custom hook internally uses multiple built-in hooks, our static metadata
104
- // understates the number of slots — every subsequent pairing drifts. By
105
- // requiring a structural match before consuming a metadata entry we can
106
- // swallow "internal" slots and keep the rest aligned. Permissive kinds
107
- // (State / Reducer / Context / Custom) reject only obvious mis-matches
108
- // (currently: the effect-record shape).
109
- const shapeMatchesKind = (raw, kind) => {
110
- switch (kind) {
111
- case 'Ref':
112
- return !!raw && typeof raw === 'object' && 'current' in raw;
113
- case 'Memo':
114
- case 'Callback':
115
- return Array.isArray(raw) && raw.length === 2 && (raw[1] === null || Array.isArray(raw[1]));
116
- case 'Effect':
117
- case 'LayoutEffect':
118
- case 'InsertionEffect':
119
- return looksLikeEffectRecord(raw);
120
- case 'Transition':
121
- return Array.isArray(raw) && raw.length === 2;
122
- case 'State':
123
- case 'Reducer':
124
- case 'Context':
125
- case 'Custom':
126
- // Permissive but not blind — drop obvious effect-node and ref-shape
127
- // slots so State/Custom metadata doesn't swallow internals of
128
- // preceding custom hooks.
129
- return !looksLikeEffectRecord(raw) && !looksLikeRefShape(raw);
130
- default:
131
- return true;
132
- }
133
- };
134
- const serializeHookValue = (raw, kind, maxDepth) => {
135
- const walk = (v) => {
136
- return (0, utils_1.serializeValue)(v, new WeakSet(), 0, maxDepth);
137
- };
138
- switch (kind) {
139
- case 'Ref': {
140
- if (!raw || typeof raw !== 'object' || !('current' in raw)) {
141
- return walk(raw);
142
- }
143
- const current = raw.current;
144
- const componentRef = resolveComponentRef(current);
145
- if (componentRef) {
146
- return { current: componentRef };
147
- }
148
- return { current: walk(current) };
149
- }
150
- case 'Memo':
151
- case 'Callback':
152
- if (Array.isArray(raw) && raw.length === 2) {
153
- return { deps: raw[1], value: walk(raw[0]) };
154
- }
155
- return walk(raw);
156
- case 'Effect':
157
- case 'LayoutEffect':
158
- case 'InsertionEffect': {
159
- // React's hook slot for effects holds { tag, create, destroy, deps, next }.
160
- // Only `deps` is safe and useful to surface.
161
- const effect = raw;
162
- return effect && typeof effect === 'object' && 'deps' in effect
163
- ? { deps: effect.deps ?? null }
164
- : null;
165
- }
166
- default:
167
- return walk(raw);
168
- }
169
- };
170
- // Estimate how many slots in fiber.memoizedState a hook function consumes.
171
- // Used by the slot-walker to advance past unannotated black-box library
172
- // hooks (e.g. `useSelector` from react-redux) which our babel plugin couldn't
173
- // expand statically — without this they'd consume only one fiber slot during
174
- // alignment, drifting the rest of the metadata out of sync.
175
- //
176
- // Three cascading strategies, falling through on miss:
177
- // 1. `fn.__mcp_hooks` recursive — accurate for any hook our plugin saw.
178
- // Custom sub-entries recurse; built-in entries count as 1.
179
- // 2. `fn.toString()` regex — counts `useXxx(` calls in the source. Works
180
- // even after Metro bundling because hook references survive as
181
- // property accesses (`(0, _react.useState)(...)`) — property names
182
- // aren't mangled. Underestimates when nested customs themselves expand
183
- // to multiple slots, but better than 1.
184
- // 3. Default 1 — native functions, bound functions, or sources we can't
185
- // parse. Same as the original behavior.
186
- //
187
- // Cached per-function via WeakMap so cost is paid once per hook fn per
188
- // session.
189
- const HOOK_SLOTS_CACHE = new WeakMap();
190
- const HOOK_NAME_RE = /\buse[A-Z]\w*\s*\(/g;
191
- const STRING_LITERAL_RE = /(['"`])(?:\\.|(?!\1).)*\1/g;
192
- const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
193
- const LINE_COMMENT_RE = /\/\/[^\n]*/g;
194
- const countHookSlots = (fn, depth = 0, seen) => {
195
- if (depth > 8)
196
- return 1;
197
- if (typeof fn !== 'function')
198
- return 1;
199
- const fnObj = fn;
200
- const cached = HOOK_SLOTS_CACHE.get(fnObj);
201
- if (cached !== undefined)
202
- return cached;
203
- const localSeen = seen ?? new WeakSet();
204
- if (localSeen.has(fnObj))
205
- return 1;
206
- localSeen.add(fnObj);
207
- // (1) Annotated metadata: recurse into sub-entries, summing their slot
208
- // counts. Each built-in entry contributes 1; each Custom contributes
209
- // however many its own fn does (recurse).
210
- const annotated = fn.__mcp_hooks;
211
- if (Array.isArray(annotated)) {
212
- let total = 0;
213
- for (const entry of annotated) {
214
- if (entry && entry.kind === 'Custom' && typeof entry.fn === 'function') {
215
- total += countHookSlots(entry.fn, depth + 1, localSeen);
216
- }
217
- else {
218
- total += 1;
219
- }
220
- }
221
- const result = Math.max(total, 1);
222
- HOOK_SLOTS_CACHE.set(fnObj, result);
223
- return result;
224
- }
225
- // (2) toString-based parsing. Strip strings/comments first to avoid
226
- // matching `'useState'` inside literals. Each `useXxx(` occurrence in
227
- // remaining source counts as one hook call (≥ 1 slot). Custom hook calls
228
- // we encounter here can't be resolved further (no scope binding from the
229
- // outer function), so we bottom out at 1 slot per occurrence — still
230
- // beats the original constant-1 fallback when the source has multiple
231
- // hook calls (e.g. `useSyncExternalStoreWithSelector` internals).
232
- let src;
233
- try {
234
- src = Function.prototype.toString.call(fn);
235
- }
236
- catch {
237
- HOOK_SLOTS_CACHE.set(fnObj, 1);
238
- return 1;
239
- }
240
- if (!src || src.includes('[native code]')) {
241
- HOOK_SLOTS_CACHE.set(fnObj, 1);
242
- return 1;
243
- }
244
- const stripped = src
245
- .replace(STRING_LITERAL_RE, '""')
246
- .replace(BLOCK_COMMENT_RE, '')
247
- .replace(LINE_COMMENT_RE, '');
248
- HOOK_NAME_RE.lastIndex = 0;
249
- const matches = stripped.match(HOOK_NAME_RE);
250
- const result = matches ? matches.length : 1;
251
- HOOK_SLOTS_CACHE.set(fnObj, result);
252
- return result;
253
- };
254
- // Flatten a metadata array by recursively inlining custom-hook sub-metadata.
255
- // Stops on cycles (hook references itself) and on Custom entries whose `fn`
256
- // isn't annotated (library hooks that bypassed the babel plugin — usually
257
- // pre-compiled node_modules). Such unannotated entries stay as single
258
- // records and rely on shape-check for alignment.
259
- //
260
- // Custom entries with annotated `fn` produce TWO records: a parent (marked
261
- // `expanded: true`, no slot consumption) followed by all flattened
262
- // children. This keeps the call-site visible in the output — without it
263
- // the agent would see e.g. `wrapperAnimStyle.areAnimationsActive` deep
264
- // in `via:` but never the `wrapperAnimStyle = useAnimatedStyle(...)`
265
- // invocation that owns those slots.
266
- const flattenHookMeta = (meta, via = [], seen = new WeakSet(), maxDepth = Infinity) => {
267
- const out = [];
268
- for (const entry of meta) {
269
- const fn = entry.fn;
270
- const sub = fn && typeof fn === 'function' && Array.isArray(fn.__mcp_hooks) ? fn.__mcp_hooks : undefined;
271
- // Stop expanding once `via.length` would reach the cap — at that point
272
- // the current entry is treated as a leaf (Custom record without
273
- // children). The slot-walker still pairs it with one slot, so output
274
- // stays internally consistent.
275
- if (sub && !seen.has(fn) && via.length < maxDepth) {
276
- seen.add(fn);
277
- out.push({ ...entry, expanded: true, via });
278
- out.push(...flattenHookMeta(sub, [...via, entry.name], seen, maxDepth));
279
- seen.delete(fn);
280
- }
281
- else {
282
- out.push({ ...entry, via });
283
- }
284
- }
285
- return out;
286
- };
287
- const flatHooksToTree = (flat) => {
288
- const root = [];
289
- // Stack of currently-open parents, indexed by their depth (= via.length
290
- // of THEIR own entry, since their children sit at via.length + 1).
291
- const parents = [];
292
- for (const entry of flat) {
293
- const depth = entry.via?.length ?? 0;
294
- while (parents.length > depth)
295
- parents.pop();
296
- const node = { kind: entry.kind, name: entry.name };
297
- if (entry.hook !== undefined)
298
- node.hook = entry.hook;
299
- if (entry.value !== undefined)
300
- node.value = entry.value;
301
- if (parents.length === 0) {
302
- root.push(node);
303
- }
304
- else {
305
- const parent = parents[parents.length - 1];
306
- if (parent) {
307
- parent.children = parent.children ?? [];
308
- parent.children.push(node);
309
- }
310
- else {
311
- root.push(node);
312
- }
313
- }
314
- if (entry.expanded) {
315
- // Push as the new active parent at this depth. Subsequent entries
316
- // with via.length > depth become this node's descendants.
317
- parents.push(node);
318
- }
319
- }
320
- return root;
321
- };
322
- const extractHooks = (fiber, filter) => {
323
- // React's wrapper machinery makes "where does metadata live" depend on
324
- // the exact HOC chain. We try the most likely homes in order:
325
- //
326
- // 1. `fiber.type.__mcp_hooks` — bare components, FunctionDeclarations,
327
- // and the outer memo fiber when the chain is just memo(fn).
328
- // 2. `fiber.elementType.__mcp_hooks` — memo(fn) without compare.
329
- // React converts the fiber to SimpleMemoComponent and rewrites
330
- // `fiber.type` to the inner function (see `updateMemoComponent` in
331
- // ReactFabric); our metadata sits on the outer memo wrapper, which
332
- // survives only on `elementType`.
333
- // 3. `fiber.type.render.__mcp_hooks` — forwardRef wrapper. React lays
334
- // out memo(forwardRef(fn)) as three fibers (memo → forwardRef →
335
- // function). When the user queries by displayName they tend to
336
- // match the middle ForwardRef fiber (whose displayName resolves
337
- // via `render.displayName`); fiber.type there is the forwardRef
338
- // wrapper, which holds the inner fn at `.render`. The babel plugin
339
- // put plain metadata on that fn via the FunctionDecl visitor.
340
- // 4. `fiber.type.type.__mcp_hooks` — memo wrapper around forwardRef
341
- // (or any non-function inner). Not the SimpleMemoComponent path,
342
- // so `fiber.type` stays as the memo wrapper and `.type` is the
343
- // inner forwardRef / class / etc. This catches getter installations
344
- // on the wrapper layer too.
345
- const candidates = [
346
- fiber.type,
347
- fiber.elementType,
348
- fiber.type?.render,
349
- fiber.type?.type,
350
- ];
351
- let rawMeta;
352
- for (const c of candidates) {
353
- const m = c?.__mcp_hooks;
354
- if (Array.isArray(m)) {
355
- rawMeta = m;
356
- break;
357
- }
358
- }
359
- if (!Array.isArray(rawMeta))
360
- return null;
361
- const meta = flattenHookMeta(rawMeta, [], new WeakSet(), filter.expansionDepth);
362
- const out = [];
363
- let state = fiber.memoizedState;
364
- let metaIdx = 0;
365
- // Walk the fiber's hook chain and pair each slot with the next metadata
366
- // entry whose `kind` is shape-compatible with the slot. Strongly-shaped
367
- // kinds (Ref / Memo / Callback / Effect) match only the corresponding
368
- // React hook shape. Permissive kinds (State / Reducer / Context / Custom)
369
- // reject obvious mismatches (effect-record, ref-shape) so they don't
370
- // swallow slots belonging to preceding custom hooks.
371
- //
372
- // For Custom-leaf entries — typically black-box library hooks
373
- // (`useSelector`, `useQuery`, etc.) where flattenHookMeta couldn't expand
374
- // because `fn.__mcp_hooks` was missing — we estimate slot count via
375
- // `countHookSlots` and advance the fiber chain by that many slots in one
376
- // step. Without this, a hook that consumes 3 internal slots only
377
- // advances by 1 in the walker, drifting all trailing entries off the end
378
- // of the chain.
379
- const emitEntry = (entry, rawValueSlot) => {
380
- const { hook, kind, name, via } = entry;
381
- const passesKind = !filter.kindsSet || filter.kindsSet.has(kind);
382
- const passesName = !filter.nameMatchers ||
383
- filter.nameMatchers.some((m) => {
384
- return m(name);
385
- });
386
- if (!(passesKind && passesName))
387
- return;
388
- const record = { kind, name };
389
- // Prefer the babel-emitted hook name; fall back to fn.name for entries
390
- // produced by older bundles that predate the `hook` field.
391
- const resolvedHook = hook ?? (typeof entry.fn === 'function' ? entry.fn.name : undefined);
392
- if (resolvedHook)
393
- record.hook = resolvedHook;
394
- if (filter.withValues && rawValueSlot !== undefined) {
395
- // Redaction guard: mask the value (but keep kind/name/hook visible)
396
- // when the entry's name OR any ancestor in `via` matches a redact
397
- // pattern. Catches both direct sensitive hooks (`password` State)
398
- // and leaves nested under sensitive customs (a `value` field of
399
- // `useCredentials()` won't slip through).
400
- const isRedacted = matchesAnyRedactPattern(name, filter.redactPatterns) ||
401
- (via?.some((v) => {
402
- return matchesAnyRedactPattern(v, filter.redactPatterns);
403
- }) ??
404
- false);
405
- if (isRedacted) {
406
- record.value = REDACTED_VALUE;
407
- }
408
- else {
409
- let value;
410
- try {
411
- value = serializeHookValue(rawValueSlot, kind, filter.maxDepth);
412
- // Final cycle / non-serialisable check — the MCP bridge will
413
- // stringify this for transport, so bail now rather than killing
414
- // the whole response if one stray value carries a cycle past our
415
- // WeakSet (e.g. Proxy, lazy getter, native-bridged object).
416
- JSON.stringify(value);
417
- }
418
- catch {
419
- value = '[Unserialisable value]';
420
- }
421
- record.value = value;
422
- }
423
- }
424
- if (via && via.length > 0)
425
- record.via = via;
426
- if (entry.expanded)
427
- record.expanded = true;
428
- out.push(record);
429
- };
430
- const advanceState = (steps) => {
431
- for (let i = 0; i < steps && state; i++)
432
- state = state.next;
433
- };
434
- while (state && metaIdx < meta.length) {
435
- const entry = meta[metaIdx];
436
- if (!entry) {
437
- metaIdx++;
438
- continue;
439
- }
440
- // Expanded parent (synthetic, marks the call-site of a recursively
441
- // expanded custom hook). Emit without consuming any fiber slot — the
442
- // children that follow are the real slot-bearing entries.
443
- if (entry.expanded) {
444
- emitEntry(entry, undefined);
445
- metaIdx++;
446
- continue;
447
- }
448
- // Custom-leaf with a known fn → recurse into source to estimate slot
449
- // count, then consume that many slots at once. The first slot's value
450
- // is exposed (best approximation of "this hook's value"); the rest are
451
- // skipped silently as internals of the library hook.
452
- if (entry.kind === 'Custom' && typeof entry.fn === 'function') {
453
- const slots = countHookSlots(entry.fn);
454
- if (slots > 1) {
455
- emitEntry(entry, state.memoizedState);
456
- advanceState(slots);
457
- metaIdx++;
458
- continue;
459
- }
460
- }
461
- if (!shapeMatchesKind(state.memoizedState, entry.kind)) {
462
- state = state.next;
463
- continue;
464
- }
465
- emitEntry(entry, state.memoizedState);
466
- metaIdx++;
467
- state = state.next;
468
- }
469
- // Any metadata entries left after the fiber chain ran dry didn't get a
470
- // slot match. Emit them anyway (without value) so the agent at least
471
- // sees the hook exists — this is strictly better than silently dropping,
472
- // and helps debug alignment issues. Common cause: a preceding Custom
473
- // hook consumed more slots than countHookSlots estimated.
474
- while (metaIdx < meta.length) {
475
- const entry = meta[metaIdx];
476
- if (entry)
477
- emitEntry(entry, undefined);
478
- metaIdx++;
479
- }
480
- return filter.format === 'tree' ? flatHooksToTree(out) : out;
481
- };
482
- const FIND_SCHEMA = {
483
- index: {
484
- description: '0-based index when several components match (default: 0).',
485
- type: 'number',
486
- },
487
- mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
488
- name: { description: 'Component name to match.', type: 'string' },
489
- testID: { description: 'testID to match.', type: 'string' },
490
- text: { description: 'Rendered text substring (not prop values).', type: 'string' },
491
- within: {
492
- description: 'Parent component path. "/" nests, ":N" picks index.',
493
- examples: ['LoginForm', 'Button:1/Pressable', 'TabBar/TabBarItem:2'],
494
- type: 'string',
495
- },
496
- };
497
- // Default redact patterns — applied to a hook's `name` AND every entry in
498
- // its `via` chain so values stay masked even when nested under a sensitive
499
- // custom hook (e.g. a leaf `value` inside a `useAuth()` expansion). Tuned
500
- // to match real-world variable names without over-matching innocent ones:
501
- // `Pin$` is anchored so it doesn't catch "Spinner"; broad terms like
502
- // `auth` are deliberately omitted (would catch `isAuthenticated`).
503
- const DEFAULT_REDACT_HOOK_NAMES = [
504
- /password/i,
505
- /token/i,
506
- /jwt/i,
507
- /secret/i,
508
- /Pin$/,
509
- /credential/i,
510
- /apiKey/i,
511
- /authorization/i,
512
- ];
513
- const REDACTED_VALUE = '[redacted]';
514
- const escapeRegExp = (s) => {
515
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
516
- };
517
- const compileRedactPatterns = (raw) => {
518
- return raw.map((p) => {
519
- return typeof p === 'string' ? new RegExp(escapeRegExp(p), 'i') : p;
520
- });
521
- };
522
- const matchesAnyRedactPattern = (name, patterns) => {
523
- for (const p of patterns) {
524
- if (p.test(name))
525
- return true;
526
- }
527
- return false;
528
- };
529
- const resolveScreenFiber = (runtime) => {
530
- const nav = runtime.navigationRef;
531
- if (!nav || typeof nav.getCurrentRoute !== 'function')
532
- return null;
533
- const route = nav.getCurrentRoute();
534
- const key = route && typeof route.key === 'string' ? route.key : undefined;
535
- if (!key)
536
- return null;
537
- return (0, utils_1.findScreenFiberByRouteKey)(runtime.root, key);
538
- };
539
- const collectByScope = (fiber, scope, runtime) => {
540
- switch (scope) {
541
- case 'self':
542
- return [fiber];
543
- case 'parent':
544
- return fiber.return ? [fiber.return] : [];
545
- case 'ancestors':
546
- return (0, utils_1.getAncestors)(fiber);
547
- case 'children':
548
- return (0, utils_1.getDirectChildren)(fiber);
549
- case 'siblings':
550
- return (0, utils_1.getSiblings)(fiber);
551
- case 'nearest_host': {
552
- const host = (0, utils_1.findHostFiber)(fiber);
553
- return host ? [host] : [];
554
- }
555
- case 'screen': {
556
- const screen = resolveScreenFiber(runtime);
557
- if (!screen)
558
- return [];
559
- return (0, utils_1.findAllByQuery)(screen, {}).filter((f) => {
560
- return f !== screen;
561
- });
562
- }
563
- case 'descendants':
564
- default:
565
- return (0, utils_1.findAllByQuery)(fiber, {}).filter((f) => {
566
- return f !== fiber;
567
- });
568
- }
569
- };
570
- const runQueryChain = (runtime, steps) => {
571
- let current = [runtime.root];
572
- for (const step of steps) {
573
- const scope = step.scope ?? 'descendants';
574
- const seen = new Set();
575
- const collected = [];
576
- for (const fiber of current) {
577
- for (const candidate of collectByScope(fiber, scope, runtime)) {
578
- if (!seen.has(candidate)) {
579
- seen.add(candidate);
580
- collected.push(candidate);
581
- }
582
- }
583
- }
584
- const filtered = collected.filter((f) => {
585
- return (0, utils_1.matchesQuery)(f, step);
586
- });
587
- if (typeof step.index === 'number') {
588
- const picked = filtered[step.index];
589
- current = picked ? [picked] : [];
590
- }
591
- else {
592
- current = filtered;
593
- }
594
- if (current.length === 0)
595
- return [];
596
- }
597
- return current;
598
- };
599
- // Keep only fibers whose ancestor chain contains no other match. Removes
600
- // wrapper cascades (PressableView → Pressable → View → RCTView) while keeping
601
- // independent siblings with overlapping bounds (e.g. absolute-positioned
602
- // overlays). Preserves original DFS order.
603
- const dedupAncestors = (matches) => {
604
- if (matches.length < 2)
605
- return matches;
606
- const matchSet = new Set(matches);
607
- return matches.filter((fiber) => {
608
- let p = fiber.return;
609
- while (p) {
610
- if (matchSet.has(p))
611
- return false;
612
- p = p.return;
613
- }
614
- return true;
615
- });
616
- };
617
- // Window dimensions → physical-pixel bounds rectangle for `onlyVisible` filter.
618
- const getVisibleRect = () => {
619
- try {
620
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
621
- const RN = require('react-native');
622
- const { Dimensions, PixelRatio } = RN;
623
- const window = Dimensions?.get?.('window');
624
- const ratio = PixelRatio?.get?.() ?? 1;
625
- if (!window || !Number.isFinite(window.width) || !Number.isFinite(window.height))
626
- return null;
627
- return {
628
- height: window.height * ratio,
629
- width: window.width * ratio,
630
- };
631
- }
632
- catch {
633
- return null;
634
- }
635
- };
636
- const intersectsRect = (bounds, rect) => {
637
- return (bounds.x + bounds.width > 0 &&
638
- bounds.y + bounds.height > 0 &&
639
- bounds.x < rect.width &&
640
- bounds.y < rect.height);
13
+ const viewport_1 = require("./viewport");
14
+ const waitFor_1 = require("./waitFor");
15
+ const PROJECTION_SCHEMA = (0, projectValue_1.makeProjectionSchema)(constants_1.FIBER_DEFAULT_DEPTH);
16
+ // Module-local 2-arg wrapper around the shared `applyProjection` so handlers
17
+ // don't have to repeat `projectFiberValue` + default-depth on every call.
18
+ const applyProjection = (result, args) => {
19
+ return (0, projectValue_1.applyProjection)(result, args, utils_1.projectFiberValue, constants_1.FIBER_DEFAULT_DEPTH);
641
20
  };
642
21
  const fiberTreeModule = (options) => {
643
22
  if (options?.rootRef) {
@@ -649,9 +28,9 @@ const fiberTreeModule = (options) => {
649
28
  // - `additionalRedactHookNames` provided → defaults + user's.
650
29
  // - Neither → defaults.
651
30
  // Pass `redactHookNames: []` to disable redaction entirely.
652
- const redactPatterns = compileRedactPatterns(options?.redactHookNames !== undefined
31
+ const redactPatterns = (0, redact_1.compileRedactPatterns)(options?.redactHookNames !== undefined
653
32
  ? options.redactHookNames
654
- : [...DEFAULT_REDACT_HOOK_NAMES, ...(options?.additionalRedactHookNames ?? [])]);
33
+ : [...redact_1.DEFAULT_REDACT_HOOK_NAMES, ...(options?.additionalRedactHookNames ?? [])]);
655
34
  // Root-version keyed cache for `runQueryChain`. When React commits, the
656
35
  // HostRoot fiber swaps — so a mismatched pointer is proof the tree changed
657
36
  // and the cached match set for the same steps is no longer valid.
@@ -660,7 +39,7 @@ const fiberTreeModule = (options) => {
660
39
  const cacheEntries = new Map();
661
40
  const runCachedQuery = (runtime, steps, useCache) => {
662
41
  if (!useCache)
663
- return runQueryChain(runtime, steps);
42
+ return (0, query_1.runQueryChain)(runtime, steps);
664
43
  if (cacheRoot !== runtime.root) {
665
44
  cacheRoot = runtime.root;
666
45
  cacheEntries.clear();
@@ -669,78 +48,25 @@ const fiberTreeModule = (options) => {
669
48
  const hit = cacheEntries.get(key);
670
49
  if (hit)
671
50
  return hit;
672
- const result = runQueryChain(runtime, steps);
51
+ const result = (0, query_1.runQueryChain)(runtime, steps);
673
52
  cacheEntries.set(key, result);
674
53
  return result;
675
54
  };
676
- const findInRoot = (root, segment) => {
677
- if (!root)
678
- return null;
679
- // Support "Name:index" format, e.g. "Button:1"
680
- const [name, indexStr] = segment.split(':');
681
- if (!name)
682
- return null;
683
- const idx = indexStr ? parseInt(indexStr, 10) : 0;
684
- const allByMcpId = (0, utils_1.findAllByQuery)(root, { mcpId: name });
685
- if (allByMcpId.length > 0)
686
- return allByMcpId[idx] ?? null;
687
- const allByTestID = (0, utils_1.findAllByQuery)(root, { testID: name });
688
- if (allByTestID.length > 0)
689
- return allByTestID[idx] ?? null;
690
- const allByName = (0, utils_1.findAllByQuery)(root, { name });
691
- return allByName[idx] ?? null;
692
- };
693
- const findComponent = (args) => {
694
- let root = (0, utils_1.getFiberRoot)();
695
- if (!root)
696
- return null;
697
- // "within" supports recursive path with index: "Parent/Child:1/GrandChild"
698
- if (args.within) {
699
- const path = args.within.split('/');
700
- for (const segment of path) {
701
- root = findInRoot(root, segment);
702
- if (!root)
703
- return null;
704
- }
705
- }
706
- const index = args.index ?? 0;
707
- if (args.mcpId) {
708
- const all = (0, utils_1.findAllByQuery)(root, { mcpId: args.mcpId });
709
- return all[index] ?? null;
710
- }
711
- if (args.testID) {
712
- const all = (0, utils_1.findAllByQuery)(root, { testID: args.testID });
713
- return all[index] ?? null;
714
- }
715
- if (args.name) {
716
- const all = (0, utils_1.findAllByQuery)(root, { name: args.name });
717
- return all[index] ?? null;
718
- }
719
- if (args.text) {
720
- const all = (0, utils_1.findAllByQuery)(root, { text: args.text });
721
- return all[index] ?? null;
722
- }
723
- return null;
724
- };
725
- const requireRoot = () => {
726
- const root = (0, utils_1.getFiberRoot)();
727
- if (!root) {
728
- return { error: 'Fiber root not available. The app may not have rendered yet.' };
729
- }
730
- return null;
731
- };
732
55
  return {
733
56
  description: `React fiber tree inspection and interaction.
734
57
 
735
58
  SCOPES (query steps)
736
59
  descendants (default) / children / parent / ancestors / siblings / self
737
- / screen / nearest_host.
60
+ / root / screen / nearest_host.
61
+ · root — the React fiber root, regardless of the previous step's
62
+ match. Use as the first step to start from the top of the tree
63
+ (e.g. dump the whole tree via select: [{ children: 5 }]).
738
64
  · screen — descendants of the currently focused React Navigation
739
65
  screen fiber. Available when the library was initialized with a
740
66
  navigationRef. Lets a first step skip "find current screen first".
741
67
  · nearest_host — walks down to the first mounted HOST_COMPONENT
742
- fiber. Useful before call_ref (focus/blur/measure) which require
743
- a host instance.
68
+ fiber. Useful before \`call({ method })\` (focus/blur/measure)
69
+ which requires a host instance.
744
70
 
745
71
  STEP CRITERIA
746
72
  name / mcpId / testID — strict equality.
@@ -759,15 +85,27 @@ STEP CRITERIA
759
85
  index — pick N-th match from this step; otherwise all matches fan out into the next step.
760
86
 
761
87
  SELECT (output fields)
762
- Default ["mcpId", "name", "testID"] — props, bounds, hooks are opt-in.
88
+ Default ["mcpId", "name", "testID"] — props, bounds, hooks,
89
+ refMethods, children are opt-in.
763
90
  bounds: { x, y, width, height, centerX, centerY } in PHYSICAL pixels,
764
91
  top-left origin. null when the fiber has no mounted host view. centerX/
765
92
  centerY feed straight into host__tap.
766
- props: full serialized props (heavy). Pair with propsInclude:
767
- ["key1","key2"] to keep only the props you actually need and avoid
768
- pulling large style maps, data arrays, or nested element trees.
769
- hooks: the component's hooks. Each entry { kind, name, hook?, via?,
770
- expanded?, value? }; configure via hooksInclude.
93
+ refMethods: list of native-ref method names (focus, blur, measure,
94
+ scrollTo, ...) available on the fiber's host instance. null when
95
+ there is no native instance (composite wrapper, unmounted,
96
+ virtualized). Feeds directly into fiber_tree__call({ method }).
97
+ props: per-field projection — \`{ props: { path?, depth?, maxBytes? } }\`.
98
+ hooks: filtered + projected — \`{ hooks: { kinds?, names?, withValues?,
99
+ expansionDepth?, format?, path?, depth?, maxBytes? } }\`. Each entry
100
+ { kind, name, hook?, via?, expanded?, value? }.
101
+ children: recursive light-only walker for tree-of-tree navigation —
102
+ short form { children: 5 } (treeDepth=5) or object form
103
+ { children: { treeDepth, select?, itemsCap? } }. select inside
104
+ children may include only mcpId / name / testID / bounds / nested
105
+ children — props/hooks throw at parse time. Use a second query against
106
+ a child mcpId to inspect its props/hooks. treeDepth max 16, itemsCap
107
+ default 50; overflow inserts a \`\${truncated}\` sentinel as the first
108
+ array item.
771
109
 
772
110
  RESPONSE
773
111
  { matches: [...], total, truncated? } — total is the unrestricted match
@@ -782,9 +120,9 @@ RESPONSE
782
120
 
783
121
  TIPS
784
122
  mcpId format "ComponentName:file:line" — stable across renders.
785
- Use query to locate, then invoke (bypasses gesture pipeline) or host__tap
786
- with bounds (real OS touch) to act. For one-shot real taps, tap_fiber
787
- collapses both steps into a single call.
123
+ Use query to locate, then call({ prop } or { method }) (bypasses gesture
124
+ pipeline) or host__tap with bounds (real OS touch) to act. For one-shot
125
+ real taps, tap_fiber collapses both steps into a single call.
788
126
  When stepping up via scope: "ancestors", prefer filtering by name (or
789
127
  testID/mcpId) over guessing an index — ancestors count is brittle and
790
128
  varies across RN versions.
@@ -793,37 +131,54 @@ TIPS
793
131
  { contains: "Search" } }\`.`,
794
132
  name: 'fiber_tree',
795
133
  tools: {
796
- call_ref: {
797
- description: "Call a method on a component's native ref (focus, blur, measure, ). Use get_ref_methods first to see what's available.",
134
+ call: {
135
+ description: "Imperative action on a fiber — invoke a prop callback OR a native-ref method. Pass `prop: 'onPress'` to call a callback prop, or `method: 'focus'` to call a method on the host instance's native ref. For simulating user taps, prefer `host__tap_fiber` — it goes through the real OS gesture pipeline so Pressable feedback / gesture responders / hit-test all behave as under a real finger. `call` is for non-gesture callbacks, off-screen / virtualised components, or imperative ref methods (focus / blur / measure / scrollTo / ...). Use `query` with `select: ['refMethods']` first to see what methods are available on a fiber.",
798
136
  handler: (args) => {
799
- const rootError = requireRoot();
137
+ const rootError = (0, finder_1.requireRoot)();
800
138
  if (rootError)
801
139
  return rootError;
802
- const fiber = findComponent(args);
140
+ const fiber = (0, finder_1.findComponent)(args);
803
141
  if (!fiber)
804
142
  return { error: 'Component not found' };
143
+ const propName = typeof args.prop === 'string' ? args.prop : undefined;
144
+ const methodName = typeof args.method === 'string' ? args.method : undefined;
145
+ if ((propName && methodName) || (!propName && !methodName)) {
146
+ return {
147
+ error: 'call requires exactly one of `prop` (callback name) or `method` (ref method name).',
148
+ };
149
+ }
150
+ const callArgs = args.args ?? [];
151
+ // Prop-callback path: read prop from memoizedProps, call directly.
152
+ if (propName) {
153
+ const callback = fiber.memoizedProps?.[propName];
154
+ if (typeof callback !== 'function') {
155
+ const availableProps = Object.keys(fiber.memoizedProps ?? {}).filter((key) => {
156
+ return typeof fiber.memoizedProps[key] === 'function';
157
+ });
158
+ return {
159
+ availableProps,
160
+ error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no "${propName}" callback prop`,
161
+ };
162
+ }
163
+ const result = callback(...callArgs);
164
+ return applyProjection({ component: (0, utils_1.getComponentName)(fiber), prop: propName, result, success: true }, args);
165
+ }
166
+ // Ref-method path: resolve native instance, call method on it.
805
167
  const instance = (0, utils_1.getNativeInstance)(fiber);
806
168
  if (!instance) {
807
169
  return { error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no native instance` };
808
170
  }
809
- const methodName = args.method;
810
- const methodArgs = args.args;
811
171
  const method = instance[methodName];
812
172
  if (typeof method !== 'function') {
813
173
  return {
814
174
  availableMethods: (0, utils_1.getAvailableMethods)(instance),
815
- error: `No method "${methodName}" on native instance`,
175
+ error: `No method "${methodName}" on native instance of "${(0, utils_1.getComponentName)(fiber)}"`,
816
176
  };
817
177
  }
818
178
  try {
819
179
  const bound = method.bind(instance);
820
- const result = bound(...(methodArgs ?? []));
821
- return {
822
- component: (0, utils_1.getComponentName)(fiber),
823
- method: methodName,
824
- result,
825
- success: true,
826
- };
180
+ const result = bound(...callArgs);
181
+ return applyProjection({ component: (0, utils_1.getComponentName)(fiber), method: methodName, result, success: true }, args);
827
182
  }
828
183
  catch (e) {
829
184
  return {
@@ -832,168 +187,21 @@ TIPS
832
187
  }
833
188
  },
834
189
  inputSchema: {
835
- ...FIND_SCHEMA,
836
- args: { description: 'Arguments passed to the method.', type: 'array' },
837
- method: {
838
- description: 'Method name to call.',
839
- examples: ['focus', 'blur', 'measure'],
840
- type: 'string',
841
- },
842
- },
843
- },
844
- get_children: {
845
- description: 'Get the children subtree of a single component.',
846
- handler: (args) => {
847
- const rootError = requireRoot();
848
- if (rootError)
849
- return rootError;
850
- const fiber = findComponent(args);
851
- if (!fiber)
852
- return { error: 'Component not found' };
853
- const depth = args.depth || DEFAULT_DEPTH;
854
- const serialized = (0, utils_1.serializeFiber)(fiber, depth);
855
- return serialized?.children ?? [];
856
- },
857
- inputSchema: {
858
- ...FIND_SCHEMA,
859
- depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
860
- },
861
- },
862
- get_component: {
863
- description: 'Find one component and return its details with children subtree (deep inspection). Use `query` for a flat list of matches.',
864
- handler: async (args) => {
865
- const rootError = requireRoot();
866
- if (rootError)
867
- return rootError;
868
- const root = (0, utils_1.getFiberRoot)();
869
- let fiber = null;
870
- if (args.mcpId) {
871
- fiber = (0, utils_1.findByMcpId)(root, args.mcpId);
872
- }
873
- else if (args.testID) {
874
- fiber = (0, utils_1.findByTestID)(root, args.testID);
875
- }
876
- else if (args.name) {
877
- fiber = (0, utils_1.findByName)(root, args.name);
878
- }
879
- else if (args.text) {
880
- fiber = (0, utils_1.findByText)(root, args.text);
881
- }
882
- if (!fiber)
883
- return { error: 'Component not found' };
884
- const depth = args.depth || DEFAULT_DEPTH;
885
- const serialized = (0, utils_1.serializeFiber)(fiber, depth);
886
- if (serialized && Array.isArray(args.select)) {
887
- const fields = new Set(args.select);
888
- if (fields.has('bounds')) {
889
- const bounds = await (0, utils_1.measureFiber)(fiber);
890
- if (bounds) {
891
- serialized.bounds = bounds;
892
- }
893
- }
894
- if (!fields.has('props')) {
895
- serialized.props = {};
896
- }
897
- }
898
- return serialized;
899
- },
900
- inputSchema: {
901
- depth: { description: 'Max child traversal depth (default: 10).', type: 'number' },
902
- mcpId: { description: 'Stable data-mcp-id to match.', type: 'string' },
903
- name: { description: 'Component name to match.', type: 'string' },
904
- select: {
905
- description: 'Fields to include on the root node. Available: name, props, bounds. Children are always included.',
906
- examples: [['name', 'bounds']],
907
- type: 'array',
908
- },
909
- testID: { description: 'testID to match.', type: 'string' },
910
- text: { description: 'Rendered text substring.', type: 'string' },
911
- },
912
- },
913
- get_props: {
914
- description: 'Get all props of one component.',
915
- handler: (args) => {
916
- const rootError = requireRoot();
917
- if (rootError)
918
- return rootError;
919
- const fiber = findComponent(args);
920
- if (!fiber)
921
- return { error: 'Component not found' };
922
- return {
923
- name: (0, utils_1.getComponentName)(fiber),
924
- props: (0, utils_1.serializeProps)(fiber.memoizedProps),
925
- };
926
- },
927
- inputSchema: FIND_SCHEMA,
928
- },
929
- get_ref_methods: {
930
- description: "List available methods on a component's native ref.",
931
- handler: (args) => {
932
- const rootError = requireRoot();
933
- if (rootError)
934
- return rootError;
935
- const fiber = findComponent(args);
936
- if (!fiber)
937
- return { error: 'Component not found' };
938
- const instance = (0, utils_1.getNativeInstance)(fiber);
939
- if (!instance) {
940
- return { error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no native instance` };
941
- }
942
- return {
943
- component: (0, utils_1.getComponentName)(fiber),
944
- methods: (0, utils_1.getAvailableMethods)(instance),
945
- };
946
- },
947
- inputSchema: FIND_SCHEMA,
948
- },
949
- get_tree: {
950
- description: 'Dump the full React component tree from the root fiber.',
951
- handler: (args) => {
952
- const rootError = requireRoot();
953
- if (rootError)
954
- return rootError;
955
- const root = (0, utils_1.getFiberRoot)();
956
- const depth = args.depth || DEFAULT_DEPTH;
957
- return (0, utils_1.serializeFiber)(root, depth);
958
- },
959
- inputSchema: {
960
- depth: { description: 'Max traversal depth (default: 10).', type: 'number' },
961
- },
962
- },
963
- invoke: {
964
- description: "Call a prop's callback function directly from JS. For simulating a user tap, prefer host__tap_fiber — it runs the real OS gesture pipeline so Pressable feedback, gesture responders, and hit-test behave as under a real finger. invoke still works for any callback when you specifically want the JS-only path (component off-screen, skipping the gesture recognizer, or driving a non-gesture prop), but it is not the default for user-behavior simulation.",
965
- handler: (args) => {
966
- const rootError = requireRoot();
967
- if (rootError)
968
- return rootError;
969
- const fiber = findComponent(args);
970
- if (!fiber)
971
- return { error: 'Component not found' };
972
- const callbackName = args.callback;
973
- const callbackArgs = args.args;
974
- const callback = fiber.memoizedProps?.[callbackName];
975
- if (typeof callback !== 'function') {
976
- const availableCallbacks = Object.keys(fiber.memoizedProps ?? {}).filter((key) => {
977
- return typeof fiber.memoizedProps[key] === 'function';
978
- });
979
- return {
980
- availableCallbacks,
981
- error: `Component "${(0, utils_1.getComponentName)(fiber)}" has no "${callbackName}" callback`,
982
- };
983
- }
984
- const result = callback(...(callbackArgs ?? []));
985
- return { component: (0, utils_1.getComponentName)(fiber), result, success: true };
986
- },
987
- inputSchema: {
988
- ...FIND_SCHEMA,
190
+ ...finder_1.FIND_SCHEMA,
191
+ ...PROJECTION_SCHEMA,
989
192
  args: {
990
- description: 'Arguments passed to the callback.',
193
+ description: 'Arguments passed to the callback / method.',
991
194
  examples: [[true], ['text']],
992
195
  type: 'array',
993
196
  },
994
- callback: {
995
- description: 'Callback prop name.',
996
- examples: ['onSkip', 'onUpdate', 'onCompleted'],
197
+ method: {
198
+ description: 'Native-ref method name. Mutually exclusive with `prop`.',
199
+ examples: ['focus', 'blur', 'measure', 'scrollTo'],
200
+ type: 'string',
201
+ },
202
+ prop: {
203
+ description: 'Callback prop name. Mutually exclusive with `method`.',
204
+ examples: ['onPress', 'onSkip', 'onChangeText'],
997
205
  type: 'string',
998
206
  },
999
207
  },
@@ -1001,187 +209,130 @@ TIPS
1001
209
  query: {
1002
210
  description: 'Chain-based fiber search. Each step narrows the result set via `scope` + criteria; multiple matches fan out into the next step. Returns { matches, total, truncated? }. Pass `waitFor` to poll until an element appears or disappears (optionally requiring stability for N ms) instead of a single-shot read. See the module description for scope, criteria, select and response reference.',
1003
211
  handler: async (args) => {
1004
- const rootError = requireRoot();
1005
- if (rootError)
1006
- return rootError;
1007
- const root = (0, utils_1.getFiberRoot)();
1008
- const steps = args.steps;
1009
- if (!Array.isArray(steps) || steps.length === 0) {
1010
- return { error: 'query requires a non-empty `steps` array' };
1011
- }
1012
- const limit = typeof args.limit === 'number' && args.limit > 0
1013
- ? Math.min(Math.floor(args.limit), QUERY_LIMIT_MAX)
1014
- : QUERY_LIMIT_DEFAULT;
1015
- const dedup = args.dedup !== false;
1016
- const useCacheDefault = args.cache !== false;
1017
- const onlyVisible = args.onlyVisible === true;
1018
- const fields = new Set(Array.isArray(args.select) ? args.select : QUERY_DEFAULT_FIELDS);
1019
- const propsInclude = Array.isArray(args.propsInclude)
1020
- ? new Set(args.propsInclude)
1021
- : null;
1022
- const hooksIncludeRaw = args.hooksInclude;
1023
- const hookKindsSet = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.kinds)
1024
- ? new Set(hooksIncludeRaw.kinds)
1025
- : null;
1026
- const hookNameMatchers = hooksIncludeRaw && Array.isArray(hooksIncludeRaw.names)
1027
- ? hooksIncludeRaw.names.map(parseNamePattern)
1028
- : null;
1029
- const hookWithValues = hooksIncludeRaw?.withValues === true;
1030
- const hookMaxDepth = typeof hooksIncludeRaw?.maxDepthInValues === 'number' &&
1031
- hooksIncludeRaw.maxDepthInValues >= 0
1032
- ? Math.min(Math.floor(hooksIncludeRaw.maxDepthInValues), 8)
1033
- : HOOK_DEFAULT_MAX_DEPTH;
1034
- const hookExpansionDepth = typeof hooksIncludeRaw?.expansionDepth === 'number' &&
1035
- hooksIncludeRaw.expansionDepth >= 0
1036
- ? Math.floor(hooksIncludeRaw.expansionDepth)
1037
- : Infinity;
1038
- const hookFormat = hooksIncludeRaw?.format === 'tree' ? 'tree' : 'flat';
1039
- const runtime = { navigationRef, root };
1040
- const runOnce = async (useCache) => {
1041
- const rawMatches = runCachedQuery(runtime, steps, useCache);
1042
- let all = dedup ? dedupAncestors(rawMatches) : rawMatches;
1043
- const boundsCache = new Map();
1044
- const measure = async (fiber) => {
1045
- if (boundsCache.has(fiber))
1046
- return boundsCache.get(fiber) ?? null;
1047
- const b = await (0, utils_1.measureFiber)(fiber);
1048
- boundsCache.set(fiber, b);
1049
- return b;
1050
- };
1051
- if (onlyVisible) {
1052
- const visibleRect = getVisibleRect();
1053
- if (visibleRect) {
1054
- const rect = visibleRect;
1055
- const measured = await Promise.all(all.map(async (fiber) => {
1056
- return { bounds: await measure(fiber), fiber };
1057
- }));
1058
- all = measured
1059
- .filter(({ bounds }) => {
1060
- return bounds && intersectsRect(bounds, rect);
1061
- })
1062
- .map(({ fiber }) => {
1063
- return fiber;
1064
- });
1065
- }
212
+ const inner = async () => {
213
+ const rootError = (0, finder_1.requireRoot)();
214
+ if (rootError)
215
+ return rootError;
216
+ const root = (0, utils_1.getFiberRoot)();
217
+ const steps = args.steps;
218
+ if (!Array.isArray(steps) || steps.length === 0) {
219
+ return { error: 'query requires a non-empty `steps` array' };
1066
220
  }
1067
- const total = all.length;
1068
- const truncated = total > limit;
1069
- const picked = truncated ? all.slice(0, limit) : all;
1070
- const matches = await Promise.all(picked.map(async (fiber) => {
1071
- const result = {};
1072
- if (fields.has('bounds')) {
1073
- result.bounds = await measure(fiber);
1074
- }
1075
- if (fields.has('mcpId')) {
1076
- result.mcpId = fiber.memoizedProps?.['data-mcp-id'];
1077
- }
1078
- if (fields.has('name')) {
1079
- result.name = (0, utils_1.getComponentName)(fiber);
1080
- }
1081
- if (fields.has('props')) {
1082
- const full = (0, utils_1.serializeProps)(fiber.memoizedProps);
1083
- if (propsInclude) {
1084
- const filtered = {};
1085
- for (const key of propsInclude) {
1086
- if (key in full)
1087
- filtered[key] = full[key];
1088
- }
1089
- result.props = filtered;
1090
- }
1091
- else {
1092
- result.props = full;
1093
- }
1094
- }
1095
- if (fields.has('testID')) {
1096
- result.testID = fiber.memoizedProps?.testID;
1097
- }
1098
- if (fields.has('hooks')) {
1099
- result.hooks = extractHooks(fiber, {
1100
- expansionDepth: hookExpansionDepth,
1101
- format: hookFormat,
1102
- kindsSet: hookKindsSet,
1103
- maxDepth: hookMaxDepth,
1104
- nameMatchers: hookNameMatchers,
1105
- redactPatterns,
1106
- withValues: hookWithValues,
1107
- });
1108
- }
1109
- return result;
1110
- }));
1111
- return truncated ? { matches, total, truncated: true } : { matches, total };
1112
- };
1113
- const waitForRaw = args.waitFor;
1114
- if (!waitForRaw || typeof waitForRaw !== 'object') {
1115
- return runOnce(useCacheDefault);
1116
- }
1117
- const until = waitForRaw.until;
1118
- if (until !== 'appear' && until !== 'disappear') {
1119
- return { error: 'waitFor.until must be "appear" or "disappear"' };
1120
- }
1121
- const waitUntil = until;
1122
- const timeout = Math.min(WAIT_TIMEOUT_MAX, Math.max(0, waitForRaw.timeout ?? WAIT_TIMEOUT_DEFAULT));
1123
- const interval = Math.max(WAIT_INTERVAL_MIN, waitForRaw.interval ?? WAIT_INTERVAL_DEFAULT);
1124
- const stable = Math.max(0, waitForRaw.stable ?? 0);
1125
- const predicate = (total) => {
1126
- return waitUntil === 'appear' ? total >= 1 : total === 0;
1127
- };
1128
- const startedAt = Date.now();
1129
- const deadline = startedAt + timeout;
1130
- let attempts = 0;
1131
- let stableSince = null;
1132
- let lastResult = await runOnce(false);
1133
- attempts++;
1134
- // eslint-disable-next-line no-constant-condition
1135
- while (true) {
1136
- const now = Date.now();
1137
- const elapsedMs = now - startedAt;
1138
- const met = predicate(lastResult.total);
1139
- if (met) {
1140
- if (stable === 0) {
1141
- return {
1142
- ...lastResult,
1143
- attempts,
1144
- elapsedMs,
1145
- timedOut: false,
1146
- until: waitUntil,
1147
- waited: true,
1148
- };
1149
- }
1150
- if (stableSince === null)
1151
- stableSince = now;
1152
- if (now - stableSince >= stable) {
1153
- return {
1154
- ...lastResult,
1155
- attempts,
1156
- elapsedMs,
1157
- stableFor: now - stableSince,
1158
- timedOut: false,
1159
- until: waitUntil,
1160
- waited: true,
1161
- };
1162
- }
221
+ const stepError = (0, query_1.validateSteps)(steps);
222
+ if (stepError)
223
+ return { error: stepError };
224
+ const limit = typeof args.limit === 'number' && args.limit > 0
225
+ ? Math.min(Math.floor(args.limit), constants_1.QUERY_LIMIT_MAX)
226
+ : constants_1.QUERY_LIMIT_DEFAULT;
227
+ const dedup = args.dedup !== false;
228
+ const useCacheDefault = args.cache !== false;
229
+ const onlyVisible = args.onlyVisible === true;
230
+ let projection;
231
+ try {
232
+ projection = (0, projection_1.parseProjection)(args.select);
1163
233
  }
1164
- else {
1165
- stableSince = null;
234
+ catch (e) {
235
+ return { error: e instanceof Error ? e.message : String(e) };
1166
236
  }
1167
- if (now >= deadline) {
1168
- return {
1169
- ...lastResult,
1170
- attempts,
1171
- elapsedMs,
1172
- timedOut: true,
1173
- until: waitUntil,
1174
- waited: true,
237
+ const { children: childrenOpts, fields, hooks: hookOpts, props: propsOpts, } = projection;
238
+ const runtime = { navigationRef, root };
239
+ const runOnce = async (useCache) => {
240
+ const rawMatches = runCachedQuery(runtime, steps, useCache);
241
+ let all = dedup ? (0, query_1.dedupAncestors)(rawMatches) : rawMatches;
242
+ const boundsCache = new Map();
243
+ const measure = async (fiber) => {
244
+ if (boundsCache.has(fiber))
245
+ return boundsCache.get(fiber) ?? null;
246
+ const b = await (0, utils_1.measureFiber)(fiber);
247
+ boundsCache.set(fiber, b);
248
+ return b;
1175
249
  };
250
+ if (onlyVisible) {
251
+ const visibleRect = (0, viewport_1.getVisibleRect)();
252
+ if (visibleRect) {
253
+ const rect = visibleRect;
254
+ const measured = await Promise.all(all.map(async (fiber) => {
255
+ return { bounds: await measure(fiber), fiber };
256
+ }));
257
+ all = measured
258
+ .filter(({ bounds }) => {
259
+ return bounds && (0, viewport_1.intersectsRect)(bounds, rect);
260
+ })
261
+ .map(({ fiber }) => {
262
+ return fiber;
263
+ });
264
+ }
265
+ }
266
+ const total = all.length;
267
+ const truncated = total > limit;
268
+ const picked = truncated ? all.slice(0, limit) : all;
269
+ const matches = await Promise.all(picked.map(async (fiber) => {
270
+ const result = {};
271
+ if (fields.has('bounds')) {
272
+ result.bounds = await measure(fiber);
273
+ }
274
+ if (fields.has('mcpId')) {
275
+ result.mcpId = fiber.memoizedProps?.['data-mcp-id'];
276
+ }
277
+ if (fields.has('name')) {
278
+ result.name = (0, utils_1.getComponentName)(fiber);
279
+ }
280
+ if (fields.has('props')) {
281
+ // Heavy field — projected here via select.props options
282
+ // (path/depth/maxBytes). Top-level response stays raw
283
+ // so mcpId/name/etc are always visible without
284
+ // projection.
285
+ result.props = (0, utils_1.projectFiberValue)(fiber.memoizedProps ?? {}, {
286
+ depth: propsOpts.depth ?? 1,
287
+ maxBytes: propsOpts.maxBytes,
288
+ path: propsOpts.path,
289
+ });
290
+ }
291
+ if (fields.has('refMethods')) {
292
+ // List of native-ref methods available on the fiber's
293
+ // host instance (focus, blur, measure, scrollTo, ...).
294
+ // null when the fiber has no native instance (composite
295
+ // wrappers, unmounted, virtualized). Feeds directly into
296
+ // `fiber_tree__call({ method })`.
297
+ const instance = (0, utils_1.getNativeInstance)(fiber);
298
+ result.refMethods = instance ? (0, utils_1.getAvailableMethods)(instance) : null;
299
+ }
300
+ if (fields.has('testID')) {
301
+ result.testID = fiber.memoizedProps?.testID;
302
+ }
303
+ if (fields.has('hooks')) {
304
+ result.hooks = (0, hooks_1.extractHooks)(fiber, {
305
+ ...hookOpts,
306
+ redactPatterns,
307
+ });
308
+ }
309
+ if (childrenOpts) {
310
+ result.children = await (0, children_1.walkChildren)(fiber, childrenOpts, childrenOpts.treeDepth, measure);
311
+ }
312
+ return result;
313
+ }));
314
+ return truncated ? { matches, total, truncated: true } : { matches, total };
315
+ };
316
+ const waitForRaw = args.waitFor;
317
+ if (!waitForRaw || typeof waitForRaw !== 'object') {
318
+ return runOnce(useCacheDefault);
1176
319
  }
1177
- const remaining = deadline - now;
1178
- const sleepMs = Math.min(interval, Math.max(0, remaining));
1179
- await new Promise((resolve) => {
1180
- return setTimeout(resolve, sleepMs);
320
+ // Cache is always bypassed inside the polling loop — `runOnce`
321
+ // with cache:true would just keep returning the stale match
322
+ // set the cache captured pre-mount.
323
+ return (0, waitFor_1.runWaitForLoop)(waitForRaw, () => {
324
+ return runOnce(false);
1181
325
  });
1182
- lastResult = await runOnce(false);
1183
- attempts++;
1184
- }
326
+ };
327
+ // No top-level projection on the query response — the response
328
+ // shell ({ matches, total, ... }) is light by construction;
329
+ // heavy values inside `props` / `hooks` are already collapsed to
330
+ // markers by per-field projection in `inner`, and the
331
+ // `select.children` walker self-bounds via treeDepth/itemsCap.
332
+ // Top-level path/depth/maxBytes are not exposed on `query` —
333
+ // drill happens via `select.props.path` / `select.hooks.path`,
334
+ // and tree-shape navigation via `select.children`.
335
+ return inner();
1185
336
  },
1186
337
  inputSchema: {
1187
338
  cache: {
@@ -1192,40 +343,28 @@ TIPS
1192
343
  description: 'Drop wrapper cascades — a fiber is removed when any of its ancestors is also in the match set (PressableView → Pressable → View → RCTView collapses to the topmost). Independent siblings with overlapping bounds are kept. Default true; pass false to keep every match.',
1193
344
  type: 'boolean',
1194
345
  },
1195
- hooksInclude: {
1196
- description: 'When select includes "hooks": `kinds` (State | Reducer | Memo | Callback | Ref | Effect | LayoutEffect | InsertionEffect | Context | Transition | DeferredValue | Id | SyncExternalStore | ImperativeHandle | Custom) and `names` (exact or `/regex/flags`) filter the list. Each entry carries { kind, name, hook?, via?, expanded? } — `hook` is the source-level hook function (`useState`, `useAnimatedStyle`); `expanded: true` marks a parent custom-hook call whose sub-hooks follow. Pass `withValues: true` for resolved values. `maxDepthInValues` caps value recursion (default 3, max 8). `expansionDepth` caps how deep custom hooks recurse — 0 = no expansion (top-level only), 1 = one level, default Infinity. `format: "tree"` returns nested `children:` instead of flat `via:`.',
1197
- examples: [
1198
- { kinds: ['State', 'Memo'] },
1199
- { names: ['count', 'scrollRef'], withValues: true },
1200
- { kinds: ['State'], names: ['/^is/'], withValues: true },
1201
- { expansionDepth: 0 },
1202
- { format: 'tree', withValues: true },
1203
- { expansionDepth: 1, format: 'tree' },
1204
- ],
1205
- type: 'object',
1206
- },
1207
346
  limit: {
1208
- description: `Max matches to return (default ${QUERY_LIMIT_DEFAULT}, max ${QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
347
+ description: `Max matches to return (default ${constants_1.QUERY_LIMIT_DEFAULT}, max ${constants_1.QUERY_LIMIT_MAX}). truncated: true is added when total exceeds limit.`,
1209
348
  type: 'number',
1210
349
  },
1211
350
  onlyVisible: {
1212
351
  description: 'Drop matches whose measured bounds do not intersect the current window rectangle (physical pixels). Also drops fibers with no measurable host view — usually virtualized or unmounted. Halves results on long lists.',
1213
352
  type: 'boolean',
1214
353
  },
1215
- propsInclude: {
1216
- description: 'When select includes "props", keep only these prop names. Unknown keys are silently dropped. Omit for full serialization.',
1217
- examples: [
1218
- ['placeholder', 'value'],
1219
- ['title', 'disabled'],
1220
- ],
1221
- type: 'array',
1222
- },
1223
354
  select: {
1224
- description: `Output fields: mcpId, name, testID, props, bounds, hooks. Default ${JSON.stringify(QUERY_DEFAULT_FIELDS)}.`,
355
+ description: `Output fields: mcpId, name, testID, props, bounds, hooks, refMethods, children. Default ${JSON.stringify(projection_1.QUERY_DEFAULT_FIELDS)}. Each entry is either a string ("mcpId" — include with defaults) or an object whose keys are field names. Object values are \`true\` / \`false\` / per-field options.\n\nLight fields (mcpId, name, testID, bounds, refMethods) — no options, just toggle. refMethods is the list of native-ref methods (focus, blur, measure, scrollTo, ...) available on the fiber's host instance; null when the fiber has no native instance. Feeds directly into \`fiber_tree__call({ method })\`.\n\nHeavy fields (props, hooks) — per-field projection via shared \`projectValue\` so nested heavy values become \`\${...}\`-keyed markers. Each takes its own \`path\` / \`depth\` / \`maxBytes\`.\n\nprops options: \`{ path?, depth?, maxBytes? }\`.\n\nhooks options: \`{ kinds?, names?, withValues?, expansionDepth?, format?, path?, depth?, maxBytes? }\`. \`kinds\`: State | Reducer | Memo | Callback | Ref | Effect | LayoutEffect | InsertionEffect | Context | Transition | DeferredValue | Id | SyncExternalStore | ImperativeHandle | Custom. \`names\`: exact or \`/regex/flags\`. \`withValues:true\` adds resolved values. \`expansionDepth\` caps custom-hook recursion (default Infinity). \`format:"tree"\` returns nested children instead of flat \`via\`.\n\nchildren — recursive light-only walker for tree-of-tree dumps.\n Short form: \`{ children: 5 }\` → treeDepth=5, default fields ['mcpId','name'].\n Object form: \`{ children: { treeDepth?, select?, itemsCap? } }\`.\n treeDepth max 16; itemsCap default 50; overflow inserts \`\${truncated}\` as the first item.\n select inside children may include only mcpId / name / testID / bounds / nested children. props/hooks throw at parse time — run a second query against a child's mcpId to inspect them.\n\nEach hook entry carries \`{ kind, name, hook?, via?, expanded? }\`.`,
1225
356
  examples: [
1226
357
  ['mcpId', 'name', 'bounds'],
1227
- ['mcpId', 'name', 'props'],
1228
- ['mcpId', 'hooks'],
358
+ ['mcpId', 'refMethods'],
359
+ ['mcpId', { props: { path: 'style' } }],
360
+ ['mcpId', { props: { depth: 3 } }],
361
+ [{ hooks: { kinds: ['State'], withValues: true }, mcpId: true }],
362
+ [{ children: 5 }],
363
+ [
364
+ 'mcpId',
365
+ 'name',
366
+ { children: { select: ['mcpId', 'name', 'testID'], treeDepth: 3 } },
367
+ ],
1229
368
  ],
1230
369
  type: 'array',
1231
370
  },
@@ -1240,7 +379,7 @@ TIPS
1240
379
  type: 'array',
1241
380
  },
1242
381
  waitFor: {
1243
- description: `Poll the query until a predicate holds, instead of reading once. \`until\` selects the target state: "appear" waits for \`total >= 1\`, "disappear" waits for \`total === 0\`. \`timeout\` (default ${WAIT_TIMEOUT_DEFAULT}ms, max ${WAIT_TIMEOUT_MAX}ms) caps the wait. \`interval\` (default ${WAIT_INTERVAL_DEFAULT}ms, min ${WAIT_INTERVAL_MIN}ms) is the gap between polls. \`stable\` (default 0) requires the predicate to hold continuously for this many ms before returning — useful to ignore transient matches during screen transitions. Cache is always bypassed while polling. On success the response carries the usual query fields plus \`{ waited: true, until, attempts, elapsedMs, timedOut: false, stableFor? }\`; on timeout \`timedOut: true\` with the last observed matches.`,
382
+ description: `Poll the query until a predicate holds, instead of reading once. \`until\` selects the target state: "appear" waits for \`total >= 1\`, "disappear" waits for \`total === 0\`. \`timeout\` (default ${constants_1.WAIT_TIMEOUT_DEFAULT}ms, max ${constants_1.WAIT_TIMEOUT_MAX}ms) caps the wait. \`interval\` (default ${constants_1.WAIT_INTERVAL_DEFAULT}ms, min ${constants_1.WAIT_INTERVAL_MIN}ms) is the gap between polls. \`stable\` (default 0) requires the predicate to hold continuously for this many ms before returning — useful to ignore transient matches during screen transitions. Cache is always bypassed while polling. On success the response carries the usual query fields plus \`{ waited: true, until, attempts, elapsedMs, timedOut: false, stableFor? }\`; on timeout \`timedOut: true\` with the last observed matches.`,
1244
383
  examples: [
1245
384
  { until: 'appear' },
1246
385
  { timeout: 5000, until: 'disappear' },