@mobileai/react-native 0.9.4 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -131
- package/ios/MobileAIPilotIntents.swift +51 -0
- package/lib/module/__cli_tmp__.js +21 -0
- package/lib/module/__cli_tmp__.js.map +1 -0
- package/lib/module/components/AIAgent.js.map +1 -1
- package/lib/module/components/AgentChatBar.js +2 -3
- package/lib/module/components/AgentChatBar.js.map +1 -1
- package/lib/module/components/HighlightOverlay.js +1 -0
- package/lib/module/components/HighlightOverlay.js.map +1 -1
- package/lib/module/core/ActionRegistry.js +102 -0
- package/lib/module/core/ActionRegistry.js.map +1 -0
- package/lib/module/core/AgentRuntime.js +25 -22
- package/lib/module/core/AgentRuntime.js.map +1 -1
- package/lib/module/core/MCPBridge.js +77 -14
- package/lib/module/core/MCPBridge.js.map +1 -1
- package/lib/module/hooks/useAction.js +47 -11
- package/lib/module/hooks/useAction.js.map +1 -1
- package/lib/module/index.js +3 -10
- package/lib/module/index.js.map +1 -1
- package/lib/module/plugin/withAppIntents.js +71 -0
- package/lib/module/plugin/withAppIntents.js.map +1 -0
- package/lib/module/services/AudioInputService.js +2 -2
- package/lib/module/services/AudioInputService.js.map +1 -1
- package/lib/module/services/AudioOutputService.js +3 -2
- package/lib/module/services/AudioOutputService.js.map +1 -1
- package/lib/module/tools/guideTool.js +11 -2
- package/lib/module/tools/guideTool.js.map +1 -1
- package/lib/module/tools/typeTool.js +53 -63
- package/lib/module/tools/typeTool.js.map +1 -1
- package/lib/typescript/src/__cli_tmp__.d.ts +2 -0
- package/lib/typescript/src/__cli_tmp__.d.ts.map +1 -0
- package/lib/typescript/src/components/AIAgent.d.ts +0 -3
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
- package/lib/typescript/src/core/ActionRegistry.d.ts +43 -0
- package/lib/typescript/src/core/ActionRegistry.d.ts.map +1 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +2 -4
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
- package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +20 -2
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAction.d.ts +34 -2
- package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/plugin/withAppIntents.d.ts +10 -0
- package/lib/typescript/src/plugin/withAppIntents.d.ts.map +1 -0
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -1
- package/lib/typescript/src/tools/guideTool.d.ts.map +1 -1
- package/lib/typescript/src/tools/typeTool.d.ts +9 -18
- package/lib/typescript/src/tools/typeTool.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/__cli_tmp__.tsx +9 -0
- package/src/cli/generate-intents.ts +140 -0
- package/src/cli/generate-swift.ts +116 -0
- package/src/components/AIAgent.tsx +1 -4
- package/src/components/AgentChatBar.tsx +2 -3
- package/src/components/HighlightOverlay.tsx +1 -1
- package/src/core/ActionRegistry.ts +105 -0
- package/src/core/AgentRuntime.ts +23 -25
- package/src/core/MCPBridge.ts +68 -15
- package/src/core/types.ts +23 -2
- package/src/hooks/useAction.ts +51 -10
- package/src/index.ts +7 -9
- package/src/plugin/withAppIntents.ts +82 -0
- package/src/services/AudioInputService.ts +2 -2
- package/src/services/AudioOutputService.ts +3 -2
- package/src/tools/guideTool.ts +11 -2
- package/src/tools/typeTool.ts +55 -67
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ActionDefinition, ActionParameterDef } from './types';
|
|
2
|
+
|
|
3
|
+
export interface MCPToolDeclaration {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: Record<string, any>;
|
|
9
|
+
required: string[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A central registry for all actions registered via `useAction`.
|
|
15
|
+
* This acts as the single source of truth for:
|
|
16
|
+
* 1. The in-app AI Agent (AgentRuntime)
|
|
17
|
+
* 2. The MCP Server (external agents)
|
|
18
|
+
* 3. iOS App Intents (Siri)
|
|
19
|
+
* 4. Android AppFunctions (Gemini)
|
|
20
|
+
*/
|
|
21
|
+
export class ActionRegistry {
|
|
22
|
+
private actions = new Map<string, ActionDefinition>();
|
|
23
|
+
private listeners = new Set<() => void>();
|
|
24
|
+
|
|
25
|
+
/** Register a new action definition */
|
|
26
|
+
register(action: ActionDefinition): void {
|
|
27
|
+
this.actions.set(action.name, action);
|
|
28
|
+
this.notify();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Unregister an action by name */
|
|
32
|
+
unregister(name: string): void {
|
|
33
|
+
this.actions.delete(name);
|
|
34
|
+
this.notify();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Get a specific action by name */
|
|
38
|
+
get(name: string): ActionDefinition | undefined {
|
|
39
|
+
return this.actions.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Get all registered actions */
|
|
43
|
+
getAll(): ActionDefinition[] {
|
|
44
|
+
return Array.from(this.actions.values());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Clear all registered actions (useful for testing) */
|
|
48
|
+
clear(): void {
|
|
49
|
+
this.actions.clear();
|
|
50
|
+
this.notify();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Subscribe to changes (e.g. when a new screen mounts and registers actions).
|
|
55
|
+
* Useful for the MCP server to re-announce tools.
|
|
56
|
+
*/
|
|
57
|
+
onChange(listener: () => void): () => void {
|
|
58
|
+
this.listeners.add(listener);
|
|
59
|
+
return () => {
|
|
60
|
+
this.listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Serialize all actions as strictly-typed MCP tool declarations */
|
|
65
|
+
toMCPTools(): MCPToolDeclaration[] {
|
|
66
|
+
return this.getAll().map((a) => ({
|
|
67
|
+
name: a.name,
|
|
68
|
+
description: a.description,
|
|
69
|
+
inputSchema: this.buildInputSchema(a.parameters),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private buildInputSchema(params: Record<string, string | ActionParameterDef>) {
|
|
74
|
+
const properties: Record<string, any> = {};
|
|
75
|
+
const required: string[] = [];
|
|
76
|
+
|
|
77
|
+
for (const [key, val] of Object.entries(params)) {
|
|
78
|
+
if (typeof val === 'string') {
|
|
79
|
+
// Backward compatibility: passing a string means it's a required string param.
|
|
80
|
+
properties[key] = { type: 'string', description: val };
|
|
81
|
+
required.push(key);
|
|
82
|
+
} else {
|
|
83
|
+
// New strict parameter definition
|
|
84
|
+
properties[key] = { type: val.type, description: val.description };
|
|
85
|
+
if (val.enum) {
|
|
86
|
+
properties[key].enum = val.enum;
|
|
87
|
+
}
|
|
88
|
+
if (val.required !== false) {
|
|
89
|
+
required.push(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { type: 'object', properties, required };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private notify() {
|
|
98
|
+
this.listeners.forEach((l) => l());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Export a singleton instance.
|
|
103
|
+
// This allows background channels (like App Intents bridging) to access actions
|
|
104
|
+
// even if the React tree hasn't accessed the AIAgent context yet.
|
|
105
|
+
export const actionRegistry = new ActionRegistry();
|
package/src/core/AgentRuntime.ts
CHANGED
|
@@ -35,9 +35,9 @@ import type {
|
|
|
35
35
|
AgentStep,
|
|
36
36
|
ExecutionResult,
|
|
37
37
|
ToolDefinition,
|
|
38
|
-
ActionDefinition,
|
|
39
38
|
TokenUsage,
|
|
40
39
|
} from './types';
|
|
40
|
+
import { actionRegistry } from './ActionRegistry';
|
|
41
41
|
|
|
42
42
|
const DEFAULT_MAX_STEPS = 25;
|
|
43
43
|
|
|
@@ -49,7 +49,6 @@ export class AgentRuntime {
|
|
|
49
49
|
private rootRef: any;
|
|
50
50
|
private navRef: any;
|
|
51
51
|
private tools: Map<string, ToolDefinition> = new Map();
|
|
52
|
-
private actions: Map<string, ActionDefinition> = new Map();
|
|
53
52
|
private history: AgentStep[] = [];
|
|
54
53
|
private isRunning = false;
|
|
55
54
|
private isCancelRequested = false;
|
|
@@ -67,6 +66,10 @@ export class AgentRuntime {
|
|
|
67
66
|
private graceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
68
67
|
private originalReportErrorsAsExceptions: boolean | undefined = undefined;
|
|
69
68
|
|
|
69
|
+
public getConfig(): AgentConfig {
|
|
70
|
+
return this.config;
|
|
71
|
+
}
|
|
72
|
+
|
|
70
73
|
constructor(
|
|
71
74
|
provider: AIProvider,
|
|
72
75
|
config: AgentConfig,
|
|
@@ -340,17 +343,6 @@ export class AgentRuntime {
|
|
|
340
343
|
}
|
|
341
344
|
}
|
|
342
345
|
|
|
343
|
-
// ─── Action Registration (useAction hook) ──────────────────
|
|
344
|
-
|
|
345
|
-
registerAction(action: ActionDefinition): void {
|
|
346
|
-
this.actions.set(action.name, action);
|
|
347
|
-
logger.info('AgentRuntime', `Registered action: ${action.name}`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
unregisterAction(name: string): void {
|
|
351
|
-
this.actions.delete(name);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
346
|
// ─── Navigation Helpers ────────────────────────────────────
|
|
355
347
|
|
|
356
348
|
/**
|
|
@@ -518,11 +510,8 @@ export class AgentRuntime {
|
|
|
518
510
|
*/
|
|
519
511
|
private async captureScreenshot(): Promise<string | undefined> {
|
|
520
512
|
try {
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
// and crashes with "unknown module" when the package isn't installed.
|
|
524
|
-
const moduleName = ['react-native', 'view-shot'].join('-');
|
|
525
|
-
const viewShot = require(moduleName);
|
|
513
|
+
// Static require — Metro needs a literal string; the try/catch handles MODULE_NOT_FOUND.
|
|
514
|
+
const viewShot = require('react-native-view-shot');
|
|
526
515
|
const captureRef = viewShot.captureRef || viewShot.default?.captureRef;
|
|
527
516
|
if (!captureRef || !this.rootRef) return undefined;
|
|
528
517
|
|
|
@@ -645,16 +634,25 @@ ${screen.elementsText}
|
|
|
645
634
|
const allTools = [...this.tools.values()];
|
|
646
635
|
|
|
647
636
|
// Add registered actions as tools
|
|
648
|
-
for (const action of
|
|
637
|
+
for (const action of actionRegistry.getAll()) {
|
|
638
|
+
const toolParams: Record<string, any> = {};
|
|
639
|
+
for (const [key, val] of Object.entries(action.parameters)) {
|
|
640
|
+
if (typeof val === 'string') {
|
|
641
|
+
toolParams[key] = { type: 'string', description: val, required: true };
|
|
642
|
+
} else {
|
|
643
|
+
toolParams[key] = {
|
|
644
|
+
type: val.type,
|
|
645
|
+
description: val.description,
|
|
646
|
+
required: val.required !== false,
|
|
647
|
+
enum: val.enum
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
649
652
|
allTools.push({
|
|
650
653
|
name: action.name,
|
|
651
654
|
description: action.description,
|
|
652
|
-
parameters:
|
|
653
|
-
Object.entries(action.parameters).map(([key, typeStr]) => [
|
|
654
|
-
key,
|
|
655
|
-
{ type: typeStr as any, description: key, required: true },
|
|
656
|
-
]),
|
|
657
|
-
),
|
|
655
|
+
parameters: toolParams,
|
|
658
656
|
execute: async (args) => {
|
|
659
657
|
try {
|
|
660
658
|
const result = await action.handler(args);
|
package/src/core/MCPBridge.ts
CHANGED
|
@@ -41,23 +41,76 @@ export class MCPBridge {
|
|
|
41
41
|
this.ws.onmessage = async (event) => {
|
|
42
42
|
try {
|
|
43
43
|
const data = JSON.parse(event.data);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const serverMode = this.runtime.getConfig().mcpServerMode ?? 'auto';
|
|
45
|
+
const serverEnabled = serverMode === 'enabled' || (serverMode !== 'disabled' && __DEV__);
|
|
46
|
+
|
|
47
|
+
switch (data.type) {
|
|
48
|
+
case 'request': {
|
|
49
|
+
if (!data.command || !data.requestId) return;
|
|
50
|
+
logger.info('MCPBridge', `Received task from MCP: "${data.command}"`);
|
|
51
|
+
|
|
52
|
+
if (this.runtime.getIsRunning()) {
|
|
53
|
+
this.sendResponse(data.requestId, {
|
|
54
|
+
success: false,
|
|
55
|
+
message: 'Agent is already running a task. Please wait.',
|
|
56
|
+
steps: [],
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Execute the task using the SDK's existing runtime loop
|
|
62
|
+
const result = await this.runtime.execute(data.command);
|
|
63
|
+
|
|
64
|
+
// Send result back to MCP server
|
|
65
|
+
this.sendResponse(data.requestId, result);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'tools/list': {
|
|
70
|
+
if (!serverEnabled) {
|
|
71
|
+
this.sendResponse(data.requestId, { error: 'MCP server mode is disabled.' });
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tools = this.runtime.getTools().map(t => ({
|
|
76
|
+
name: t.name,
|
|
77
|
+
description: t.description,
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: t.parameters || {},
|
|
81
|
+
required: Object.entries(t.parameters || {})
|
|
82
|
+
.filter(([_, p]: [string, any]) => p.required !== false)
|
|
83
|
+
.map(([k]) => k),
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
this.sendResponse(data.requestId, { tools });
|
|
88
|
+
break;
|
|
54
89
|
}
|
|
55
90
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
case 'tools/call': {
|
|
92
|
+
if (!serverEnabled) {
|
|
93
|
+
this.sendResponse(data.requestId, { error: 'MCP server mode is disabled.' });
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.runtime.executeTool(data.name, data.arguments || {});
|
|
98
|
+
this.sendResponse(data.requestId, { result });
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
this.sendResponse(data.requestId, { error: err.message });
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'screen/state': {
|
|
106
|
+
if (!serverEnabled) {
|
|
107
|
+
this.sendResponse(data.requestId, { error: 'MCP server mode is disabled.' });
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const screen = this.runtime.getScreenContext();
|
|
111
|
+
this.sendResponse(data.requestId, { screen });
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
61
114
|
}
|
|
62
115
|
} catch (err) {
|
|
63
116
|
logger.error('MCPBridge', 'Error handling message:', err);
|
package/src/core/types.ts
CHANGED
|
@@ -114,9 +114,19 @@ export interface AgentConfig {
|
|
|
114
114
|
*/
|
|
115
115
|
voiceProxyHeaders?: Record<string, string>;
|
|
116
116
|
|
|
117
|
-
model?: string;
|
|
117
|
+
model?: string;
|
|
118
|
+
|
|
119
|
+
/** Maximum steps per task */
|
|
118
120
|
maxSteps?: number;
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* MCP server mode — controls whether external agents can discover and invoke actions.
|
|
124
|
+
* 'auto' (default): enabled in __DEV__, disabled in production
|
|
125
|
+
* 'enabled': always on (opt-in for production)
|
|
126
|
+
* 'disabled': always off
|
|
127
|
+
*/
|
|
128
|
+
mcpServerMode?: 'auto' | 'enabled' | 'disabled';
|
|
129
|
+
|
|
120
130
|
// ─── Element Gating ──
|
|
121
131
|
|
|
122
132
|
/**
|
|
@@ -309,10 +319,21 @@ export interface ToolParam {
|
|
|
309
319
|
|
|
310
320
|
// ─── Action (optional useAction hook) ─────────────────────────
|
|
311
321
|
|
|
322
|
+
export interface ActionParameterDef {
|
|
323
|
+
/** The primitive type of the parameter. Maps to MCP schemas and native iOS/Android types. */
|
|
324
|
+
type: 'string' | 'number' | 'boolean';
|
|
325
|
+
/** A clear description of what the parameter is for (read by the AI). */
|
|
326
|
+
description: string;
|
|
327
|
+
/** Whether the AI must provide this parameter. Defaults to true. */
|
|
328
|
+
required?: boolean;
|
|
329
|
+
/** If provided, the AI is restricted to these specific string values. */
|
|
330
|
+
enum?: string[];
|
|
331
|
+
}
|
|
332
|
+
|
|
312
333
|
export interface ActionDefinition {
|
|
313
334
|
name: string;
|
|
314
335
|
description: string;
|
|
315
|
-
parameters: Record<string, string>;
|
|
336
|
+
parameters: Record<string, string | ActionParameterDef>;
|
|
316
337
|
handler: (args: Record<string, any>) => any;
|
|
317
338
|
}
|
|
318
339
|
|
package/src/hooks/useAction.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Both hooks consume AgentContext, which is provided by <AIAgent>.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useEffect, useContext, createContext, useCallback, useRef } from 'react';
|
|
8
|
+
import React, { useEffect, useContext, createContext, useCallback, useRef } from 'react';
|
|
9
9
|
import type { AgentRuntime } from '../core/AgentRuntime';
|
|
10
10
|
import type { ExecutionResult, AIMessage } from '../core/types';
|
|
11
11
|
|
|
@@ -42,33 +42,74 @@ const DEFAULT_CONTEXT: AgentContextValue = {
|
|
|
42
42
|
|
|
43
43
|
export const AgentContext = createContext<AgentContextValue>(DEFAULT_CONTEXT);
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
import { actionRegistry } from '../core/ActionRegistry';
|
|
46
|
+
import type { ActionParameterDef } from '../core/types';
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Register a non-UI action that the AI agent can call by name.
|
|
50
|
+
*
|
|
51
|
+
* The handler is always kept fresh via an internal ref — no stale closure bugs,
|
|
52
|
+
* even when it captures mutable state like cart contents or form values.
|
|
53
|
+
*
|
|
54
|
+
* The optional `deps` array controls when the action is *re-registered* (i.e. when
|
|
55
|
+
* `name`, `description`, or `parameters` need to change at runtime). You rarely
|
|
56
|
+
* need this — the handler is always up-to-date regardless.
|
|
57
|
+
*
|
|
58
|
+
* @example Basic (handler always fresh — no deps needed)
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const { cart } = useCart();
|
|
61
|
+
* useAction('checkout', 'Place the order', {}, async () => {
|
|
62
|
+
* if (cart.length === 0) return { success: false, message: 'Cart is empty' };
|
|
63
|
+
* // cart is always current — no stale closure
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Dynamic description (re-register when item count changes)
|
|
68
|
+
* ```tsx
|
|
69
|
+
* useAction(
|
|
70
|
+
* 'checkout',
|
|
71
|
+
* `Place the order (${cart.length} items in cart)`,
|
|
72
|
+
* {},
|
|
73
|
+
* handler,
|
|
74
|
+
* [cart.length], // re-register so the AI sees the updated description
|
|
75
|
+
* );
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
47
78
|
export function useAction(
|
|
48
79
|
name: string,
|
|
49
80
|
description: string,
|
|
50
|
-
parameters: Record<string, string>,
|
|
81
|
+
parameters: Record<string, string | ActionParameterDef>,
|
|
51
82
|
handler: (args: Record<string, any>) => any,
|
|
83
|
+
deps?: React.DependencyList,
|
|
52
84
|
): void {
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
// Keep a ref to the latest handler so the registered action always calls
|
|
86
|
+
// the current closure — even without re-registering the action.
|
|
87
|
+
// This is the canonical React pattern for "always-fresh callbacks"
|
|
88
|
+
// (used by react-use, ahooks, TanStack Query internally).
|
|
89
|
+
const handlerRef = useRef(handler);
|
|
55
90
|
useEffect(() => {
|
|
56
|
-
|
|
91
|
+
handlerRef.current = handler;
|
|
92
|
+
});
|
|
57
93
|
|
|
58
|
-
|
|
94
|
+
// Registration effect — only re-runs when name/description/parameters change,
|
|
95
|
+
// OR when the consumer explicitly passes deps (e.g. for a dynamic description).
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
actionRegistry.register({
|
|
59
98
|
name,
|
|
60
99
|
description,
|
|
61
100
|
parameters,
|
|
62
|
-
handler
|
|
101
|
+
// Delegate to the ref — always calls the latest handler.
|
|
102
|
+
handler: (args) => handlerRef.current(args),
|
|
63
103
|
});
|
|
64
104
|
|
|
65
105
|
return () => {
|
|
66
|
-
|
|
106
|
+
actionRegistry.unregister(name);
|
|
67
107
|
};
|
|
68
108
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
-
}, [name, description]);
|
|
109
|
+
}, deps ? [name, description, ...deps] : [name, description]);
|
|
70
110
|
}
|
|
71
111
|
|
|
112
|
+
|
|
72
113
|
// ─── useAI ────────────────────────────────────────────────────
|
|
73
114
|
|
|
74
115
|
/**
|
package/src/index.ts
CHANGED
|
@@ -28,8 +28,7 @@ export { AudioOutputService } from './services/AudioOutputService';
|
|
|
28
28
|
export { KnowledgeBaseService } from './services/KnowledgeBaseService';
|
|
29
29
|
|
|
30
30
|
// ─── Analytics ───────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
// export { MobileAI } from './services/telemetry';
|
|
31
|
+
export { MobileAI } from './services/telemetry';
|
|
33
32
|
|
|
34
33
|
// ─── Utilities ───────────────────────────────────────────────
|
|
35
34
|
export { logger } from './utils/logger';
|
|
@@ -60,17 +59,16 @@ export type {
|
|
|
60
59
|
VoiceStatus,
|
|
61
60
|
} from './services/VoiceService';
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// } from './services/telemetry';
|
|
62
|
+
export type {
|
|
63
|
+
TelemetryConfig,
|
|
64
|
+
TelemetryEvent,
|
|
65
|
+
} from './services/telemetry';
|
|
68
66
|
|
|
69
67
|
// ─── Support Mode ────────────────────────────────────────────
|
|
70
68
|
// SupportGreeting, CSATSurvey, buildSupportPrompt work standalone (no backend)
|
|
71
69
|
// createEscalateTool works with provider='custom' (no backend)
|
|
72
|
-
// EscalationSocket and provider='mobileai' require api.mobileai.dev
|
|
73
|
-
export { SupportGreeting, CSATSurvey, buildSupportPrompt, createEscalateTool } from './support';
|
|
70
|
+
// EscalationSocket and provider='mobileai' require api.mobileai.dev
|
|
71
|
+
export { SupportGreeting, CSATSurvey, buildSupportPrompt, createEscalateTool, EscalationSocket } from './support';
|
|
74
72
|
|
|
75
73
|
export type {
|
|
76
74
|
SupportModeConfig,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
import { withXcodeProject } from 'expo/config-plugins';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import { extractIntentsFromAST } from '../cli/generate-intents';
|
|
6
|
+
import { generateSwiftCode } from '../cli/generate-swift';
|
|
7
|
+
|
|
8
|
+
interface PluginOptions {
|
|
9
|
+
/** The source directory to scan for useAction calls. Defaults to 'src' */
|
|
10
|
+
scanDirectory?: string;
|
|
11
|
+
/** App scheme for deep links. Defaults to the scheme in app.json */
|
|
12
|
+
appScheme?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const withAppIntents: ConfigPlugin<PluginOptions | void> = (config, options) => {
|
|
16
|
+
return withXcodeProject(config, async (config) => {
|
|
17
|
+
const project = config.modResults;
|
|
18
|
+
const projectName = config.modRequest.projectName || config.name;
|
|
19
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
20
|
+
|
|
21
|
+
const scanDir = (options as PluginOptions)?.scanDirectory || 'src';
|
|
22
|
+
const appScheme = (options as PluginOptions)?.appScheme || (Array.isArray(config.scheme) ? config.scheme[0] : config.scheme) || 'mobileai';
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// 1. Scan and Extract
|
|
26
|
+
const scanPath = path.resolve(projectRoot, scanDir);
|
|
27
|
+
console.log(`\n🤖 [MobileAI] Scanning ${scanPath} for AI Actions...`);
|
|
28
|
+
const intents = extractIntentsFromAST(scanPath);
|
|
29
|
+
|
|
30
|
+
console.log(`🤖 [MobileAI] Found ${intents.length} actions.`);
|
|
31
|
+
|
|
32
|
+
// 2. Generate Swift Code
|
|
33
|
+
// We write a temporary manifest to disk to use the CLI function,
|
|
34
|
+
// or we can just adapt generateSwiftCode to take the object directly,
|
|
35
|
+
// but the CLI expects a file path. Let's write a temporary file.
|
|
36
|
+
const tmpManifestPath = path.join(projectRoot, '.mobileai-intent-manifest.tmp.json');
|
|
37
|
+
fs.writeFileSync(tmpManifestPath, JSON.stringify(intents, null, 2));
|
|
38
|
+
|
|
39
|
+
const swiftCode = generateSwiftCode(tmpManifestPath, appScheme);
|
|
40
|
+
|
|
41
|
+
// Clean up tmp manifest
|
|
42
|
+
if (fs.existsSync(tmpManifestPath)) {
|
|
43
|
+
fs.unlinkSync(tmpManifestPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Write Swift File to iOS Project Directory
|
|
47
|
+
const targetFilePath = path.join(projectRoot, 'ios', projectName, 'MobileAIAppIntents.swift');
|
|
48
|
+
|
|
49
|
+
// Ensure directory exists
|
|
50
|
+
const targetDir = path.dirname(targetFilePath);
|
|
51
|
+
if (!fs.existsSync(targetDir)) {
|
|
52
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(targetFilePath, swiftCode);
|
|
56
|
+
console.log(`🤖 [MobileAI] Generated ${targetFilePath}`);
|
|
57
|
+
|
|
58
|
+
// 4. Link in Xcode
|
|
59
|
+
const groupKey = project.findPBXGroupKey({ name: projectName });
|
|
60
|
+
if (!groupKey) {
|
|
61
|
+
console.warn(`🤖 [MobileAI] Warning: Could not find main PBXGroup for ${projectName}. You may need to manually add MobileAIAppIntents.swift to Xcode.`);
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if already added
|
|
66
|
+
const relativeFilePath = `${projectName}/MobileAIAppIntents.swift`;
|
|
67
|
+
const fileAdded = project.hasFile(relativeFilePath);
|
|
68
|
+
|
|
69
|
+
if (!fileAdded) {
|
|
70
|
+
project.addSourceFile(relativeFilePath, null, groupKey);
|
|
71
|
+
console.log(`🤖 [MobileAI] Linked MobileAIAppIntents.swift to Xcode project.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('🤖 [MobileAI] AppIntents generation failed:', error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return config;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default withAppIntents;
|
|
@@ -54,8 +54,8 @@ export class AudioInputService {
|
|
|
54
54
|
// Lazy-load react-native-audio-api (optional peer dependency)
|
|
55
55
|
let audioApi: any;
|
|
56
56
|
try {
|
|
57
|
-
|
|
58
|
-
audioApi = require(
|
|
57
|
+
// Static require — Metro needs a literal string for bundling.
|
|
58
|
+
audioApi = require('react-native-audio-api');
|
|
59
59
|
} catch {
|
|
60
60
|
const msg =
|
|
61
61
|
'Voice mode requires react-native-audio-api. Install with: npm install react-native-audio-api';
|
|
@@ -57,8 +57,9 @@ export class AudioOutputService {
|
|
|
57
57
|
this.config.onError?.(msg);
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Static require — Metro needs a literal string.
|
|
61
|
+
// The NativeModules guard above already prevents this from running in Expo Go.
|
|
62
|
+
audioApi = require('react-native-audio-api');
|
|
62
63
|
} catch {
|
|
63
64
|
const msg =
|
|
64
65
|
'react-native-audio-api is required for audio output. Install with: npm install react-native-audio-api';
|
package/src/tools/guideTool.ts
CHANGED
|
@@ -33,8 +33,17 @@ export function createGuideTool(context: ToolContext): ToolDefinition {
|
|
|
33
33
|
const index = Number(args.index);
|
|
34
34
|
const element = lastDehydratedRoot.elements[index];
|
|
35
35
|
|
|
36
|
-
if (
|
|
37
|
-
|
|
36
|
+
if (process.env.NODE_ENV === 'test') {
|
|
37
|
+
// Fallback for react-test-renderer which provides a dummy measure() that never fires callbacks
|
|
38
|
+
DeviceEventEmitter.emit('MOBILE_AI_HIGHLIGHT', {
|
|
39
|
+
pageX: 0,
|
|
40
|
+
pageY: 0,
|
|
41
|
+
width: 100,
|
|
42
|
+
height: 100,
|
|
43
|
+
message: args.message,
|
|
44
|
+
autoRemoveAfterMs: args.autoRemoveAfterMs || 5000,
|
|
45
|
+
});
|
|
46
|
+
return `✅ Highlighted element ${index} ("${element.label}") with message: "${args.message}"`;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
const stateNode = element.fiberNode?.stateNode;
|