@mobileai/react-native 0.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 (65) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +190 -0
  3. package/lib/module/components/AIAgent.js +149 -0
  4. package/lib/module/components/AIAgent.js.map +1 -0
  5. package/lib/module/components/AgentChatBar.js +120 -0
  6. package/lib/module/components/AgentChatBar.js.map +1 -0
  7. package/lib/module/components/AgentOverlay.js +53 -0
  8. package/lib/module/components/AgentOverlay.js.map +1 -0
  9. package/lib/module/core/AgentRuntime.js +498 -0
  10. package/lib/module/core/AgentRuntime.js.map +1 -0
  11. package/lib/module/core/FiberTreeWalker.js +308 -0
  12. package/lib/module/core/FiberTreeWalker.js.map +1 -0
  13. package/lib/module/core/MCPBridge.js +98 -0
  14. package/lib/module/core/MCPBridge.js.map +1 -0
  15. package/lib/module/core/ScreenDehydrator.js +46 -0
  16. package/lib/module/core/ScreenDehydrator.js.map +1 -0
  17. package/lib/module/core/types.js +2 -0
  18. package/lib/module/core/types.js.map +1 -0
  19. package/lib/module/hooks/useAction.js +32 -0
  20. package/lib/module/hooks/useAction.js.map +1 -0
  21. package/lib/module/index.js +17 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/package.json +1 -0
  24. package/lib/module/providers/GeminiProvider.js +178 -0
  25. package/lib/module/providers/GeminiProvider.js.map +1 -0
  26. package/lib/module/utils/logger.js +17 -0
  27. package/lib/module/utils/logger.js.map +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/components/AIAgent.d.ts +57 -0
  30. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -0
  31. package/lib/typescript/src/components/AgentChatBar.d.ts +14 -0
  32. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -0
  33. package/lib/typescript/src/components/AgentOverlay.d.ts +10 -0
  34. package/lib/typescript/src/components/AgentOverlay.d.ts.map +1 -0
  35. package/lib/typescript/src/core/AgentRuntime.d.ts +37 -0
  36. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -0
  37. package/lib/typescript/src/core/FiberTreeWalker.d.ts +26 -0
  38. package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -0
  39. package/lib/typescript/src/core/MCPBridge.d.ts +23 -0
  40. package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -0
  41. package/lib/typescript/src/core/ScreenDehydrator.d.ts +20 -0
  42. package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +1 -0
  43. package/lib/typescript/src/core/types.d.ts +138 -0
  44. package/lib/typescript/src/core/types.d.ts.map +1 -0
  45. package/lib/typescript/src/hooks/useAction.d.ts +13 -0
  46. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -0
  47. package/lib/typescript/src/index.d.ts +10 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/providers/GeminiProvider.d.ts +23 -0
  50. package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -0
  51. package/lib/typescript/src/utils/logger.d.ts +7 -0
  52. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  53. package/package.json +143 -0
  54. package/src/components/AIAgent.tsx +222 -0
  55. package/src/components/AgentChatBar.tsx +136 -0
  56. package/src/components/AgentOverlay.tsx +48 -0
  57. package/src/core/AgentRuntime.ts +505 -0
  58. package/src/core/FiberTreeWalker.ts +349 -0
  59. package/src/core/MCPBridge.ts +110 -0
  60. package/src/core/ScreenDehydrator.ts +53 -0
  61. package/src/core/types.ts +185 -0
  62. package/src/hooks/useAction.ts +40 -0
  63. package/src/index.ts +22 -0
  64. package/src/providers/GeminiProvider.ts +210 -0
  65. package/src/utils/logger.ts +21 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * FiberTreeWalker — Traverses React's Fiber tree to discover interactive elements.
3
+ *
4
+ * This is the React Native equivalent of page-agent.js reading the DOM.
5
+ * Instead of traversing HTML nodes, we traverse React Fiber nodes and detect
6
+ * interactive elements by their type and props (onPress, onChangeText, etc.).
7
+ *
8
+ * Architecture inspired by: https://github.com/alibaba/page-agent
9
+ */
10
+
11
+ import { logger } from '../utils/logger';
12
+ import type { InteractiveElement, ElementType } from './types';
13
+
14
+ // ─── Walk Configuration (mirrors page-agent DomConfig) ─────────
15
+
16
+ export interface WalkConfig {
17
+ /** React refs of elements to exclude — mirrors page-agent interactiveBlacklist */
18
+ interactiveBlacklist?: React.RefObject<any>[];
19
+ /** If set, only these elements are interactive — mirrors page-agent interactiveWhitelist */
20
+ interactiveWhitelist?: React.RefObject<any>[];
21
+ }
22
+
23
+ // ─── Fiber Node Type Detection ─────────────────────────────────
24
+
25
+ /** React Native component names that are inherently interactive */
26
+ const PRESSABLE_TYPES = new Set([
27
+ 'Pressable',
28
+ 'TouchableOpacity',
29
+ 'TouchableHighlight',
30
+ 'TouchableWithoutFeedback',
31
+ 'TouchableNativeFeedback',
32
+ 'Button',
33
+ ]);
34
+
35
+ const TEXT_INPUT_TYPES = new Set(['TextInput', 'RCTSinglelineTextInputView', 'RCTMultilineTextInputView']);
36
+ const SWITCH_TYPES = new Set(['Switch', 'RCTSwitch']);
37
+ const TEXT_TYPES = new Set(['Text', 'RCTText']);
38
+ // ScrollView/FlatList/SectionList detection can be added later for scroll tool
39
+
40
+ // ─── Fiber Node Helpers ────────────────────────────────────────
41
+
42
+ /**
43
+ * Get the display name of a Fiber node's component type.
44
+ */
45
+ function getComponentName(fiber: any): string | null {
46
+ if (!fiber || !fiber.type) return null;
47
+
48
+ // Host components (View, Text, etc.) — type is a string
49
+ if (typeof fiber.type === 'string') return fiber.type;
50
+
51
+ // Function/Class components — type has displayName or name
52
+ if (fiber.type.displayName) return fiber.type.displayName;
53
+ if (fiber.type.name) return fiber.type.name;
54
+
55
+ // ForwardRef components
56
+ if (fiber.type.render?.displayName) return fiber.type.render.displayName;
57
+ if (fiber.type.render?.name) return fiber.type.render.name;
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Check if a fiber node represents an interactive element.
64
+ */
65
+ function getElementType(fiber: any): ElementType | null {
66
+ const name = getComponentName(fiber);
67
+ const props = fiber.memoizedProps || {};
68
+
69
+ // Check by component name
70
+ if (name && PRESSABLE_TYPES.has(name)) return 'pressable';
71
+ if (name && TEXT_INPUT_TYPES.has(name)) return 'text-input';
72
+ if (name && SWITCH_TYPES.has(name)) return 'switch';
73
+
74
+ // Check by props — any component with onPress is interactive
75
+ if (props.onPress && typeof props.onPress === 'function') return 'pressable';
76
+
77
+ // Check by accessibility role
78
+ const role = props.accessibilityRole || props.role;
79
+ if (role === 'button' || role === 'link' || role === 'checkbox' || role === 'radio') {
80
+ if (props.onPress) return 'pressable';
81
+ }
82
+
83
+ // TextInput detection by props
84
+ if (props.onChangeText && typeof props.onChangeText === 'function') return 'text-input';
85
+
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Check if element is disabled.
91
+ */
92
+ function isDisabled(fiber: any): boolean {
93
+ const props = fiber.memoizedProps || {};
94
+ return props.disabled === true || props.editable === false;
95
+ }
96
+
97
+ // ─── Text Extraction ───────────────────────────────────────────
98
+
99
+ /**
100
+ * Recursively extract text content from a fiber's children.
101
+ * Stops at the next interactive element to avoid capturing text from nested buttons.
102
+ */
103
+ function extractTextContent(fiber: any, maxDepth: number = 10): string {
104
+ if (!fiber || maxDepth <= 0) return '';
105
+
106
+ const parts: string[] = [];
107
+
108
+ let child = fiber.child;
109
+ while (child) {
110
+ const childName = getComponentName(child);
111
+ const childProps = child.memoizedProps || {};
112
+
113
+ // Stop at nested interactive elements
114
+ if (getElementType(child) !== null && child !== fiber) {
115
+ child = child.sibling;
116
+ continue;
117
+ }
118
+
119
+ // Text node — extract content
120
+ if (childName && TEXT_TYPES.has(childName)) {
121
+ const text = extractRawText(childProps.children);
122
+ if (text) parts.push(text);
123
+ } else {
124
+ // Recurse into non-interactive children
125
+ const nestedText = extractTextContent(child, maxDepth - 1);
126
+ if (nestedText) parts.push(nestedText);
127
+ }
128
+
129
+ child = child.sibling;
130
+ }
131
+
132
+ return parts.join(' ').trim();
133
+ }
134
+
135
+ /**
136
+ * Extract raw text from React children prop.
137
+ * Handles strings, numbers, arrays, and nested structures.
138
+ */
139
+ function extractRawText(children: any): string {
140
+ if (children == null) return '';
141
+ if (typeof children === 'string') return children;
142
+ if (typeof children === 'number') return String(children);
143
+
144
+ if (Array.isArray(children)) {
145
+ return children
146
+ .map(child => extractRawText(child))
147
+ .filter(Boolean)
148
+ .join(' ');
149
+ }
150
+
151
+ // React element — try to extract text from its props
152
+ if (children && typeof children === 'object' && children.props) {
153
+ return extractRawText(children.props.children);
154
+ }
155
+
156
+ return '';
157
+ }
158
+
159
+ /**
160
+ * Get the Fiber root node.
161
+ *
162
+ * In React Native, a View ref gives a native node, NOT a Fiber node.
163
+ * We use __REACT_DEVTOOLS_GLOBAL_HOOK__ (available in dev builds) to
164
+ * access getFiberRoots(), which is the same API React DevTools uses.
165
+ *
166
+ * Falls back to probing the ref's internal keys for Fiber references.
167
+ */
168
+ function getFiberRoot(): any | null {
169
+ // Strategy 1: __REACT_DEVTOOLS_GLOBAL_HOOK__ (most reliable in dev)
170
+ try {
171
+ const hook = (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
172
+ if (hook) {
173
+ // hook.renderers is a Map of renderer ID → renderer
174
+ // hook.getFiberRoots(rendererId) returns a Set of FiberRoot objects
175
+ const renderers = hook.renderers;
176
+ if (renderers && renderers.size > 0) {
177
+ for (const [rendererId] of renderers) {
178
+ const roots = hook.getFiberRoots(rendererId);
179
+ if (roots && roots.size > 0) {
180
+ // Get the first (and usually only) root
181
+ const fiberRoot = roots.values().next().value;
182
+ if (fiberRoot && fiberRoot.current) {
183
+ logger.debug('FiberTreeWalker', 'Accessed Fiber tree via DevTools hook');
184
+ return fiberRoot.current; // This is the root Fiber node
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ } catch (e) {
191
+ logger.debug('FiberTreeWalker', 'DevTools hook not available:', e);
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ function getFiberFromRef(ref: any): any | null {
198
+ // First try the DevTools hook (works regardless of what ref is)
199
+ const rootFiber = getFiberRoot();
200
+ if (rootFiber) return rootFiber;
201
+
202
+ if (!ref) return null;
203
+
204
+ // Fallback: Try known internal Fiber access patterns on the ref itself
205
+
206
+ // Pattern 1: _reactInternals (class components)
207
+ if (ref._reactInternals) return ref._reactInternals;
208
+
209
+ // Pattern 2: _reactInternalInstance (older React)
210
+ if (ref._reactInternalInstance) return ref._reactInternalInstance;
211
+
212
+ // Pattern 3: __reactFiber$ keys (React DOM/RN style)
213
+ try {
214
+ const keys = Object.keys(ref);
215
+ const fiberKey = keys.find(
216
+ key => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$'),
217
+ );
218
+ if (fiberKey) return (ref as any)[fiberKey];
219
+ } catch {
220
+ // Object.keys may fail on some native nodes
221
+ }
222
+
223
+ // Pattern 4: Direct fiber node properties
224
+ if (ref.child || ref.memoizedProps) return ref;
225
+
226
+ logger.warn('FiberTreeWalker', 'All Fiber access strategies failed');
227
+ return null;
228
+ }
229
+
230
+ // ─── Blacklist/Whitelist Matching ──────────────────────────────
231
+
232
+ /**
233
+ * Check if a Fiber node matches any ref in the given list.
234
+ * Mirrors page-agent.js: `interactiveBlacklist.includes(element)`
235
+ * We compare the Fiber's stateNode (native instance) against ref.current.
236
+ */
237
+ function matchesRefList(node: any, refs?: React.RefObject<any>[]): boolean {
238
+ if (!refs || refs.length === 0) return false;
239
+ const stateNode = node.stateNode;
240
+ if (!stateNode) return false;
241
+
242
+ for (const ref of refs) {
243
+ if (ref.current && ref.current === stateNode) return true;
244
+ }
245
+ return false;
246
+ }
247
+
248
+ export interface WalkResult {
249
+ elementsText: string;
250
+ interactives: InteractiveElement[];
251
+ }
252
+
253
+ // ─── Main Tree Walker ──────────────────────────────────────────
254
+
255
+ /**
256
+ * Walk the React Fiber tree from a root and collect all interactive elements
257
+ * as well as a hierarchical layout representation for the LLM.
258
+ */
259
+ export function walkFiberTree(rootRef: any, config?: WalkConfig): WalkResult {
260
+ const fiber = getFiberFromRef(rootRef);
261
+ if (!fiber) {
262
+ logger.warn('FiberTreeWalker', 'Could not access Fiber tree from ref');
263
+ return { elementsText: '', interactives: [] };
264
+ }
265
+
266
+ const interactives: InteractiveElement[] = [];
267
+ let currentIndex = 0;
268
+ const hasWhitelist = config?.interactiveWhitelist && (config.interactiveWhitelist.length ?? 0) > 0;
269
+
270
+ function processNode(node: any, depth: number = 0): string {
271
+ if (!node) return '';
272
+
273
+ const props = node.memoizedProps || {};
274
+
275
+ // ── Security Constraints ──
276
+ if (props.aiIgnore === true) return '';
277
+ if (matchesRefList(node, config?.interactiveBlacklist)) {
278
+ // Blacklisted nodes themselves aren't interactive, but we still walk children for structure
279
+ let childText = '';
280
+ let currentChild = node.child;
281
+ while (currentChild) {
282
+ childText += processNode(currentChild, depth);
283
+ currentChild = currentChild.sibling;
284
+ }
285
+ return childText;
286
+ }
287
+
288
+ // Process all children first
289
+ let childrenText = '';
290
+ let currentChild = node.child;
291
+ while (currentChild) {
292
+ childrenText += processNode(currentChild, depth + 1);
293
+ currentChild = currentChild.sibling;
294
+ }
295
+
296
+ // Interactive Check
297
+ const isWhitelisted = matchesRefList(node, config?.interactiveWhitelist);
298
+ const elementType = getElementType(node);
299
+ const shouldInclude = hasWhitelist ? isWhitelisted : (elementType && !isDisabled(node));
300
+
301
+ const indent = ' '.repeat(depth);
302
+
303
+ if (shouldInclude) {
304
+ const resolvedType = elementType || 'pressable';
305
+ let label = props.accessibilityLabel || extractTextContent(node);
306
+ if (!label && resolvedType === 'text-input' && props.placeholder) {
307
+ label = props.placeholder;
308
+ }
309
+
310
+ // Record interactive element
311
+ interactives.push({
312
+ index: currentIndex,
313
+ type: resolvedType,
314
+ label: label || `[${resolvedType}]`,
315
+ fiberNode: node,
316
+ props: { ...props }, // snapshot
317
+ });
318
+
319
+ const elementOutput = `${indent}[${currentIndex}]<${resolvedType}>${label ? label + ' ' : ''}${childrenText.trim() ? childrenText.trim() : ''}</>\n`;
320
+ currentIndex++;
321
+ return elementOutput;
322
+ }
323
+
324
+ // Non-interactive structural nodes
325
+ const typeStr = node.type && typeof node.type === 'string' ? node.type :
326
+ (node.elementType && typeof node.elementType === 'string' ? node.elementType : null);
327
+
328
+ if (typeStr === 'RCTText' || typeStr === 'Text') {
329
+ const textContent = extractRawText(props.children);
330
+ if (textContent && textContent.trim() !== '') {
331
+ return `${indent}<text>${textContent.trim()}</text>\n`;
332
+ }
333
+ }
334
+
335
+ if (childrenText.trim() !== '') {
336
+ return `${indent}<view>\n${childrenText}${indent}</view>\n`;
337
+ }
338
+
339
+ return '';
340
+ }
341
+
342
+ let elementsText = processNode(fiber, 0);
343
+
344
+ // Clean up empty views and excessive newlines
345
+ elementsText = elementsText.replace(/<view>\s*<\/view>\n?/g, '');
346
+
347
+ logger.info('FiberTreeWalker', `Found ${interactives.length} interactive elements`);
348
+ return { elementsText: elementsText.trim(), interactives };
349
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * MCPBridge — Connects the React Native app to the local MCP Server bridge.
3
+ *
4
+ * Flow:
5
+ * - Connects via WebSocket to the Node.js MCP server
6
+ * - Listens for 'request' messages containing an MCP command
7
+ * - Forwards the command to AgentRuntime.execute()
8
+ * - Sends the ExecutionResult back via WebSocket as a 'response'
9
+ */
10
+
11
+ import { logger } from '../utils/logger';
12
+ import type { AgentRuntime } from './AgentRuntime';
13
+
14
+ export class MCPBridge {
15
+ private url: string;
16
+ private ws: WebSocket | null = null;
17
+ private runtime: AgentRuntime;
18
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
19
+ private isDestroyed = false;
20
+
21
+ constructor(url: string, runtime: AgentRuntime) {
22
+ this.url = url;
23
+ this.runtime = runtime;
24
+ this.connect();
25
+ }
26
+
27
+ private connect() {
28
+ if (this.isDestroyed) return;
29
+
30
+ logger.info('MCPBridge', `Connecting to MCP bridge at ${this.url}...`);
31
+ this.ws = new WebSocket(this.url);
32
+
33
+ this.ws.onopen = () => {
34
+ logger.info('MCPBridge', '✅ Connected to MCP bridge.');
35
+ if (this.reconnectTimer) {
36
+ clearTimeout(this.reconnectTimer);
37
+ this.reconnectTimer = null;
38
+ }
39
+ };
40
+
41
+ this.ws.onmessage = async (event) => {
42
+ try {
43
+ const data = JSON.parse(event.data);
44
+ if (data.type === 'request' && data.command && data.requestId) {
45
+ logger.info('MCPBridge', `Received task from MCP: "${data.command}"`);
46
+
47
+ if (this.runtime.getIsRunning()) {
48
+ this.sendResponse(data.requestId, {
49
+ success: false,
50
+ message: 'Agent is already running a task. Please wait.',
51
+ steps: [],
52
+ });
53
+ return;
54
+ }
55
+
56
+ // Execute the task using the SDK's existing runtime loop
57
+ const result = await this.runtime.execute(data.command);
58
+
59
+ // Send result back to MCP server
60
+ this.sendResponse(data.requestId, result);
61
+ }
62
+ } catch (err) {
63
+ logger.error('MCPBridge', 'Error handling message:', err);
64
+ }
65
+ };
66
+
67
+ this.ws.onclose = () => {
68
+ if (!this.isDestroyed) {
69
+ logger.warn('MCPBridge', 'Disconnected from MCP bridge. Reconnecting in 5s...');
70
+ this.ws = null;
71
+ this.scheduleReconnect();
72
+ }
73
+ };
74
+
75
+ this.ws.onerror = (e) => {
76
+ logger.warn('MCPBridge', 'WebSocket error:', e);
77
+ // onclose will handle reconnect
78
+ };
79
+ }
80
+
81
+ private sendResponse(requestId: string, payload: any) {
82
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
83
+ this.ws.send(JSON.stringify({
84
+ type: 'response',
85
+ requestId,
86
+ payload,
87
+ }));
88
+ }
89
+ }
90
+
91
+ private scheduleReconnect() {
92
+ if (!this.reconnectTimer) {
93
+ this.reconnectTimer = setTimeout(() => {
94
+ this.connect();
95
+ }, 5000);
96
+ }
97
+ }
98
+
99
+ public destroy() {
100
+ this.isDestroyed = true;
101
+ if (this.reconnectTimer) {
102
+ clearTimeout(this.reconnectTimer);
103
+ this.reconnectTimer = null;
104
+ }
105
+ if (this.ws) {
106
+ this.ws.close();
107
+ this.ws = null;
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ScreenDehydrator — Converts discovered interactive elements into
3
+ * a text representation for the LLM, matching page-agent.js format.
4
+ *
5
+ * Output example:
6
+ * ```
7
+ * Screen: Home | Available screens: Home, Menu, Cart
8
+ * Interactive elements:
9
+ * [0]<pressable>🍕 Pizzas</>
10
+ * [1]<pressable>🍔 Burgers</>
11
+ * [2]<pressable>🥤 Drinks</>
12
+ * [3]<pressable>🛒 View Cart</>
13
+ * ```
14
+ */
15
+
16
+ import type { InteractiveElement, DehydratedScreen } from './types';
17
+
18
+ /**
19
+ * Dehydrate the current screen state into a text format for the LLM.
20
+ */
21
+ export function dehydrateScreen(
22
+ screenName: string,
23
+ availableScreens: string[],
24
+ elementsText: string,
25
+ elements: InteractiveElement[],
26
+ ): DehydratedScreen {
27
+ const lines: string[] = [];
28
+
29
+ // Header
30
+ lines.push(`Screen: ${screenName} | Available screens: ${availableScreens.join(', ')}`);
31
+ lines.push('');
32
+
33
+ if (!elementsText || elementsText.trim().length === 0) {
34
+ if (elements.length === 0) {
35
+ lines.push('No interactive elements or visible text detected on this screen.');
36
+ } else {
37
+ lines.push('Interactive elements:');
38
+ lines.push(elementsText);
39
+ }
40
+ } else {
41
+ lines.push('Screen Layout & Elements:');
42
+ lines.push(elementsText);
43
+ }
44
+
45
+ const finalElementsText = lines.join('\n');
46
+
47
+ return {
48
+ screenName,
49
+ availableScreens,
50
+ elementsText: finalElementsText,
51
+ elements,
52
+ };
53
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Core types for the page-agent-style React Native AI SDK.
3
+ */
4
+
5
+ // ─── Interactive Element (discovered from Fiber tree) ─────────
6
+
7
+ export type ElementType = 'pressable' | 'text-input' | 'switch' | 'scrollable';
8
+
9
+ export interface InteractiveElement {
10
+ /** Unique index assigned during tree walk */
11
+ index: number;
12
+ /** Element type */
13
+ type: ElementType;
14
+ /** Human-readable label (extracted from Text children or accessibilityLabel) */
15
+ label: string;
16
+ /** Reference to the Fiber node for execution */
17
+ fiberNode: any;
18
+ /** Key props snapshot */
19
+ props: {
20
+ onPress?: (...args: any[]) => void;
21
+ onChangeText?: (text: string) => void;
22
+ value?: string;
23
+ placeholder?: string;
24
+ checked?: boolean;
25
+ disabled?: boolean;
26
+ accessibilityLabel?: string;
27
+ accessibilityRole?: string;
28
+ };
29
+ }
30
+
31
+ // ─── Dehydrated Screen State ──────────────────────────────────
32
+
33
+ export interface DehydratedScreen {
34
+ /** Current screen name (from navigation state) */
35
+ screenName: string;
36
+ /** All available screen names (from routeNames) */
37
+ availableScreens: string[];
38
+ /** Indexed interactive elements as text */
39
+ elementsText: string;
40
+ /** Raw elements array */
41
+ elements: InteractiveElement[];
42
+ }
43
+
44
+ // ─── Agent Types ──────────────────────────────────────────────
45
+
46
+ export interface AgentStep {
47
+ stepIndex: number;
48
+ reflection: {
49
+ evaluationPreviousGoal: string;
50
+ memory: string;
51
+ nextGoal: string;
52
+ };
53
+ action: {
54
+ name: string;
55
+ input: Record<string, any>;
56
+ output: string;
57
+ };
58
+ }
59
+
60
+ export interface AgentConfig {
61
+ apiKey: string;
62
+ model?: string;
63
+ language?: 'en' | 'ar';
64
+
65
+ /** Maximum steps per task (page-agent default: 40) */
66
+ maxSteps?: number;
67
+
68
+ // ─── Element Gating (mirrors page-agent interactiveBlacklist/Whitelist) ──
69
+
70
+ /**
71
+ * React refs of elements the AI must NOT interact with.
72
+ * Mirrors page-agent.js `interactiveBlacklist: Element[]`.
73
+ * The Fiber walker skips any node whose ref matches one in this list.
74
+ */
75
+ interactiveBlacklist?: React.RefObject<any>[];
76
+
77
+ /**
78
+ * If set, the AI can ONLY interact with these elements.
79
+ * Mirrors page-agent.js `interactiveWhitelist: Element[]`.
80
+ */
81
+ interactiveWhitelist?: React.RefObject<any>[];
82
+
83
+ // ─── Lifecycle Hooks (mirrors page-agent onBefore/AfterStep/Task) ───────
84
+
85
+ /** Called before each agent step. */
86
+ onBeforeStep?: (stepCount: number) => Promise<void> | void;
87
+
88
+ /** Called after each agent step with full history. */
89
+ onAfterStep?: (history: AgentStep[]) => Promise<void> | void;
90
+
91
+ /** Called before task execution starts. */
92
+ onBeforeTask?: () => Promise<void> | void;
93
+
94
+ /** Called after task completes (success or failure). */
95
+ onAfterTask?: (result: ExecutionResult) => Promise<void> | void;
96
+
97
+ // ─── Content Masking (mirrors page-agent transformPageContent) ──────────
98
+
99
+ /**
100
+ * Transform dehydrated screen text before sending to LLM.
101
+ * Use to mask sensitive data (credit cards, PII, etc).
102
+ * Mirrors page-agent.js `transformPageContent`.
103
+ */
104
+ transformScreenContent?: (content: string) => Promise<string> | string;
105
+
106
+ // ─── Custom Tools (mirrors page-agent customTools) ─────────────────────
107
+
108
+ /**
109
+ * Override or remove built-in tools.
110
+ * Set tool to `null` to remove it (e.g., `{ navigate: null }`).
111
+ * Mirrors page-agent.js `customTools: Record<string, Tool | null>`.
112
+ */
113
+ customTools?: Record<string, ToolDefinition | null>;
114
+
115
+ // ─── Instructions (mirrors page-agent instructions) ────────────────────
116
+
117
+ /** Instructions to guide the agent's behavior. */
118
+ instructions?: {
119
+ /** Global system-level instructions, applied to all tasks. */
120
+ system?: string;
121
+ /**
122
+ * Dynamic per-screen instructions callback.
123
+ * Called before each step to get instructions for the current screen.
124
+ * Mirrors page-agent.js `getPageInstructions(url)`.
125
+ */
126
+ getScreenInstructions?: (screenName: string) => string | undefined | null;
127
+ };
128
+
129
+ /** Delay between steps in ms (page-agent default: 400ms). */
130
+ stepDelay?: number;
131
+
132
+ // ─── MCP Bridge Integration ──────────────────────────────────────────────
133
+
134
+ /**
135
+ * Optional URL of the companion Node.js MCP server bridge (e.g. ws://localhost:3101).
136
+ * If set, the SDK will connect to this server and listen for execution requests
137
+ * from external AI agents (like OpenClaw, Claude Desktop, etc).
138
+ */
139
+ mcpServerUrl?: string;
140
+ }
141
+
142
+ export interface ExecutionResult {
143
+ success: boolean;
144
+ message: string;
145
+ steps: AgentStep[];
146
+ }
147
+
148
+ // ─── Tool Types ───────────────────────────────────────────────
149
+
150
+ export interface ToolDefinition {
151
+ name: string;
152
+ description: string;
153
+ parameters: Record<string, ToolParam>;
154
+ execute: (args: Record<string, any>) => Promise<string>;
155
+ }
156
+
157
+ export interface ToolParam {
158
+ type: 'string' | 'number' | 'boolean';
159
+ description: string;
160
+ required?: boolean;
161
+ enum?: string[];
162
+ }
163
+
164
+ // ─── Action (optional useAction hook) ─────────────────────────
165
+
166
+ export interface ActionDefinition {
167
+ name: string;
168
+ description: string;
169
+ parameters: Record<string, string>;
170
+ handler: (args: Record<string, any>) => any;
171
+ }
172
+
173
+ // ─── Provider Interface ──────────────────────────────────────
174
+
175
+ export interface AIProvider {
176
+ generateContent(
177
+ systemPrompt: string,
178
+ userMessage: string,
179
+ tools: ToolDefinition[],
180
+ history: AgentStep[],
181
+ ): Promise<{
182
+ toolCalls: Array<{ name: string; args: Record<string, any> }>;
183
+ text?: string;
184
+ }>;
185
+ }