@mobileai/react-native 0.9.5 → 0.9.10

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 (64) hide show
  1. package/README.md +73 -131
  2. package/ios/MobileAIPilotIntents.swift +51 -0
  3. package/lib/module/__cli_tmp__.js +21 -0
  4. package/lib/module/__cli_tmp__.js.map +1 -0
  5. package/lib/module/components/AIAgent.js.map +1 -1
  6. package/lib/module/components/AgentChatBar.js +2 -3
  7. package/lib/module/components/AgentChatBar.js.map +1 -1
  8. package/lib/module/components/HighlightOverlay.js +1 -0
  9. package/lib/module/components/HighlightOverlay.js.map +1 -1
  10. package/lib/module/core/ActionRegistry.js +102 -0
  11. package/lib/module/core/ActionRegistry.js.map +1 -0
  12. package/lib/module/core/AgentRuntime.js +25 -22
  13. package/lib/module/core/AgentRuntime.js.map +1 -1
  14. package/lib/module/core/MCPBridge.js +77 -14
  15. package/lib/module/core/MCPBridge.js.map +1 -1
  16. package/lib/module/hooks/useAction.js +47 -11
  17. package/lib/module/hooks/useAction.js.map +1 -1
  18. package/lib/module/index.js +3 -10
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/plugin/withAppIntents.js +71 -0
  21. package/lib/module/plugin/withAppIntents.js.map +1 -0
  22. package/lib/module/services/AudioInputService.js +2 -2
  23. package/lib/module/services/AudioInputService.js.map +1 -1
  24. package/lib/module/services/AudioOutputService.js +3 -2
  25. package/lib/module/services/AudioOutputService.js.map +1 -1
  26. package/lib/module/tools/guideTool.js +11 -2
  27. package/lib/module/tools/guideTool.js.map +1 -1
  28. package/lib/typescript/src/__cli_tmp__.d.ts +2 -0
  29. package/lib/typescript/src/__cli_tmp__.d.ts.map +1 -0
  30. package/lib/typescript/src/components/AIAgent.d.ts +0 -3
  31. package/lib/typescript/src/components/AIAgent.d.ts.map +1 -1
  32. package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -1
  33. package/lib/typescript/src/core/ActionRegistry.d.ts +43 -0
  34. package/lib/typescript/src/core/ActionRegistry.d.ts.map +1 -0
  35. package/lib/typescript/src/core/AgentRuntime.d.ts +2 -4
  36. package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -1
  37. package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -1
  38. package/lib/typescript/src/core/types.d.ts +20 -2
  39. package/lib/typescript/src/core/types.d.ts.map +1 -1
  40. package/lib/typescript/src/hooks/useAction.d.ts +34 -2
  41. package/lib/typescript/src/hooks/useAction.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +3 -1
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/plugin/withAppIntents.d.ts +10 -0
  45. package/lib/typescript/src/plugin/withAppIntents.d.ts.map +1 -0
  46. package/lib/typescript/src/services/AudioOutputService.d.ts.map +1 -1
  47. package/lib/typescript/src/tools/guideTool.d.ts.map +1 -1
  48. package/package.json +4 -1
  49. package/src/__cli_tmp__.tsx +9 -0
  50. package/src/cli/generate-intents.ts +140 -0
  51. package/src/cli/generate-swift.ts +116 -0
  52. package/src/components/AIAgent.tsx +1 -4
  53. package/src/components/AgentChatBar.tsx +2 -3
  54. package/src/components/HighlightOverlay.tsx +1 -1
  55. package/src/core/ActionRegistry.ts +105 -0
  56. package/src/core/AgentRuntime.ts +23 -25
  57. package/src/core/MCPBridge.ts +68 -15
  58. package/src/core/types.ts +23 -2
  59. package/src/hooks/useAction.ts +51 -10
  60. package/src/index.ts +7 -9
  61. package/src/plugin/withAppIntents.ts +82 -0
  62. package/src/services/AudioInputService.ts +2 -2
  63. package/src/services/AudioOutputService.ts +3 -2
  64. package/src/tools/guideTool.ts +11 -2
@@ -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
- // ─── useAction ────────────────────────────────────────────────
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
- const { runtime: agentRuntime } = useContext(AgentContext);
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
- if (!agentRuntime) return;
91
+ handlerRef.current = handler;
92
+ });
57
93
 
58
- agentRuntime.registerAction({
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
- agentRuntime.unregisterAction(name);
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
- // Requires api.mobileai.dev hidden until backend is live
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
- // Requires api.mobileai.dev — hidden until backend is live
64
- // export type {
65
- // TelemetryConfig,
66
- // TelemetryEvent,
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 — hidden
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
- const audioApiModule = ['react-native', 'audio-api'].join('-');
58
- audioApi = require(audioApiModule);
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
- const audioApiModule = ['react-native', 'audio-api'].join('-');
61
- audioApi = require(audioApiModule);
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';
@@ -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 (!element) {
37
- return `❌ Element at index ${index} not found on current screen.`;
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;