@ateam-ai-mobile/expo 0.1.4 → 0.1.7

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 (158) hide show
  1. package/android/build.gradle +16 -0
  2. package/android/src/main/java/expo/modules/adavision/AdaVisionModule.kt +75 -0
  3. package/dist/attachmentCache-DhQTqi2E.d.mts +18 -0
  4. package/dist/attachmentCache-DhQTqi2E.d.ts +18 -0
  5. package/dist/bootstrap.d.mts +95 -0
  6. package/dist/bootstrap.d.ts +95 -0
  7. package/dist/bootstrap.js +2473 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/bootstrap.mjs +64 -0
  10. package/dist/bootstrap.mjs.map +1 -0
  11. package/dist/chunk-4BMWVN25.mjs +91 -0
  12. package/dist/chunk-4BMWVN25.mjs.map +1 -0
  13. package/dist/chunk-4XYXJYN5.mjs +1513 -0
  14. package/dist/chunk-4XYXJYN5.mjs.map +1 -0
  15. package/dist/chunk-5HM6UA2K.mjs +571 -0
  16. package/dist/chunk-5HM6UA2K.mjs.map +1 -0
  17. package/dist/chunk-6LXF3JVX.mjs +96 -0
  18. package/dist/chunk-6LXF3JVX.mjs.map +1 -0
  19. package/dist/chunk-FF6ZZZXH.mjs +71 -0
  20. package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
  21. package/dist/chunk-KOXF2FZM.mjs +92 -0
  22. package/dist/chunk-KOXF2FZM.mjs.map +1 -0
  23. package/dist/chunk-L4V7OCJ3.mjs +71 -0
  24. package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
  25. package/dist/chunk-TZXWVYYR.mjs +67 -0
  26. package/dist/chunk-TZXWVYYR.mjs.map +1 -0
  27. package/dist/chunk-VQJPSLHP.mjs +1801 -0
  28. package/dist/chunk-VQJPSLHP.mjs.map +1 -0
  29. package/dist/chunk-WMJKH4XE.mjs +34 -0
  30. package/dist/chunk-WMJKH4XE.mjs.map +1 -0
  31. package/dist/chunk-XQSY6MHP.mjs +213 -0
  32. package/dist/chunk-XQSY6MHP.mjs.map +1 -0
  33. package/dist/createBridge.mjs +2 -0
  34. package/dist/createBridge.mjs.map +1 -1
  35. package/dist/device/createDeviceProvider.d.mts +23 -0
  36. package/dist/device/createDeviceProvider.d.ts +23 -0
  37. package/dist/device/createDeviceProvider.js +967 -0
  38. package/dist/device/createDeviceProvider.js.map +1 -0
  39. package/dist/device/createDeviceProvider.mjs +17 -0
  40. package/dist/device/createDeviceProvider.mjs.map +1 -0
  41. package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
  42. package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
  43. package/dist/device-bridge/collectors/appEngagement.js +128 -0
  44. package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
  45. package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
  46. package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
  47. package/dist/device-bridge/collectors/health.d.mts +24 -0
  48. package/dist/device-bridge/collectors/health.d.ts +24 -0
  49. package/dist/device-bridge/collectors/health.js +238 -0
  50. package/dist/device-bridge/collectors/health.js.map +1 -0
  51. package/dist/device-bridge/collectors/health.mjs +18 -0
  52. package/dist/device-bridge/collectors/health.mjs.map +1 -0
  53. package/dist/device-bridge/cookies.d.mts +49 -0
  54. package/dist/device-bridge/cookies.d.ts +49 -0
  55. package/dist/device-bridge/cookies.js +90 -0
  56. package/dist/device-bridge/cookies.js.map +1 -0
  57. package/dist/device-bridge/cookies.mjs +14 -0
  58. package/dist/device-bridge/cookies.mjs.map +1 -0
  59. package/dist/device-bridge/index.d.mts +483 -0
  60. package/dist/device-bridge/index.d.ts +483 -0
  61. package/dist/device-bridge/index.js +2084 -0
  62. package/dist/device-bridge/index.js.map +1 -0
  63. package/dist/device-bridge/index.mjs +32 -0
  64. package/dist/device-bridge/index.mjs.map +1 -0
  65. package/dist/device-bridge/nfc.d.mts +30 -0
  66. package/dist/device-bridge/nfc.d.ts +30 -0
  67. package/dist/device-bridge/nfc.js +109 -0
  68. package/dist/device-bridge/nfc.js.map +1 -0
  69. package/dist/device-bridge/nfc.mjs +14 -0
  70. package/dist/device-bridge/nfc.mjs.map +1 -0
  71. package/dist/index.d.mts +493 -18
  72. package/dist/index.d.ts +493 -18
  73. package/dist/index.js +1409 -6
  74. package/dist/index.js.map +1 -1
  75. package/dist/index.mjs +23 -194
  76. package/dist/index.mjs.map +1 -1
  77. package/dist/oauth/expoOAuth.d.mts +18 -0
  78. package/dist/oauth/expoOAuth.d.ts +18 -0
  79. package/dist/oauth/expoOAuth.js +102 -0
  80. package/dist/oauth/expoOAuth.js.map +1 -0
  81. package/dist/oauth/expoOAuth.mjs +8 -0
  82. package/dist/oauth/expoOAuth.mjs.map +1 -0
  83. package/dist/types-BRanJYTn.d.mts +138 -0
  84. package/dist/types-BRanJYTn.d.ts +138 -0
  85. package/dist/vision/index.d.mts +81 -0
  86. package/dist/vision/index.d.ts +81 -0
  87. package/dist/vision/index.js +101 -0
  88. package/dist/vision/index.js.map +1 -0
  89. package/dist/vision/index.mjs +28 -0
  90. package/dist/vision/index.mjs.map +1 -0
  91. package/expo-module.config.json +9 -0
  92. package/ios/AdaVision.podspec +28 -0
  93. package/ios/AdaVisionModule.swift +432 -0
  94. package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
  95. package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
  96. package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
  97. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
  98. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
  99. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
  100. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
  101. package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
  102. package/package.json +107 -7
  103. package/src/bootstrap.ts +159 -0
  104. package/src/camera/CameraTaskHost.tsx +175 -0
  105. package/src/camera/cameraTasks.ts +129 -0
  106. package/src/device/createDeviceProvider.ts +832 -0
  107. package/src/device-bridge/actions/calendar-actions.ts +217 -0
  108. package/src/device-bridge/actions/dnd.ts +74 -0
  109. package/src/device-bridge/actions/health-actions.ts +58 -0
  110. package/src/device-bridge/actions/index.ts +12 -0
  111. package/src/device-bridge/actions/messaging.ts +74 -0
  112. package/src/device-bridge/actions/navigation.ts +88 -0
  113. package/src/device-bridge/actions/queue-poller.ts +151 -0
  114. package/src/device-bridge/background.ts +137 -0
  115. package/src/device-bridge/biometrics.ts +64 -0
  116. package/src/device-bridge/capabilities.ts +218 -0
  117. package/src/device-bridge/collectors/appEngagement.ts +120 -0
  118. package/src/device-bridge/collectors/battery.ts +83 -0
  119. package/src/device-bridge/collectors/calendar.ts +162 -0
  120. package/src/device-bridge/collectors/camera.ts +27 -0
  121. package/src/device-bridge/collectors/connectivity.ts +60 -0
  122. package/src/device-bridge/collectors/contacts.ts +200 -0
  123. package/src/device-bridge/collectors/health.ts +299 -0
  124. package/src/device-bridge/collectors/index.ts +12 -0
  125. package/src/device-bridge/collectors/location.ts +224 -0
  126. package/src/device-bridge/collectors/mood.ts +141 -0
  127. package/src/device-bridge/collectors/notifications.ts +70 -0
  128. package/src/device-bridge/cookies.ts +89 -0
  129. package/src/device-bridge/dynamic-connector.ts +422 -0
  130. package/src/device-bridge/index.ts +467 -0
  131. package/src/device-bridge/nfc.ts +85 -0
  132. package/src/device-bridge/plugin.ts +71 -0
  133. package/src/device-bridge/relay-client.ts +237 -0
  134. package/src/device-bridge/scanner.ts +42 -0
  135. package/src/device-bridge/sharing.ts +35 -0
  136. package/src/device-bridge/types.ts +326 -0
  137. package/src/device-state/activityClassifier.ts +151 -0
  138. package/src/device-state/activityEngine.ts +252 -0
  139. package/src/device-state/collectors.ts +31 -0
  140. package/src/device-state/domains/activity.ts +93 -0
  141. package/src/device-state/domains/capability.ts +60 -0
  142. package/src/device-state/domains/connectivity.ts +60 -0
  143. package/src/device-state/domains/deviceContext.ts +55 -0
  144. package/src/device-state/domains/interaction.ts +59 -0
  145. package/src/device-state/domains/spatial.ts +283 -0
  146. package/src/device-state/domains/visual.ts +100 -0
  147. package/src/device-state/headingSource.ts +84 -0
  148. package/src/device-state/index.ts +77 -0
  149. package/src/device-state/quality.ts +60 -0
  150. package/src/device-state/replay.ts +220 -0
  151. package/src/device-state/turnDetector.ts +121 -0
  152. package/src/device-state/visualEngine.ts +341 -0
  153. package/src/haptics.ts +4 -0
  154. package/src/index.ts +37 -0
  155. package/src/oauth/expoOAuth.tsx +113 -0
  156. package/src/vision/analysis.js +80 -0
  157. package/src/vision/index.ts +144 -0
  158. package/src/vision/ring.js +35 -0
@@ -0,0 +1,1801 @@
1
+ import {
2
+ getToday
3
+ } from "./chunk-KOXF2FZM.mjs";
4
+ import {
5
+ collectHistory,
6
+ collectMoodExtras,
7
+ collectToday,
8
+ hasPermission,
9
+ requestPermission
10
+ } from "./chunk-XQSY6MHP.mjs";
11
+ import {
12
+ __export
13
+ } from "./chunk-WMJKH4XE.mjs";
14
+
15
+ // src/device-bridge/relay-client.ts
16
+ var RelayClient = class {
17
+ constructor(config) {
18
+ this.offlineQueue = [];
19
+ this.baseUrl = config.relayUrl.replace(/\/$/, "");
20
+ this.deviceId = config.deviceId;
21
+ this.tenant = config.tenant;
22
+ this.actorId = config.actorId ?? "default";
23
+ this.apiKey = config.apiKey ?? "";
24
+ this.timeout = config.timeout ?? 1e4;
25
+ }
26
+ // ─── Data Sync ───
27
+ /**
28
+ * Push a full device data bundle to the relay.
29
+ * If offline, buffers the bundle and retries on next sync.
30
+ */
31
+ async pushData(bundle) {
32
+ try {
33
+ const response = await this.fetch(
34
+ `/api/device/${this.deviceId}/sync`,
35
+ {
36
+ method: "POST",
37
+ body: JSON.stringify(bundle)
38
+ }
39
+ );
40
+ if (response.ok) {
41
+ await this.flushOfflineQueue();
42
+ return { ok: true };
43
+ }
44
+ return {
45
+ ok: false,
46
+ error: `Relay returned ${response.status}: ${await response.text()}`
47
+ };
48
+ } catch (error) {
49
+ this.offlineQueue.push(bundle);
50
+ return {
51
+ ok: false,
52
+ error: `Offline \u2014 buffered (${this.offlineQueue.length} pending)`
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * Push a single domain update (partial sync).
58
+ */
59
+ async pushDomain(domain, data) {
60
+ try {
61
+ const response = await this.fetch(
62
+ `/api/device/${this.deviceId}/sync/${domain}`,
63
+ {
64
+ method: "POST",
65
+ body: JSON.stringify({ domain, data, timestamp: (/* @__PURE__ */ new Date()).toISOString(), actorId: this.actorId })
66
+ }
67
+ );
68
+ if (response.ok) return { ok: true };
69
+ return {
70
+ ok: false,
71
+ error: `Relay returned ${response.status}`
72
+ };
73
+ } catch (error) {
74
+ return { ok: false, error: "Network error \u2014 domain sync failed" };
75
+ }
76
+ }
77
+ // ─── Action Queue ───
78
+ /**
79
+ * Poll for pending actions the phone needs to execute.
80
+ */
81
+ async pollActions() {
82
+ try {
83
+ const response = await this.fetch(
84
+ `/api/device/${this.deviceId}/actions`,
85
+ { method: "GET" }
86
+ );
87
+ if (response.ok) {
88
+ const data = await response.json();
89
+ return { ok: true, data: data.actions ?? [] };
90
+ }
91
+ return { ok: false, error: `Relay returned ${response.status}` };
92
+ } catch (error) {
93
+ return { ok: false, error: "Network error \u2014 action poll failed" };
94
+ }
95
+ }
96
+ /**
97
+ * Report action execution result and remove from queue.
98
+ */
99
+ async completeAction(result) {
100
+ try {
101
+ const response = await this.fetch(
102
+ `/api/device/${this.deviceId}/actions/${result.id}`,
103
+ {
104
+ method: "DELETE",
105
+ body: JSON.stringify(result)
106
+ }
107
+ );
108
+ if (response.ok) return { ok: true };
109
+ return { ok: false, error: `Relay returned ${response.status}` };
110
+ } catch (error) {
111
+ return { ok: false, error: "Network error \u2014 action complete failed" };
112
+ }
113
+ }
114
+ // ─── Health ───
115
+ /**
116
+ * Ping the relay to check connectivity.
117
+ */
118
+ async ping() {
119
+ try {
120
+ const response = await this.fetch(
121
+ `/api/health`,
122
+ { method: "GET" },
123
+ 3e3
124
+ // short timeout for health check
125
+ );
126
+ return response.ok;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ /**
132
+ * Get count of buffered offline bundles.
133
+ */
134
+ getOfflineQueueSize() {
135
+ return this.offlineQueue.length;
136
+ }
137
+ // ─── Internal ───
138
+ async fetch(path, init, timeout) {
139
+ const controller = new AbortController();
140
+ const timer = setTimeout(
141
+ () => controller.abort(),
142
+ timeout ?? this.timeout
143
+ );
144
+ try {
145
+ return await fetch(`${this.baseUrl}${path}`, {
146
+ ...init,
147
+ signal: controller.signal,
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ "X-ADAS-TENANT": this.tenant,
151
+ "X-ADAS-ACTOR": this.actorId,
152
+ "X-Device-Id": this.deviceId,
153
+ // The relay (post-2026-04-25) gates every /api/device/* call on
154
+ // a shared secret. Send it as `Authorization: Bearer` so the
155
+ // server's relayAuth middleware accepts the request. Empty/missing
156
+ // secret = 401 from the relay (correct fail-closed behavior).
157
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
158
+ ...init.headers || {}
159
+ }
160
+ });
161
+ } finally {
162
+ clearTimeout(timer);
163
+ }
164
+ }
165
+ async flushOfflineQueue() {
166
+ if (this.offlineQueue.length === 0) return;
167
+ const queue = [...this.offlineQueue];
168
+ this.offlineQueue = [];
169
+ for (const bundle of queue) {
170
+ try {
171
+ const response = await this.fetch(
172
+ `/api/device/${this.deviceId}/sync`,
173
+ {
174
+ method: "POST",
175
+ body: JSON.stringify(bundle)
176
+ }
177
+ );
178
+ if (!response.ok) {
179
+ this.offlineQueue.push(bundle);
180
+ }
181
+ } catch {
182
+ this.offlineQueue.push(bundle);
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ };
188
+
189
+ // src/device-bridge/actions/messaging.ts
190
+ import * as SMS from "expo-sms";
191
+ async function execute(actionId, params) {
192
+ const phone = params.phone;
193
+ const message = params.message;
194
+ if (!phone || !message) {
195
+ return {
196
+ id: actionId,
197
+ ok: false,
198
+ error: "Missing phone or message parameter",
199
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
200
+ };
201
+ }
202
+ try {
203
+ const available = await SMS.isAvailableAsync();
204
+ if (!available) {
205
+ return {
206
+ id: actionId,
207
+ ok: false,
208
+ error: "SMS not available on this device",
209
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
210
+ };
211
+ }
212
+ const { result } = await SMS.sendSMSAsync([phone], message);
213
+ return {
214
+ id: actionId,
215
+ ok: result === "sent" || result === "unknown",
216
+ // 'unknown' on Android means it was sent
217
+ result: {
218
+ to: params.contact_name || phone,
219
+ phone,
220
+ message,
221
+ status: result,
222
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
223
+ },
224
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
225
+ };
226
+ } catch (error) {
227
+ return {
228
+ id: actionId,
229
+ ok: false,
230
+ error: `SMS send failed: ${error instanceof Error ? error.message : String(error)}`,
231
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
232
+ };
233
+ }
234
+ }
235
+
236
+ // src/device-bridge/actions/calendar-actions.ts
237
+ import * as ExpoCalendar from "expo-calendar";
238
+ async function getDefaultCalendarId() {
239
+ const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);
240
+ const defaultCal = calendars.find((c) => c.isPrimary) || calendars[0];
241
+ return defaultCal?.id || null;
242
+ }
243
+ async function executeCreate(actionId, params) {
244
+ const title = params.title;
245
+ const date = params.date;
246
+ const startTime = params.start_time;
247
+ const endTime = params.end_time;
248
+ if (!title || !date || !startTime) {
249
+ return {
250
+ id: actionId,
251
+ ok: false,
252
+ error: "Missing title, date, or start_time",
253
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
254
+ };
255
+ }
256
+ try {
257
+ const calendarId = await getDefaultCalendarId();
258
+ if (!calendarId) {
259
+ return {
260
+ id: actionId,
261
+ ok: false,
262
+ error: "No calendar found on device",
263
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
264
+ };
265
+ }
266
+ const [year, month, day] = date.split("-").map(Number);
267
+ const [startH, startM] = startTime.split(":").map(Number);
268
+ const startDate = new Date(year, month - 1, day, startH, startM);
269
+ let endDate;
270
+ if (endTime) {
271
+ const [endH, endM] = endTime.split(":").map(Number);
272
+ endDate = new Date(year, month - 1, day, endH, endM);
273
+ } else {
274
+ endDate = new Date(startDate.getTime() + 60 * 60 * 1e3);
275
+ }
276
+ const eventId = await ExpoCalendar.createEventAsync(calendarId, {
277
+ title,
278
+ startDate,
279
+ endDate,
280
+ location: params.location || void 0,
281
+ notes: params.notes || void 0,
282
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
283
+ });
284
+ return {
285
+ id: actionId,
286
+ ok: true,
287
+ result: {
288
+ event_id: eventId,
289
+ title,
290
+ date,
291
+ start: startTime,
292
+ end: endTime || formatTime(endDate),
293
+ message: `Event "${title}" created successfully`
294
+ },
295
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
296
+ };
297
+ } catch (error) {
298
+ return {
299
+ id: actionId,
300
+ ok: false,
301
+ error: `Calendar create failed: ${error instanceof Error ? error.message : String(error)}`,
302
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
303
+ };
304
+ }
305
+ }
306
+ async function executeReschedule(actionId, params) {
307
+ const eventId = params.event_id;
308
+ if (!eventId) {
309
+ return {
310
+ id: actionId,
311
+ ok: false,
312
+ error: "Missing event_id",
313
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
314
+ };
315
+ }
316
+ try {
317
+ const updates = {};
318
+ if (params.new_date && params.new_start_time) {
319
+ const [year, month, day] = params.new_date.split("-").map(Number);
320
+ const [h, m] = params.new_start_time.split(":").map(Number);
321
+ updates.startDate = new Date(year, month - 1, day, h, m);
322
+ if (params.new_end_time) {
323
+ const [eh, em] = params.new_end_time.split(":").map(Number);
324
+ updates.endDate = new Date(year, month - 1, day, eh, em);
325
+ } else {
326
+ updates.endDate = new Date(updates.startDate.getTime() + 60 * 60 * 1e3);
327
+ }
328
+ }
329
+ await ExpoCalendar.updateEventAsync(eventId, updates);
330
+ return {
331
+ id: actionId,
332
+ ok: true,
333
+ result: { event_id: eventId, message: "Event rescheduled successfully" },
334
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
335
+ };
336
+ } catch (error) {
337
+ return {
338
+ id: actionId,
339
+ ok: false,
340
+ error: `Calendar reschedule failed: ${error instanceof Error ? error.message : String(error)}`,
341
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
342
+ };
343
+ }
344
+ }
345
+ async function executeCancel(actionId, params) {
346
+ const eventId = params.event_id;
347
+ if (!eventId) {
348
+ return {
349
+ id: actionId,
350
+ ok: false,
351
+ error: "Missing event_id",
352
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
353
+ };
354
+ }
355
+ try {
356
+ await ExpoCalendar.deleteEventAsync(eventId);
357
+ return {
358
+ id: actionId,
359
+ ok: true,
360
+ result: { event_id: eventId, message: "Event cancelled successfully" },
361
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
362
+ };
363
+ } catch (error) {
364
+ return {
365
+ id: actionId,
366
+ ok: false,
367
+ error: `Calendar cancel failed: ${error instanceof Error ? error.message : String(error)}`,
368
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
369
+ };
370
+ }
371
+ }
372
+ function formatTime(date) {
373
+ return date.toLocaleTimeString("en-US", {
374
+ hour: "2-digit",
375
+ minute: "2-digit",
376
+ hour12: false
377
+ });
378
+ }
379
+
380
+ // src/device-bridge/actions/dnd.ts
381
+ var currentDndState = {
382
+ enabled: false,
383
+ schedule: { start: "22:00", end: "07:00" },
384
+ allowlist: []
385
+ };
386
+ async function execute2(actionId, params) {
387
+ try {
388
+ if (params.enabled !== void 0) {
389
+ currentDndState.enabled = params.enabled;
390
+ }
391
+ if (params.schedule_start) {
392
+ currentDndState.schedule.start = params.schedule_start;
393
+ }
394
+ if (params.schedule_end) {
395
+ currentDndState.schedule.end = params.schedule_end;
396
+ }
397
+ if (params.allowlist) {
398
+ currentDndState.allowlist = params.allowlist;
399
+ }
400
+ return {
401
+ id: actionId,
402
+ ok: true,
403
+ result: {
404
+ message: `DND ${currentDndState.enabled ? "enabled" : "disabled"}`,
405
+ dnd: currentDndState
406
+ },
407
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
408
+ };
409
+ } catch (error) {
410
+ return {
411
+ id: actionId,
412
+ ok: false,
413
+ error: `DND set failed: ${error instanceof Error ? error.message : String(error)}`,
414
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
415
+ };
416
+ }
417
+ }
418
+
419
+ // src/device-bridge/actions/navigation.ts
420
+ import * as Linking from "expo-linking";
421
+ import { Platform } from "react-native";
422
+ async function execute3(actionId, params) {
423
+ const destination = params.destination;
424
+ const mode = params.mode || "driving";
425
+ if (!destination) {
426
+ return {
427
+ id: actionId,
428
+ ok: false,
429
+ error: "Missing destination parameter",
430
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
431
+ };
432
+ }
433
+ try {
434
+ const encodedDest = encodeURIComponent(destination);
435
+ let url;
436
+ if (Platform.OS === "ios") {
437
+ const modeMap = {
438
+ driving: "d",
439
+ transit: "r",
440
+ walking: "w"
441
+ };
442
+ url = `maps://maps.apple.com/?daddr=${encodedDest}&dirflg=${modeMap[mode] || "d"}`;
443
+ } else {
444
+ const modeMap = {
445
+ driving: "driving",
446
+ transit: "transit",
447
+ walking: "walking"
448
+ };
449
+ url = `google.navigation:q=${encodedDest}&mode=${modeMap[mode] || "driving"}`;
450
+ }
451
+ const canOpen = await Linking.canOpenURL(url);
452
+ if (!canOpen) {
453
+ const webModeMap = {
454
+ driving: "driving",
455
+ transit: "transit",
456
+ walking: "walking"
457
+ };
458
+ url = `https://www.google.com/maps/dir/?api=1&destination=${encodedDest}&travelmode=${webModeMap[mode] || "driving"}`;
459
+ }
460
+ await Linking.openURL(url);
461
+ return {
462
+ id: actionId,
463
+ ok: true,
464
+ result: {
465
+ message: `Navigation started to ${destination} via ${mode}`,
466
+ destination,
467
+ mode,
468
+ status: "navigating"
469
+ },
470
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
471
+ };
472
+ } catch (error) {
473
+ return {
474
+ id: actionId,
475
+ ok: false,
476
+ error: `Navigation failed: ${error instanceof Error ? error.message : String(error)}`,
477
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
478
+ };
479
+ }
480
+ }
481
+
482
+ // src/device-bridge/actions/health-actions.ts
483
+ async function executeToday(actionId) {
484
+ try {
485
+ const today = await collectToday();
486
+ return {
487
+ id: actionId,
488
+ ok: true,
489
+ result: { today },
490
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
491
+ };
492
+ } catch (e) {
493
+ return {
494
+ id: actionId,
495
+ ok: false,
496
+ error: String(e?.message || e),
497
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
498
+ };
499
+ }
500
+ }
501
+ async function executeHistory(actionId, params) {
502
+ try {
503
+ const days = Number(params?.days) || 30;
504
+ const list = await collectHistory(days);
505
+ return {
506
+ id: actionId,
507
+ ok: true,
508
+ result: { days: list },
509
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
510
+ };
511
+ } catch (e) {
512
+ return {
513
+ id: actionId,
514
+ ok: false,
515
+ error: String(e?.message || e),
516
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
517
+ };
518
+ }
519
+ }
520
+
521
+ // src/device-bridge/actions/queue-poller.ts
522
+ var QueuePoller = class {
523
+ constructor(config) {
524
+ this.timer = null;
525
+ this.running = false;
526
+ this.relayClient = config.relayClient;
527
+ this.pollInterval = config.pollInterval ?? 5e3;
528
+ this.onActionExecuted = config.onActionExecuted;
529
+ this.onError = config.onError;
530
+ }
531
+ /**
532
+ * Start polling for actions.
533
+ */
534
+ start() {
535
+ if (this.running) return;
536
+ this.running = true;
537
+ this.poll();
538
+ this.timer = setInterval(() => this.poll(), this.pollInterval);
539
+ }
540
+ /**
541
+ * Stop polling.
542
+ */
543
+ stop() {
544
+ this.running = false;
545
+ if (this.timer) {
546
+ clearInterval(this.timer);
547
+ this.timer = null;
548
+ }
549
+ }
550
+ /**
551
+ * Check if poller is running.
552
+ */
553
+ isRunning() {
554
+ return this.running;
555
+ }
556
+ /**
557
+ * Execute a single poll cycle.
558
+ */
559
+ async poll() {
560
+ if (!this.running) return;
561
+ try {
562
+ const response = await this.relayClient.pollActions();
563
+ if (!response.ok || !response.data) return;
564
+ for (const action of response.data) {
565
+ if (action.status && action.status !== "pending") continue;
566
+ const result = await this.executeAction(action);
567
+ await this.relayClient.completeAction(result);
568
+ this.onActionExecuted?.(result);
569
+ }
570
+ } catch (error) {
571
+ this.onError?.(
572
+ error instanceof Error ? error : new Error(String(error))
573
+ );
574
+ }
575
+ }
576
+ /**
577
+ * Route an action to the appropriate executor.
578
+ */
579
+ async executeAction(action) {
580
+ const { id, type, params } = action;
581
+ try {
582
+ switch (type) {
583
+ case "messaging.send":
584
+ return await execute(id, params);
585
+ case "calendar.create":
586
+ return await executeCreate(id, params);
587
+ case "calendar.reschedule":
588
+ return await executeReschedule(id, params);
589
+ case "calendar.cancel":
590
+ return await executeCancel(id, params);
591
+ case "dnd.set":
592
+ return await execute2(id, params);
593
+ case "navigation.start":
594
+ return await execute3(id, params);
595
+ case "health.today":
596
+ return await executeToday(id);
597
+ case "health.history":
598
+ return await executeHistory(id, params);
599
+ default:
600
+ return {
601
+ id,
602
+ ok: false,
603
+ error: `Unknown action type: ${type}`,
604
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
605
+ };
606
+ }
607
+ } catch (error) {
608
+ return {
609
+ id,
610
+ ok: false,
611
+ error: `Action execution failed: ${error instanceof Error ? error.message : String(error)}`,
612
+ executed_at: (/* @__PURE__ */ new Date()).toISOString()
613
+ };
614
+ }
615
+ }
616
+ };
617
+
618
+ // src/device-bridge/background.ts
619
+ import * as TaskManager from "expo-task-manager";
620
+ import * as BackgroundFetch from "expo-background-fetch";
621
+
622
+ // src/device-bridge/capabilities.ts
623
+ var capabilities_exports = {};
624
+ __export(capabilities_exports, {
625
+ allBuiltIn: () => allBuiltIn,
626
+ batteryCapability: () => batteryCapability,
627
+ calendarCapability: () => calendarCapability,
628
+ cameraCapability: () => cameraCapability,
629
+ connectivityCapability: () => connectivityCapability,
630
+ contactsCapability: () => contactsCapability,
631
+ healthCapability: () => healthCapability,
632
+ locationCapability: () => locationCapability,
633
+ moodCapability: () => moodCapability,
634
+ notificationsCapability: () => notificationsCapability
635
+ });
636
+
637
+ // src/device-bridge/collectors/calendar.ts
638
+ import * as ExpoCalendar2 from "expo-calendar";
639
+ async function requestPermission2() {
640
+ const { status } = await ExpoCalendar2.requestCalendarPermissionsAsync();
641
+ return status === "granted";
642
+ }
643
+ async function hasPermission2() {
644
+ const { status } = await ExpoCalendar2.getCalendarPermissionsAsync();
645
+ return status === "granted";
646
+ }
647
+ async function collectToday2() {
648
+ const now = /* @__PURE__ */ new Date();
649
+ const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
650
+ const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
651
+ const calendars = await ExpoCalendar2.getCalendarsAsync(ExpoCalendar2.EntityTypes.EVENT);
652
+ const userCalendarIds = getUserCalendarIds(calendars);
653
+ const events = await ExpoCalendar2.getEventsAsync(userCalendarIds, startOfDay, endOfDay);
654
+ const filtered = filterHolidays(events);
655
+ const mapped = filtered.map((e) => ({
656
+ id: e.id,
657
+ title: e.title || "Untitled",
658
+ date: startOfDay.toISOString().slice(0, 10),
659
+ start: formatTime2(new Date(e.startDate)),
660
+ end: formatTime2(new Date(e.endDate)),
661
+ location: e.location || "",
662
+ attendees: (e.attendees || []).map((a) => a.name || a.email || "Unknown"),
663
+ notes: e.notes || ""
664
+ }));
665
+ mapped.sort((a, b) => a.start.localeCompare(b.start));
666
+ return {
667
+ ok: true,
668
+ date: startOfDay.toISOString().slice(0, 10),
669
+ day_of_week: now.toLocaleDateString("en-US", { weekday: "long" }),
670
+ event_count: mapped.length,
671
+ events: mapped
672
+ };
673
+ }
674
+ async function collectUpcoming(daysAhead = 3) {
675
+ const now = /* @__PURE__ */ new Date();
676
+ const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
677
+ const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead + 1);
678
+ const calendars = await ExpoCalendar2.getCalendarsAsync(ExpoCalendar2.EntityTypes.EVENT);
679
+ const userCalendarIds = getUserCalendarIds(calendars);
680
+ const events = await ExpoCalendar2.getEventsAsync(userCalendarIds, tomorrow, endDate);
681
+ const filtered = filterHolidays(events);
682
+ const mapped = filtered.map((e) => {
683
+ const startDt = new Date(e.startDate);
684
+ return {
685
+ id: e.id,
686
+ title: e.title || "Untitled",
687
+ date: startDt.toISOString().slice(0, 10),
688
+ start: formatTime2(startDt),
689
+ end: formatTime2(new Date(e.endDate)),
690
+ location: e.location || "",
691
+ attendees: (e.attendees || []).map((a) => a.name || a.email || "Unknown"),
692
+ notes: e.notes || ""
693
+ };
694
+ });
695
+ mapped.sort(
696
+ (a, b) => a.date === b.date ? a.start.localeCompare(b.start) : a.date.localeCompare(b.date)
697
+ );
698
+ return {
699
+ ok: true,
700
+ from: tomorrow.toISOString().slice(0, 10),
701
+ days_ahead: daysAhead,
702
+ event_count: mapped.length,
703
+ events: mapped
704
+ };
705
+ }
706
+ function getUserCalendarIds(calendars) {
707
+ const SKIP_SOURCES = ["birthdays", "other"];
708
+ const SKIP_TYPES = ["birthdays", "other"];
709
+ return calendars.filter((c) => {
710
+ const sourceType = (c.source?.type || "").toLowerCase();
711
+ const calType = (c.type || "").toLowerCase();
712
+ if (SKIP_SOURCES.includes(sourceType)) return false;
713
+ if (SKIP_TYPES.includes(calType)) return false;
714
+ return true;
715
+ }).map((c) => c.id);
716
+ }
717
+ function filterHolidays(events) {
718
+ const HOLIDAY_PATTERNS = [
719
+ /holiday/i,
720
+ /observance/i,
721
+ /eve\b.*\btentative\b/i,
722
+ /\btentative\b/i
723
+ ];
724
+ return events.filter((e) => {
725
+ const isAllDay = e.allDay === true;
726
+ const notes = (e.notes || "").toLowerCase();
727
+ if (isAllDay && HOLIDAY_PATTERNS.some((p) => p.test(notes))) return false;
728
+ if (isAllDay && HOLIDAY_PATTERNS.some((p) => p.test(e.title || ""))) return false;
729
+ return true;
730
+ });
731
+ }
732
+ function formatTime2(date) {
733
+ return date.toLocaleTimeString("en-US", {
734
+ hour: "2-digit",
735
+ minute: "2-digit",
736
+ hour12: false
737
+ });
738
+ }
739
+
740
+ // src/device-bridge/collectors/contacts.ts
741
+ import * as ExpoContacts from "expo-contacts";
742
+ import AsyncStorage from "@react-native-async-storage/async-storage";
743
+ var HASH_STORE_KEY = "device-bridge.contacts.hashes.v1";
744
+ async function requestPermission3() {
745
+ const { status } = await ExpoContacts.requestPermissionsAsync();
746
+ return status === "granted";
747
+ }
748
+ async function hasPermission3() {
749
+ const { status } = await ExpoContacts.getPermissionsAsync();
750
+ return status === "granted";
751
+ }
752
+ var lastContactsStats = null;
753
+ async function collectAll() {
754
+ const result = await ExpoContacts.getContactsAsync({
755
+ fields: [
756
+ ExpoContacts.Fields.Name,
757
+ ExpoContacts.Fields.PhoneNumbers,
758
+ ExpoContacts.Fields.Emails,
759
+ ExpoContacts.Fields.Birthday
760
+ ],
761
+ sort: ExpoContacts.SortTypes.LastName
762
+ });
763
+ const raw = Array.isArray(result?.data) ? result.data : [];
764
+ const contacts = [];
765
+ const currentHashes = {};
766
+ for (let i = 0; i < raw.length; i++) {
767
+ const c = raw[i];
768
+ const id = c.id || `c${i}`;
769
+ const mapped = {
770
+ id,
771
+ name: c.name || `${c.firstName || ""} ${c.lastName || ""}`.trim() || "Unknown",
772
+ relationship: "",
773
+ // dropped — was the iOS Unauthorized-Keys trigger
774
+ phone: c.phoneNumbers?.[0]?.number || null,
775
+ email: c.emails?.[0]?.email || null,
776
+ birthday: formatBirthday(c.birthday),
777
+ notes: ""
778
+ // dropped — Notes is restricted on iOS 16+
779
+ };
780
+ contacts.push(mapped);
781
+ currentHashes[id] = hashContact(mapped);
782
+ }
783
+ let stored = {};
784
+ try {
785
+ const blob = await AsyncStorage.getItem(HASH_STORE_KEY);
786
+ if (blob) stored = JSON.parse(blob);
787
+ } catch {
788
+ }
789
+ const fullSync = Object.keys(stored).length === 0;
790
+ let added = 0;
791
+ let updated = 0;
792
+ let unchanged = 0;
793
+ for (const id of Object.keys(currentHashes)) {
794
+ const prev = stored[id];
795
+ if (prev === void 0) added++;
796
+ else if (prev !== currentHashes[id]) updated++;
797
+ else unchanged++;
798
+ }
799
+ let removed = 0;
800
+ for (const id of Object.keys(stored)) {
801
+ if (currentHashes[id] === void 0) removed++;
802
+ }
803
+ lastContactsStats = {
804
+ total: contacts.length,
805
+ added,
806
+ updated,
807
+ removed,
808
+ unchanged,
809
+ full_sync: fullSync
810
+ };
811
+ if (!fullSync && added === 0 && updated === 0 && removed === 0) {
812
+ return null;
813
+ }
814
+ try {
815
+ await AsyncStorage.setItem(HASH_STORE_KEY, JSON.stringify(currentHashes));
816
+ } catch {
817
+ }
818
+ return contacts;
819
+ }
820
+ function fnv1a(str) {
821
+ let h = 2166136261;
822
+ for (let i = 0; i < str.length; i++) {
823
+ h ^= str.charCodeAt(i);
824
+ h = Math.imul(h, 16777619);
825
+ }
826
+ return (h >>> 0).toString(16).padStart(8, "0");
827
+ }
828
+ function hashContact(c) {
829
+ const parts = [
830
+ c.id,
831
+ (c.name || "").trim(),
832
+ c.phone || "\0",
833
+ (c.email || "\0").toLowerCase(),
834
+ c.birthday || "\0"
835
+ ];
836
+ return fnv1a(parts.join(""));
837
+ }
838
+ function formatBirthday(birthday) {
839
+ if (!birthday) return null;
840
+ const { year, month, day } = birthday;
841
+ if (month === void 0 || day === void 0) return null;
842
+ const m = String(month + 1).padStart(2, "0");
843
+ const d = String(day).padStart(2, "0");
844
+ if (!year) return `--${m}-${d}`;
845
+ return `${year}-${m}-${d}`;
846
+ }
847
+
848
+ // src/device-bridge/collectors/location.ts
849
+ var location_exports = {};
850
+ __export(location_exports, {
851
+ collectCurrent: () => collectCurrent,
852
+ getPosition: () => getPosition,
853
+ hasPermission: () => hasPermission4,
854
+ requestPermission: () => requestPermission4,
855
+ watchPosition: () => watchPosition
856
+ });
857
+ import * as ExpoLocation from "expo-location";
858
+ async function requestPermission4() {
859
+ const { status: fg } = await ExpoLocation.requestForegroundPermissionsAsync();
860
+ if (fg !== "granted") return false;
861
+ try {
862
+ await ExpoLocation.requestBackgroundPermissionsAsync();
863
+ } catch {
864
+ }
865
+ return fg === "granted";
866
+ }
867
+ async function hasPermission4() {
868
+ const { status } = await ExpoLocation.getForegroundPermissionsAsync();
869
+ return status === "granted";
870
+ }
871
+ async function collectCurrent() {
872
+ const location = await ExpoLocation.getCurrentPositionAsync({
873
+ accuracy: ExpoLocation.Accuracy.Balanced
874
+ });
875
+ let place = "Current Location";
876
+ let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;
877
+ let locationType = "unknown";
878
+ try {
879
+ const [geocoded] = await ExpoLocation.reverseGeocodeAsync({
880
+ latitude: location.coords.latitude,
881
+ longitude: location.coords.longitude
882
+ });
883
+ if (geocoded) {
884
+ place = geocoded.name || geocoded.street || place;
885
+ address = [
886
+ geocoded.streetNumber,
887
+ geocoded.street,
888
+ geocoded.city,
889
+ geocoded.region
890
+ ].filter(Boolean).join(", ") || address;
891
+ locationType = classifyLocation(geocoded);
892
+ }
893
+ } catch {
894
+ }
895
+ return {
896
+ ok: true,
897
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
898
+ location: {
899
+ place,
900
+ address,
901
+ lat: location.coords.latitude,
902
+ lng: location.coords.longitude,
903
+ type: locationType
904
+ }
905
+ };
906
+ }
907
+ async function ensurePermission() {
908
+ if (await hasPermission4()) return;
909
+ const granted = await requestPermission4();
910
+ if (!granted) {
911
+ throw new Error(
912
+ "Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services."
913
+ );
914
+ }
915
+ }
916
+ async function getPosition(opts = {}) {
917
+ await ensurePermission();
918
+ const fix = await ExpoLocation.getCurrentPositionAsync({
919
+ accuracy: opts.accuracy === "high" ? ExpoLocation.Accuracy.High : ExpoLocation.Accuracy.Balanced
920
+ });
921
+ return toPosition(fix);
922
+ }
923
+ function toPosition(fix) {
924
+ return {
925
+ latitude: fix.coords.latitude,
926
+ longitude: fix.coords.longitude,
927
+ altitude: fix.coords.altitude ?? void 0,
928
+ accuracy: nonNegative(fix.coords.accuracy),
929
+ timestamp: fix.timestamp ?? Date.now(),
930
+ speed: nonNegative(fix.coords.speed),
931
+ course: nonNegative(fix.coords.heading)
932
+ };
933
+ }
934
+ function nonNegative(v) {
935
+ return typeof v === "number" && v >= 0 ? v : void 0;
936
+ }
937
+ function watchPosition(cb) {
938
+ let subscription = null;
939
+ let cancelled = false;
940
+ (async () => {
941
+ try {
942
+ await ensurePermission();
943
+ if (cancelled) return;
944
+ subscription = await ExpoLocation.watchPositionAsync(
945
+ { accuracy: ExpoLocation.Accuracy.Balanced },
946
+ (fix) => cb(toPosition(fix))
947
+ );
948
+ if (cancelled) {
949
+ subscription?.remove();
950
+ subscription = null;
951
+ }
952
+ } catch (err) {
953
+ console.warn("[location] watchPosition failed:", err?.message || String(err));
954
+ }
955
+ })();
956
+ return () => {
957
+ cancelled = true;
958
+ subscription?.remove();
959
+ subscription = null;
960
+ };
961
+ }
962
+ function classifyLocation(geocoded) {
963
+ const name = (geocoded.name || "").toLowerCase();
964
+ const street = (geocoded.street || "").toLowerCase();
965
+ if (name.includes("home") || name.includes("residence")) return "home";
966
+ if (name.includes("office") || name.includes("workplace") || name.includes("tech park")) return "work";
967
+ if (name.includes("school") || name.includes("elementary") || name.includes("university")) return "school";
968
+ if (name.includes("gym") || name.includes("fitness")) return "fitness";
969
+ if (name.includes("hospital") || name.includes("clinic") || name.includes("dental")) return "medical";
970
+ return "other";
971
+ }
972
+
973
+ // src/device-bridge/collectors/battery.ts
974
+ import * as ExpoBattery from "expo-battery";
975
+ async function collectBattery() {
976
+ const level = await ExpoBattery.getBatteryLevelAsync();
977
+ const state = await ExpoBattery.getBatteryStateAsync();
978
+ const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();
979
+ return {
980
+ ok: true,
981
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
982
+ battery: {
983
+ level: Math.round(level * 100),
984
+ // expo-battery returns 0-1
985
+ state: mapBatteryState(state),
986
+ low_power_mode: lowPower
987
+ }
988
+ };
989
+ }
990
+ function subscribeToBattery(callback) {
991
+ let lastLevel = null;
992
+ const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {
993
+ const pct = Math.round(batteryLevel * 100);
994
+ if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {
995
+ lastLevel = pct;
996
+ callback();
997
+ }
998
+ });
999
+ const stateSub = ExpoBattery.addBatteryStateListener(() => {
1000
+ callback();
1001
+ });
1002
+ return () => {
1003
+ levelSub?.remove?.();
1004
+ stateSub?.remove?.();
1005
+ };
1006
+ }
1007
+ function mapBatteryState(state) {
1008
+ switch (state) {
1009
+ case ExpoBattery.BatteryState.CHARGING:
1010
+ return "charging";
1011
+ case ExpoBattery.BatteryState.FULL:
1012
+ return "full";
1013
+ case ExpoBattery.BatteryState.UNPLUGGED:
1014
+ default:
1015
+ return "unplugged";
1016
+ }
1017
+ }
1018
+
1019
+ // src/device-bridge/collectors/connectivity.ts
1020
+ var connectivity_exports = {};
1021
+ __export(connectivity_exports, {
1022
+ collectConnectivity: () => collectConnectivity
1023
+ });
1024
+ import * as ExpoNetwork from "expo-network";
1025
+ async function collectConnectivity() {
1026
+ const networkState = await ExpoNetwork.getNetworkStateAsync();
1027
+ let type = "none";
1028
+ let wifiName;
1029
+ let cellularType;
1030
+ if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {
1031
+ type = "wifi";
1032
+ try {
1033
+ wifiName = await getWifiName();
1034
+ } catch {
1035
+ wifiName = void 0;
1036
+ }
1037
+ } else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {
1038
+ type = "cellular";
1039
+ cellularType = getCellularGeneration();
1040
+ }
1041
+ return {
1042
+ ok: true,
1043
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1044
+ connectivity: {
1045
+ type,
1046
+ is_connected: networkState.isConnected ?? false,
1047
+ ...wifiName && { wifi_name: wifiName },
1048
+ ...cellularType && { cellular_type: cellularType }
1049
+ }
1050
+ };
1051
+ }
1052
+ async function getWifiName() {
1053
+ return void 0;
1054
+ }
1055
+ function getCellularGeneration() {
1056
+ return "LTE";
1057
+ }
1058
+
1059
+ // src/device-bridge/collectors/notifications.ts
1060
+ import * as ExpoNotifications from "expo-notifications";
1061
+ async function requestPermission5() {
1062
+ const { status } = await ExpoNotifications.requestPermissionsAsync();
1063
+ return status === "granted";
1064
+ }
1065
+ async function hasPermission5() {
1066
+ const { status } = await ExpoNotifications.getPermissionsAsync();
1067
+ return status === "granted";
1068
+ }
1069
+ async function collectNotifications() {
1070
+ const presented = await ExpoNotifications.getPresentedNotificationsAsync();
1071
+ const notifications = presented.map((n, i) => ({
1072
+ id: n.request.identifier || `n${i}`,
1073
+ app: n.request.content.data?.app || "A-Team",
1074
+ title: n.request.content.title || "",
1075
+ body: n.request.content.body || "",
1076
+ timestamp: n.date ? new Date(n.date).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
1077
+ sender: n.request.content.data?.sender || null,
1078
+ priority: mapPriority(n.request.content.data?.priority),
1079
+ read: false
1080
+ // Presented notifications are unread by definition
1081
+ }));
1082
+ const unread = notifications.filter((n) => !n.read).length;
1083
+ return {
1084
+ ok: true,
1085
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1086
+ total: notifications.length,
1087
+ unread,
1088
+ notifications
1089
+ };
1090
+ }
1091
+ function mapPriority(p) {
1092
+ if (p === "high" || p === "max") return "high";
1093
+ if (p === "low" || p === "min") return "low";
1094
+ return "medium";
1095
+ }
1096
+
1097
+ // src/device-bridge/collectors/mood.ts
1098
+ function emptySignals() {
1099
+ return {
1100
+ steps: null,
1101
+ sleep_min: null,
1102
+ screen_min: null,
1103
+ night_screen_min: null,
1104
+ places_count: null,
1105
+ distance_km: null,
1106
+ social_app_min: null,
1107
+ video_app_min: null,
1108
+ productivity_app_min: null,
1109
+ messages_sent: null,
1110
+ calls_count: null,
1111
+ app_sessions: null,
1112
+ app_active_min: null,
1113
+ checkin_mood: null
1114
+ };
1115
+ }
1116
+ async function collectToday3() {
1117
+ const today = emptySignals();
1118
+ try {
1119
+ const h = await collectToday();
1120
+ if (h) {
1121
+ today.steps = typeof h.steps === "number" ? h.steps : null;
1122
+ today.distance_km = typeof h.distance_km === "number" ? h.distance_km : null;
1123
+ }
1124
+ } catch {
1125
+ }
1126
+ try {
1127
+ const x = await collectMoodExtras();
1128
+ today.sleep_min = x.sleep_min;
1129
+ today.resting_hr = x.resting_hr;
1130
+ today.active_energy_kcal = x.active_energy_kcal;
1131
+ today.exercise_min = x.exercise_min;
1132
+ } catch {
1133
+ }
1134
+ try {
1135
+ const e = await getToday();
1136
+ today.app_sessions = e.app_sessions;
1137
+ today.app_active_min = e.app_active_min;
1138
+ } catch {
1139
+ }
1140
+ let timezone = "UTC";
1141
+ try {
1142
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
1143
+ } catch {
1144
+ }
1145
+ return {
1146
+ window: "today",
1147
+ timezone,
1148
+ collected_at: (/* @__PURE__ */ new Date()).toISOString(),
1149
+ today
1150
+ };
1151
+ }
1152
+
1153
+ // src/device-bridge/collectors/camera.ts
1154
+ import { Camera } from "expo-camera";
1155
+ async function hasPermission6() {
1156
+ try {
1157
+ return (await Camera.getCameraPermissionsAsync()).granted;
1158
+ } catch {
1159
+ return false;
1160
+ }
1161
+ }
1162
+ async function requestPermission6() {
1163
+ try {
1164
+ return (await Camera.requestCameraPermissionsAsync()).granted;
1165
+ } catch {
1166
+ return false;
1167
+ }
1168
+ }
1169
+
1170
+ // src/device-bridge/capabilities.ts
1171
+ function calendarCapability() {
1172
+ return {
1173
+ id: "calendar",
1174
+ name: "Calendar",
1175
+ description: "Syncs your calendar events so your assistant knows your schedule.",
1176
+ reason: "Needed for scheduling, meeting prep, commute alerts, and daily briefings.",
1177
+ icon: "calendar",
1178
+ requiresPermission: true,
1179
+ permissionState: "not_requested",
1180
+ requestPermission: requestPermission2,
1181
+ checkPermission: hasPermission2,
1182
+ collect: async () => {
1183
+ const [today, upcoming] = await Promise.all([
1184
+ collectToday2(),
1185
+ collectUpcoming(7)
1186
+ ]);
1187
+ return { today, upcoming };
1188
+ },
1189
+ defaultInterval: 6e4
1190
+ // 1 min
1191
+ };
1192
+ }
1193
+ function contactsCapability() {
1194
+ return {
1195
+ id: "contacts",
1196
+ name: "Contacts",
1197
+ description: "Syncs your contacts so your assistant can find people and relationships.",
1198
+ reason: "Needed for messaging, birthday reminders, and relationship insights.",
1199
+ icon: "people",
1200
+ requiresPermission: true,
1201
+ permissionState: "not_requested",
1202
+ requestPermission: requestPermission3,
1203
+ checkPermission: hasPermission3,
1204
+ collect: async () => {
1205
+ const list = await collectAll();
1206
+ if (list === null) return null;
1207
+ return { contacts: list };
1208
+ },
1209
+ defaultInterval: 3e5
1210
+ // 5 min
1211
+ };
1212
+ }
1213
+ function locationCapability() {
1214
+ return {
1215
+ id: "location",
1216
+ name: "Location",
1217
+ description: "Syncs your location for weather, commute, and context-aware assistance.",
1218
+ reason: "Needed for commute estimates, weather at your location, and situation detection.",
1219
+ icon: "location",
1220
+ requiresPermission: true,
1221
+ permissionState: "not_requested",
1222
+ requestPermission: requestPermission4,
1223
+ checkPermission: hasPermission4,
1224
+ collect: collectCurrent,
1225
+ defaultInterval: 3e4
1226
+ // 30 sec
1227
+ };
1228
+ }
1229
+ function batteryCapability() {
1230
+ return {
1231
+ id: "battery",
1232
+ name: "Battery",
1233
+ description: "Syncs battery level and charging status.",
1234
+ reason: "Helps your assistant adjust behavior when battery is low.",
1235
+ icon: "battery",
1236
+ requiresPermission: false,
1237
+ permissionState: "not_needed",
1238
+ requestPermission: async () => true,
1239
+ checkPermission: async () => true,
1240
+ collect: collectBattery,
1241
+ defaultInterval: 6e4,
1242
+ // 1 min
1243
+ subscribe: subscribeToBattery
1244
+ };
1245
+ }
1246
+ function connectivityCapability() {
1247
+ return {
1248
+ id: "connectivity",
1249
+ name: "Connectivity",
1250
+ description: "Syncs WiFi/cellular status.",
1251
+ reason: "Helps your assistant know if you have internet access.",
1252
+ icon: "wifi",
1253
+ requiresPermission: false,
1254
+ permissionState: "not_needed",
1255
+ requestPermission: async () => true,
1256
+ checkPermission: async () => true,
1257
+ collect: collectConnectivity,
1258
+ defaultInterval: 3e4
1259
+ // 30 sec
1260
+ };
1261
+ }
1262
+ function healthCapability() {
1263
+ return {
1264
+ id: "health",
1265
+ name: "Health",
1266
+ description: "Reads daily steps, floors, calories, and distance from Apple Health.",
1267
+ reason: "Powers the personal health coach and pairs activity data with nutrition tracking.",
1268
+ icon: "heart",
1269
+ requiresPermission: true,
1270
+ permissionState: "not_requested",
1271
+ requestPermission,
1272
+ checkPermission: hasPermission,
1273
+ collect: async () => {
1274
+ const days = await collectHistory(30);
1275
+ const today = days[0] || await collectToday();
1276
+ return { today, days };
1277
+ },
1278
+ defaultInterval: 3e5
1279
+ // 5 min
1280
+ };
1281
+ }
1282
+ function moodCapability() {
1283
+ return {
1284
+ id: "mood",
1285
+ name: "Mood",
1286
+ description: "Sends a daily behavioral-signal summary (activity, routine) to infer mood \u2014 not a medical diagnosis.",
1287
+ reason: "Lets ada gently notice changes in energy, stress, and routine and check in.",
1288
+ icon: "heart",
1289
+ // No OS permission of its own — it derives from collectors that gate
1290
+ // themselves (e.g. health). Treated as not_needed so the bridge syncs it
1291
+ // whenever the user has the Mood toggle on.
1292
+ requiresPermission: false,
1293
+ permissionState: "not_needed",
1294
+ requestPermission: async () => true,
1295
+ checkPermission: async () => true,
1296
+ collect: async () => {
1297
+ return collectToday3();
1298
+ },
1299
+ defaultInterval: 36e5
1300
+ // hourly — it's a daily summary, no need to spam
1301
+ };
1302
+ }
1303
+ function cameraCapability() {
1304
+ return {
1305
+ id: "camera",
1306
+ name: "Camera",
1307
+ description: "Lets ada see through your camera to guide you \u2014 reading text and describing what's in front of you. Frames are analysed on the phone; nothing is stored or uploaded.",
1308
+ reason: "Powers visual guidance (Ada Guide): a live view with on-device text and scene understanding.",
1309
+ icon: "camera",
1310
+ requiresPermission: true,
1311
+ permissionState: "not_requested",
1312
+ requestPermission: requestPermission6,
1313
+ checkPermission: hasPermission6,
1314
+ // LIVE, not synced: the camera is used in the moment (deviceState.visual / a
1315
+ // widget's preview) and never collected to the relay. So collect never
1316
+ // pushes — this capability's value is the USER GRANT + OS permission, exposed
1317
+ // to the backend like every other, not a data payload. `null` = nothing to
1318
+ // push, which the bridge skips (same contract contacts uses when unchanged).
1319
+ collect: async () => null,
1320
+ defaultInterval: 36e5
1321
+ // never really collects; keep the poll rare
1322
+ };
1323
+ }
1324
+ function notificationsCapability() {
1325
+ return {
1326
+ id: "notifications",
1327
+ name: "Notifications",
1328
+ description: "Syncs your notifications for triage and smart grouping.",
1329
+ reason: "Needed for notification triage, priority filtering, and Do Not Disturb.",
1330
+ icon: "notifications",
1331
+ requiresPermission: true,
1332
+ permissionState: "not_requested",
1333
+ requestPermission: requestPermission5,
1334
+ checkPermission: hasPermission5,
1335
+ collect: collectNotifications,
1336
+ defaultInterval: 15e3
1337
+ // 15 sec
1338
+ };
1339
+ }
1340
+ function allBuiltIn() {
1341
+ return [
1342
+ calendarCapability(),
1343
+ contactsCapability(),
1344
+ locationCapability(),
1345
+ batteryCapability(),
1346
+ connectivityCapability(),
1347
+ notificationsCapability(),
1348
+ healthCapability(),
1349
+ moodCapability(),
1350
+ cameraCapability()
1351
+ ];
1352
+ }
1353
+
1354
+ // src/device-bridge/background.ts
1355
+ var BACKGROUND_TASK_NAME = "DEVICE_BRIDGE_BACKGROUND_SYNC";
1356
+ var CAPABILITY_FACTORIES = {
1357
+ calendar: calendarCapability,
1358
+ contacts: contactsCapability,
1359
+ location: locationCapability,
1360
+ battery: batteryCapability,
1361
+ connectivity: connectivityCapability,
1362
+ notifications: notificationsCapability,
1363
+ health: healthCapability
1364
+ };
1365
+ async function registerBackgroundSync(config) {
1366
+ TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {
1367
+ try {
1368
+ const hasNew = await syncDomains(config, ["location", "battery", "calendar"]);
1369
+ return hasNew ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData;
1370
+ } catch (error) {
1371
+ console.error("[DeviceBridge] Background sync failed:", error);
1372
+ return BackgroundFetch.BackgroundFetchResult.Failed;
1373
+ }
1374
+ });
1375
+ await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {
1376
+ minimumInterval: 300,
1377
+ // 5 minutes (iOS will clamp to ~15 min)
1378
+ stopOnTerminate: false,
1379
+ startOnBoot: true
1380
+ });
1381
+ }
1382
+ async function unregisterBackgroundSync() {
1383
+ const isRegistered = await TaskManager.isTaskRegisteredAsync(
1384
+ BACKGROUND_TASK_NAME
1385
+ );
1386
+ if (isRegistered) {
1387
+ await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);
1388
+ }
1389
+ }
1390
+ async function syncDomains(config, domains) {
1391
+ const relay = new RelayClient({
1392
+ relayUrl: config.relayUrl,
1393
+ deviceId: config.deviceId,
1394
+ tenant: config.tenant,
1395
+ actorId: config.actorId,
1396
+ apiKey: config.apiKey,
1397
+ timeout: 15e3
1398
+ // longer than foreground — background tasks are budget-limited but not latency-sensitive
1399
+ });
1400
+ const results = await Promise.allSettled(
1401
+ domains.map((d) => collectAndPush(relay, d))
1402
+ );
1403
+ return results.some((r) => r.status === "fulfilled" && r.value);
1404
+ }
1405
+ async function collectAndPush(relay, domain) {
1406
+ const factory = CAPABILITY_FACTORIES[domain];
1407
+ if (!factory) {
1408
+ console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);
1409
+ return false;
1410
+ }
1411
+ const cap = factory();
1412
+ if (cap.requiresPermission) {
1413
+ const granted = await cap.checkPermission();
1414
+ if (!granted) return false;
1415
+ }
1416
+ const data = await cap.collect();
1417
+ const result = await relay.pushDomain(domain, data);
1418
+ return result.ok;
1419
+ }
1420
+
1421
+ // src/device-bridge/biometrics.ts
1422
+ var biometrics_exports = {};
1423
+ __export(biometrics_exports, {
1424
+ authenticate: () => authenticate,
1425
+ capabilities: () => capabilities
1426
+ });
1427
+ import * as LocalAuthentication from "expo-local-authentication";
1428
+ async function capabilities() {
1429
+ const [hasHardware, isEnrolled, types] = await Promise.all([
1430
+ LocalAuthentication.hasHardwareAsync(),
1431
+ LocalAuthentication.isEnrolledAsync(),
1432
+ LocalAuthentication.supportedAuthenticationTypesAsync()
1433
+ ]);
1434
+ const supportedTypes = types.map((t) => {
1435
+ if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return "fingerprint";
1436
+ if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return "face";
1437
+ if (t === LocalAuthentication.AuthenticationType.IRIS) return "iris";
1438
+ return "none";
1439
+ }).filter((t) => t !== "none");
1440
+ return { hasHardware, isEnrolled, supportedTypes };
1441
+ }
1442
+ async function authenticate(opts) {
1443
+ try {
1444
+ const res = await LocalAuthentication.authenticateAsync({
1445
+ promptMessage: opts.reason,
1446
+ cancelLabel: opts.cancelLabel,
1447
+ disableDeviceFallback: opts.fallbackToPasscode === false
1448
+ });
1449
+ if (res.success) return { success: true };
1450
+ return { success: false, error: res.error || "unknown" };
1451
+ } catch (e) {
1452
+ return { success: false, error: String(e?.message || e) };
1453
+ }
1454
+ }
1455
+
1456
+ // src/device-bridge/sharing.ts
1457
+ var sharing_exports = {};
1458
+ __export(sharing_exports, {
1459
+ isAvailable: () => isAvailable,
1460
+ shareFile: () => shareFile
1461
+ });
1462
+ import * as Sharing from "expo-sharing";
1463
+ async function shareFile(fileUri, opts = {}) {
1464
+ try {
1465
+ const available = await Sharing.isAvailableAsync();
1466
+ if (!available) return { ok: false, error: "sharing_unavailable" };
1467
+ await Sharing.shareAsync(fileUri, opts);
1468
+ return { ok: true };
1469
+ } catch (e) {
1470
+ return { ok: false, error: String(e?.message || e) };
1471
+ }
1472
+ }
1473
+ async function isAvailable() {
1474
+ return Sharing.isAvailableAsync();
1475
+ }
1476
+
1477
+ // src/device-bridge/scanner.ts
1478
+ var scanner_exports = {};
1479
+ __export(scanner_exports, {
1480
+ CameraView: () => CameraView,
1481
+ normalizeResult: () => normalizeResult,
1482
+ requestPermissions: () => requestPermissions,
1483
+ useCameraPermissions: () => useCameraPermissions
1484
+ });
1485
+ import { CameraView, useCameraPermissions } from "expo-camera";
1486
+ async function requestPermissions() {
1487
+ const { Camera: Camera2 } = await import("expo-camera");
1488
+ const { status } = await Camera2.requestCameraPermissionsAsync();
1489
+ return { granted: status === "granted", status };
1490
+ }
1491
+ function normalizeResult(r) {
1492
+ return { type: r.type, data: r.data };
1493
+ }
1494
+
1495
+ // src/device-bridge/index.ts
1496
+ var DeviceBridge = class {
1497
+ constructor(config) {
1498
+ // Capability registry
1499
+ this.caps = /* @__PURE__ */ new Map();
1500
+ this.syncTimers = /* @__PURE__ */ new Map();
1501
+ this.subscriptions = /* @__PURE__ */ new Map();
1502
+ this.domainStatus = /* @__PURE__ */ new Map();
1503
+ this.running = false;
1504
+ this.config = config;
1505
+ this.relay = new RelayClient({
1506
+ relayUrl: config.relayUrl,
1507
+ deviceId: config.deviceId,
1508
+ tenant: config.tenant,
1509
+ actorId: config.actorId,
1510
+ apiKey: config.apiKey
1511
+ });
1512
+ this.queuePoller = new QueuePoller({
1513
+ relayClient: this.relay,
1514
+ pollInterval: 5e3,
1515
+ onActionExecuted: () => this.notifyStatusChange(),
1516
+ onError: (error) => this.config.onError?.({
1517
+ domain: "actions",
1518
+ message: error.message,
1519
+ code: "ACTION_ERROR",
1520
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1521
+ })
1522
+ });
1523
+ }
1524
+ // ─── Capability Registration ───
1525
+ /**
1526
+ * Register a capability. Does NOT request permission — just makes it known.
1527
+ * Can be called before or after startSync().
1528
+ */
1529
+ register(cap) {
1530
+ this.caps.set(cap.id, cap);
1531
+ this.domainStatus.set(cap.id, {
1532
+ domain: cap.id,
1533
+ lastSync: null,
1534
+ state: "idle"
1535
+ });
1536
+ }
1537
+ /**
1538
+ * Register multiple capabilities at once.
1539
+ */
1540
+ registerAll(caps) {
1541
+ for (const cap of caps) {
1542
+ this.register(cap);
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Get a registered capability by ID.
1547
+ * Use this to read name/description/reason for your permission UI.
1548
+ */
1549
+ getCapability(id) {
1550
+ return this.caps.get(id);
1551
+ }
1552
+ /**
1553
+ * Get all registered capabilities with their current status.
1554
+ */
1555
+ listCapabilities() {
1556
+ return Array.from(this.caps.values()).map((cap) => ({
1557
+ id: cap.id,
1558
+ name: cap.name,
1559
+ permission: cap.permissionState,
1560
+ sync: this.domainStatus.get(cap.id) || {
1561
+ domain: cap.id,
1562
+ lastSync: null,
1563
+ state: "idle"
1564
+ }
1565
+ }));
1566
+ }
1567
+ // ─── Permissions (host app controls the flow) ───
1568
+ /**
1569
+ * Request permission for ONE capability.
1570
+ * Call after showing your own explanation screen.
1571
+ * If bridge is already running and granted, syncing starts automatically.
1572
+ */
1573
+ async requestPermission(capId) {
1574
+ const cap = this.caps.get(capId);
1575
+ if (!cap) throw new Error(`Capability "${capId}" not registered`);
1576
+ if (!cap.requiresPermission) {
1577
+ cap.permissionState = "not_needed";
1578
+ return true;
1579
+ }
1580
+ const granted = await cap.requestPermission();
1581
+ cap.permissionState = granted ? "granted" : "denied";
1582
+ if (granted && this.running) {
1583
+ this.startCapabilitySync(cap);
1584
+ }
1585
+ this.notifyStatusChange();
1586
+ return granted;
1587
+ }
1588
+ /**
1589
+ * Check permission WITHOUT prompting.
1590
+ */
1591
+ async checkPermission(capId) {
1592
+ const cap = this.caps.get(capId);
1593
+ if (!cap) throw new Error(`Capability "${capId}" not registered`);
1594
+ if (!cap.requiresPermission) return "not_needed";
1595
+ const granted = await cap.checkPermission();
1596
+ cap.permissionState = granted ? "granted" : cap.permissionState;
1597
+ return cap.permissionState;
1598
+ }
1599
+ /**
1600
+ * Request ALL permissions sequentially (one at a time, not bombarded).
1601
+ */
1602
+ async requestAllPermissions() {
1603
+ const results = /* @__PURE__ */ new Map();
1604
+ for (const [id, cap] of this.caps) {
1605
+ if (!cap.requiresPermission) {
1606
+ cap.permissionState = "not_needed";
1607
+ results.set(id, true);
1608
+ continue;
1609
+ }
1610
+ const granted = await cap.requestPermission();
1611
+ cap.permissionState = granted ? "granted" : "denied";
1612
+ results.set(id, granted);
1613
+ }
1614
+ this.notifyStatusChange();
1615
+ return results;
1616
+ }
1617
+ /**
1618
+ * Get capabilities that still need permission.
1619
+ * Use this to drive a "next permission" onboarding flow.
1620
+ */
1621
+ getPendingPermissions() {
1622
+ return Array.from(this.caps.values()).filter(
1623
+ (cap) => cap.requiresPermission && cap.permissionState === "not_requested"
1624
+ );
1625
+ }
1626
+ // ─── Sync Control ───
1627
+ /**
1628
+ * Start syncing all permitted capabilities + action queue poller.
1629
+ */
1630
+ async startSync() {
1631
+ if (this.running) return;
1632
+ this.running = true;
1633
+ for (const [, cap] of this.caps) {
1634
+ const permitted = !cap.requiresPermission || cap.permissionState === "granted" || cap.permissionState === "not_needed";
1635
+ if (permitted) {
1636
+ this.startCapabilitySync(cap);
1637
+ } else {
1638
+ this.updateDomainStatus(cap.id, "error", "Permission not granted");
1639
+ }
1640
+ }
1641
+ this.queuePoller.start();
1642
+ if (this.config.tenant && this.config.apiKey) {
1643
+ registerBackgroundSync({
1644
+ relayUrl: this.config.relayUrl,
1645
+ deviceId: this.config.deviceId,
1646
+ tenant: this.config.tenant,
1647
+ actorId: this.config.actorId,
1648
+ apiKey: this.config.apiKey
1649
+ }).catch((e) => console.warn("[adas/bridge] registerBackgroundSync failed:", e?.message || e));
1650
+ }
1651
+ this.notifyStatusChange();
1652
+ }
1653
+ /**
1654
+ * Stop all syncing.
1655
+ */
1656
+ stopSync() {
1657
+ this.running = false;
1658
+ for (const [id, timer] of this.syncTimers) {
1659
+ clearInterval(timer);
1660
+ this.updateDomainStatus(id, "stopped");
1661
+ }
1662
+ this.syncTimers.clear();
1663
+ for (const unsub of this.subscriptions.values()) {
1664
+ try {
1665
+ unsub();
1666
+ } catch {
1667
+ }
1668
+ }
1669
+ this.subscriptions.clear();
1670
+ this.queuePoller.stop();
1671
+ unregisterBackgroundSync().catch((e) => console.warn("[adas/bridge] unregisterBackgroundSync failed:", e?.message || e));
1672
+ this.notifyStatusChange();
1673
+ }
1674
+ /**
1675
+ * Force immediate sync of all permitted capabilities.
1676
+ */
1677
+ async syncNow() {
1678
+ const promises = Array.from(this.caps.values()).filter((c) => c.permissionState === "granted" || c.permissionState === "not_needed").map((c) => this.syncCapability(c));
1679
+ await Promise.allSettled(promises);
1680
+ }
1681
+ /**
1682
+ * Force immediate sync of a single capability by id. Used by the host to
1683
+ * refresh fast-moving sources (battery) on app-foreground so the relay
1684
+ * gets a fresh reading the moment the user opens the app.
1685
+ */
1686
+ async syncCapabilityNow(capId) {
1687
+ const cap = this.caps.get(capId);
1688
+ if (!cap) return;
1689
+ const permitted = !cap.requiresPermission || cap.permissionState === "granted" || cap.permissionState === "not_needed";
1690
+ if (!permitted) return;
1691
+ await this.syncCapability(cap);
1692
+ }
1693
+ /**
1694
+ * Get current bridge status.
1695
+ */
1696
+ getStatus() {
1697
+ return {
1698
+ running: this.running,
1699
+ capabilities: this.listCapabilities(),
1700
+ lastFullSync: this.getLastFullSync(),
1701
+ pendingActions: this.relay.getOfflineQueueSize()
1702
+ };
1703
+ }
1704
+ /**
1705
+ * Check if the relay is reachable.
1706
+ */
1707
+ async checkConnectivity() {
1708
+ return this.relay.ping();
1709
+ }
1710
+ // ─── Internal ───
1711
+ startCapabilitySync(cap) {
1712
+ if (this.syncTimers.has(cap.id)) return;
1713
+ const interval = this.config.syncIntervals?.[cap.id] ?? cap.defaultInterval;
1714
+ this.syncCapability(cap);
1715
+ const timer = setInterval(() => this.syncCapability(cap), interval);
1716
+ this.syncTimers.set(cap.id, timer);
1717
+ if (cap.subscribe && !this.subscriptions.has(cap.id)) {
1718
+ try {
1719
+ const unsub = cap.subscribe(() => {
1720
+ if (this.running) this.syncCapability(cap);
1721
+ });
1722
+ this.subscriptions.set(cap.id, unsub);
1723
+ } catch (err) {
1724
+ const message = err instanceof Error ? err.message : String(err);
1725
+ this.config.onError?.({
1726
+ domain: cap.id,
1727
+ message: `subscribe failed: ${message}`,
1728
+ code: "SUBSCRIBE_ERROR",
1729
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1730
+ });
1731
+ }
1732
+ }
1733
+ }
1734
+ async syncCapability(cap) {
1735
+ if (!this.running) return;
1736
+ this.updateDomainStatus(cap.id, "syncing");
1737
+ try {
1738
+ const data = await cap.collect();
1739
+ if (data === null) {
1740
+ this.updateDomainStatus(cap.id, "idle");
1741
+ return;
1742
+ }
1743
+ const result = await this.relay.pushDomain(cap.id, data);
1744
+ if (result.ok) {
1745
+ this.updateDomainStatus(cap.id, "idle");
1746
+ } else {
1747
+ this.updateDomainStatus(cap.id, "error", result.error);
1748
+ this.config.onError?.({
1749
+ domain: cap.id,
1750
+ message: result.error || "Push failed",
1751
+ code: "RELAY_ERROR",
1752
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1753
+ });
1754
+ }
1755
+ } catch (error) {
1756
+ const message = error instanceof Error ? error.message : String(error);
1757
+ this.updateDomainStatus(cap.id, "error", message);
1758
+ this.config.onError?.({
1759
+ domain: cap.id,
1760
+ message,
1761
+ code: "COLLECTOR_ERROR",
1762
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1763
+ });
1764
+ }
1765
+ }
1766
+ updateDomainStatus(id, state, error) {
1767
+ const current = this.domainStatus.get(id);
1768
+ this.domainStatus.set(id, {
1769
+ domain: id,
1770
+ lastSync: state === "idle" ? (/* @__PURE__ */ new Date()).toISOString() : current?.lastSync ?? null,
1771
+ state,
1772
+ error,
1773
+ itemCount: current?.itemCount
1774
+ });
1775
+ }
1776
+ getLastFullSync() {
1777
+ const syncs = Array.from(this.domainStatus.values()).map((d) => d.lastSync).filter((s) => s !== null);
1778
+ if (syncs.length === 0) return null;
1779
+ return syncs.sort()[0];
1780
+ }
1781
+ notifyStatusChange() {
1782
+ this.config.onStatusChange?.(this.getStatus());
1783
+ }
1784
+ };
1785
+
1786
+ export {
1787
+ RelayClient,
1788
+ QueuePoller,
1789
+ location_exports,
1790
+ connectivity_exports,
1791
+ capabilities_exports,
1792
+ BACKGROUND_TASK_NAME,
1793
+ registerBackgroundSync,
1794
+ unregisterBackgroundSync,
1795
+ syncDomains,
1796
+ biometrics_exports,
1797
+ sharing_exports,
1798
+ scanner_exports,
1799
+ DeviceBridge
1800
+ };
1801
+ //# sourceMappingURL=chunk-VQJPSLHP.mjs.map