@ateam-ai-mobile/expo 0.1.19 → 0.1.21

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 (76) hide show
  1. package/dist/aiLive/provider.d.mts +20 -0
  2. package/dist/aiLive/provider.d.ts +20 -0
  3. package/dist/aiLive/provider.js +400 -0
  4. package/dist/aiLive/provider.js.map +1 -0
  5. package/dist/aiLive/provider.mjs +15 -0
  6. package/dist/aiLive/provider.mjs.map +1 -0
  7. package/dist/bootstrap.js +350 -3
  8. package/dist/bootstrap.js.map +1 -1
  9. package/dist/bootstrap.mjs +17 -12
  10. package/dist/bootstrap.mjs.map +1 -1
  11. package/dist/capabilities/index.d.mts +40 -1
  12. package/dist/capabilities/index.d.ts +40 -1
  13. package/dist/capabilities/index.js +50 -14
  14. package/dist/capabilities/index.js.map +1 -1
  15. package/dist/capabilities/index.mjs +49 -32
  16. package/dist/capabilities/index.mjs.map +1 -1
  17. package/dist/{chunk-42FP5DTO.mjs → chunk-2ZQEQJQ6.mjs} +1 -1
  18. package/dist/chunk-2ZQEQJQ6.mjs.map +1 -0
  19. package/dist/chunk-AE55BKYV.mjs +24 -0
  20. package/dist/chunk-AE55BKYV.mjs.map +1 -0
  21. package/dist/chunk-BAZFMTCC.mjs +815 -0
  22. package/dist/chunk-BAZFMTCC.mjs.map +1 -0
  23. package/dist/{chunk-Z5AI2SXX.mjs → chunk-C62NQA4F.mjs} +203 -1004
  24. package/dist/chunk-C62NQA4F.mjs.map +1 -0
  25. package/dist/{chunk-DE6Y4VRI.mjs → chunk-GA7DS3PA.mjs} +1 -1
  26. package/dist/{chunk-DE6Y4VRI.mjs.map → chunk-GA7DS3PA.mjs.map} +1 -1
  27. package/dist/chunk-GW7YGXOW.mjs +47 -0
  28. package/dist/chunk-GW7YGXOW.mjs.map +1 -0
  29. package/dist/chunk-JUBLG3PN.mjs +351 -0
  30. package/dist/chunk-JUBLG3PN.mjs.map +1 -0
  31. package/dist/{chunk-Z3P7RUCC.mjs → chunk-OEX4OKYQ.mjs} +13 -13
  32. package/dist/{chunk-6JX222YX.mjs → chunk-WQK6PUBN.mjs} +9 -2
  33. package/dist/chunk-WQK6PUBN.mjs.map +1 -0
  34. package/dist/device/createDeviceProvider.js.map +1 -1
  35. package/dist/device/createDeviceProvider.mjs +7 -6
  36. package/dist/device-bridge/index.mjs +8 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +1 -1
  39. package/dist/push/index.d.mts +24 -0
  40. package/dist/push/index.d.ts +24 -0
  41. package/dist/push/index.js +224 -0
  42. package/dist/push/index.js.map +1 -0
  43. package/dist/push/index.mjs +174 -0
  44. package/dist/push/index.mjs.map +1 -0
  45. package/dist/push/silentSync.d.mts +8 -0
  46. package/dist/push/silentSync.d.ts +8 -0
  47. package/dist/push/silentSync.js +1075 -0
  48. package/dist/push/silentSync.js.map +1 -0
  49. package/dist/push/silentSync.mjs +67 -0
  50. package/dist/push/silentSync.mjs.map +1 -0
  51. package/dist/vision/index.d.mts +7 -1
  52. package/dist/vision/index.d.ts +7 -1
  53. package/dist/vision/index.js +8 -0
  54. package/dist/vision/index.js.map +1 -1
  55. package/dist/vision/index.mjs +3 -1
  56. package/ios/AdaVisionModule.swift +44 -1
  57. package/package.json +28 -6
  58. package/src/aiLive/provider.ts +242 -0
  59. package/src/aiLive/streamProtocol.ts +90 -0
  60. package/src/bootstrap.ts +7 -1
  61. package/src/capabilities/choices.ts +65 -0
  62. package/src/capabilities/config.ts +5 -4
  63. package/src/capabilities/index.ts +2 -0
  64. package/src/capabilities/picturesToAi.ts +40 -0
  65. package/src/capabilities/store.ts +38 -37
  66. package/src/index.ts +3 -0
  67. package/src/push/index.ts +18 -0
  68. package/src/push/messaging.ts +49 -0
  69. package/src/push/registration.ts +31 -0
  70. package/src/push/silentSync.ts +83 -0
  71. package/src/push/usePushNotifications.ts +224 -0
  72. package/src/vision/index.ts +14 -0
  73. package/dist/chunk-42FP5DTO.mjs.map +0 -1
  74. package/dist/chunk-6JX222YX.mjs.map +0 -1
  75. package/dist/chunk-Z5AI2SXX.mjs.map +0 -1
  76. /package/dist/{chunk-Z3P7RUCC.mjs.map → chunk-OEX4OKYQ.mjs.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/push/silentSync.ts","../../src/device-bridge/background.ts","../../src/device-bridge/relay-client.ts","../../src/device-bridge/collectors/calendar.ts","../../src/device-bridge/collectors/contacts.ts","../../src/device-bridge/collectors/location.ts","../../src/maps/index.ts","../../src/maps/normalize.ts","../../src/device-bridge/collectors/battery.ts","../../src/device-bridge/collectors/connectivity.ts","../../src/device-bridge/collectors/notifications.ts","../../src/device-bridge/collectors/health.ts","../../src/device-bridge/collectors/appEngagement.ts","../../src/device-bridge/collectors/camera.ts","../../src/device-bridge/capabilities.ts","../../src/device-bridge/deviceId.ts","../../src/capabilities/config.ts","../../src/capabilities/choices.ts","../../src/push/messaging.ts"],"sourcesContent":["/**\n * Silent-push device sync — the server wakes the phone to refresh device data.\n *\n * Call registerSilentPushSync({ loadToken }) once in your app's entry file,\n * before the app component registers: Firebase wants its background handler\n * set at the top level, outside React. It also needs device sync configured\n * (bootstrapExpoAdas({ deviceSync }) or configureDeviceSync) — read when a push\n * arrives, so the order of the two calls doesn't matter.\n *\n * Contract for a silent (data-only) push:\n * data: { action: \"sync\", domains: '[\"location\",\"calendar\"]' } (JSON string array)\n * Anything else is ignored here — visible pushes are usePushNotifications'.\n *\n * Syncs as the signed-in user and team from the SAVED session token (the app\n * may be woken with no session restored) and under this install's device id;\n * no token, no user, no team or no device id → nothing is sent. Only domains\n * the user shares are synced (their saved choices), as the foreground bridge does.\n *\n * Moved from ateam-mobile's pushBackgroundHandler (2026-10-29). Changed on the\n * way: it syncs only the domains the user turned on (it synced any domain the\n * push named), and never without a user (it would sync as 'default').\n */\nimport { syncDomains } from '../device-bridge/background';\nimport { readDeviceId } from '../device-bridge/deviceId';\nimport { getDeviceSyncConfig } from '../capabilities/config';\nimport { sharedDomains } from '../capabilities/choices';\nimport { tenantFromToken, userFromJwt } from '@ateam-ai-mobile/core';\nimport { getMessaging, type RemoteMessage } from './messaging';\n\nexport interface SilentPushSyncOptions {\n /** The saved session token — the same loader your auth session's persistence uses. */\n loadToken: () => Promise<string | null>;\n}\n\n/** Register the background handler. No Firebase in this build → nothing to do. */\nexport function registerSilentPushSync(opts: SilentPushSyncOptions): void {\n const messaging = getMessaging();\n if (!messaging) return;\n messaging().setBackgroundMessageHandler((msg) => handleSilentPush(msg, opts));\n}\n\nasync function handleSilentPush(msg: RemoteMessage, opts: SilentPushSyncOptions): Promise<void> {\n const data = msg?.data || {};\n if (data.action !== 'sync') return;\n\n let requested: unknown = data.domains;\n try {\n if (typeof requested === 'string') requested = JSON.parse(requested);\n } catch (err) {\n console.warn('[adas/push] silent sync: bad domains:', err);\n return;\n }\n if (!Array.isArray(requested)) return;\n const domains = await sharedDomains(requested.filter((d): d is string => typeof d === 'string'));\n if (domains.length === 0) return;\n\n const deviceSync = getDeviceSyncConfig();\n if (!deviceSync) {\n console.warn('[adas/push] silent sync skipped: device sync is not configured');\n return;\n }\n // No id = nothing has synced from this install yet, so nothing to refresh.\n const deviceId = await readDeviceId(deviceSync.secureStorage);\n if (!deviceId) return;\n\n // Identity AND team from the session token's own claims — never a stored\n // copy, never a default. Signed out → no token → nothing to sync as.\n const jwt = await opts.loadToken();\n if (!jwt) return;\n const tenant = tenantFromToken(jwt);\n const actorId = userFromJwt(jwt)?.actorId;\n if (!tenant || !actorId) return;\n\n console.log(`[adas/push] silent sync: ${domains.join(', ')}`);\n try {\n await syncDomains(\n { relayUrl: deviceSync.relayUrl(), deviceId, tenant, actorId, apiKey: deviceSync.relayKey },\n domains,\n );\n } catch (err) {\n console.warn('[adas/push] silent sync failed:', err);\n }\n}\n","/**\n * Background Sync — registers an expo-task-manager task for periodic\n * data sync when the app is in the background, and exposes a one-shot\n * `syncDomains()` for silent-push triggered syncs.\n *\n * iOS minimum background fetch interval is ~15 minutes.\n * Android allows more frequent background execution via WorkManager.\n *\n * Usage:\n * import { registerBackgroundSync, syncDomains } from '@mobile-pa/device-bridge';\n *\n * // Periodic background fetch — call once at app startup, outside any component\n * registerBackgroundSync({\n * relayUrl: 'https://relay.example.com',\n * deviceId: 'device-123',\n * tenant: 'mobile-pa',\n * actorId: 'user-abc',\n * apiKey: 'key-xxx',\n * });\n *\n * // Silent-push triggered sync — from FCM background message handler\n * await syncDomains(config, ['location', 'calendar']);\n */\n\nimport * as TaskManager from 'expo-task-manager';\nimport * as BackgroundFetch from 'expo-background-fetch';\nimport { RelayClient } from './relay-client';\nimport * as caps from './capabilities';\nimport type { Capability } from './types';\n\nexport const BACKGROUND_TASK_NAME = 'DEVICE_BRIDGE_BACKGROUND_SYNC';\n\nexport interface BackgroundSyncConfig {\n relayUrl: string;\n deviceId: string;\n tenant: string;\n actorId?: string;\n apiKey: string;\n}\n\nconst CAPABILITY_FACTORIES: Record<string, () => Capability> = {\n calendar: caps.calendarCapability,\n contacts: caps.contactsCapability,\n location: caps.locationCapability,\n battery: caps.batteryCapability,\n connectivity: caps.connectivityCapability,\n notifications: caps.notificationsCapability,\n health: caps.healthCapability,\n};\n\n/**\n * Register the periodic background-fetch task.\n * Call once at app startup, outside any React component.\n */\nexport async function registerBackgroundSync(\n config: BackgroundSyncConfig\n): Promise<void> {\n TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {\n try {\n const hasNew = await syncDomains(config, ['location', 'battery', 'calendar']);\n return hasNew\n ? BackgroundFetch.BackgroundFetchResult.NewData\n : BackgroundFetch.BackgroundFetchResult.NoData;\n } catch (error) {\n console.error('[DeviceBridge] Background sync failed:', error);\n return BackgroundFetch.BackgroundFetchResult.Failed;\n }\n });\n\n await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {\n minimumInterval: 300, // 5 minutes (iOS will clamp to ~15 min)\n stopOnTerminate: false,\n startOnBoot: true,\n });\n}\n\n/**\n * Unregister the background sync task.\n */\nexport async function unregisterBackgroundSync(): Promise<void> {\n const isRegistered = await TaskManager.isTaskRegisteredAsync(\n BACKGROUND_TASK_NAME\n );\n if (isRegistered) {\n await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);\n }\n}\n\n/**\n * Collect and push the listed domains once. Used by both the periodic\n * background-fetch task and the silent-push handler.\n *\n * Returns true if at least one domain pushed new data.\n */\nexport async function syncDomains(\n config: BackgroundSyncConfig,\n domains: string[]\n): Promise<boolean> {\n const relay = new RelayClient({\n relayUrl: config.relayUrl,\n deviceId: config.deviceId,\n tenant: config.tenant,\n actorId: config.actorId,\n apiKey: config.apiKey,\n timeout: 15_000, // longer than foreground — background tasks are budget-limited but not latency-sensitive\n });\n\n const results = await Promise.allSettled(\n domains.map(d => collectAndPush(relay, d))\n );\n\n return results.some(r => r.status === 'fulfilled' && r.value);\n}\n\n// ─── Internal ───\n\nasync function collectAndPush(\n relay: RelayClient,\n domain: string\n): Promise<boolean> {\n const factory = CAPABILITY_FACTORIES[domain];\n if (!factory) {\n console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);\n return false;\n }\n\n const cap = factory();\n\n if (cap.requiresPermission) {\n const granted = await cap.checkPermission();\n if (!granted) return false;\n }\n\n const data = await cap.collect();\n const result = await relay.pushDomain(domain, data);\n return result.ok;\n}\n","/**\n * Relay Client — REST client for the cloud relay service.\n * Handles data push, action polling, and offline buffering.\n *\n * Endpoints:\n * POST /api/device/{deviceId}/sync ← push data bundle\n * GET /api/device/{deviceId}/data/{domain} ← read latest (used by connector, not phone)\n * POST /api/device/{deviceId}/actions ← queue action (used by connector)\n * GET /api/device/{deviceId}/actions ← poll pending actions\n * DELETE /api/device/{deviceId}/actions/{id} ← confirm action executed\n */\n\nimport type {\n DeviceDataBundle,\n PendingAction,\n ActionResult,\n RelayResponse,\n} from './types';\n\nexport interface RelayClientConfig {\n relayUrl: string;\n deviceId: string;\n tenant: string;\n actorId?: string;\n apiKey?: string;\n timeout?: number; // ms, default 10000\n}\n\nexport class RelayClient {\n private baseUrl: string;\n private deviceId: string;\n private tenant: string;\n private actorId: string;\n private apiKey: string;\n private timeout: number;\n private offlineQueue: DeviceDataBundle[] = [];\n\n constructor(config: RelayClientConfig) {\n // Strip trailing slash\n this.baseUrl = config.relayUrl.replace(/\\/$/, '');\n this.deviceId = config.deviceId;\n this.tenant = config.tenant;\n this.actorId = config.actorId ?? 'default';\n this.apiKey = config.apiKey ?? '';\n this.timeout = config.timeout ?? 10_000;\n }\n\n // ─── Data Sync ───\n\n /**\n * Push a full device data bundle to the relay.\n * If offline, buffers the bundle and retries on next sync.\n */\n async pushData(bundle: DeviceDataBundle): Promise<RelayResponse<void>> {\n try {\n const response = await this.fetch(\n `/api/device/${this.deviceId}/sync`,\n {\n method: 'POST',\n body: JSON.stringify(bundle),\n }\n );\n\n if (response.ok) {\n // Flush any offline queue on successful connection\n await this.flushOfflineQueue();\n return { ok: true };\n }\n\n return {\n ok: false,\n error: `Relay returned ${response.status}: ${await response.text()}`,\n };\n } catch (error) {\n // Network error — buffer for later\n this.offlineQueue.push(bundle);\n return {\n ok: false,\n error: `Offline — buffered (${this.offlineQueue.length} pending)`,\n };\n }\n }\n\n /**\n * Push a single domain update (partial sync).\n */\n async pushDomain(\n domain: string,\n data: unknown\n ): Promise<RelayResponse<void>> {\n try {\n const response = await this.fetch(\n `/api/device/${this.deviceId}/sync/${domain}`,\n {\n method: 'POST',\n body: JSON.stringify({ domain, data, timestamp: new Date().toISOString(), actorId: this.actorId }),\n }\n );\n\n if (response.ok) return { ok: true };\n return {\n ok: false,\n error: `Relay returned ${response.status}`,\n };\n } catch (error) {\n return { ok: false, error: 'Network error — domain sync failed' };\n }\n }\n\n // ─── Action Queue ───\n\n /**\n * Poll for pending actions the phone needs to execute.\n */\n async pollActions(): Promise<RelayResponse<PendingAction[]>> {\n try {\n const response = await this.fetch(\n `/api/device/${this.deviceId}/actions`,\n { method: 'GET' }\n );\n\n if (response.ok) {\n const data = await response.json();\n return { ok: true, data: data.actions ?? [] };\n }\n return { ok: false, error: `Relay returned ${response.status}` };\n } catch (error) {\n return { ok: false, error: 'Network error — action poll failed' };\n }\n }\n\n /**\n * Report action execution result and remove from queue.\n */\n async completeAction(result: ActionResult): Promise<RelayResponse<void>> {\n try {\n const response = await this.fetch(\n `/api/device/${this.deviceId}/actions/${result.id}`,\n {\n method: 'DELETE',\n body: JSON.stringify(result),\n }\n );\n\n if (response.ok) return { ok: true };\n return { ok: false, error: `Relay returned ${response.status}` };\n } catch (error) {\n return { ok: false, error: 'Network error — action complete failed' };\n }\n }\n\n // ─── Health ───\n\n /**\n * Ping the relay to check connectivity.\n */\n async ping(): Promise<boolean> {\n try {\n const response = await this.fetch(\n `/api/health`,\n { method: 'GET' },\n 3_000 // short timeout for health check\n );\n return response.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Get count of buffered offline bundles.\n */\n getOfflineQueueSize(): number {\n return this.offlineQueue.length;\n }\n\n // ─── Internal ───\n\n private async fetch(\n path: string,\n init: RequestInit,\n timeout?: number\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(),\n timeout ?? this.timeout\n );\n\n try {\n return await fetch(`${this.baseUrl}${path}`, {\n ...init,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'X-ADAS-TENANT': this.tenant,\n 'X-ADAS-ACTOR': this.actorId,\n 'X-Device-Id': this.deviceId,\n // The relay (post-2026-04-25) gates every /api/device/* call on\n // a shared secret. Send it as `Authorization: Bearer` so the\n // server's relayAuth middleware accepts the request. Empty/missing\n // secret = 401 from the relay (correct fail-closed behavior).\n ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),\n ...(init.headers || {}),\n },\n });\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async flushOfflineQueue(): Promise<void> {\n if (this.offlineQueue.length === 0) return;\n\n const queue = [...this.offlineQueue];\n this.offlineQueue = [];\n\n for (const bundle of queue) {\n try {\n const response = await this.fetch(\n `/api/device/${this.deviceId}/sync`,\n {\n method: 'POST',\n body: JSON.stringify(bundle),\n }\n );\n if (!response.ok) {\n // Put it back if still failing\n this.offlineQueue.push(bundle);\n }\n } catch {\n this.offlineQueue.push(bundle);\n break; // Stop trying if still offline\n }\n }\n }\n}\n","/**\n * Calendar Collector — reads device calendar via expo-calendar.\n * Output format matches device-mock-mcp's calendar.today / calendar.upcoming schemas.\n */\n\nimport * as ExpoCalendar from 'expo-calendar';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoCalendar.Calendar`-style annotations need this.\n// expo-calendar's types are not visible through the package entry under\n// bundler resolution, so derive them from the APIs that return them —\n// the shape then always matches the installed version.\ntype Calendar = Awaited<ReturnType<typeof ExpoCalendar.getCalendarsAsync>>[number];\ntype Event = Awaited<ReturnType<typeof ExpoCalendar.getEventsAsync>>[number];\nimport type { CalendarEvent, CalendarTodayData, CalendarUpcomingData } from '../types';\n\n/**\n * Request calendar permissions.\n * @returns true if granted\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.requestCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if calendar permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoCalendar.getCalendarPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Get all calendar events for today.\n * Matches the device.calendar.today tool output schema.\n */\nexport async function collectToday(): Promise<CalendarTodayData> {\n const now = new Date();\n const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, startOfDay, endOfDay);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => ({\n id: e.id,\n title: e.title || 'Untitled',\n date: startOfDay.toISOString().slice(0, 10),\n start: formatTime(new Date(e.startDate)),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n }));\n\n // Sort by start time\n mapped.sort((a, b) => a.start.localeCompare(b.start));\n\n return {\n ok: true,\n date: startOfDay.toISOString().slice(0, 10),\n day_of_week: now.toLocaleDateString('en-US', { weekday: 'long' }),\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n/**\n * Get upcoming calendar events for the next N days (tomorrow and beyond).\n * Matches the device.calendar.upcoming tool output schema.\n */\nexport async function collectUpcoming(daysAhead: number = 3): Promise<CalendarUpcomingData> {\n const now = new Date();\n const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\n const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead + 1);\n\n const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);\n const userCalendarIds = getUserCalendarIds(calendars);\n\n const events = await ExpoCalendar.getEventsAsync(userCalendarIds, tomorrow, endDate);\n const filtered = filterHolidays(events);\n\n const mapped: CalendarEvent[] = filtered.map(e => {\n const startDt = new Date(e.startDate);\n return {\n id: e.id,\n title: e.title || 'Untitled',\n date: startDt.toISOString().slice(0, 10),\n start: formatTime(startDt),\n end: formatTime(new Date(e.endDate)),\n location: e.location || '',\n attendees: ((e as any).attendees || []).map((a: { name?: string; email?: string }) => a.name || a.email || 'Unknown'),\n notes: e.notes || '',\n };\n });\n\n // Sort by date then start time\n mapped.sort((a, b) =>\n a.date === b.date ? a.start.localeCompare(b.start) : a.date.localeCompare(b.date)\n );\n\n return {\n ok: true,\n from: tomorrow.toISOString().slice(0, 10),\n days_ahead: daysAhead,\n event_count: mapped.length,\n events: mapped,\n };\n}\n\n// ─── Helpers ───\n\n/**\n * Filter out holiday/subscription calendars — only keep user-owned calendars\n * (local, CalDAV, Exchange, Google). Subscription calendars are read-only\n * holiday feeds that clutter the real schedule.\n */\nfunction getUserCalendarIds(calendars: Calendar[]): string[] {\n const SKIP_SOURCES = ['birthdays', 'other']; // 'other' = subscribed holiday feeds on iOS\n const SKIP_TYPES = ['birthdays', 'other'];\n\n return calendars\n .filter(c => {\n const sourceType = ((c.source as any)?.type || '').toLowerCase();\n const calType = (c.type || '').toLowerCase();\n // Skip subscription/holiday calendars\n if (SKIP_SOURCES.includes(sourceType)) return false;\n if (SKIP_TYPES.includes(calType)) return false;\n return true;\n })\n .map(c => c.id);\n}\n\n/**\n * Extra safety: filter out all-day events with holiday-like patterns\n * in case they slip through from non-subscription calendars.\n */\nfunction filterHolidays(events: Event[]): Event[] {\n const HOLIDAY_PATTERNS = [\n /holiday/i, /observance/i, /eve\\b.*\\btentative\\b/i, /\\btentative\\b/i,\n ];\n\n return events.filter(e => {\n // All-day events spanning exactly one day with \"tentative\" or \"observance\" in notes → holiday\n const isAllDay = (e as any).allDay === true;\n const notes = (e.notes || '').toLowerCase();\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(notes))) return false;\n if (isAllDay && HOLIDAY_PATTERNS.some(p => p.test(e.title || ''))) return false;\n return true;\n });\n}\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n });\n}\n","/**\n * Contacts Collector — reads device contacts via expo-contacts.\n *\n * Two important properties for users with large address books (6000+ contacts):\n *\n * 1. Hash-based change detection. We persist a map of `contactId → contentHash`\n * across launches and on each collect cycle compare the freshly-read hashes\n * to the stored ones. If nothing changed, `collectAll()` returns null and\n * `syncCapability` skips the relay push entirely — no per-tick 1MB upload.\n *\n * 2. Reduced field set. iOS 16+ restricts which contact fields are accessible\n * without explicit user grants. Requesting `Note`/`Relationships` causes\n * `getContactsAsync` to throw \"Failed to serialize contact data:\n * Unauthorized Keys\", failing the entire collect. We stick to the four\n * fields actually used downstream (Name, PhoneNumbers, Emails, Birthday).\n *\n * Output format on push (when there ARE changes) is unchanged from before —\n * a full `Contact[]` — so the relay/MCP layer doesn't have to know about the\n * incremental scheme. The relay simply gets pushed less often.\n */\nimport * as ExpoContacts from 'expo-contacts';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoContacts.Calendar`-style annotations need this.\n// expo-contacts exports no `Date` type; take the shape from Contact itself.\ntype ExpoContact = Awaited<ReturnType<typeof ExpoContacts.getContactsAsync>>['data'][number];\ntype ContactDate = NonNullable<ExpoContact['birthday']>;\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport type { Contact } from '../types';\n\nconst HASH_STORE_KEY = 'device-bridge.contacts.hashes.v1';\n\n/** Request contacts permissions. */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/** Check if contacts permission is granted. */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoContacts.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Diagnostic counters from the most recent collect — exposed so the Profile\n * debug view can surface \"what changed since last sync\".\n */\nexport interface ContactsCollectStats {\n total: number;\n added: number;\n updated: number;\n removed: number;\n unchanged: number;\n full_sync: boolean;\n}\nexport let lastContactsStats: ContactsCollectStats | null = null;\n\n/**\n * Read all contacts and detect changes vs the previously-stored hash map.\n *\n * Returns:\n * - the full Contact[] when anything changed (or this is the first sync),\n * - null when every contact's hash matches what we pushed last time, signalling\n * the bridge that no relay push is needed this cycle.\n */\nexport async function collectAll(): Promise<Contact[] | null> {\n // Only the four fields we actually surface downstream. Adding more reawakens\n // the iOS \"Unauthorized Keys\" failure unless the user has granted them via\n // a separate flow.\n const result = await ExpoContacts.getContactsAsync({\n fields: [\n ExpoContacts.Fields.Name,\n ExpoContacts.Fields.PhoneNumbers,\n ExpoContacts.Fields.Emails,\n ExpoContacts.Fields.Birthday,\n ],\n sort: ExpoContacts.SortTypes.LastName,\n });\n\n const raw = Array.isArray(result?.data) ? result.data : [];\n\n // Map raw contacts to our schema, computing a stable per-contact hash that's\n // independent of array ordering and resilient to spurious whitespace/case\n // changes in fields we don't actually compare on.\n const contacts: Contact[] = [];\n const currentHashes: Record<string, string> = {};\n for (let i = 0; i < raw.length; i++) {\n const c = raw[i];\n const id = c.id || `c${i}`;\n const mapped: Contact = {\n id,\n name: c.name || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unknown',\n relationship: '', // dropped — was the iOS Unauthorized-Keys trigger\n phone: c.phoneNumbers?.[0]?.number || null,\n email: c.emails?.[0]?.email || null,\n birthday: formatBirthday(c.birthday),\n notes: '', // dropped — Notes is restricted on iOS 16+\n };\n contacts.push(mapped);\n currentHashes[id] = hashContact(mapped);\n }\n\n // Compare against last-pushed hashes.\n let stored: Record<string, string> = {};\n try {\n const blob = await AsyncStorage.getItem(HASH_STORE_KEY);\n if (blob) stored = JSON.parse(blob) as Record<string, string>;\n } catch {\n /* corrupted store — treat as empty, will full-sync */\n }\n const fullSync = Object.keys(stored).length === 0;\n\n let added = 0;\n let updated = 0;\n let unchanged = 0;\n for (const id of Object.keys(currentHashes)) {\n const prev = stored[id];\n if (prev === undefined) added++;\n else if (prev !== currentHashes[id]) updated++;\n else unchanged++;\n }\n let removed = 0;\n for (const id of Object.keys(stored)) {\n if (currentHashes[id] === undefined) removed++;\n }\n\n lastContactsStats = {\n total: contacts.length,\n added,\n updated,\n removed,\n unchanged,\n full_sync: fullSync,\n };\n\n // No changes → tell the bridge to skip the relay push this cycle. Saves\n // a 6000-row payload going up the wire every 5 minutes when nothing moved.\n if (!fullSync && added === 0 && updated === 0 && removed === 0) {\n return null;\n }\n\n // Persist the new hash map AFTER deciding to push. If the push fails we'll\n // re-detect the same changes on the next cycle (push is idempotent enough).\n try {\n await AsyncStorage.setItem(HASH_STORE_KEY, JSON.stringify(currentHashes));\n } catch {\n /* a failed persist just means we'll redo the diff next time — non-fatal */\n }\n\n return contacts;\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Stable string hash (FNV-1a 32-bit, hex). Pure JS, no native dep, fine for\n * change detection. NOT cryptographic — a determined collision attacker could\n * mask a contact change, but that's not in our threat model.\n */\nfunction fnv1a(str: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\nfunction hashContact(c: Contact): string {\n // Concatenate the canonical-cased, trimmed values of every field we surface.\n // Order matters and is fixed; null becomes a single sentinel character so\n // empty-vs-missing differs from \"actual whitespace\" in the hash space.\n const parts = [\n c.id,\n (c.name || '').trim(),\n c.phone || '\\0',\n (c.email || '\\0').toLowerCase(),\n c.birthday || '\\0',\n ];\n return fnv1a(parts.join('\u001f')); // unit separator — won't appear in any field\n}\n\nfunction formatBirthday(birthday: ContactDate | undefined): string | null {\n if (!birthday) return null;\n const { year, month, day } = birthday;\n if (month === undefined || day === undefined) return null;\n const m = String(month + 1).padStart(2, '0');\n const d = String(day).padStart(2, '0');\n\n // No year is the COMMON case on iOS — the contact editor lets you omit it,\n // and most people do. Substituting 1900 made that indistinguishable from a\n // real birth year, and the server dutifully computed an age from it: \"Dana\n // is turning 126 next Tuesday.\"\n //\n // `--MM-DD` is the ISO 8601 form for a recurring month-day with no year, so\n // the absence is stated rather than filled in.\n if (!year) return `--${m}-${d}`;\n\n return `${year}-${m}-${d}`;\n}\n","/**\n * Location Collector — reads device location via expo-location.\n * Output format matches device-mock-mcp's location.current schema.\n */\n\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress } from '@ateam-ai-mobile/plugin-runtime';\n// The SDK's one reverse geocode — the same call native.maps.reverseGeocode makes.\nimport { reverseGeocode } from '../../maps';\nimport type { LocationData } from '../types';\n\n/**\n * Request location permissions (foreground + background).\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status: fg } = await ExpoLocation.requestForegroundPermissionsAsync();\n if (fg !== 'granted') return false;\n\n // Background is optional — skip in Expo Go (crashes without native module)\n try {\n await ExpoLocation.requestBackgroundPermissionsAsync();\n } catch {\n // Expected in Expo Go — foreground is enough for basic functionality\n }\n return fg === 'granted';\n}\n\n/**\n * Check if location permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoLocation.getForegroundPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect current location.\n * Matches the device.location.current tool output schema.\n */\nexport async function collectCurrent(): Promise<LocationData> {\n const location = await ExpoLocation.getCurrentPositionAsync({\n accuracy: ExpoLocation.Accuracy.Balanced,\n });\n\n // Reverse geocode to get address\n let place = 'Current Location';\n let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;\n let locationType = 'unknown';\n\n try {\n const geocoded = await reverseGeocode({\n latitude: location.coords.latitude,\n longitude: location.coords.longitude,\n });\n\n if (geocoded) {\n place = geocoded.name || geocoded.street || place;\n address = [\n geocoded.streetNumber,\n geocoded.street,\n geocoded.city,\n geocoded.region,\n ]\n .filter(Boolean)\n .join(', ') || address;\n\n // Attempt to classify location type\n locationType = classifyLocation(geocoded);\n }\n } catch {\n // Reverse geocoding can fail — use coordinates only\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n location: {\n place,\n address,\n lat: location.coords.latitude,\n lng: location.coords.longitude,\n type: locationType,\n },\n };\n}\n\n// ─── Plugin-facing position API ───\n// `collectCurrent` above answers the device-context question (\"where am I, in\n// words\") and reverse-geocodes. A plugin tool usually wants the raw fix and\n// nothing else, so these stay separate: no geocoding round trip, no address.\n\nexport interface Position {\n latitude: number;\n longitude: number;\n altitude?: number;\n accuracy?: number;\n timestamp: number;\n /**\n * Metres per second. `undefined` = the OS gave no estimate.\n *\n * iOS reports -1 rather than omitting it, and -1 m/s passed through as a\n * speed would read as \"moving backwards\"; it is normalised away here so no\n * caller has to know the platform's sentinel.\n */\n speed?: number;\n /**\n * COURSE OVER GROUND in degrees — the direction the user is TRAVELLING.\n *\n * Deliberately not called `heading`: this and the compass answer different\n * questions and disagree constantly. Walking backwards down a corridor gives\n * a course of south and a compass heading of north; a phone held sideways\n * while walking forward gives the same course and a heading 90° off.\n * \"Turn right\" needs the compass. \"You are heading north on Main Street\"\n * needs this. The old code assigned one to the other, which is only ever\n * right by accident.\n *\n * `undefined` when not moving or not estimated — iOS reports -1 for both.\n */\n course?: number;\n}\n\n/** Ensure foreground permission, asking once if we do not have it yet. */\nasync function ensurePermission(): Promise<void> {\n if (await hasPermission()) return;\n const granted = await requestPermission();\n if (!granted) {\n // Callers surface this verbatim to the user (a voice assistant reads it\n // out), so it has to say what the user can do about it.\n throw new Error(\n 'Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services.',\n );\n }\n}\n\n/** One position fix. `accuracy: \"high\"` costs battery and time — default low. */\nexport async function getPosition(\n opts: { accuracy?: 'low' | 'high' } = {},\n): Promise<Position> {\n await ensurePermission();\n const fix = await ExpoLocation.getCurrentPositionAsync({\n accuracy:\n opts.accuracy === 'high'\n ? ExpoLocation.Accuracy.High\n : ExpoLocation.Accuracy.Balanced,\n });\n return toPosition(fix);\n}\n\n/**\n * One place that turns an expo fix into a `Position`.\n *\n * It exists because the sentinel handling has to be identical on the one-shot\n * and the streamed path. It was not: `watchPosition` passed lat/lon only, so\n * the semantic Spatial domain could never report speed or course no matter\n * what the OS supplied, and reported `null` as if the phone had no GPS.\n */\nfunction toPosition(fix: any): Position {\n return {\n latitude: fix.coords.latitude,\n longitude: fix.coords.longitude,\n altitude: fix.coords.altitude ?? undefined,\n accuracy: nonNegative(fix.coords.accuracy),\n timestamp: fix.timestamp ?? Date.now(),\n speed: nonNegative(fix.coords.speed),\n course: nonNegative(fix.coords.heading),\n };\n}\n\n/**\n * iOS uses -1 for \"no estimate\" on accuracy, speed and course alike. Dropping\n * the field says \"not measured\"; passing -1 through says \"measured, and the\n * answer is minus one\", which a model will happily repeat.\n */\nfunction nonNegative(v: unknown): number | undefined {\n return typeof v === 'number' && v >= 0 ? v : undefined;\n}\n\nconst WATCH_TIER = {\n navigation: ExpoLocation.Accuracy.BestForNavigation,\n high: ExpoLocation.Accuracy.High,\n balanced: ExpoLocation.Accuracy.Balanced,\n low: ExpoLocation.Accuracy.Low,\n} as const;\n\n/**\n * Stream position updates. Returns an unsubscribe function — call it, or the\n * GPS keeps running and drains the battery.\n *\n * `accuracy` picks the GPS tier. The default stays Balanced (≈100 m on iOS),\n * which is right for \"which city block\" and wrong for \"turn at the next corner\":\n * a navigation session asks for BestForNavigation and pays the battery for it.\n */\nexport function watchPosition(\n cb: (pos: Position) => void,\n opts?: { accuracy?: keyof typeof WATCH_TIER },\n): () => void {\n let subscription: { remove(): void } | null = null;\n let cancelled = false;\n\n (async () => {\n try {\n await ensurePermission();\n if (cancelled) return;\n subscription = await ExpoLocation.watchPositionAsync(\n { accuracy: WATCH_TIER[opts?.accuracy ?? 'balanced'] },\n fix => cb(toPosition(fix)),\n );\n // Unsubscribed while we were still asking for permission.\n if (cancelled) { subscription?.remove(); subscription = null; }\n } catch (err: any) {\n console.warn('[location] watchPosition failed:', err?.message || String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n subscription?.remove();\n subscription = null;\n };\n}\n\n// ─── Helpers ───\n\nfunction classifyLocation(geocoded: MapsAddress): string {\n // Simple heuristic — can be enhanced with user-labeled places from memory\n const name = (geocoded.name || '').toLowerCase();\n const street = (geocoded.street || '').toLowerCase();\n\n if (name.includes('home') || name.includes('residence')) return 'home';\n if (name.includes('office') || name.includes('workplace') || name.includes('tech park')) return 'work';\n if (name.includes('school') || name.includes('elementary') || name.includes('university')) return 'school';\n if (name.includes('gym') || name.includes('fitness')) return 'fitness';\n if (name.includes('hospital') || name.includes('clinic') || name.includes('dental')) return 'medical';\n\n return 'other';\n}\n","// ada-maps — place search + walking directions (Apple MapKit), our own native\n// code, plus the SDK's one reverse geocode.\n//\n// NOT on-device — the opposite of ada-vision. Every call sends the query and the\n// coordinates it is given to a map service over the network. None of it reads\n// the device's location: the caller supplies every coordinate.\n//\n// search(...) — MKLocalSearch (AdaMaps module, iOS only)\n// route(...) — MKDirections (AdaMaps module, iOS only)\n// reverseGeocode(at) — expo-location's reverseGeocodeAsync, which IS CLGeocoder\n// on iOS and the platform Geocoder on Android. No Swift for\n// it, and the ONLY reverse geocode in the SDK: the location\n// collector and the connector bridge call this too.\n//\n// requireOptionalNativeModule: on a binary WITHOUT the module the import is null,\n// isSupported() is false, and search/route refuse cleanly (no crash). Android has\n// no AdaMaps at all, so it refuses by name the same way.\n\nimport { Platform } from 'react-native';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport * as ExpoLocation from 'expo-location';\nimport type { MapsAddress, MapsCoordinate } from '@ateam-ai-mobile/plugin-runtime';\nimport { toMapsAddress } from './normalize';\nimport type { RawMapsPlace, RawMapsRoute } from './normalize';\n\ninterface AdaMapsNative {\n isSupported(): boolean;\n search(query: string, lat: number, lng: number, hasNear: boolean, radiusM: number, limit: number): Promise<RawMapsPlace[]>;\n route(fromLat: number, fromLng: number, toLat: number, toLng: number): Promise<RawMapsRoute | null>;\n}\n\nconst native = requireOptionalNativeModule<AdaMapsNative>('AdaMaps');\n\n/** What search/route are missing in THIS build, as the clause after \"needs\" —\n * or null when they can run. A voice assistant reads it out, so it names the\n * missing thing rather than giving a code. */\nexport function missingReason(): string | null {\n if (Platform.OS !== 'ios') return `iOS (Apple MapKit) — ${Platform.OS} has no implementation yet`;\n if (!native) return 'the app build with the AdaMaps module (this build predates it — a native rebuild is needed)';\n return null;\n}\n\n/** Is the native maps module present in this binary (and on a platform it runs on)? */\nexport function isSupported(): boolean {\n try { return missingReason() === null && (native?.isSupported() ?? false); } catch { return false; }\n}\n\n// search/route hand their arguments to native AS GIVEN: a NaN or Infinity\n// `limit` traps Swift's Int conversion and kills the app. createMapsAPI checks\n// every argument first (searchArgs / coordinateArg in ./normalize); a host\n// calling these directly must pass what those checks would accept.\n\n/** MKLocalSearch around `near` (when given). [] = looked, found nothing; throws =\n * couldn't look, with a string code OFFLINE / THROTTLED / UNAVAILABLE. */\nexport async function search(\n query: string,\n near: MapsCoordinate | null,\n radiusM: number,\n limit: number,\n): Promise<RawMapsPlace[]> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n // Without `near`, hasNear is false and Swift never reads lat/lng — the zeros\n // are placeholders for a primitive argument, not a place.\n return near\n ? native.search(query, near.latitude, near.longitude, true, radiusM, limit)\n : native.search(query, 0, 0, false, radiusM, limit);\n}\n\n/** MKDirections, walking, between two caller-supplied points. null = no route. */\nexport async function route(from: MapsCoordinate, to: MapsCoordinate): Promise<RawMapsRoute | null> {\n if (!native) throw new Error('ada-maps native module not present in this build');\n return native.route(from.latitude, from.longitude, to.latitude, to.longitude);\n}\n\n/** The address at a coordinate, or null when the geocoder has nothing there.\n * Throws when it could not look (network, rate limit, Android without location\n * permission) — callers decide whether that is fatal. */\nexport async function reverseGeocode(at: MapsCoordinate): Promise<MapsAddress | null> {\n const [first] = await ExpoLocation.reverseGeocodeAsync({ latitude: at.latitude, longitude: at.longitude });\n return first ? toMapsAddress(first) : null;\n}\n","// Pure shaping for native.maps — no Expo, no React Native, so it runs under\n// plain `node --test` (tests/adaMapsNormalize.test.mjs pins it).\n//\n// The native side hands back loose dictionaries — AdaMaps (MapKit) for search\n// and route, expo-location's geocoder for reverse geocoding. This turns them\n// into the typed MapsAPI shapes, and one set of failure codes. Unknown is\n// `null`, never '' or 0: an empty street reads as a street with no name, and a\n// 0 m route reads as \"you have arrived\".\n//\n// It also checks what a caller sends BEFORE it crosses the bridge (searchArgs,\n// coordinateArg). A remote bundle is plain JS at runtime, so the types promise\n// nothing: a NaN or Infinity `limit` traps Swift's Int conversion and kills the\n// app, and a `{ lat, lng }` near would search around 0,0 and answer \"found\n// nothing\" with confidence.\n\nimport type {\n MapsAddress,\n MapsCoordinate,\n MapsPlace,\n MapsRoute,\n MapsRouteStep,\n} from '@ateam-ai-mobile/plugin-runtime';\nimport { distanceOf } from '../geo';\nimport { countArg, refuseArgument as refuse, shown, type ArgumentError } from '../args';\n\n/** An address as the native geocoders spell it. AdaMaps sends exactly these\n * keys; expo-location sends these plus `subregion`/`timezone`/\n * `formattedAddress`, which MapsAddress does not carry. */\nexport interface RawMapsAddress {\n name?: string | null;\n street?: string | null;\n streetNumber?: string | null;\n district?: string | null;\n city?: string | null;\n region?: string | null;\n postalCode?: string | null;\n country?: string | null;\n isoCountryCode?: string | null;\n}\n\n/** One AdaMaps search hit (AdaMapsModule.place). */\nexport interface RawMapsPlace {\n name: string | null;\n latitude: number;\n longitude: number;\n /** MKPointOfInterestCategory raw value, e.g. \"MKPOICategoryPharmacy\". */\n category: string | null;\n address: RawMapsAddress;\n}\n\n/** An AdaMaps walking route (AdaMapsModule.route) — distances in metres,\n * expectedTravelTime in SECONDS, as MapKit reports them. */\nexport interface RawMapsRoute {\n distance: number;\n expectedTravelTime: number;\n steps: Array<{ instructions: string | null; distance: number; notice: string | null; polyline: MapsCoordinate[] }>;\n polyline: MapsCoordinate[];\n}\n\n/** The three ways a maps call can fail to look — see MapsAPI. */\nexport type MapsFailureCode = 'UNAVAILABLE' | 'OFFLINE' | 'THROTTLED';\n\n/** A malformed call, refused before anything is sent. Not a fourth way to fail\n * to look: retrying will not help, fixing the call will. */\nexport type MapsArgumentError = ArgumentError;\n\nconst SEARCH_RADIUS_DEFAULT_M = 3000;\n/** Keeps the region MapKit is handed a small, valid one. A \"near\" search wider\n * than this is a different question. */\nconst SEARCH_RADIUS_MAX_M = 100_000;\nconst SEARCH_LIMIT_DEFAULT = 8;\nconst SEARCH_LIMIT_MAX = 25;\n\nconst text = (v: unknown): string | null =>\n typeof v === 'string' && v.trim() !== '' ? v : null;\n\nconst refuseArgument = (verb: string, problem: string, nothing: string): MapsArgumentError =>\n refuse('maps', verb, problem, nothing);\n\n/** A caller's coordinate, checked and copied to exactly `{ latitude, longitude }`.\n * Anything else — `{ lat, lng }`, strings, NaN, out of range — refuses BY NAME\n * rather than being sent as 0,0 or NaN. */\nexport function coordinateArg(verb: string, name: string, value: unknown, nothing: string): MapsCoordinate {\n const c = value as { latitude?: unknown; longitude?: unknown } | null | undefined;\n const latitude = c?.latitude, longitude = c?.longitude;\n if (\n typeof latitude === 'number' && Number.isFinite(latitude) && Math.abs(latitude) <= 90 &&\n typeof longitude === 'number' && Number.isFinite(longitude) && Math.abs(longitude) <= 180\n ) {\n return { latitude, longitude };\n }\n throw refuseArgument(\n verb,\n `${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,\n nothing,\n );\n}\n\n/** search's arguments, checked and defaulted (radiusM 3000, limit 8). `limit`\n * is floored and capped at 25; everything else out of range refuses. */\nexport function searchArgs(\n query: unknown,\n opts: { near?: unknown; radiusM?: unknown; limit?: unknown } | null | undefined,\n): { query: string; near: MapsCoordinate | null; radiusM: number; limit: number } {\n const nothing = 'nothing was searched';\n if (typeof query !== 'string' || query.trim() === '') {\n throw refuseArgument('search', `query must be a non-empty string, not ${shown(query)}`, nothing);\n }\n const near = opts?.near == null ? null : coordinateArg('search', 'near', opts.near, nothing);\n const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;\n if (typeof radiusM !== 'number' || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {\n throw refuseArgument(\n 'search', `radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`, nothing);\n }\n const limit = countArg('maps', 'search', 'limit', opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);\n return { query, near, radiusM, limit };\n}\n\nconst point = (c: MapsCoordinate): MapsCoordinate => ({\n latitude: Number(c.latitude),\n longitude: Number(c.longitude),\n});\n\nexport function toMapsAddress(raw: RawMapsAddress | null | undefined): MapsAddress {\n const r = raw ?? {};\n return {\n name: text(r.name),\n street: text(r.street),\n streetNumber: text(r.streetNumber),\n district: text(r.district),\n city: text(r.city),\n region: text(r.region),\n postalCode: text(r.postalCode),\n country: text(r.country),\n isoCountryCode: text(r.isoCountryCode),\n };\n}\n\n/** \"MKPOICategoryPharmacy\" → \"Pharmacy\": the vocabulary term, without the\n * framework's prefix. */\nexport function toMapsCategory(raw: unknown): string | null {\n const c = text(raw);\n return c ? c.replace(/^MKPOICategory/, '') || null : null;\n}\n\n/** Search hits → places. `distanceM` is straight-line from `near`, and only\n * when the caller gave one — a distance from nowhere would be invented. */\nexport function toMapsPlaces(raw: RawMapsPlace[] | null | undefined, near: MapsCoordinate | null): MapsPlace[] {\n return (raw ?? []).map(r => {\n const coordinate = point(r);\n return {\n name: text(r.name),\n coordinate,\n address: toMapsAddress(r.address),\n category: toMapsCategory(r.category),\n distanceM: near ? distanceOf([near, coordinate]) : null,\n };\n });\n}\n\n/** A reverse-geocoded address → a place AT the point asked about. The platform\n * geocoder returns an address, not a snapped point or a category. */\nexport function toReverseGeocodedPlace(at: MapsCoordinate, address: MapsAddress): MapsPlace {\n return {\n name: address.name,\n coordinate: point(at),\n address,\n category: null,\n distanceM: null,\n };\n}\n\n/** A MapKit route → MapsRoute. Seconds become ms; every step is kept, including\n * Apple's empty-instruction origin step, so indexes match Apple's. */\nexport function toMapsRoute(raw: RawMapsRoute): MapsRoute {\n const steps: MapsRouteStep[] = (raw.steps ?? []).map(s => ({\n instructions: typeof s.instructions === 'string' ? s.instructions : '',\n distanceM: Number(s.distance),\n notice: text(s.notice),\n polyline: (s.polyline ?? []).map(point),\n }));\n return {\n mode: 'walking',\n distanceM: Number(raw.distance),\n expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1000),\n steps,\n polyline: (raw.polyline ?? []).map(point),\n };\n}\n\n/**\n * A native rejection → an Error whose `code` is one of MapsAPI's three.\n *\n * AdaMaps already rejects with OFFLINE / THROTTLED / UNAVAILABLE. expo-location\n * (reverseGeocode) has its own codes: on iOS a CLError.network becomes\n * ERR_GEOCODING_NETWORK, which Apple uses for BOTH a lost connection and a rate\n * limit — so it maps to OFFLINE and the message says it may be either. Anything\n * else is UNAVAILABLE, with the platform's own words kept beside ours.\n */\nexport function mapsFailure(verb: string, err: unknown): Error & { code: MapsFailureCode } {\n const nativeCode = String((err as any)?.code ?? '');\n const detail = String((err as any)?.message ?? err ?? '').trim();\n let code: MapsFailureCode;\n let why: string;\n if (nativeCode === 'THROTTLED') {\n code = 'THROTTLED';\n why = 'Apple is rate-limiting map requests from this app — wait before retrying';\n } else if (nativeCode === 'OFFLINE') {\n code = 'OFFLINE';\n why = 'the map service could not be reached — the phone may be offline';\n } else if (nativeCode === 'ERR_GEOCODING_NETWORK') {\n code = 'OFFLINE';\n why = 'the geocoder could not be reached — the phone may be offline, or Apple is ' +\n 'rate-limiting requests (iOS reports both the same way)';\n } else {\n code = 'UNAVAILABLE';\n why = 'the map service failed';\n }\n const failure = new Error(\n `maps.${verb} could not look: ${why}. This is not \"found nothing\".` +\n (detail ? ` (${nativeCode ? nativeCode + ': ' : ''}${detail})` : ''),\n ) as Error & { code: MapsFailureCode };\n failure.code = code;\n return failure;\n}\n","/**\n * Battery Collector — reads battery status via expo-battery.\n * Output format matches device-mock-mcp's device.battery schema.\n */\n\nimport * as ExpoBattery from 'expo-battery';\n// Type-only alias: the value namespace above cannot be used in type\n// position, so `ExpoBattery.Calendar`-style annotations need this.\n// BatteryState is not re-exported from the package entry — derive it from\n// the API that returns it so we never hand-maintain a copy of the enum.\ntype BatteryState = Awaited<ReturnType<typeof ExpoBattery.getBatteryStateAsync>>;\nimport type { BatteryData } from '../types';\n\n// No permission needed for battery\n\n/**\n * Collect current battery status.\n * Matches the device.battery tool output schema.\n */\nexport async function collectBattery(): Promise<BatteryData> {\n const level = await ExpoBattery.getBatteryLevelAsync();\n const state = await ExpoBattery.getBatteryStateAsync();\n const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n battery: {\n level: Math.round(level * 100), // expo-battery returns 0-1\n state: mapBatteryState(state),\n low_power_mode: lowPower,\n },\n };\n}\n\n/**\n * Subscribe to OS-level battery changes (level + charging state).\n *\n * iOS suspends JS timers when the app is backgrounded, so a pure setInterval\n * sync drifts: the relay ends up stuck on whatever level we last sampled\n * before the app went to sleep (commonly 100% from an overnight charge).\n * expo-battery's listeners fire from the native side and survive that gap,\n * so we get a fresh push as soon as the OS notices a change.\n *\n * Level changes are throttled to >=5pp deltas — without that, iOS emits a\n * level event for every 1% tick which would spam the relay.\n */\nexport function subscribeToBattery(callback: () => void): () => void {\n let lastLevel: number | null = null;\n\n const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {\n const pct = Math.round(batteryLevel * 100);\n if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {\n lastLevel = pct;\n callback();\n }\n });\n\n const stateSub = ExpoBattery.addBatteryStateListener(() => {\n callback();\n });\n\n return () => {\n levelSub?.remove?.();\n stateSub?.remove?.();\n };\n}\n\n// ─── Helpers ───\n\nfunction mapBatteryState(\n state: BatteryState\n): 'charging' | 'unplugged' | 'full' {\n switch (state) {\n case ExpoBattery.BatteryState.CHARGING:\n return 'charging';\n case ExpoBattery.BatteryState.FULL:\n return 'full';\n case ExpoBattery.BatteryState.UNPLUGGED:\n default:\n return 'unplugged';\n }\n}\n","/**\n * Connectivity Collector — reads network state via expo-network.\n * Output format matches device-mock-mcp's device.connectivity schema.\n */\n\nimport * as ExpoNetwork from 'expo-network';\nimport type { ConnectivityData } from '../types';\n\n// No special permission needed for network state\n\n/**\n * Collect current connectivity status.\n * Matches the device.connectivity tool output schema.\n */\nexport async function collectConnectivity(): Promise<ConnectivityData> {\n const networkState = await ExpoNetwork.getNetworkStateAsync();\n\n let type: 'wifi' | 'cellular' | 'none' = 'none';\n let wifiName: string | undefined;\n let cellularType: string | undefined;\n\n if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {\n type = 'wifi';\n // Wi-Fi SSID requires additional permissions on newer OS versions\n // and may not always be available\n try {\n wifiName = await getWifiName();\n } catch {\n wifiName = undefined;\n }\n } else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {\n type = 'cellular';\n cellularType = getCellularGeneration();\n }\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n connectivity: {\n type,\n is_connected: networkState.isConnected ?? false,\n ...(wifiName && { wifi_name: wifiName }),\n ...(cellularType && { cellular_type: cellularType }),\n },\n };\n}\n\n// ─── Helpers ───\n\nasync function getWifiName(): Promise<string | undefined> {\n // expo-network doesn't expose SSID directly on all platforms\n // This would need a native module or expo-wifi in production\n return undefined;\n}\n\nfunction getCellularGeneration(): string {\n // expo-network doesn't expose cellular generation\n // In production, use a native module for this\n return 'LTE';\n}\n","/**\n * Notifications Collector — reads notification state via expo-notifications.\n * Output format matches device-mock-mcp's notifications.list schema.\n *\n * Note: On iOS/Android, expo-notifications only gives access to notifications\n * that YOUR app has posted or received. System-wide notification access\n * requires platform-specific APIs (NotificationListenerService on Android,\n * UNUserNotificationCenter on iOS with app extensions).\n *\n * For the PA, the relay approach means we capture notifications delivered\n * TO our app (from the PA system), not FROM other apps.\n * Other-app notifications would need a native module or accessibility service.\n */\n\nimport * as ExpoNotifications from 'expo-notifications';\nimport type { Notification, NotificationsData } from '../types';\n\n/**\n * Request notification permissions.\n */\nexport async function requestPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.requestPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Check if notification permission is granted.\n */\nexport async function hasPermission(): Promise<boolean> {\n const { status } = await ExpoNotifications.getPermissionsAsync();\n return status === 'granted';\n}\n\n/**\n * Collect delivered notifications from the notification tray.\n * Only captures notifications delivered to THIS app.\n * Matches the device.notifications.list tool output schema.\n */\nexport async function collectNotifications(): Promise<NotificationsData> {\n const presented = await ExpoNotifications.getPresentedNotificationsAsync();\n\n const notifications: Notification[] = presented.map((n, i) => ({\n id: n.request.identifier || `n${i}`,\n app: (n.request.content.data?.app as string) || 'A-Team',\n title: n.request.content.title || '',\n body: n.request.content.body || '',\n timestamp: n.date ? new Date(n.date).toISOString() : new Date().toISOString(),\n sender: (n.request.content.data?.sender as string) || null,\n priority: mapPriority(n.request.content.data?.priority as string),\n read: false, // Presented notifications are unread by definition\n }));\n\n const unread = notifications.filter(n => !n.read).length;\n\n return {\n ok: true,\n timestamp: new Date().toISOString(),\n total: notifications.length,\n unread,\n notifications,\n };\n}\n\n// ─── Helpers ───\n\nfunction mapPriority(p: string | undefined): 'high' | 'medium' | 'low' {\n if (p === 'high' || p === 'max') return 'high';\n if (p === 'low' || p === 'min') return 'low';\n return 'medium';\n}\n","/**\n * Health Collector — reads HealthKit aggregates via @kingstinct/react-native-healthkit\n * (TurboModule / Nitro-Modules based; works in React Native's New Architecture).\n *\n * iOS only. On Android the functions resolve with zeros so the caller doesn't\n * need to platform-check.\n *\n * Public:\n * - hasPermission() — returns true once authorization has been requested.\n * - requestPermission() — triggers the iOS HealthKit permission sheet (red heart).\n * - collectToday() — one HealthDayMetrics for today.\n * - collectHistory(days) — array of HealthDayMetrics, newest-first.\n */\nimport { Platform } from 'react-native';\nimport type { HealthDayMetrics } from '../types';\n\n// Lazy-loaded so the module is harmless on Android / Expo Go / non-iOS.\n// In Expo Go the JS module resolves but the native bridge is missing —\n// resulting object exists yet methods like requestAuthorization are undefined.\nlet _HK: any = null;\nlet _hkProbed = false;\nfunction getHK(): any | null {\n if (Platform.OS !== 'ios') return null;\n if (_hkProbed) return _HK;\n _hkProbed = true;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod = require('@kingstinct/react-native-healthkit');\n if (mod && typeof mod.requestAuthorization === 'function') {\n _HK = mod;\n } else {\n console.warn('[health] @kingstinct/react-native-healthkit native bridge unavailable — HealthKit disabled (Expo Go?)');\n }\n } catch (e) {\n console.warn('[health] @kingstinct/react-native-healthkit require failed:', e);\n }\n return _HK;\n}\n\n// Last error from requestAuthorization — surfaced to UI so we don't fail silently.\nexport let lastHealthInitError: string | null = null;\n\nconst READ_TYPES = [\n 'HKQuantityTypeIdentifierStepCount',\n 'HKQuantityTypeIdentifierFlightsClimbed',\n 'HKQuantityTypeIdentifierActiveEnergyBurned',\n 'HKQuantityTypeIdentifierDistanceWalkingRunning',\n // Added for mood signals — all read-only, covered by the same auth sheet.\n 'HKQuantityTypeIdentifierRestingHeartRate',\n 'HKQuantityTypeIdentifierAppleExerciseTime',\n 'HKCategoryTypeIdentifierSleepAnalysis',\n] as const;\n\nlet _initPromise: Promise<boolean> | null = null;\n\n/** Request HealthKit authorization once; subsequent calls reuse the result. */\nasync function ensureInit(): Promise<boolean> {\n const HK = getHK();\n if (!HK) {\n lastHealthInitError = '@kingstinct/react-native-healthkit native module not loaded';\n return false;\n }\n if (_initPromise) return _initPromise;\n _initPromise = (async () => {\n try {\n // requestAuthorization triggers the iOS HealthKit permission sheet.\n // Resolves once the user dismisses it (regardless of allow/deny).\n // Returns the authorization status — but iOS deliberately does not\n // disclose per-type read permission state for privacy, so we just\n // treat \"we asked\" as \"ready to read\" and let reads return 0 if denied.\n await HK.requestAuthorization({ toRead: READ_TYPES, toWrite: [] });\n lastHealthInitError = null;\n return true;\n } catch (e: any) {\n lastHealthInitError = `requestAuthorization threw: ${e?.message || String(e)}`;\n console.warn('[health]', lastHealthInitError);\n _initPromise = null;\n return false;\n }\n })();\n return _initPromise;\n}\n\nexport async function hasPermission(): Promise<boolean> {\n return ensureInit();\n}\n\nexport async function requestPermission(): Promise<boolean> {\n return ensureInit();\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setHours(0, 0, 0, 0);\n return out;\n}\n\nfunction dateKey(d: Date): string {\n // Local-date YYYY-MM-DD\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n}\n\n/**\n * Read a single daily-aggregate sum for [start, end) in the given unit.\n * Returns 0 when no data, HealthKit unavailable, or the call throws.\n *\n * `queryStatisticsForQuantity` is the modern statistics API; we ask only for\n * the cumulative sum since that's what we need for these data types.\n */\nasync function readDailySum(identifier: string, unit: string, start: Date, end: Date): Promise<number> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return 0;\n try {\n // The kingstinct API takes the date range nested under filter.date — not\n // top-level from/to. Without filter.date the call returns the cumulative\n // sum across ALL historical data, which is what we hit before this fix\n // (millions of steps, thousands of km, hundreds of thousands of kcal).\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['cumulativeSum'],\n {\n unit,\n filter: { date: { startDate: start, endDate: end } },\n },\n );\n const q = result?.sumQuantity?.quantity;\n return typeof q === 'number' ? q : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Daily discrete-average for a quantity (e.g. resting heart rate). Returns\n * null when no sample exists for the day so the backend can tell \"no data\"\n * apart from a real zero.\n */\nasync function readDailyAverage(identifier: string, unit: string, start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n if (!HK?.queryStatisticsForQuantity) return null;\n try {\n const result: any = await HK.queryStatisticsForQuantity(\n identifier,\n ['discreteAverage'],\n { unit, filter: { date: { startDate: start, endDate: end } } },\n );\n const q = result?.averageQuantity?.quantity;\n return typeof q === 'number' ? q : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Total minutes asleep for [start, end) from HKCategoryTypeIdentifierSleepAnalysis.\n * Sums sample durations that represent actual sleep (any \"asleep*\" value),\n * excluding inBed/awake. Heavily guarded — sleep-stage enums differ across iOS\n * versions and the kingstinct API surface, so any failure returns null.\n */\nasync function readSleepMinutes(start: Date, end: Date): Promise<number | null> {\n const HK = getHK();\n const query = HK?.queryCategorySamples || HK?.queryCategorySamplesAsync || HK?.querySamples;\n if (!query) return null;\n try {\n const samples: any[] = await query.call(\n HK,\n 'HKCategoryTypeIdentifierSleepAnalysis',\n { filter: { date: { startDate: start, endDate: end } } },\n );\n if (!Array.isArray(samples) || samples.length === 0) return null;\n // value semantics: 0 = inBed, 2 = awake; everything else = a sleep stage.\n // Treat anything that isn't explicitly inBed/awake as asleep.\n let ms = 0;\n for (const s of samples) {\n const v = s?.value;\n const isAsleep = v !== 0 && v !== 2; // exclude inBed + awake\n if (!isAsleep) continue;\n const a = new Date(s?.startDate).getTime();\n const b = new Date(s?.endDate).getTime();\n if (Number.isFinite(a) && Number.isFinite(b) && b > a) ms += b - a;\n }\n return ms > 0 ? Math.round(ms / 60000) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Mood-relevant HealthKit extras for today: sleep minutes, resting heart rate,\n * active energy (kcal), exercise minutes. Each is null when unavailable so the\n * backend weights only present fields.\n */\nexport async function collectMoodExtras(): Promise<{\n sleep_min: number | null;\n resting_hr: number | null;\n active_energy_kcal: number | null;\n exercise_min: number | null;\n}> {\n const ok = await ensureInit();\n if (!ok) return { sleep_min: null, resting_hr: null, active_energy_kcal: null, exercise_min: null };\n const start = startOfDay(new Date());\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n // Sleep window: look back from this morning over the past night (~last 18h)\n // so an evening collection still captures the prior night.\n const sleepStart = new Date(start);\n sleepStart.setHours(sleepStart.getHours() - 6); // include after-midnight + prev evening within today's window\n const [sleep_min, resting_hr, energy, exercise] = await Promise.all([\n readSleepMinutes(sleepStart, end),\n readDailyAverage('HKQuantityTypeIdentifierRestingHeartRate', 'count/min', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n readDailySum('HKQuantityTypeIdentifierAppleExerciseTime', 'min', start, end),\n ]);\n return {\n sleep_min,\n resting_hr: resting_hr != null ? Math.round(resting_hr) : null,\n active_energy_kcal: energy ? Math.round(energy) : null,\n exercise_min: exercise ? Math.round(exercise) : null,\n };\n}\n\nasync function readDay(date: Date): Promise<HealthDayMetrics> {\n const start = startOfDay(date);\n const end = new Date(start);\n end.setDate(end.getDate() + 1);\n\n const [steps, floors, energy, distance] = await Promise.all([\n readDailySum('HKQuantityTypeIdentifierStepCount', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierFlightsClimbed', 'count', start, end),\n readDailySum('HKQuantityTypeIdentifierActiveEnergyBurned', 'kcal', start, end),\n // Ask for kilometers directly so we don't have to convert from meters.\n readDailySum('HKQuantityTypeIdentifierDistanceWalkingRunning', 'km', start, end),\n ]);\n\n return {\n date: dateKey(start),\n available: true,\n steps: Math.round(steps),\n floors: Math.round(floors),\n active_energy_kcal: Math.round(energy),\n distance_km: Math.round(distance * 100) / 100,\n };\n}\n\n// ─── Public API ─────────────────────────────────────────────\n\n/**\n * A day we could not read: metrics null, availability stated.\n *\n * The distinction this preserves is the whole point — \"no data\" and \"a day\n * with no activity\" look identical once you write a zero, and only one of\n * them is something to tell the user about.\n */\nfunction unavailableDay(date: Date): HealthDayMetrics {\n return {\n date: dateKey(date),\n available: false,\n steps: null,\n floors: null,\n active_energy_kcal: null,\n distance_km: null,\n };\n}\n\nexport async function collectToday(): Promise<HealthDayMetrics> {\n const ok = await ensureInit();\n if (!ok) {\n // HealthKit unavailable or access denied. NOT a day with no activity —\n // reporting zeros here is how \"you have taken 0 steps today\" reached the\n // user as a fact, with a coaching skill ready to act on it.\n return unavailableDay(new Date());\n }\n return readDay(new Date());\n}\n\n/**\n * Last `days` days, newest-first. Index 0 is today.\n * Caller passes 1..30 — we clamp defensively.\n */\nexport async function collectHistory(days: number): Promise<HealthDayMetrics[]> {\n const n = Math.max(1, Math.min(30, Math.floor(days)));\n const ok = await ensureInit();\n if (!ok) return [];\n\n const today = startOfDay(new Date());\n const out: HealthDayMetrics[] = [];\n // Sequential to avoid hammering HealthKit with 30 parallel queries.\n for (let i = 0; i < n; i++) {\n const d = new Date(today);\n d.setDate(d.getDate() - i);\n out.push(await readDay(d));\n }\n return out;\n}\n","/**\n * App Engagement Collector — our OWN usage analytics (no OS permission, fully\n * available on every platform). Feeds the mood summary's app_sessions +\n * app_active_min, which iOS otherwise blocks for third-party apps.\n *\n * app_sessions — count of foreground sessions today (a background→active\n * transition, or a cold start, starts a session).\n * app_active_min — total minutes the app was in the foreground today.\n *\n * Counters are kept per local day and persisted to AsyncStorage so they\n * survive relaunches. start() must be called once at app boot to attach the\n * AppState listener; getToday() returns the current day's tallies.\n */\nimport { AppState, type AppStateStatus } from 'react-native';\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\nconst KEY = 'ateam.appEngagement.v1';\n\ninterface DayTally {\n date: string; // local YYYY-MM-DD\n sessions: number;\n activeMs: number;\n}\n\nlet _tally: DayTally | null = null;\nlet _foregroundSince: number | null = null;\nlet _started = false;\nlet _sub: { remove: () => void } | null = null;\n\nfunction todayKey(): string {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n}\n\nfunction fresh(): DayTally {\n return { date: todayKey(), sessions: 0, activeMs: 0 };\n}\n\nasync function load(): Promise<void> {\n try {\n const raw = await AsyncStorage.getItem(KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as DayTally;\n _tally = parsed?.date === todayKey() ? parsed : fresh();\n } else {\n _tally = fresh();\n }\n } catch {\n _tally = fresh();\n }\n}\n\nfunction persist(): void {\n if (_tally) AsyncStorage.setItem(KEY, JSON.stringify(_tally)).catch(() => {});\n}\n\nfunction rollIfNewDay(): void {\n if (!_tally || _tally.date !== todayKey()) {\n _tally = fresh();\n persist();\n }\n}\n\n/** Fold the time spent foregrounded since the last checkpoint into activeMs. */\nfunction flushForeground(): void {\n if (_foregroundSince == null || !_tally) return;\n const now = Date.now();\n const delta = now - _foregroundSince;\n if (delta > 0) _tally.activeMs += delta;\n _foregroundSince = now;\n}\n\nfunction onChange(state: AppStateStatus): void {\n rollIfNewDay();\n if (state === 'active') {\n // New foreground session.\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n } else {\n // Leaving foreground — bank the elapsed active time.\n flushForeground();\n _foregroundSince = null;\n persist();\n }\n}\n\n/** Attach the AppState listener. Idempotent; call once at app boot. */\nexport async function start(): Promise<void> {\n if (_started) return;\n _started = true;\n await load();\n // Count the launch itself as the first active session.\n rollIfNewDay();\n if (_tally) _tally.sessions += 1;\n _foregroundSince = Date.now();\n persist();\n _sub = AppState.addEventListener('change', onChange);\n}\n\nexport function stop(): void {\n flushForeground();\n persist();\n _sub?.remove();\n _sub = null;\n _started = false;\n}\n\n/** Current day's tallies for the mood summary. */\nexport async function getToday(): Promise<{ app_sessions: number | null; app_active_min: number | null }> {\n if (!_tally) await load();\n rollIfNewDay();\n flushForeground(); // include the in-progress foreground stretch\n persist();\n if (!_tally) return { app_sessions: null, app_active_min: null };\n return {\n app_sessions: _tally.sessions,\n app_active_min: Math.round(_tally.activeMs / 60000),\n };\n}\n","/**\n * Camera capability — permission only, NO collector.\n *\n * Unlike calendar/contacts/health, the camera is a LIVE capability: frames are\n * used in the moment (deviceState.visual, a widget's camera preview) and are\n * analysed on-device — never collected to the relay. So this module carries only\n * the OS-permission check/request that the capability toggle drives; there is no\n * `collect`. The user's grant (this toggle) is what gates access, on the same\n * path as location/contacts.\n */\nimport { Camera } from 'expo-camera';\n\nexport async function hasPermission(): Promise<boolean> {\n try {\n return (await Camera.getCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n\nexport async function requestPermission(): Promise<boolean> {\n try {\n return (await Camera.requestCameraPermissionsAsync()).granted;\n } catch {\n return false;\n }\n}\n","/**\n * Built-in Capabilities — wraps each collector as a registerable Capability.\n *\n * Each capability defines:\n * - id, name, description, reason (for UI)\n * - permission check/request\n * - collect function\n *\n * The host app can register these selectively, or add custom ones.\n */\n\nimport * as calendar from './collectors/calendar';\nimport * as contacts from './collectors/contacts';\nimport * as location from './collectors/location';\nimport * as battery from './collectors/battery';\nimport * as connectivity from './collectors/connectivity';\nimport * as notifications from './collectors/notifications';\nimport * as health from './collectors/health';\nimport * as mood from './collectors/mood';\nimport * as camera from './collectors/camera';\nimport type { Capability } from './types';\n\n// ─── Factory functions — call these to get a Capability instance ───\n\nexport function calendarCapability(): Capability {\n return {\n id: 'calendar',\n name: 'Calendar',\n description: 'Syncs your calendar events so your assistant knows your schedule.',\n reason: 'Needed for scheduling, meeting prep, commute alerts, and daily briefings.',\n icon: 'calendar',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: calendar.requestPermission,\n checkPermission: calendar.hasPermission,\n collect: async () => {\n const [today, upcoming] = await Promise.all([\n calendar.collectToday(),\n calendar.collectUpcoming(7),\n ]);\n return { today, upcoming };\n },\n defaultInterval: 60_000, // 1 min\n };\n}\n\nexport function contactsCapability(): Capability {\n return {\n id: 'contacts',\n name: 'Contacts',\n description: 'Syncs your contacts so your assistant can find people and relationships.',\n reason: 'Needed for messaging, birthday reminders, and relationship insights.',\n icon: 'people',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: contacts.requestPermission,\n checkPermission: contacts.hasPermission,\n collect: async () => {\n const list = await contacts.collectAll();\n // null = no changes since last successful sync → bridge skips push.\n // Critical for users with 6000+ contacts: full payload is ~1MB and\n // would otherwise re-upload every 5 min with no actual change.\n if (list === null) return null as any;\n return { contacts: list };\n },\n defaultInterval: 300_000, // 5 min\n };\n}\n\nexport function locationCapability(): Capability {\n return {\n id: 'location',\n name: 'Location',\n description: 'Syncs your location for weather, commute, and context-aware assistance.',\n reason: 'Needed for commute estimates, weather at your location, and situation detection.',\n icon: 'location',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: location.requestPermission,\n checkPermission: location.hasPermission,\n collect: location.collectCurrent,\n defaultInterval: 30_000, // 30 sec\n };\n}\n\nexport function batteryCapability(): Capability {\n return {\n id: 'battery',\n name: 'Battery',\n description: 'Syncs battery level and charging status.',\n reason: 'Helps your assistant adjust behavior when battery is low.',\n icon: 'battery',\n requiresPermission: false,\n permissionState: 'not_needed',\n requestPermission: async () => true,\n checkPermission: async () => true,\n collect: battery.collectBattery,\n defaultInterval: 60_000, // 1 min\n subscribe: battery.subscribeToBattery,\n };\n}\n\nexport function connectivityCapability(): Capability {\n return {\n id: 'connectivity',\n name: 'Connectivity',\n description: 'Syncs WiFi/cellular status.',\n reason: 'Helps your assistant know if you have internet access.',\n icon: 'wifi',\n requiresPermission: false,\n permissionState: 'not_needed',\n requestPermission: async () => true,\n checkPermission: async () => true,\n collect: connectivity.collectConnectivity,\n defaultInterval: 30_000, // 30 sec\n };\n}\n\nexport function healthCapability(): Capability {\n return {\n id: 'health',\n name: 'Health',\n description: 'Reads daily steps, floors, calories, and distance from Apple Health.',\n reason: 'Powers the personal health coach and pairs activity data with nutrition tracking.',\n icon: 'heart',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: health.requestPermission,\n checkPermission: health.hasPermission,\n collect: async () => {\n // Push today AND the last 30 days every sync. The connector slices\n // `days` to whatever range the caller asked for. ~30 sequential\n // HealthKit reads on iOS take <300ms total — cheap.\n const days = await health.collectHistory(30);\n const today = days[0] || (await health.collectToday());\n return { today, days };\n },\n defaultInterval: 300_000, // 5 min\n };\n}\n\nexport function moodCapability(): Capability {\n return {\n id: 'mood',\n name: 'Mood',\n description: 'Sends a daily behavioral-signal summary (activity, routine) to infer mood — not a medical diagnosis.',\n reason: 'Lets ada gently notice changes in energy, stress, and routine and check in.',\n icon: 'heart',\n // No OS permission of its own — it derives from collectors that gate\n // themselves (e.g. health). Treated as not_needed so the bridge syncs it\n // whenever the user has the Mood toggle on.\n requiresPermission: false,\n permissionState: 'not_needed',\n requestPermission: async () => true,\n checkPermission: async () => true,\n collect: async () => {\n // One compact daily summary (window/timezone/collected_at/today).\n // Pushed as the `mood` relay domain; backend runs /api/mood/analyze.\n return mood.collectToday();\n },\n defaultInterval: 3_600_000, // hourly — it's a daily summary, no need to spam\n };\n}\n\nexport function cameraCapability(): Capability {\n return {\n id: 'camera',\n name: 'Camera',\n description:\n \"Lets ada see through your camera to guide you — reading text and describing what's in \" +\n 'front of you. Frames are analysed on the phone; nothing is stored or uploaded.',\n reason: 'Powers visual guidance (Ada Guide): a live view with on-device text and scene understanding.',\n icon: 'camera',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: camera.requestPermission,\n checkPermission: camera.hasPermission,\n // LIVE, not synced: the camera is used in the moment (deviceState.visual / a\n // widget's preview) and never collected to the relay. So collect never\n // pushes — this capability's value is the USER GRANT + OS permission, exposed\n // to the backend like every other, not a data payload. `null` = nothing to\n // push, which the bridge skips (same contract contacts uses when unchanged).\n collect: async () => null as any,\n defaultInterval: 3_600_000, // never really collects; keep the poll rare\n };\n}\n\nexport function notificationsCapability(): Capability {\n return {\n id: 'notifications',\n name: 'Notifications',\n description: 'Syncs your notifications for triage and smart grouping.',\n reason: 'Needed for notification triage, priority filtering, and Do Not Disturb.',\n icon: 'notifications',\n requiresPermission: true,\n permissionState: 'not_requested',\n requestPermission: notifications.requestPermission,\n checkPermission: notifications.hasPermission,\n collect: notifications.collectNotifications,\n defaultInterval: 15_000, // 15 sec\n };\n}\n\n// ─── Convenience: get all built-in capabilities ───\n\nexport function allBuiltIn(): Capability[] {\n return [\n calendarCapability(),\n contactsCapability(),\n locationCapability(),\n batteryCapability(),\n connectivityCapability(),\n notificationsCapability(),\n healthCapability(),\n moodCapability(),\n cameraCapability(),\n ];\n}\n","/**\n * The install's device id — the name this phone syncs under on the relay.\n *\n * ONE owner of the key: the device-permissions store creates it on the first\n * bridge start, and a silent-push background sync only reads it (no id yet =\n * nothing has synced from this install, so there is nothing to refresh). The\n * key is the one the app used before the SDK owned it, so an existing install\n * keeps its id and the relay keeps its data under it.\n *\n * No imports on purpose: a background push wake loads this file alone.\n */\n\n/** Secure key-value storage the host provides (Keychain / Keystore). */\nexport interface SecureKv {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n}\n\nexport const DEVICE_ID_KEY = 'ateam.deviceId';\n\n/** The saved device id, or null when this install has never synced. */\nexport function readDeviceId(storage: SecureKv): Promise<string | null> {\n return storage.getItem(DEVICE_ID_KEY);\n}\n\n/** The saved device id; makes and saves one on the first call. */\nexport async function getOrCreateDeviceId(storage: SecureKv): Promise<string> {\n const saved = await storage.getItem(DEVICE_ID_KEY);\n if (saved) return saved;\n const id = `device-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n await storage.setItem(DEVICE_ID_KEY, id);\n return id;\n}\n","/**\n * Device-sync settings the host provides once (bootstrapExpoAdas `deviceSync`,\n * or configureDeviceSync directly). Kept apart from the store so the bootstrap\n * can set them without loading the device bridge at module load.\n */\nimport type { SecureKv } from '../device-bridge/deviceId';\n\nexport interface DeviceSyncConfig {\n /** Relay base URL getter — read on every start, so an env switch flows through. */\n relayUrl: () => string;\n /** The relay's shared key, sent as `Authorization: Bearer`. Empty = the relay answers 401. */\n relayKey: string;\n /** Secure storage for the install's device id. */\n secureStorage: SecureKv;\n}\n\n// Module-private on purpose: plugin bundles run in the same JS context, so the\n// relay key and the secure-storage reader must not sit anywhere they can reach.\n// (The CJS build gives the bootstrap, capabilities and silent-push entries each\n// their own copy of this file; a require() host calls configureDeviceSync from\n// '@ateam-ai-mobile/expo/capabilities' instead of the bootstrap option, and its\n// silent-push sync finds no config and skips. Metro uses the source and the ESM\n// build shares one copy.)\nlet config: DeviceSyncConfig | null = null;\n\nexport function configureDeviceSync(next: DeviceSyncConfig): void {\n config = next;\n}\n\nexport function getDeviceSyncConfig(): DeviceSyncConfig | null {\n return config;\n}\n","/**\n * The user's device-sharing choices as saved — the one reader, used by the\n * capabilities store and by a silent-push background sync, which must share\n * only what the user turned on (it loads this file, not the store).\n */\nimport AsyncStorage from '@react-native-async-storage/async-storage';\n\n// The keys the app used before the SDK owned them — existing installs keep their choices.\nexport const STORAGE_KEY = 'ateam.capabilities';\nexport const SETUP_KEY = 'ateam.capabilities.setup';\n\n// Default enabled state per capability (first-time only)\nconst DEFAULT_ENABLED: Record<string, boolean> = {\n calendar: true,\n contacts: true,\n location: true,\n battery: true,\n connectivity: true,\n health: true,\n mood: true,\n // Opt-IN: the camera is more invasive than the sync collectors and should not\n // prompt every user for camera access on first launch. It lights up only when\n // the user turns it on (which then requests the OS permission), so a solution\n // that wants a camera preview asks the user to enable it.\n camera: false,\n};\n\n// Capabilities hidden from the user-facing list — the bridge never registers\n// them. The host's push-notifications toggle is the single user control; the\n// notifications collector is internal plumbing and shouldn't surface as a\n// separate switch.\nexport const HIDDEN_FROM_UI = new Set(['notifications']);\n\nexport interface SavedChoices {\n setupDone: boolean;\n /** The user's saved toggles; a capability missing here has its default. */\n prefs: Record<string, boolean>;\n}\n\n/** Read the saved choices. Throws on a corrupt save — callers decide what that means. */\nexport async function loadChoices(): Promise<SavedChoices> {\n const setupDone = await AsyncStorage.getItem(SETUP_KEY);\n const saved = await AsyncStorage.getItem(STORAGE_KEY);\n return { setupDone: setupDone === 'true', prefs: saved ? JSON.parse(saved) : {} };\n}\n\n/** Whether the user's choices turn this capability on. */\nexport function isEnabled(id: string, prefs: Record<string, boolean>): boolean {\n return prefs[id] ?? DEFAULT_ENABLED[id] ?? true;\n}\n\n/**\n * The domains among `domains` this device may sync now: setup done, shown to\n * the user, and turned on. Anything unreadable → none.\n */\nexport async function sharedDomains(domains: string[]): Promise<string[]> {\n let choices: SavedChoices;\n try {\n choices = await loadChoices();\n } catch {\n return [];\n }\n if (!choices.setupDone) return [];\n return domains.filter((d) => !HIDDEN_FROM_UI.has(d) && isEnabled(d, choices.prefs));\n}\n","/**\n * Firebase Cloud Messaging, if this build has it.\n *\n * @react-native-firebase/messaging is an optional peer: in Expo Go (or a host\n * without Firebase) the JS module may exist but its native module does not, so\n * we probe by calling messaging() once. No native module → null, and push is\n * off (the hook and the background sync become no-ops). Probed on first use,\n * not at import.\n */\n\n/** The slice of the RNFB messaging API the SDK uses. */\nexport interface RemoteMessage {\n notification?: { title?: string; body?: string };\n data?: Record<string, unknown>;\n}\n\nexport interface MessagingInstance {\n requestPermission(): Promise<number>;\n isDeviceRegisteredForRemoteMessages: boolean;\n registerDeviceForRemoteMessages(): Promise<void>;\n getAPNSToken(): Promise<string | null>;\n getToken(): Promise<string>;\n onTokenRefresh(cb: (token: string) => void): () => void;\n onMessage(cb: (msg: RemoteMessage) => void | Promise<void>): () => void;\n onNotificationOpenedApp(cb: (msg: RemoteMessage) => void): () => void;\n getInitialNotification(): Promise<RemoteMessage | null>;\n setBackgroundMessageHandler(cb: (msg: RemoteMessage) => Promise<void>): void;\n}\n\nexport interface Messaging {\n (): MessagingInstance;\n AuthorizationStatus: { AUTHORIZED: number; PROVISIONAL: number };\n}\n\nlet probed = false;\nlet messaging: Messaging | null = null;\n\nexport function getMessaging(): Messaging | null {\n if (probed) return messaging;\n probed = true;\n try {\n const candidate = require('@react-native-firebase/messaging').default as Messaging;\n candidate(); // throws \"Native module RNFBAppModule not found\" in Expo Go\n messaging = candidate;\n } catch {\n console.warn('[adas/push] @react-native-firebase/messaging native module unavailable — push disabled (Expo Go?)');\n }\n return messaging;\n}\n"],"mappings":"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","names":["config","startOfDay","requestPermission","hasPermission","AsyncStorage","ExpoLocation","requestPermission","hasPermission","requestPermission","hasPermission","import_react_native","hasPermission","requestPermission","collectToday","import_react_native","import_async_storage","requestPermission","hasPermission","collectToday","requestPermission","hasPermission","config","import_async_storage","AsyncStorage","messaging"]}
@@ -0,0 +1,67 @@
1
+ import {
2
+ getMessaging
3
+ } from "../chunk-AE55BKYV.mjs";
4
+ import {
5
+ sharedDomains
6
+ } from "../chunk-GW7YGXOW.mjs";
7
+ import {
8
+ getDeviceSyncConfig
9
+ } from "../chunk-GA7DS3PA.mjs";
10
+ import {
11
+ syncDomains
12
+ } from "../chunk-C62NQA4F.mjs";
13
+ import "../chunk-XQSY6MHP.mjs";
14
+ import "../chunk-KOXF2FZM.mjs";
15
+ import "../chunk-7FWYEHN5.mjs";
16
+ import "../chunk-J6XRNX7P.mjs";
17
+ import {
18
+ readDeviceId
19
+ } from "../chunk-IJPR5ITF.mjs";
20
+ import "../chunk-WMJKH4XE.mjs";
21
+
22
+ // src/push/silentSync.ts
23
+ import { tenantFromToken, userFromJwt } from "@ateam-ai-mobile/core";
24
+ function registerSilentPushSync(opts) {
25
+ const messaging = getMessaging();
26
+ if (!messaging) return;
27
+ messaging().setBackgroundMessageHandler((msg) => handleSilentPush(msg, opts));
28
+ }
29
+ async function handleSilentPush(msg, opts) {
30
+ const data = msg?.data || {};
31
+ if (data.action !== "sync") return;
32
+ let requested = data.domains;
33
+ try {
34
+ if (typeof requested === "string") requested = JSON.parse(requested);
35
+ } catch (err) {
36
+ console.warn("[adas/push] silent sync: bad domains:", err);
37
+ return;
38
+ }
39
+ if (!Array.isArray(requested)) return;
40
+ const domains = await sharedDomains(requested.filter((d) => typeof d === "string"));
41
+ if (domains.length === 0) return;
42
+ const deviceSync = getDeviceSyncConfig();
43
+ if (!deviceSync) {
44
+ console.warn("[adas/push] silent sync skipped: device sync is not configured");
45
+ return;
46
+ }
47
+ const deviceId = await readDeviceId(deviceSync.secureStorage);
48
+ if (!deviceId) return;
49
+ const jwt = await opts.loadToken();
50
+ if (!jwt) return;
51
+ const tenant = tenantFromToken(jwt);
52
+ const actorId = userFromJwt(jwt)?.actorId;
53
+ if (!tenant || !actorId) return;
54
+ console.log(`[adas/push] silent sync: ${domains.join(", ")}`);
55
+ try {
56
+ await syncDomains(
57
+ { relayUrl: deviceSync.relayUrl(), deviceId, tenant, actorId, apiKey: deviceSync.relayKey },
58
+ domains
59
+ );
60
+ } catch (err) {
61
+ console.warn("[adas/push] silent sync failed:", err);
62
+ }
63
+ }
64
+ export {
65
+ registerSilentPushSync
66
+ };
67
+ //# sourceMappingURL=silentSync.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/push/silentSync.ts"],"sourcesContent":["/**\n * Silent-push device sync — the server wakes the phone to refresh device data.\n *\n * Call registerSilentPushSync({ loadToken }) once in your app's entry file,\n * before the app component registers: Firebase wants its background handler\n * set at the top level, outside React. It also needs device sync configured\n * (bootstrapExpoAdas({ deviceSync }) or configureDeviceSync) — read when a push\n * arrives, so the order of the two calls doesn't matter.\n *\n * Contract for a silent (data-only) push:\n * data: { action: \"sync\", domains: '[\"location\",\"calendar\"]' } (JSON string array)\n * Anything else is ignored here — visible pushes are usePushNotifications'.\n *\n * Syncs as the signed-in user and team from the SAVED session token (the app\n * may be woken with no session restored) and under this install's device id;\n * no token, no user, no team or no device id → nothing is sent. Only domains\n * the user shares are synced (their saved choices), as the foreground bridge does.\n *\n * Moved from ateam-mobile's pushBackgroundHandler (2026-10-29). Changed on the\n * way: it syncs only the domains the user turned on (it synced any domain the\n * push named), and never without a user (it would sync as 'default').\n */\nimport { syncDomains } from '../device-bridge/background';\nimport { readDeviceId } from '../device-bridge/deviceId';\nimport { getDeviceSyncConfig } from '../capabilities/config';\nimport { sharedDomains } from '../capabilities/choices';\nimport { tenantFromToken, userFromJwt } from '@ateam-ai-mobile/core';\nimport { getMessaging, type RemoteMessage } from './messaging';\n\nexport interface SilentPushSyncOptions {\n /** The saved session token — the same loader your auth session's persistence uses. */\n loadToken: () => Promise<string | null>;\n}\n\n/** Register the background handler. No Firebase in this build → nothing to do. */\nexport function registerSilentPushSync(opts: SilentPushSyncOptions): void {\n const messaging = getMessaging();\n if (!messaging) return;\n messaging().setBackgroundMessageHandler((msg) => handleSilentPush(msg, opts));\n}\n\nasync function handleSilentPush(msg: RemoteMessage, opts: SilentPushSyncOptions): Promise<void> {\n const data = msg?.data || {};\n if (data.action !== 'sync') return;\n\n let requested: unknown = data.domains;\n try {\n if (typeof requested === 'string') requested = JSON.parse(requested);\n } catch (err) {\n console.warn('[adas/push] silent sync: bad domains:', err);\n return;\n }\n if (!Array.isArray(requested)) return;\n const domains = await sharedDomains(requested.filter((d): d is string => typeof d === 'string'));\n if (domains.length === 0) return;\n\n const deviceSync = getDeviceSyncConfig();\n if (!deviceSync) {\n console.warn('[adas/push] silent sync skipped: device sync is not configured');\n return;\n }\n // No id = nothing has synced from this install yet, so nothing to refresh.\n const deviceId = await readDeviceId(deviceSync.secureStorage);\n if (!deviceId) return;\n\n // Identity AND team from the session token's own claims — never a stored\n // copy, never a default. Signed out → no token → nothing to sync as.\n const jwt = await opts.loadToken();\n if (!jwt) return;\n const tenant = tenantFromToken(jwt);\n const actorId = userFromJwt(jwt)?.actorId;\n if (!tenant || !actorId) return;\n\n console.log(`[adas/push] silent sync: ${domains.join(', ')}`);\n try {\n await syncDomains(\n { relayUrl: deviceSync.relayUrl(), deviceId, tenant, actorId, apiKey: deviceSync.relayKey },\n domains,\n );\n } catch (err) {\n console.warn('[adas/push] silent sync failed:', err);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAS,iBAAiB,mBAAmB;AAStC,SAAS,uBAAuB,MAAmC;AACxE,QAAM,YAAY,aAAa;AAC/B,MAAI,CAAC,UAAW;AAChB,YAAU,EAAE,4BAA4B,CAAC,QAAQ,iBAAiB,KAAK,IAAI,CAAC;AAC9E;AAEA,eAAe,iBAAiB,KAAoB,MAA4C;AAC9F,QAAM,OAAO,KAAK,QAAQ,CAAC;AAC3B,MAAI,KAAK,WAAW,OAAQ;AAE5B,MAAI,YAAqB,KAAK;AAC9B,MAAI;AACF,QAAI,OAAO,cAAc,SAAU,aAAY,KAAK,MAAM,SAAS;AAAA,EACrE,SAAS,KAAK;AACZ,YAAQ,KAAK,yCAAyC,GAAG;AACzD;AAAA,EACF;AACA,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG;AAC/B,QAAM,UAAU,MAAM,cAAc,UAAU,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,CAAC;AAC/F,MAAI,QAAQ,WAAW,EAAG;AAE1B,QAAM,aAAa,oBAAoB;AACvC,MAAI,CAAC,YAAY;AACf,YAAQ,KAAK,gEAAgE;AAC7E;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,aAAa,WAAW,aAAa;AAC5D,MAAI,CAAC,SAAU;AAIf,QAAM,MAAM,MAAM,KAAK,UAAU;AACjC,MAAI,CAAC,IAAK;AACV,QAAM,SAAS,gBAAgB,GAAG;AAClC,QAAM,UAAU,YAAY,GAAG,GAAG;AAClC,MAAI,CAAC,UAAU,CAAC,QAAS;AAEzB,UAAQ,IAAI,4BAA4B,QAAQ,KAAK,IAAI,CAAC,EAAE;AAC5D,MAAI;AACF,UAAM;AAAA,MACJ,EAAE,UAAU,WAAW,SAAS,GAAG,UAAU,QAAQ,SAAS,QAAQ,WAAW,SAAS;AAAA,MAC1F;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,KAAK,mCAAmC,GAAG;AAAA,EACrD;AACF;","names":[]}
@@ -86,5 +86,11 @@ declare function describe(id: string, maxLabels?: number): Promise<VisionLabel[]
86
86
  * items with boxes. [] = looked, found nothing above `minConfidence`; throws =
87
87
  * couldn't look (id aged out, or the model isn't in this build). */
88
88
  declare function detectObjects(id: string, maxItems?: number, minConfidence?: number): Promise<VisionObject[]>;
89
+ /**
90
+ * A published frame as a base64 JPEG, its longer side at most `maxSide`. FOR THE SDK'S AI LIVE SENDER ONLY
91
+ * (aiLive/provider.ts), which calls it after the two-sided "Pictures to AI" consent: this is the one way a
92
+ * picture leaves native, and no plugin-facing API reaches it. An older binary refuses by name.
93
+ */
94
+ declare function jpegById(id: string, maxSide?: number, quality?: number): Promise<string>;
89
95
 
90
- export { CameraPreview, type RawFrameQuality, type VisionFacing, type VisionFrame, type VisionLabel, type VisionObject, type VisionText, assessQuality, describe, detectObjects, isSupported, onVisionFrame, qualityById, readText, startVision, stopVision, visionRetentionMs };
96
+ export { CameraPreview, type RawFrameQuality, type VisionFacing, type VisionFrame, type VisionLabel, type VisionObject, type VisionText, assessQuality, describe, detectObjects, isSupported, jpegById, onVisionFrame, qualityById, readText, startVision, stopVision, visionRetentionMs };
@@ -86,5 +86,11 @@ declare function describe(id: string, maxLabels?: number): Promise<VisionLabel[]
86
86
  * items with boxes. [] = looked, found nothing above `minConfidence`; throws =
87
87
  * couldn't look (id aged out, or the model isn't in this build). */
88
88
  declare function detectObjects(id: string, maxItems?: number, minConfidence?: number): Promise<VisionObject[]>;
89
+ /**
90
+ * A published frame as a base64 JPEG, its longer side at most `maxSide`. FOR THE SDK'S AI LIVE SENDER ONLY
91
+ * (aiLive/provider.ts), which calls it after the two-sided "Pictures to AI" consent: this is the one way a
92
+ * picture leaves native, and no plugin-facing API reaches it. An older binary refuses by name.
93
+ */
94
+ declare function jpegById(id: string, maxSide?: number, quality?: number): Promise<string>;
89
95
 
90
- export { CameraPreview, type RawFrameQuality, type VisionFacing, type VisionFrame, type VisionLabel, type VisionObject, type VisionText, assessQuality, describe, detectObjects, isSupported, onVisionFrame, qualityById, readText, startVision, stopVision, visionRetentionMs };
96
+ export { CameraPreview, type RawFrameQuality, type VisionFacing, type VisionFrame, type VisionLabel, type VisionObject, type VisionText, assessQuality, describe, detectObjects, isSupported, jpegById, onVisionFrame, qualityById, readText, startVision, stopVision, visionRetentionMs };
@@ -25,6 +25,7 @@ __export(vision_exports, {
25
25
  describe: () => describe,
26
26
  detectObjects: () => detectObjects,
27
27
  isSupported: () => isSupported,
28
+ jpegById: () => jpegById,
28
29
  onVisionFrame: () => onVisionFrame,
29
30
  qualityById: () => qualityById,
30
31
  readText: () => readText,
@@ -95,6 +96,12 @@ async function describe(id, maxLabels = 5) {
95
96
  async function detectObjects(id, maxItems = 10, minConfidence = 0.25) {
96
97
  return verb("detectObjects")(id, maxItems, minConfidence);
97
98
  }
99
+ async function jpegById(id, maxSide = 768, quality = 0.7) {
100
+ if (native && typeof native.jpegById !== "function") {
101
+ throw new Error("sending pictures to AI needs a newer app build \u2014 this one cannot make a picture from a camera frame");
102
+ }
103
+ return verb("jpegById")(id, maxSide, quality);
104
+ }
98
105
  // Annotate the CommonJS export names for ESM import in node:
99
106
  0 && (module.exports = {
100
107
  CameraPreview,
@@ -102,6 +109,7 @@ async function detectObjects(id, maxItems = 10, minConfidence = 0.25) {
102
109
  describe,
103
110
  detectObjects,
104
111
  isSupported,
112
+ jpegById,
105
113
  onVisionFrame,
106
114
  qualityById,
107
115
  readText,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/vision/index.ts"],"sourcesContent":["// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n /** Absent from binaries built before the front camera existed. */\n startVisionCamera?(intervalMs: number, facing: VisionFacing): Promise<void>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** The native function, or a refusal by name: Android's AdaVision has assessQuality only\n * (AI Models F4), so a verb it lacks says so instead of failing as \"not a function\". */\nfunction verb<K extends keyof AdaVisionNative>(name: K): AdaVisionNative[K] {\n if (!native) throw new Error('ada-vision native module not present in this build');\n const fn = native[name];\n if (typeof fn !== 'function') {\n throw Object.assign(new Error(`UNAVAILABLE: ada-vision ${String(name)} is not implemented on this platform (iOS only)`), { code: 'UNAVAILABLE' });\n }\n return (fn as Function).bind(native) as AdaVisionNative[K];\n}\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Which camera the vision session reads. */\nexport type VisionFacing = 'front' | 'back';\n\n/**\n * Start the vision session on `facing` — publishes a timeline frame every\n * `intervalMs`. The back camera works on every binary; the front camera needs a\n * binary built with it, and an older one REFUSES it rather than quietly reading\n * the back camera instead.\n */\nexport async function startVision(intervalMs = 1000, facing: VisionFacing = 'back'): Promise<void> {\n if (facing !== 'front' && facing !== 'back') {\n throw new Error(`Unknown camera \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n if (facing === 'back') return verb('startVision')(intervalMs);\n if (native && typeof native.startVisionCamera !== 'function') {\n throw new Error('the front camera needs a newer app build — this one can read the back camera only');\n }\n return verb('startVisionCamera')!(intervalMs, facing);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n return verb('readText')(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n return verb('describe')(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n return verb('detectObjects')(id, maxItems, minConfidence);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,+BAA4C;AAC5C,kBAAkC;AAK3B,IAAM,iBAA4C,MAAM;AAC7D,MAAI;AAAE,eAAO,+BAAkB,WAAW;AAAA,EAAG,QAAQ;AAAE,WAAO;AAAA,EAAM;AACtE,GAAG;AAgEH,IAAM,aAAS,sDAA6C,WAAW;AAIvE,SAAS,KAAsC,MAA6B;AAC1E,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oDAAoD;AACjF,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,OAAO,OAAO,YAAY;AAC5B,UAAM,OAAO,OAAO,IAAI,MAAM,2BAA2B,OAAO,IAAI,CAAC,iDAAiD,GAAG,EAAE,MAAM,cAAc,CAAC;AAAA,EAClJ;AACA,SAAQ,GAAgB,KAAK,MAAM;AACrC;AAGO,SAAS,cAAuB;AACrC,MAAI;AAAE,WAAO,QAAQ,YAAY,KAAK;AAAA,EAAO,QAAQ;AAAE,WAAO;AAAA,EAAO;AACvE;AAGA,eAAsB,cAAc,KAAuC;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oDAAoD;AACjF,SAAO,OAAO,cAAc,GAAG;AACjC;AAaA,eAAsB,YAAY,aAAa,KAAM,SAAuB,QAAuB;AACjG,MAAI,WAAW,WAAW,WAAW,QAAQ;AAC3C,UAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,CAAC,gCAA2B;AAAA,EAC9E;AACA,MAAI,WAAW,OAAQ,QAAO,KAAK,aAAa,EAAE,UAAU;AAC5D,MAAI,UAAU,OAAO,OAAO,sBAAsB,YAAY;AAC5D,UAAM,IAAI,MAAM,wFAAmF;AAAA,EACrG;AACA,SAAO,KAAK,mBAAmB,EAAG,YAAY,MAAM;AACtD;AAGO,SAAS,aAAmB;AACjC,UAAQ,aAAa;AACvB;AAGO,SAAS,cAAc,IAA4C;AACxE,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS;AAAA,EAAC,EAAE;AAClC,SAAO,OAAO,YAAY,eAAe,EAAE;AAC7C;AAGO,SAAS,oBAA4B;AAC1C,SAAO,QAAQ,oBAAoB,KAAK;AAC1C;AAGO,SAAS,YAAY,IAAoC;AAC9D,SAAO,QAAQ,cAAc,EAAE,KAAK;AACtC;AAGA,eAAsB,SAAS,IAAY,WAA6C;AACtF,SAAO,KAAK,UAAU,EAAE,IAAI,aAAa,IAAI;AAC/C;AAGA,eAAsB,SAAS,IAAY,YAAY,GAA2B;AAChF,SAAO,KAAK,UAAU,EAAE,IAAI,SAAS;AACvC;AAKA,eAAsB,cAAc,IAAY,WAAW,IAAI,gBAAgB,MAA+B;AAC5G,SAAO,KAAK,eAAe,EAAE,IAAI,UAAU,aAAa;AAC1D;","names":[]}
1
+ {"version":3,"sources":["../../src/vision/index.ts"],"sourcesContent":["// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n /** Absent from binaries built before the front camera existed. */\n startVisionCamera?(intervalMs: number, facing: VisionFacing): Promise<void>;\n /** Absent from binaries built before AI live existed. A base64 JPEG. */\n jpegById?(id: string, maxSide: number, quality: number): Promise<string>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** The native function, or a refusal by name: Android's AdaVision has assessQuality only\n * (AI Models F4), so a verb it lacks says so instead of failing as \"not a function\". */\nfunction verb<K extends keyof AdaVisionNative>(name: K): AdaVisionNative[K] {\n if (!native) throw new Error('ada-vision native module not present in this build');\n const fn = native[name];\n if (typeof fn !== 'function') {\n throw Object.assign(new Error(`UNAVAILABLE: ada-vision ${String(name)} is not implemented on this platform (iOS only)`), { code: 'UNAVAILABLE' });\n }\n return (fn as Function).bind(native) as AdaVisionNative[K];\n}\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Which camera the vision session reads. */\nexport type VisionFacing = 'front' | 'back';\n\n/**\n * Start the vision session on `facing` — publishes a timeline frame every\n * `intervalMs`. The back camera works on every binary; the front camera needs a\n * binary built with it, and an older one REFUSES it rather than quietly reading\n * the back camera instead.\n */\nexport async function startVision(intervalMs = 1000, facing: VisionFacing = 'back'): Promise<void> {\n if (facing !== 'front' && facing !== 'back') {\n throw new Error(`Unknown camera \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n if (facing === 'back') return verb('startVision')(intervalMs);\n if (native && typeof native.startVisionCamera !== 'function') {\n throw new Error('the front camera needs a newer app build — this one can read the back camera only');\n }\n return verb('startVisionCamera')!(intervalMs, facing);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n return verb('readText')(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n return verb('describe')(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n return verb('detectObjects')(id, maxItems, minConfidence);\n}\n\n/**\n * A published frame as a base64 JPEG, its longer side at most `maxSide`. FOR THE SDK'S AI LIVE SENDER ONLY\n * (aiLive/provider.ts), which calls it after the two-sided \"Pictures to AI\" consent: this is the one way a\n * picture leaves native, and no plugin-facing API reaches it. An older binary refuses by name.\n */\nexport async function jpegById(id: string, maxSide = 768, quality = 0.7): Promise<string> {\n if (native && typeof native.jpegById !== 'function') {\n throw new Error('sending pictures to AI needs a newer app build — this one cannot make a picture from a camera frame');\n }\n return verb('jpegById')!(id, maxSide, quality);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,+BAA4C;AAC5C,kBAAkC;AAK3B,IAAM,iBAA4C,MAAM;AAC7D,MAAI;AAAE,eAAO,+BAAkB,WAAW;AAAA,EAAG,QAAQ;AAAE,WAAO;AAAA,EAAM;AACtE,GAAG;AAkEH,IAAM,aAAS,sDAA6C,WAAW;AAIvE,SAAS,KAAsC,MAA6B;AAC1E,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oDAAoD;AACjF,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,OAAO,OAAO,YAAY;AAC5B,UAAM,OAAO,OAAO,IAAI,MAAM,2BAA2B,OAAO,IAAI,CAAC,iDAAiD,GAAG,EAAE,MAAM,cAAc,CAAC;AAAA,EAClJ;AACA,SAAQ,GAAgB,KAAK,MAAM;AACrC;AAGO,SAAS,cAAuB;AACrC,MAAI;AAAE,WAAO,QAAQ,YAAY,KAAK;AAAA,EAAO,QAAQ;AAAE,WAAO;AAAA,EAAO;AACvE;AAGA,eAAsB,cAAc,KAAuC;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,oDAAoD;AACjF,SAAO,OAAO,cAAc,GAAG;AACjC;AAaA,eAAsB,YAAY,aAAa,KAAM,SAAuB,QAAuB;AACjG,MAAI,WAAW,WAAW,WAAW,QAAQ;AAC3C,UAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,CAAC,gCAA2B;AAAA,EAC9E;AACA,MAAI,WAAW,OAAQ,QAAO,KAAK,aAAa,EAAE,UAAU;AAC5D,MAAI,UAAU,OAAO,OAAO,sBAAsB,YAAY;AAC5D,UAAM,IAAI,MAAM,wFAAmF;AAAA,EACrG;AACA,SAAO,KAAK,mBAAmB,EAAG,YAAY,MAAM;AACtD;AAGO,SAAS,aAAmB;AACjC,UAAQ,aAAa;AACvB;AAGO,SAAS,cAAc,IAA4C;AACxE,MAAI,CAAC,OAAQ,QAAO,EAAE,SAAS;AAAA,EAAC,EAAE;AAClC,SAAO,OAAO,YAAY,eAAe,EAAE;AAC7C;AAGO,SAAS,oBAA4B;AAC1C,SAAO,QAAQ,oBAAoB,KAAK;AAC1C;AAGO,SAAS,YAAY,IAAoC;AAC9D,SAAO,QAAQ,cAAc,EAAE,KAAK;AACtC;AAGA,eAAsB,SAAS,IAAY,WAA6C;AACtF,SAAO,KAAK,UAAU,EAAE,IAAI,aAAa,IAAI;AAC/C;AAGA,eAAsB,SAAS,IAAY,YAAY,GAA2B;AAChF,SAAO,KAAK,UAAU,EAAE,IAAI,SAAS;AACvC;AAKA,eAAsB,cAAc,IAAY,WAAW,IAAI,gBAAgB,MAA+B;AAC5G,SAAO,KAAK,eAAe,EAAE,IAAI,UAAU,aAAa;AAC1D;AAOA,eAAsB,SAAS,IAAY,UAAU,KAAK,UAAU,KAAsB;AACxF,MAAI,UAAU,OAAO,OAAO,aAAa,YAAY;AACnD,UAAM,IAAI,MAAM,0GAAqG;AAAA,EACvH;AACA,SAAO,KAAK,UAAU,EAAG,IAAI,SAAS,OAAO;AAC/C;","names":[]}
@@ -4,13 +4,14 @@ import {
4
4
  describe,
5
5
  detectObjects,
6
6
  isSupported,
7
+ jpegById,
7
8
  onVisionFrame,
8
9
  qualityById,
9
10
  readText,
10
11
  startVision,
11
12
  stopVision,
12
13
  visionRetentionMs
13
- } from "../chunk-6JX222YX.mjs";
14
+ } from "../chunk-WQK6PUBN.mjs";
14
15
  import "../chunk-WMJKH4XE.mjs";
15
16
  export {
16
17
  CameraPreview,
@@ -18,6 +19,7 @@ export {
18
19
  describe,
19
20
  detectObjects,
20
21
  isSupported,
22
+ jpegById,
21
23
  onVisionFrame,
22
24
  qualityById,
23
25
  readText,
@@ -12,7 +12,10 @@ import UIKit
12
12
  // reads each live frame's luma plane, publishes a
13
13
  // timeline frame (id + quality, never pixels) at a
14
14
  // cadence, and resolves readText/describe/detectObjects
15
- // by id INSIDE native. No photo, no JPEG, no file.
15
+ // by id INSIDE native. No photo, no file.
16
+ // jpegById(id, maxSide, q) — the ONE way a frame leaves native: a JPEG for the SDK's
17
+ // AI live sender, after the two-sided "Pictures to AI" consent
18
+ // (plugin-runtime mayPicturesLeavePhone). Plugins never call it.
16
19
  public class AdaVisionModule: Module {
17
20
  private let vision = VisionSession()
18
21
 
@@ -77,6 +80,16 @@ public class AdaVisionModule: Module {
77
80
  }
78
81
  }
79
82
 
83
+ /** A published frame as a base64 JPEG whose longer side is at most `maxSide`. Only the SDK's AI live
84
+ * sender calls it, after both sides said yes to "Pictures to AI"; a plugin never gets pixels. Rejects
85
+ * if the id is unknown or aged out of the ring. */
86
+ AsyncFunction("jpegById") { (id: String, maxSide: Int, quality: Double, promise: Promise) in
87
+ self.vision.jpegById(id, maxSide: maxSide, quality: quality) { b64, err in
88
+ if let err = err { promise.reject("AdaVision", err.localizedDescription) }
89
+ else { promise.resolve(b64) }
90
+ }
91
+ }
92
+
80
93
  AsyncFunction("assessQuality") { (uri: String) throws -> [String: Any] in
81
94
  guard let image = AdaVisionModule.loadImage(uri) else {
82
95
  throw Exception(name: "AdaVisionUnreadable", description: "Could not read frame at \(uri)")
@@ -446,6 +459,36 @@ final class VisionSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat
446
459
  return vn
447
460
  }()
448
461
 
462
+ // MARK: jpegById — a published frame as a JPEG, for the AI live sender only.
463
+ // Same latest-wins queue as readText/describe. Scaled down (never up) so the longer
464
+ // side is at most maxSide: the size the chosen setup was tested at.
465
+ func jpegById(_ id: String, maxSide: Int, quality: Double, completion: @escaping (String?, Error?) -> Void) {
466
+ queue.async {
467
+ guard let cg = self.ring.first(where: { $0.id == id })?.image else {
468
+ completion(nil, NSError(domain: "AdaVision", code: 6,
469
+ userInfo: [NSLocalizedDescriptionKey: "No frame for id \(id) — unknown or aged out of the ring"]))
470
+ return
471
+ }
472
+ let longest = max(cg.width, cg.height)
473
+ var image = UIImage(cgImage: cg)
474
+ if maxSide > 0 && longest > maxSide {
475
+ let scale = Double(maxSide) / Double(longest)
476
+ let size = CGSize(width: (Double(cg.width) * scale).rounded(), height: (Double(cg.height) * scale).rounded())
477
+ let format = UIGraphicsImageRendererFormat.default()
478
+ format.scale = 1
479
+ image = UIGraphicsImageRenderer(size: size, format: format).image { _ in
480
+ UIImage(cgImage: cg).draw(in: CGRect(origin: .zero, size: size))
481
+ }
482
+ }
483
+ guard let data = image.jpegData(compressionQuality: CGFloat(min(max(quality, 0.1), 1.0))) else {
484
+ completion(nil, NSError(domain: "AdaVision", code: 8,
485
+ userInfo: [NSLocalizedDescriptionKey: "Could not encode the frame as a JPEG"]))
486
+ return
487
+ }
488
+ completion(data.base64EncodedString(), nil)
489
+ }
490
+ }
491
+
449
492
  // MARK: detectObjects — on-device object detection (bundled Core ML), by id.
450
493
  // Same latest-wins discipline as readText/describe (serial `queue`). Boxes go
451
494
  // through the same topLeftBox as readText: normalised 0..1, TOP-left origin, in