@ateam-ai-mobile/expo 0.1.5 → 0.1.8

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 (179) 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 +2991 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/bootstrap.mjs +66 -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-6LXF3JVX.mjs +96 -0
  14. package/dist/chunk-6LXF3JVX.mjs.map +1 -0
  15. package/dist/chunk-7FWYEHN5.mjs +203 -0
  16. package/dist/chunk-7FWYEHN5.mjs.map +1 -0
  17. package/dist/chunk-C7M5WAHA.mjs +1798 -0
  18. package/dist/chunk-C7M5WAHA.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-GF2MPTAB.mjs +647 -0
  22. package/dist/chunk-GF2MPTAB.mjs.map +1 -0
  23. package/dist/chunk-J6XRNX7P.mjs +19 -0
  24. package/dist/chunk-J6XRNX7P.mjs.map +1 -0
  25. package/dist/chunk-KOXF2FZM.mjs +92 -0
  26. package/dist/chunk-KOXF2FZM.mjs.map +1 -0
  27. package/dist/chunk-L4V7OCJ3.mjs +71 -0
  28. package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
  29. package/dist/chunk-PCAEUX2N.mjs +67 -0
  30. package/dist/chunk-PCAEUX2N.mjs.map +1 -0
  31. package/dist/chunk-WMJKH4XE.mjs +34 -0
  32. package/dist/chunk-WMJKH4XE.mjs.map +1 -0
  33. package/dist/chunk-XQSY6MHP.mjs +213 -0
  34. package/dist/chunk-XQSY6MHP.mjs.map +1 -0
  35. package/dist/chunk-Z5AI2SXX.mjs +1810 -0
  36. package/dist/chunk-Z5AI2SXX.mjs.map +1 -0
  37. package/dist/createBridge.js +41 -10
  38. package/dist/createBridge.js.map +1 -1
  39. package/dist/createBridge.mjs +7 -1
  40. package/dist/createBridge.mjs.map +1 -1
  41. package/dist/device/createDeviceProvider.d.mts +23 -0
  42. package/dist/device/createDeviceProvider.d.ts +23 -0
  43. package/dist/device/createDeviceProvider.js +1228 -0
  44. package/dist/device/createDeviceProvider.js.map +1 -0
  45. package/dist/device/createDeviceProvider.mjs +19 -0
  46. package/dist/device/createDeviceProvider.mjs.map +1 -0
  47. package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
  48. package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
  49. package/dist/device-bridge/collectors/appEngagement.js +128 -0
  50. package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
  51. package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
  52. package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
  53. package/dist/device-bridge/collectors/health.d.mts +24 -0
  54. package/dist/device-bridge/collectors/health.d.ts +24 -0
  55. package/dist/device-bridge/collectors/health.js +238 -0
  56. package/dist/device-bridge/collectors/health.js.map +1 -0
  57. package/dist/device-bridge/collectors/health.mjs +18 -0
  58. package/dist/device-bridge/collectors/health.mjs.map +1 -0
  59. package/dist/device-bridge/cookies.d.mts +49 -0
  60. package/dist/device-bridge/cookies.d.ts +49 -0
  61. package/dist/device-bridge/cookies.js +90 -0
  62. package/dist/device-bridge/cookies.js.map +1 -0
  63. package/dist/device-bridge/cookies.mjs +14 -0
  64. package/dist/device-bridge/cookies.mjs.map +1 -0
  65. package/dist/device-bridge/index.d.mts +495 -0
  66. package/dist/device-bridge/index.d.ts +495 -0
  67. package/dist/device-bridge/index.js +2121 -0
  68. package/dist/device-bridge/index.js.map +1 -0
  69. package/dist/device-bridge/index.mjs +34 -0
  70. package/dist/device-bridge/index.mjs.map +1 -0
  71. package/dist/device-bridge/nfc.d.mts +30 -0
  72. package/dist/device-bridge/nfc.d.ts +30 -0
  73. package/dist/device-bridge/nfc.js +109 -0
  74. package/dist/device-bridge/nfc.js.map +1 -0
  75. package/dist/device-bridge/nfc.mjs +14 -0
  76. package/dist/device-bridge/nfc.mjs.map +1 -0
  77. package/dist/index.d.mts +509 -18
  78. package/dist/index.d.ts +509 -18
  79. package/dist/index.js +1706 -6
  80. package/dist/index.js.map +1 -1
  81. package/dist/index.mjs +24 -194
  82. package/dist/index.mjs.map +1 -1
  83. package/dist/maps/index.d.mts +56 -0
  84. package/dist/maps/index.d.ts +56 -0
  85. package/dist/maps/index.js +95 -0
  86. package/dist/maps/index.js.map +1 -0
  87. package/dist/maps/index.mjs +17 -0
  88. package/dist/maps/index.mjs.map +1 -0
  89. package/dist/oauth/expoOAuth.d.mts +18 -0
  90. package/dist/oauth/expoOAuth.d.ts +18 -0
  91. package/dist/oauth/expoOAuth.js +102 -0
  92. package/dist/oauth/expoOAuth.js.map +1 -0
  93. package/dist/oauth/expoOAuth.mjs +8 -0
  94. package/dist/oauth/expoOAuth.mjs.map +1 -0
  95. package/dist/types-BRanJYTn.d.mts +138 -0
  96. package/dist/types-BRanJYTn.d.ts +138 -0
  97. package/dist/vision/index.d.mts +83 -0
  98. package/dist/vision/index.d.ts +83 -0
  99. package/dist/vision/index.js +101 -0
  100. package/dist/vision/index.js.map +1 -0
  101. package/dist/vision/index.mjs +28 -0
  102. package/dist/vision/index.mjs.map +1 -0
  103. package/expo-module.config.json +9 -0
  104. package/ios/AdaMaps/AdaMapsModule.swift +180 -0
  105. package/ios/AdaMaps.podspec +25 -0
  106. package/ios/AdaVision.podspec +32 -0
  107. package/ios/AdaVisionModule.swift +486 -0
  108. package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
  109. package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
  110. package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
  111. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
  112. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
  113. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
  114. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
  115. package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
  116. package/package.json +113 -7
  117. package/src/args.ts +56 -0
  118. package/src/bootstrap.ts +159 -0
  119. package/src/camera/CameraTaskHost.tsx +175 -0
  120. package/src/camera/cameraTasks.ts +129 -0
  121. package/src/createBridge.ts +3 -1
  122. package/src/device/createDeviceProvider.ts +917 -0
  123. package/src/device-bridge/actions/calendar-actions.ts +217 -0
  124. package/src/device-bridge/actions/dnd.ts +74 -0
  125. package/src/device-bridge/actions/health-actions.ts +58 -0
  126. package/src/device-bridge/actions/index.ts +12 -0
  127. package/src/device-bridge/actions/messaging.ts +74 -0
  128. package/src/device-bridge/actions/navigation.ts +88 -0
  129. package/src/device-bridge/actions/queue-poller.ts +151 -0
  130. package/src/device-bridge/background.ts +137 -0
  131. package/src/device-bridge/biometrics.ts +64 -0
  132. package/src/device-bridge/capabilities.ts +218 -0
  133. package/src/device-bridge/collectors/appEngagement.ts +120 -0
  134. package/src/device-bridge/collectors/battery.ts +83 -0
  135. package/src/device-bridge/collectors/calendar.ts +162 -0
  136. package/src/device-bridge/collectors/camera.ts +27 -0
  137. package/src/device-bridge/collectors/connectivity.ts +60 -0
  138. package/src/device-bridge/collectors/contacts.ts +200 -0
  139. package/src/device-bridge/collectors/health.ts +299 -0
  140. package/src/device-bridge/collectors/index.ts +12 -0
  141. package/src/device-bridge/collectors/location.ts +236 -0
  142. package/src/device-bridge/collectors/mood.ts +141 -0
  143. package/src/device-bridge/collectors/notifications.ts +70 -0
  144. package/src/device-bridge/cookies.ts +89 -0
  145. package/src/device-bridge/dynamic-connector.ts +422 -0
  146. package/src/device-bridge/index.ts +467 -0
  147. package/src/device-bridge/nfc.ts +85 -0
  148. package/src/device-bridge/plugin.ts +71 -0
  149. package/src/device-bridge/relay-client.ts +237 -0
  150. package/src/device-bridge/scanner.ts +42 -0
  151. package/src/device-bridge/sharing.ts +35 -0
  152. package/src/device-bridge/types.ts +326 -0
  153. package/src/device-state/activityClassifier.ts +151 -0
  154. package/src/device-state/activityEngine.ts +252 -0
  155. package/src/device-state/collectors.ts +35 -0
  156. package/src/device-state/domains/activity.ts +93 -0
  157. package/src/device-state/domains/capability.ts +60 -0
  158. package/src/device-state/domains/connectivity.ts +60 -0
  159. package/src/device-state/domains/deviceContext.ts +57 -0
  160. package/src/device-state/domains/interaction.ts +60 -0
  161. package/src/device-state/domains/spatial.ts +367 -0
  162. package/src/device-state/domains/visual.ts +121 -0
  163. package/src/device-state/events.ts +49 -0
  164. package/src/device-state/headingSource.ts +119 -0
  165. package/src/device-state/index.ts +78 -0
  166. package/src/device-state/quality.ts +60 -0
  167. package/src/device-state/replay.ts +285 -0
  168. package/src/device-state/spatialEvents.ts +300 -0
  169. package/src/device-state/turnDetector.ts +121 -0
  170. package/src/device-state/visualEngine.ts +341 -0
  171. package/src/geo.ts +23 -0
  172. package/src/haptics.ts +4 -0
  173. package/src/index.ts +37 -0
  174. package/src/maps/index.ts +81 -0
  175. package/src/maps/normalize.ts +225 -0
  176. package/src/oauth/expoOAuth.tsx +113 -0
  177. package/src/vision/analysis.js +80 -0
  178. package/src/vision/index.ts +146 -0
  179. package/src/vision/ring.js +35 -0
@@ -0,0 +1,2991 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/device-bridge/cookies.ts
34
+ var cookies_exports = {};
35
+ __export(cookies_exports, {
36
+ clearAll: () => clearAll,
37
+ get: () => get,
38
+ set: () => set
39
+ });
40
+ async function get(url, includeHttpOnly = true) {
41
+ const raw = await import_cookies.default.get(url, includeHttpOnly);
42
+ const out = [];
43
+ for (const [name, c] of Object.entries(raw || {})) {
44
+ if (!c || typeof c !== "object") continue;
45
+ const value = typeof c.value === "string" ? c.value : String(c.value ?? "");
46
+ const cookie = { name, value };
47
+ if (c.domain) cookie.domain = String(c.domain);
48
+ if (c.path) cookie.path = String(c.path);
49
+ if (c.expires) {
50
+ const t = Date.parse(c.expires);
51
+ if (!Number.isNaN(t)) cookie.expires = Math.floor(t / 1e3);
52
+ }
53
+ if (typeof c.httpOnly === "boolean") cookie.httpOnly = c.httpOnly;
54
+ if (typeof c.secure === "boolean") cookie.secure = c.secure;
55
+ if (c.sameSite === "Lax" || c.sameSite === "Strict" || c.sameSite === "None") {
56
+ cookie.sameSite = c.sameSite;
57
+ }
58
+ out.push(cookie);
59
+ }
60
+ return out;
61
+ }
62
+ async function clearAll() {
63
+ try {
64
+ await import_cookies.default.clearAll(true);
65
+ return { ok: true };
66
+ } catch (e) {
67
+ return { ok: false, error: String(e?.message || e) };
68
+ }
69
+ }
70
+ async function set(url, cookie) {
71
+ try {
72
+ await import_cookies.default.set(url, {
73
+ name: cookie.name,
74
+ value: cookie.value,
75
+ domain: cookie.domain,
76
+ path: cookie.path || "/",
77
+ httpOnly: cookie.httpOnly,
78
+ secure: cookie.secure
79
+ }, true);
80
+ return { ok: true };
81
+ } catch (e) {
82
+ return { ok: false, error: String(e?.message || e) };
83
+ }
84
+ }
85
+ var import_cookies;
86
+ var init_cookies = __esm({
87
+ "src/device-bridge/cookies.ts"() {
88
+ "use strict";
89
+ import_cookies = __toESM(require("@react-native-cookies/cookies"));
90
+ }
91
+ });
92
+
93
+ // src/device-bridge/nfc.ts
94
+ var nfc_exports = {};
95
+ __export(nfc_exports, {
96
+ isEnabled: () => isEnabled,
97
+ isSupported: () => isSupported3,
98
+ readOnce: () => readOnce
99
+ });
100
+ async function ensureInit() {
101
+ if (_initialized) return true;
102
+ try {
103
+ const supported = await import_react_native_nfc_manager.default.isSupported();
104
+ if (!supported) return false;
105
+ await import_react_native_nfc_manager.default.start();
106
+ _initialized = true;
107
+ return true;
108
+ } catch (e) {
109
+ return false;
110
+ }
111
+ }
112
+ async function isSupported3() {
113
+ try {
114
+ return await import_react_native_nfc_manager.default.isSupported();
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+ async function isEnabled() {
120
+ try {
121
+ return await import_react_native_nfc_manager.default.isEnabled();
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ async function readOnce(timeoutMs = 3e4) {
127
+ const ready = await ensureInit();
128
+ if (!ready) return { ok: false, error: "nfc_unsupported" };
129
+ try {
130
+ const tag = await Promise.race([
131
+ import_react_native_nfc_manager.default.requestTechnology(import_react_native_nfc_manager.NfcTech.Ndef).then(() => import_react_native_nfc_manager.default.getTag()),
132
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
133
+ ]);
134
+ if (!tag) {
135
+ try {
136
+ await import_react_native_nfc_manager.default.cancelTechnologyRequest();
137
+ } catch {
138
+ }
139
+ return { ok: false, error: "timeout" };
140
+ }
141
+ const records = [];
142
+ if (tag.ndefMessage) {
143
+ for (const r of tag.ndefMessage) {
144
+ try {
145
+ const type = String.fromCharCode(...r.type || []);
146
+ const payload = import_react_native_nfc_manager.Ndef.text.decodePayload(r.payload || []);
147
+ records.push({ type, payload: String(payload) });
148
+ } catch {
149
+ }
150
+ }
151
+ }
152
+ const tagId = tag.id ? Array.from(tag.id).map((b) => b.toString(16).padStart(2, "0")).join("") : void 0;
153
+ return { ok: true, tagId, records };
154
+ } catch (e) {
155
+ return { ok: false, error: String(e?.message || e) };
156
+ } finally {
157
+ try {
158
+ await import_react_native_nfc_manager.default.cancelTechnologyRequest();
159
+ } catch {
160
+ }
161
+ }
162
+ }
163
+ var import_react_native_nfc_manager, _initialized;
164
+ var init_nfc = __esm({
165
+ "src/device-bridge/nfc.ts"() {
166
+ "use strict";
167
+ import_react_native_nfc_manager = __toESM(require("react-native-nfc-manager"));
168
+ _initialized = false;
169
+ }
170
+ });
171
+
172
+ // src/bootstrap.ts
173
+ var bootstrap_exports = {};
174
+ __export(bootstrap_exports, {
175
+ bootstrapExpoAdas: () => bootstrapExpoAdas
176
+ });
177
+ module.exports = __toCommonJS(bootstrap_exports);
178
+ var import_core2 = require("@ateam-ai-mobile/core");
179
+ var import_plugin_runtime3 = require("@ateam-ai-mobile/plugin-runtime");
180
+ var import_ui2 = require("@ateam-ai-mobile/ui");
181
+
182
+ // src/index.ts
183
+ var ExpoWebBrowser = __toESM(require("expo-web-browser"));
184
+ var ExpoLinking = __toESM(require("expo-linking"));
185
+ var ExpoClipboard = __toESM(require("expo-clipboard"));
186
+ var import_async_storage3 = __toESM(require("@react-native-async-storage/async-storage"));
187
+ var import_core = require("@ateam-ai-mobile/core");
188
+ var import_plugin_runtime2 = require("@ateam-ai-mobile/plugin-runtime");
189
+ var import_ui = require("@ateam-ai-mobile/ui");
190
+
191
+ // src/haptics.ts
192
+ var Haptics = __toESM(require("expo-haptics"));
193
+ var expoHaptics = {
194
+ impact(style) {
195
+ const map = {
196
+ light: Haptics.ImpactFeedbackStyle.Light,
197
+ medium: Haptics.ImpactFeedbackStyle.Medium,
198
+ heavy: Haptics.ImpactFeedbackStyle.Heavy
199
+ };
200
+ Haptics.impactAsync(map[style]).catch(() => {
201
+ });
202
+ },
203
+ notification(type) {
204
+ const map = {
205
+ success: Haptics.NotificationFeedbackType.Success,
206
+ warning: Haptics.NotificationFeedbackType.Warning,
207
+ error: Haptics.NotificationFeedbackType.Error
208
+ };
209
+ Haptics.notificationAsync(map[type]).catch(() => {
210
+ });
211
+ },
212
+ selection() {
213
+ Haptics.selectionAsync().catch(() => {
214
+ });
215
+ },
216
+ // Shorthand for notification('error') — the form plugins actually call.
217
+ error() {
218
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {
219
+ });
220
+ }
221
+ };
222
+
223
+ // src/bundleStorage.ts
224
+ var FileSystem = __toESM(require("expo-file-system/legacy"));
225
+ var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"));
226
+ var CACHE_DIR = `${FileSystem.cacheDirectory}plugin-bundles/`;
227
+ var DEFAULT_CACHE_INDEX_KEY = "adas-plugin-bundles-index-v1";
228
+ var ExpoBundleStorage = class {
229
+ /** Pass `indexKey` when migrating from an app that already persisted a
230
+ * bundle index under a different key — keeps the existing cache warm. */
231
+ constructor(opts) {
232
+ this.dirReady = false;
233
+ this.indexKey = opts?.indexKey || DEFAULT_CACHE_INDEX_KEY;
234
+ }
235
+ async ensureDir() {
236
+ if (this.dirReady) return;
237
+ try {
238
+ const info = await FileSystem.getInfoAsync(CACHE_DIR);
239
+ if (!info.exists) {
240
+ await FileSystem.makeDirectoryAsync(CACHE_DIR, { intermediates: true });
241
+ }
242
+ } catch (err) {
243
+ console.warn("[ExpoBundleStorage] cache dir error:", err);
244
+ }
245
+ this.dirReady = true;
246
+ }
247
+ async loadIndex() {
248
+ try {
249
+ const raw = await import_async_storage.default.getItem(this.indexKey);
250
+ return raw ? JSON.parse(raw) : {};
251
+ } catch {
252
+ return {};
253
+ }
254
+ }
255
+ async saveIndex(index) {
256
+ try {
257
+ await import_async_storage.default.setItem(this.indexKey, JSON.stringify(index));
258
+ } catch (err) {
259
+ console.warn("[ExpoBundleStorage] index save error:", err);
260
+ }
261
+ }
262
+ async readBundle(localPath) {
263
+ try {
264
+ const info = await FileSystem.getInfoAsync(localPath);
265
+ if (!info.exists) return null;
266
+ return await FileSystem.readAsStringAsync(localPath);
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+ async writeBundle(key, content) {
272
+ await this.ensureDir();
273
+ const localPath = `${CACHE_DIR}${key}`;
274
+ await FileSystem.writeAsStringAsync(localPath, content);
275
+ return localPath;
276
+ }
277
+ async deleteBundle(localPath) {
278
+ await FileSystem.deleteAsync(localPath, { idempotent: true }).catch(() => {
279
+ });
280
+ }
281
+ async clearAll() {
282
+ await FileSystem.deleteAsync(CACHE_DIR, { idempotent: true }).catch(() => {
283
+ });
284
+ await import_async_storage.default.removeItem(this.indexKey).catch(() => {
285
+ });
286
+ this.dirReady = false;
287
+ }
288
+ };
289
+
290
+ // src/attachmentCache.ts
291
+ var FileSystem2 = __toESM(require("expo-file-system/legacy"));
292
+ var import_async_storage2 = __toESM(require("@react-native-async-storage/async-storage"));
293
+ var DEFAULT_INDEX_KEY = "adas-chat-attachment-index-v1";
294
+ var DIR = `${FileSystem2.documentDirectory}chat-attachments/`;
295
+ var MAX_JOBS = 100;
296
+ var ExpoAttachmentCache = class {
297
+ /** Pass `indexKey` when migrating from an app that already persisted an
298
+ * index under a different AsyncStorage key — otherwise existing users
299
+ * lose every cached preview on upgrade. */
300
+ constructor(opts) {
301
+ this.index = {};
302
+ const indexKey = opts?.indexKey || DEFAULT_INDEX_KEY;
303
+ this.indexKey = indexKey;
304
+ this.loaded = (async () => {
305
+ try {
306
+ const info = await FileSystem2.getInfoAsync(DIR);
307
+ if (!info.exists) await FileSystem2.makeDirectoryAsync(DIR, { intermediates: true });
308
+ const raw = await import_async_storage2.default.getItem(indexKey);
309
+ if (raw) this.index = JSON.parse(raw);
310
+ } catch {
311
+ }
312
+ })();
313
+ }
314
+ async ready() {
315
+ await this.loaded;
316
+ }
317
+ async save(jobId, attachments) {
318
+ await this.loaded;
319
+ const entries = [];
320
+ for (let i = 0; i < attachments.length; i++) {
321
+ const att = attachments[i];
322
+ const ext = att.mimeType.startsWith("image/") ? att.mimeType.split("/")[1] || "jpg" : "bin";
323
+ const filePath = `${DIR}${jobId}_${i}.${ext}`;
324
+ try {
325
+ await FileSystem2.writeAsStringAsync(filePath, att.data, { encoding: FileSystem2.EncodingType.Base64 });
326
+ entries.push({ mimeType: att.mimeType, name: att.name, filePath });
327
+ } catch (err) {
328
+ console.warn("[ExpoAttachmentCache] write failed:", err);
329
+ }
330
+ }
331
+ if (entries.length === 0) return;
332
+ this.index[jobId] = entries;
333
+ const keys = Object.keys(this.index);
334
+ if (keys.length > MAX_JOBS) {
335
+ for (const k of keys.slice(0, keys.length - MAX_JOBS)) {
336
+ const old = this.index[k];
337
+ if (old) for (const o of old) FileSystem2.deleteAsync(o.filePath, { idempotent: true }).catch(() => {
338
+ });
339
+ delete this.index[k];
340
+ }
341
+ }
342
+ try {
343
+ await import_async_storage2.default.setItem(this.indexKey, JSON.stringify(this.index));
344
+ } catch {
345
+ }
346
+ }
347
+ get(jobId) {
348
+ const entries = this.index[jobId];
349
+ if (!entries || entries.length === 0) return void 0;
350
+ return entries.map((e) => ({
351
+ mimeType: e.mimeType,
352
+ name: e.name,
353
+ previewUri: e.filePath
354
+ }));
355
+ }
356
+ };
357
+
358
+ // src/device-state/domains/deviceContext.ts
359
+ var ExpoBattery = __toESM(require("expo-battery"));
360
+
361
+ // src/device-state/quality.ts
362
+ var import_plugin_runtime = require("@ateam-ai-mobile/plugin-runtime");
363
+ var FRESH_MS = 1e4;
364
+ var AGING_MS = 6e4;
365
+ function qualityFrom(observedAt, confidence) {
366
+ const ageMs = Date.now() - observedAt;
367
+ return {
368
+ freshness: {
369
+ state: ageMs < FRESH_MS ? "fresh" : ageMs < AGING_MS ? "aging" : "stale",
370
+ observedAt,
371
+ ageMs
372
+ },
373
+ ...confidence ? { confidence } : {}
374
+ };
375
+ }
376
+ function qualityUnknown() {
377
+ return { freshness: (0, import_plugin_runtime.neverObserved)() };
378
+ }
379
+ function unsupported(what) {
380
+ const err = new Error(
381
+ `${what} is not implemented in this build \u2014 the native engine behind it does not exist yet.`
382
+ );
383
+ err.code = "UNSUPPORTED";
384
+ return err;
385
+ }
386
+
387
+ // src/device-state/events.ts
388
+ function stateEvent(domain, name, payload, timestamp = Date.now()) {
389
+ return payload === void 0 ? { domain, name, timestamp } : { domain, name, timestamp, payload };
390
+ }
391
+ function listenerSet() {
392
+ const listeners = /* @__PURE__ */ new Set();
393
+ return {
394
+ add(fn) {
395
+ listeners.add(fn);
396
+ return { unsubscribe: () => {
397
+ listeners.delete(fn);
398
+ } };
399
+ },
400
+ emit(event) {
401
+ for (const fn of listeners) {
402
+ try {
403
+ fn(event);
404
+ } catch {
405
+ }
406
+ }
407
+ }
408
+ };
409
+ }
410
+
411
+ // src/device-state/domains/deviceContext.ts
412
+ function createDeviceContextDomain() {
413
+ return {
414
+ async getSnapshot() {
415
+ const observedAt = Date.now();
416
+ try {
417
+ const [level, state, lowPower] = await Promise.all([
418
+ ExpoBattery.getBatteryLevelAsync(),
419
+ ExpoBattery.getBatteryStateAsync(),
420
+ ExpoBattery.isLowPowerModeEnabledAsync()
421
+ ]);
422
+ return {
423
+ power: {
424
+ // expo-battery returns -1 when it cannot tell. That is not a level.
425
+ batteryLevel: typeof level === "number" && level >= 0 ? level : null,
426
+ charging: state === ExpoBattery.BatteryState.CHARGING || state === ExpoBattery.BatteryState.FULL,
427
+ lowPowerMode: lowPower
428
+ },
429
+ // No Expo API for either. iOS needs ProcessInfo.thermalState, Android
430
+ // PowerManager.getThermalStatus — a native module that does not exist.
431
+ thermal: { state: "unknown" },
432
+ resources: { memoryPressure: "unknown", storagePressure: "unknown" },
433
+ quality: qualityFrom(observedAt)
434
+ };
435
+ } catch {
436
+ return {
437
+ power: { batteryLevel: null, charging: null, lowPowerMode: null },
438
+ thermal: { state: "unknown" },
439
+ resources: { memoryPressure: "unknown", storagePressure: "unknown" },
440
+ quality: qualityUnknown()
441
+ };
442
+ }
443
+ },
444
+ // Real events, not a placeholder: expo-battery exposes native listeners,
445
+ // so a plugin that subscribes genuinely hears about a change rather than
446
+ // sitting on a subscription that can never fire. Nudges without a payload:
447
+ // no Device Context payload schema is decided, so the subscriber re-reads.
448
+ subscribe(listener) {
449
+ const subs = [
450
+ ExpoBattery.addBatteryLevelListener(() => listener(stateEvent("deviceContext", "batteryLevelChanged"))),
451
+ ExpoBattery.addBatteryStateListener(() => listener(stateEvent("deviceContext", "chargingChanged")))
452
+ ];
453
+ return { unsubscribe: () => subs.forEach((x) => x?.remove?.()) };
454
+ }
455
+ };
456
+ }
457
+
458
+ // src/device-state/domains/connectivity.ts
459
+ function createConnectivityDomain(collectors) {
460
+ return {
461
+ async getSnapshot() {
462
+ const observedAt = Date.now();
463
+ try {
464
+ if (!collectors.connectivity) throw unsupported("connectivity collector");
465
+ const data = await collectors.connectivity.collectConnectivity();
466
+ const online = data?.is_connected === true;
467
+ const type = String(data?.type || "").toLowerCase();
468
+ return {
469
+ reachability: data?.is_connected == null ? "unknown" : online ? "online" : "offline",
470
+ transport: type === "wifi" ? "wifi" : type === "cellular" ? "cellular" : type === "ethernet" ? "ethernet" : online ? "other" : data?.is_connected === false ? "none" : "unknown",
471
+ // All four below need measurement nobody takes yet. `unknown` is the
472
+ // designed answer — "return unknown rather than stale precision".
473
+ qualityClass: "unknown",
474
+ stability: "unknown",
475
+ latencyMs: null,
476
+ bandwidthClass: "unknown",
477
+ metered: null,
478
+ constrained: null,
479
+ quality: qualityFrom(observedAt)
480
+ };
481
+ } catch {
482
+ return {
483
+ reachability: "unknown",
484
+ transport: "unknown",
485
+ qualityClass: "unknown",
486
+ stability: "unknown",
487
+ latencyMs: null,
488
+ bandwidthClass: "unknown",
489
+ metered: null,
490
+ constrained: null,
491
+ quality: qualityUnknown()
492
+ };
493
+ }
494
+ },
495
+ // Intent selects a native sampling strategy — latency probes, quality
496
+ // classification. Nothing here samples anything, so accepting an intent
497
+ // would be accepting an instruction we do not carry out.
498
+ async start() {
499
+ throw unsupported("connectivity.start");
500
+ },
501
+ async stop() {
502
+ throw unsupported("connectivity.stop");
503
+ },
504
+ // A subscription that can never fire is worse than none: the caller
505
+ // reasonably reads silence as "nothing changed".
506
+ subscribe() {
507
+ throw unsupported("connectivity.subscribe");
508
+ }
509
+ };
510
+ }
511
+
512
+ // src/device-state/domains/capability.ts
513
+ function statusFrom(granted) {
514
+ return {
515
+ support: "unknown",
516
+ permission: granted === null ? "unknown" : granted ? "granted" : "denied",
517
+ availability: "unknown"
518
+ };
519
+ }
520
+ var UNKNOWN_STATUS = {
521
+ support: "unknown",
522
+ permission: "unknown",
523
+ availability: "unknown"
524
+ };
525
+ function createCapabilityDomain(collectors) {
526
+ return {
527
+ async getSnapshot() {
528
+ const observedAt = Date.now();
529
+ const locationGranted = collectors.location ? await collectors.location.hasPermission().catch(() => null) : null;
530
+ return {
531
+ // Only location has a real reader today. The rest are declared by the
532
+ // contract and answered honestly rather than omitted.
533
+ camera: UNKNOWN_STATUS,
534
+ microphone: UNKNOWN_STATUS,
535
+ location: {
536
+ ...statusFrom(locationGranted),
537
+ // iOS 14+ distinguishes precise from approximate; nothing reads it.
538
+ precision: "unknown",
539
+ backgroundPermission: "unknown"
540
+ },
541
+ motion: UNKNOWN_STATUS,
542
+ bluetooth: UNKNOWN_STATUS,
543
+ uwb: UNKNOWN_STATUS,
544
+ nfc: UNKNOWN_STATUS,
545
+ quality: qualityFrom(observedAt)
546
+ };
547
+ },
548
+ // No OS permission-change stream is wired. Same reasoning as connectivity:
549
+ // silence would be indistinguishable from stability.
550
+ subscribe() {
551
+ throw unsupported("capability.subscribe");
552
+ }
553
+ };
554
+ }
555
+
556
+ // src/device-state/domains/interaction.ts
557
+ var import_react_native = require("react-native");
558
+ function createInteractionDomain() {
559
+ const mapState = (s) => s === "active" ? "foreground" : s === "background" ? "background" : s === "inactive" ? "inactive" : "unknown";
560
+ return {
561
+ async getSnapshot() {
562
+ const observedAt = Date.now();
563
+ return {
564
+ lifecycle: {
565
+ appState: mapState(import_react_native.AppState.currentState),
566
+ screenOn: null,
567
+ userActive: null,
568
+ idleDurationMs: null
569
+ },
570
+ audio: {
571
+ captureActive: null,
572
+ playbackActive: null,
573
+ route: "unknown",
574
+ interruption: "unknown"
575
+ },
576
+ // The voice block is OMITTED rather than filled with nulls: the design
577
+ // says a platform that cannot report voice session state leaves the
578
+ // group out instead of guessing `false` for each field. The app's own
579
+ // voice store knows this, but it is not reachable from the SDK.
580
+ quality: qualityFrom(observedAt)
581
+ };
582
+ },
583
+ // Real: AppState genuinely emits on every foreground/background change,
584
+ // which is the one part of this domain that works.
585
+ subscribe(listener) {
586
+ const sub2 = import_react_native.AppState.addEventListener("change", (next) => {
587
+ listener(stateEvent("interaction", "appStateChanged", { appState: mapState(next) }));
588
+ });
589
+ return { unsubscribe: () => sub2.remove() };
590
+ }
591
+ };
592
+ }
593
+
594
+ // src/device-state/headingSource.ts
595
+ var ExpoLocation = __toESM(require("expo-location"));
596
+
597
+ // src/device-state/turnDetector.ts
598
+ function signedDelta(a, b) {
599
+ return (b - a + 540) % 360 - 180;
600
+ }
601
+ var TurnDetector = class {
602
+ constructor(opts = {}) {
603
+ /** The heading currently treated as "straight ahead". */
604
+ this.refDeg = null;
605
+ this.stableSince = 0;
606
+ this.lastTurn = null;
607
+ this.minTurnDeg = opts.minTurnDeg ?? 35;
608
+ this.stableDeg = opts.stableDeg ?? 12;
609
+ this.stableMs = opts.stableMs ?? 2500;
610
+ }
611
+ /** Feed one compass reading. `null` degrees means the compass said nothing. */
612
+ push(deg, now) {
613
+ if (deg === null) return null;
614
+ if (this.refDeg === null) {
615
+ this.refDeg = deg;
616
+ this.stableSince = now;
617
+ return null;
618
+ }
619
+ const delta = signedDelta(this.refDeg, deg);
620
+ if (Math.abs(delta) >= this.minTurnDeg) {
621
+ this.lastTurn = { deg: Math.round(delta), timestamp: now };
622
+ this.refDeg = deg;
623
+ this.stableSince = now;
624
+ return this.lastTurn;
625
+ }
626
+ if (Math.abs(delta) > this.stableDeg) {
627
+ this.stableSince = now;
628
+ return null;
629
+ }
630
+ if (now - this.stableSince >= this.stableMs) {
631
+ this.refDeg = deg;
632
+ this.stableSince = now;
633
+ }
634
+ return null;
635
+ }
636
+ /**
637
+ * The most recent completed turn, or null if none this session.
638
+ *
639
+ * It does NOT expire on its own: the age is derivable from `timestamp`, and
640
+ * "recent enough to mention" is a product judgement this layer must not make
641
+ * on the caller's behalf.
642
+ */
643
+ last() {
644
+ return this.lastTurn;
645
+ }
646
+ reset() {
647
+ this.refDeg = null;
648
+ this.stableSince = 0;
649
+ this.lastTurn = null;
650
+ }
651
+ };
652
+
653
+ // src/device-state/headingSource.ts
654
+ var latest = null;
655
+ var sub = null;
656
+ var opening = null;
657
+ var owners = /* @__PURE__ */ new Set();
658
+ var turns = new TurnDetector();
659
+ var turnListeners = listenerSet();
660
+ function startHeading(owner) {
661
+ owners.add(owner);
662
+ if (sub) return Promise.resolve();
663
+ opening ?? (opening = open().finally(() => {
664
+ opening = null;
665
+ }));
666
+ return opening;
667
+ }
668
+ async function open() {
669
+ try {
670
+ const s = await ExpoLocation.watchHeadingAsync((h) => {
671
+ if (!owners.size) return;
672
+ const raw = h.trueHeading >= 0 ? h.trueHeading : h.magHeading;
673
+ const deg = typeof raw === "number" && raw >= 0 ? raw : null;
674
+ const cal = typeof h.accuracy === "number" && h.accuracy >= 0 ? h.accuracy : null;
675
+ const now = Date.now();
676
+ latest = { headingDeg: deg, calibration: cal, timestamp: now };
677
+ const turn = turns.push(deg, now);
678
+ if (turn) turnListeners.emit(turn);
679
+ });
680
+ if (!owners.size) {
681
+ s.remove();
682
+ return;
683
+ }
684
+ sub = s;
685
+ } catch {
686
+ sub = null;
687
+ }
688
+ }
689
+ function stopHeading(owner) {
690
+ owners.delete(owner);
691
+ if (owners.size) return;
692
+ sub?.remove();
693
+ sub = null;
694
+ latest = null;
695
+ turns.reset();
696
+ }
697
+ function onTurn(fn) {
698
+ return turnListeners.add(fn);
699
+ }
700
+ function currentHeading() {
701
+ return latest ? { headingDeg: latest.headingDeg } : null;
702
+ }
703
+ function currentHeadingDetail() {
704
+ return latest;
705
+ }
706
+ function lastMeaningfulTurn() {
707
+ return turns.last();
708
+ }
709
+
710
+ // src/geo.ts
711
+ function distanceOf(path) {
712
+ const R = 6371e3;
713
+ let total = 0;
714
+ for (let i = 1; i < path.length; i++) {
715
+ const a = path[i - 1], b = path[i];
716
+ const dLat = (b.latitude - a.latitude) * Math.PI / 180;
717
+ const dLon = (b.longitude - a.longitude) * Math.PI / 180;
718
+ const la1 = a.latitude * Math.PI / 180, la2 = b.latitude * Math.PI / 180;
719
+ const h = Math.sin(dLat / 2) ** 2 + Math.cos(la1) * Math.cos(la2) * Math.sin(dLon / 2) ** 2;
720
+ total += 2 * R * Math.asin(Math.sqrt(h));
721
+ }
722
+ return Math.round(total);
723
+ }
724
+
725
+ // src/device-state/spatialEvents.ts
726
+ var STATIONARY_MPS = 0.4;
727
+ function movementStateOf(speedMps) {
728
+ return speedMps === null ? "unknown" : speedMps < STATIONARY_MPS ? "stationary" : "moving";
729
+ }
730
+ function fixConfidence(accuracyM) {
731
+ return accuracyM === null ? "unknown" : accuracyM <= 10 ? "high" : accuracyM <= 50 ? "medium" : "low";
732
+ }
733
+ var SPATIAL_EVENT_PROFILES = {
734
+ navigation: { movingHoldMs: 3e3, stoppedHoldMs: 5e3, confidenceHoldMs: 3e3, minMoveM: 10 },
735
+ fitness: { movingHoldMs: 3e3, stoppedHoldMs: 5e3, confidenceHoldMs: 5e3, minMoveM: 10 },
736
+ preciseLocate: { movingHoldMs: 2e3, stoppedHoldMs: 3e3, confidenceHoldMs: 2e3, minMoveM: 3 },
737
+ geofence: { movingHoldMs: 1e4, stoppedHoldMs: 3e4, confidenceHoldMs: 1e4, minMoveM: 25 },
738
+ lowPowerTracking: { movingHoldMs: 1e4, stoppedHoldMs: 3e4, confidenceHoldMs: 1e4, minMoveM: 50 }
739
+ };
740
+ function moveThresholdM(anchor, fix) {
741
+ const a = anchor.accuracyM ?? 0, b = fix.accuracyM ?? 0;
742
+ return 2 * Math.sqrt(a * a + b * b);
743
+ }
744
+ function spatialEventProfile(intent) {
745
+ return SPATIAL_EVENT_PROFILES[intent] ?? SPATIAL_EVENT_PROFILES.navigation;
746
+ }
747
+ var RANK = { high: 3, medium: 2, low: 1 };
748
+ var MIN_FIXES = 2;
749
+ function extend(run, state, fixAt2, now) {
750
+ if (!run || run.state !== state) return { state, since: now, fixAt: fixAt2, fixes: fixAt2 === null ? 0 : 1 };
751
+ if (fixAt2 !== null && fixAt2 !== run.fixAt) {
752
+ run.fixAt = fixAt2;
753
+ run.fixes++;
754
+ }
755
+ return run;
756
+ }
757
+ var settled = (run, now, holdMs) => run.fixes >= MIN_FIXES && now - run.since >= holdMs;
758
+ var fixAt = (s) => s.position?.timestamp ?? null;
759
+ var SpatialEventDetector = class {
760
+ constructor(profile = SPATIAL_EVENT_PROFILES.navigation) {
761
+ /** The movement state last settled on. Only MEASURED states settle. */
762
+ this.movement = "unknown";
763
+ this.run = null;
764
+ /** The completed turn already on record — reported turns, and the one the
765
+ * session opened onto. Only a turn with a new timestamp is news. */
766
+ this.lastTurnAt = null;
767
+ /** The GPS band last settled on; null until the session's first band. */
768
+ this.band = null;
769
+ this.bandRun = null;
770
+ /** The last position REPORTED — distance is measured from here, not from
771
+ * the previous fix, so a slow walk still adds up to a change. */
772
+ this.anchor = null;
773
+ this.staleArmed = true;
774
+ this.profile = profile;
775
+ }
776
+ /**
777
+ * Forget everything — a new session, or replay seeking backwards. Emits
778
+ * nothing. `turnOnRecordAt` is the timestamp of the turn already completed
779
+ * when the session opens (a visual session shares the compass): history, not
780
+ * news. Seeded from the turn itself rather than compared against a clock, so
781
+ * it holds whichever clock the turn was stamped with — epoch live, the
782
+ * recording's epoch in replay, where `now` is virtual.
783
+ */
784
+ reset(profile, turnOnRecordAt = null) {
785
+ if (profile) this.profile = profile;
786
+ this.movement = "unknown";
787
+ this.run = null;
788
+ this.lastTurnAt = turnOnRecordAt;
789
+ this.band = null;
790
+ this.bandRun = null;
791
+ this.anchor = null;
792
+ this.staleArmed = true;
793
+ }
794
+ /**
795
+ * Feed the newest snapshot. Returns what changed since the last one, in this
796
+ * order: movement, turn, position, confidence, staleness. Fields are read
797
+ * defensively — replay hands over recorded snapshots verbatim, and a recording
798
+ * may lack a block the live engine always fills.
799
+ */
800
+ push(snapshot, now) {
801
+ const out = [];
802
+ const emit = (name, payload) => {
803
+ out.push(stateEvent("spatial", name, payload, now));
804
+ };
805
+ this.movementFrom(snapshot, now, emit);
806
+ this.turnFrom(snapshot, emit);
807
+ this.positionFrom(snapshot, emit);
808
+ this.confidenceFrom(snapshot, now, emit);
809
+ this.staleFrom(snapshot, emit);
810
+ return out;
811
+ }
812
+ movementFrom(s, now, emit) {
813
+ const state = s.movement?.state ?? "unknown";
814
+ this.run = extend(this.run, state, fixAt(s), now);
815
+ if (state === this.movement) return;
816
+ if (state === "moving" && settled(this.run, now, this.profile.movingHoldMs)) {
817
+ emit("movementStarted", {
818
+ previous: this.movement === "stationary" ? "stationary" : "unknown",
819
+ speedMps: s.movement?.speedMps ?? null,
820
+ bearingDeg: s.movement?.bearingDeg ?? null,
821
+ since: this.run.since
822
+ });
823
+ this.movement = "moving";
824
+ } else if (state === "stationary" && settled(this.run, now, this.profile.stoppedHoldMs)) {
825
+ if (this.movement === "moving") emit("movementStopped", { previous: "moving", since: this.run.since });
826
+ this.movement = "stationary";
827
+ }
828
+ }
829
+ turnFrom(s, emit) {
830
+ const at = s.changes?.timestamp ?? null;
831
+ const deg = s.changes?.lastMeaningfulTurnDeg ?? null;
832
+ if (at === null || deg === null || at === this.lastTurnAt) return;
833
+ this.lastTurnAt = at;
834
+ emit("lastMeaningfulTurnChanged", { lastMeaningfulTurnDeg: deg, timestamp: at });
835
+ }
836
+ positionFrom(s, emit) {
837
+ const p = s.position ?? null;
838
+ if (!p) return;
839
+ if (!this.anchor) {
840
+ emit("positionChanged", { position: { ...p }, previous: null, distanceM: null });
841
+ this.anchor = p;
842
+ return;
843
+ }
844
+ const d = distanceOf([this.anchor, p]);
845
+ if (d < Math.max(this.profile.minMoveM, moveThresholdM(this.anchor, p))) {
846
+ const aNew = p.accuracyM ?? null, aOld = this.anchor.accuracyM ?? null;
847
+ if (aNew != null && aOld != null && aNew * 2 < aOld) this.anchor = p;
848
+ return;
849
+ }
850
+ emit("positionChanged", { position: { ...p }, previous: { ...this.anchor }, distanceM: d });
851
+ this.anchor = p;
852
+ }
853
+ confidenceFrom(s, now, emit) {
854
+ const p = s.position ?? null;
855
+ if (!p) return;
856
+ const level = fixConfidence(p.accuracyM ?? null);
857
+ if (level === "unknown") return;
858
+ if (this.band === null) {
859
+ this.band = level;
860
+ return;
861
+ }
862
+ if (level === this.band) {
863
+ this.bandRun = null;
864
+ return;
865
+ }
866
+ this.bandRun = extend(this.bandRun, level, fixAt(s), now);
867
+ if (!settled(this.bandRun, now, this.profile.confidenceHoldMs)) return;
868
+ emit("positionConfidenceChanged", {
869
+ level,
870
+ previous: this.band,
871
+ accuracyM: p.accuracyM ?? null,
872
+ direction: RANK[level] < RANK[this.band] ? "dropped" : "recovered"
873
+ });
874
+ this.band = level;
875
+ this.bandRun = null;
876
+ }
877
+ staleFrom(s, emit) {
878
+ const f = s.quality?.freshness;
879
+ if (!f) return;
880
+ if (f.state === "fresh") {
881
+ this.staleArmed = true;
882
+ return;
883
+ }
884
+ if (f.state !== "stale" || !this.staleArmed) return;
885
+ emit("spatialStateStale", { observedAt: f.observedAt, ageMs: f.ageMs });
886
+ this.staleArmed = false;
887
+ }
888
+ };
889
+
890
+ // src/device-state/domains/spatial.ts
891
+ var WATCH_ACCURACY = {
892
+ navigation: "navigation",
893
+ preciseLocate: "high",
894
+ fitness: "high",
895
+ geofence: "balanced",
896
+ lowPowerTracking: "low"
897
+ };
898
+ function createSpatialDomain(collectors, speedRef) {
899
+ const RECENT_PATH_MAX = 60;
900
+ let recentPath = [];
901
+ let latest2 = null;
902
+ let latestSpeed = null;
903
+ let latestCourse = null;
904
+ let stopWatch = null;
905
+ let session = null;
906
+ const detector = new SpatialEventDetector();
907
+ const listeners = listenerSet();
908
+ let ticker = null;
909
+ let turnSub = null;
910
+ const nonNegative2 = (v) => typeof v === "number" && v >= 0 ? v : null;
911
+ const derivedSpeed = () => {
912
+ if (recentPath.length < 2) return null;
913
+ const b = recentPath[recentPath.length - 1];
914
+ const a = recentPath[recentPath.length - 2];
915
+ const dtS = (b.timestamp - a.timestamp) / 1e3;
916
+ if (dtS <= 0 || dtS > 10) return null;
917
+ return distanceOf([a, b]) / dtS;
918
+ };
919
+ const derivedCourse = () => {
920
+ if (recentPath.length < 2) return null;
921
+ const b = recentPath[recentPath.length - 1];
922
+ const a = recentPath[recentPath.length - 2];
923
+ if (b.timestamp - a.timestamp > 1e4) return null;
924
+ if (distanceOf([a, b]) < 3) return null;
925
+ return bearingBetween(a, b);
926
+ };
927
+ const snapshotFrom = (point2, measuredSpeed, measuredCourse) => {
928
+ let speedMps = measuredSpeed;
929
+ let courseDeg = measuredCourse;
930
+ let derived = false;
931
+ if (speedMps === null) {
932
+ const d = derivedSpeed();
933
+ if (d !== null) {
934
+ speedMps = d;
935
+ derived = true;
936
+ }
937
+ }
938
+ if (courseDeg === null) {
939
+ const d = derivedCourse();
940
+ if (d !== null) {
941
+ courseDeg = d;
942
+ derived = true;
943
+ }
944
+ }
945
+ const heading = currentHeadingDetail();
946
+ const turn = lastMeaningfulTurn();
947
+ return {
948
+ position: point2,
949
+ movement: {
950
+ speedMps,
951
+ bearingDeg: courseDeg,
952
+ // Needs two points from the SAME session; without a stream there is
953
+ // nothing to measure against, and a distance of 0 would read as
954
+ // "the user has not moved".
955
+ distanceRecentM: recentPath.length >= 2 ? distanceOf(recentPath) : null,
956
+ state: movementStateOf(speedMps)
957
+ },
958
+ orientation: {
959
+ headingDeg: heading?.headingDeg ?? null,
960
+ // NOT the calibration rank. iOS reports 0–3 (none…high), which is not
961
+ // a number of degrees, and converting one to the other would be an
962
+ // invented error bar. The rank drives `confidence` below instead.
963
+ headingAccuracyDeg: null
964
+ },
965
+ changes: {
966
+ lastMeaningfulTurnDeg: turn ? turn.deg : null,
967
+ timestamp: turn ? turn.timestamp : null
968
+ },
969
+ recentPath: [...recentPath],
970
+ quality: qualityFrom(
971
+ point2.timestamp,
972
+ spatialConfidence(point2.accuracyM ?? null, heading?.calibration ?? null, derived)
973
+ )
974
+ };
975
+ };
976
+ const noFix = () => ({
977
+ position: null,
978
+ movement: { speedMps: null, bearingDeg: null, distanceRecentM: null, state: "unknown" },
979
+ orientation: { headingDeg: currentHeadingDetail()?.headingDeg ?? null },
980
+ recentPath: [],
981
+ quality: qualityUnknown()
982
+ });
983
+ const warmSnapshot = () => latest2 ? snapshotFrom(latest2, latestSpeed, latestCourse) : null;
984
+ const observe = () => {
985
+ const snapshot = warmSnapshot();
986
+ if (!snapshot) return;
987
+ for (const event of detector.push(snapshot, Date.now())) listeners.emit(event);
988
+ };
989
+ const startTicking = () => {
990
+ ticker = setInterval(observe, 1e3);
991
+ };
992
+ const stopTicking = () => {
993
+ if (ticker) clearInterval(ticker);
994
+ ticker = null;
995
+ };
996
+ return {
997
+ async getSnapshot() {
998
+ const warm = warmSnapshot();
999
+ if (warm) return warm;
1000
+ if (session) return noFix();
1001
+ let point2;
1002
+ let speedMps;
1003
+ let courseDeg;
1004
+ try {
1005
+ if (!collectors.location) throw unsupported("location collector");
1006
+ const fix = await collectors.location.getPosition({ accuracy: "low" });
1007
+ point2 = {
1008
+ latitude: fix.latitude,
1009
+ longitude: fix.longitude,
1010
+ altitude: fix.altitude ?? null,
1011
+ accuracyM: nonNegative2(fix.accuracy),
1012
+ timestamp: fix.timestamp ?? Date.now()
1013
+ };
1014
+ speedMps = nonNegative2(fix.speed);
1015
+ courseDeg = nonNegative2(fix.course);
1016
+ } catch {
1017
+ return noFix();
1018
+ }
1019
+ return snapshotFrom(point2, speedMps, courseDeg);
1020
+ },
1021
+ /**
1022
+ * Open the warm window: position stream + compass + event feed.
1023
+ *
1024
+ * The intent selects how eager the EVENTS are (spatialEvents.ts profiles)
1025
+ * and the GPS TIER the stream runs at (WATCH_ACCURACY): navigation pays the
1026
+ * battery for BestForNavigation; low-power tracking does not.
1027
+ *
1028
+ * A second `start()` while one is open joins the running session and is
1029
+ * told ITS intent, not handed back the one it asked for: answering with the
1030
+ * caller's intent would claim a profile that is not in force.
1031
+ */
1032
+ async start(options) {
1033
+ if (session) return { ...session };
1034
+ if (!collectors.location) throw unsupported("location collector");
1035
+ const startedAt = Date.now();
1036
+ session = { id: `spatial_${startedAt}`, intent: options.intent, startedAt };
1037
+ detector.reset(spatialEventProfile(options.intent), lastMeaningfulTurn()?.timestamp ?? null);
1038
+ void startHeading("spatial");
1039
+ turnSub = onTurn(observe);
1040
+ stopWatch = collectors.location.watchPosition((pos) => {
1041
+ const p = {
1042
+ latitude: pos.latitude,
1043
+ longitude: pos.longitude,
1044
+ altitude: pos.altitude ?? null,
1045
+ accuracyM: nonNegative2(pos.accuracy),
1046
+ timestamp: pos.timestamp ?? Date.now()
1047
+ };
1048
+ latest2 = p;
1049
+ latestSpeed = nonNegative2(pos.speed);
1050
+ latestCourse = nonNegative2(pos.course);
1051
+ speedRef.mps = latestSpeed;
1052
+ speedRef.at = p.timestamp;
1053
+ recentPath = [...recentPath, p].slice(-RECENT_PATH_MAX);
1054
+ observe();
1055
+ }, { accuracy: WATCH_ACCURACY[options.intent] ?? "balanced" });
1056
+ startTicking();
1057
+ return { ...session };
1058
+ },
1059
+ async stop() {
1060
+ stopWatch?.();
1061
+ stopWatch = null;
1062
+ session = null;
1063
+ stopTicking();
1064
+ turnSub?.unsubscribe();
1065
+ turnSub = null;
1066
+ detector.reset();
1067
+ recentPath = [];
1068
+ latest2 = null;
1069
+ latestSpeed = null;
1070
+ latestCourse = null;
1071
+ speedRef.mps = null;
1072
+ speedRef.at = 0;
1073
+ stopHeading("spatial");
1074
+ },
1075
+ /**
1076
+ * The six spatial events (see `SpatialEvent`). Subscribing opens no sensor:
1077
+ * events flow only while a session is open, and a subscription made before
1078
+ * `start()` hears that session from its first fix.
1079
+ */
1080
+ subscribe(listener) {
1081
+ return listeners.add(listener);
1082
+ }
1083
+ };
1084
+ }
1085
+ function spatialConfidence(accuracyM, calibration, derived) {
1086
+ const rank = (v) => v === "high" ? 3 : v === "medium" ? 2 : v === "low" ? 1 : 0;
1087
+ const fromFix = fixConfidence(accuracyM);
1088
+ const fromCompass = calibration === null ? "unknown" : calibration >= 3 ? "high" : calibration >= 2 ? "medium" : "low";
1089
+ const parts = [fromFix, fromCompass, ...derived ? ["low"] : []];
1090
+ const known = parts.filter((p) => p !== "unknown");
1091
+ if (!known.length) return { level: "unknown" };
1092
+ const worst = known.reduce((a, b) => rank(a) <= rank(b) ? a : b);
1093
+ return { level: worst };
1094
+ }
1095
+ function bearingBetween(a, b) {
1096
+ const toRad = (d) => d * Math.PI / 180;
1097
+ const la1 = toRad(a.latitude), la2 = toRad(b.latitude);
1098
+ const dLon = toRad(b.longitude - a.longitude);
1099
+ const y = Math.sin(dLon) * Math.cos(la2);
1100
+ const x = Math.cos(la1) * Math.sin(la2) - Math.sin(la1) * Math.cos(la2) * Math.cos(dLon);
1101
+ return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
1102
+ }
1103
+
1104
+ // src/device-state/visualEngine.ts
1105
+ var PROFILES = {
1106
+ navigation: { intervalMs: 1e3, windowMs: 5e3 },
1107
+ accessibility: { intervalMs: 1e3, windowMs: 5e3 },
1108
+ stream: { intervalMs: 500, windowMs: 5e3 },
1109
+ inspection: { intervalMs: 2e3, windowMs: 8e3 },
1110
+ shopping: { intervalMs: 2e3, windowMs: 5e3 },
1111
+ document: { intervalMs: 3e3, windowMs: 3e3 },
1112
+ ocr: { intervalMs: 3e3, windowMs: 3e3 },
1113
+ highQuality: { intervalMs: 5e3, windowMs: 5e3 }
1114
+ };
1115
+ var VisualEngine = class {
1116
+ constructor() {
1117
+ this.frames = [];
1118
+ this.session = null;
1119
+ this.listeners = /* @__PURE__ */ new Set();
1120
+ /** Set by the host component while a camera is actually mounted and ready. */
1121
+ this.cameraReady = false;
1122
+ /** Optional: supplies heading/position stamped onto each frame. */
1123
+ this.context = null;
1124
+ // The native ring's actual retention (ms). The engine's effective window is
1125
+ // min(intent window, retention) — set by whoever owns the clamp semantics; this
1126
+ // just stores the input so that logic has one source of truth to read.
1127
+ this._retentionMs = null;
1128
+ }
1129
+ /** Host DI. Without it frames carry no orientation, and say so with nulls. */
1130
+ setContextProvider(p) {
1131
+ this.context = p;
1132
+ }
1133
+ // ── session ──
1134
+ startSession(intent) {
1135
+ if (this.session) return this.session;
1136
+ this.session = {
1137
+ id: `visual_${Date.now()}`,
1138
+ intent: PROFILES[intent] ? intent : "navigation",
1139
+ startedAt: Date.now(),
1140
+ suspended: false
1141
+ };
1142
+ this.notify();
1143
+ return this.session;
1144
+ }
1145
+ stopSession() {
1146
+ this.session = null;
1147
+ this.frames = [];
1148
+ this.cameraReady = false;
1149
+ this.notify();
1150
+ }
1151
+ /**
1152
+ * Background: suspend, do not end.
1153
+ *
1154
+ * iOS tears the camera down when the app leaves the foreground. The session
1155
+ * is a STATE that survives that; what does not survive is the claim that the
1156
+ * camera is live. While suspended the ring is kept but ages — a read gets
1157
+ * old frames clearly marked old, rather than a fresh-looking lie.
1158
+ */
1159
+ suspend() {
1160
+ if (!this.session || this.session.suspended) return;
1161
+ this.session.suspended = true;
1162
+ this.cameraReady = false;
1163
+ this.notify();
1164
+ }
1165
+ resume() {
1166
+ if (!this.session?.suspended) return;
1167
+ this.session.suspended = false;
1168
+ this.notify();
1169
+ }
1170
+ getSession() {
1171
+ return this.session;
1172
+ }
1173
+ /** Is a camera mounted and ready right now? Only the host knows. */
1174
+ setCameraReady(ready) {
1175
+ this.cameraReady = ready;
1176
+ this.notify();
1177
+ }
1178
+ isCameraReady() {
1179
+ return this.cameraReady && !this.session?.suspended;
1180
+ }
1181
+ // ── the ring ──
1182
+ profile() {
1183
+ const intent = this.session?.intent || "navigation";
1184
+ return PROFILES[intent];
1185
+ }
1186
+ /**
1187
+ * Feed from the native vision session (VisionSession, iOS) — the FAST frame
1188
+ * source that replaces the takePictureAsync loop. The frame is IN-MEMORY: the
1189
+ * native ring holds the pixels keyed by `id`, and native.vision resolves that
1190
+ * id inside the trusted layer. No `uri` crosses here; the quality is a REAL
1191
+ * measurement (not `unknown`), and `expiresAt` is the ring's real eviction time.
1192
+ * The public VisualFrameRef strip to id-only is owned by the device-state session.
1193
+ */
1194
+ pushFrameMeta(meta) {
1195
+ let heading = null;
1196
+ try {
1197
+ heading = this.context?.heading() ?? null;
1198
+ } catch {
1199
+ }
1200
+ this.frames.push({
1201
+ // In-memory: no file. native.vision resolves the id inside the native ring.
1202
+ handle: { id: meta.id, mimeType: "image/none", expiresAt: meta.expiresAt ?? null },
1203
+ timestamp: meta.timestamp,
1204
+ orientation: { headingDeg: heading?.headingDeg ?? null, pitchDeg: null, rollDeg: null },
1205
+ quality: meta.quality ?? { blur: "unknown", exposure: "unknown", obstructed: null }
1206
+ });
1207
+ this.evict();
1208
+ this.notify();
1209
+ }
1210
+ setRetentionMs(ms) {
1211
+ this._retentionMs = ms && ms > 0 ? ms : null;
1212
+ }
1213
+ retentionMs() {
1214
+ return this._retentionMs;
1215
+ }
1216
+ /**
1217
+ * How far back the timeline ACTUALLY reaches: `min(intent window, native
1218
+ * retention)`.
1219
+ *
1220
+ * The intent asks for a window; native decides what it can afford to keep.
1221
+ * Full-res luma at ~24fps is tens of megabytes for a few seconds, so the ring
1222
+ * is memory-bounded and shrinks under pressure — `retentionMs` is native
1223
+ * publishing what it is really holding.
1224
+ *
1225
+ * EVERY FRAME IN THE TIMELINE CAN BE ANALYSED. That is the one sentence a
1226
+ * plugin author has to hold, and this clamp is what makes it true. The
1227
+ * alternative — listing frames past retention marked "not analysable" — was
1228
+ * considered and rejected: a caller would have to check a flag before every
1229
+ * use, and forgetting throws mid-guidance on a frame the model can see
1230
+ * listed. Present-but-unusable is the same trap as an empty rowset reading
1231
+ * as "no rows matched".
1232
+ *
1233
+ * If per-frame history beyond retention is ever needed, the shape is an
1234
+ * AGGREGATE on the snapshot ("the view has been unusable for N ms"), never a
1235
+ * per-entry flag.
1236
+ */
1237
+ effectiveWindowMs() {
1238
+ const intent = this.profile().windowMs;
1239
+ const retention = this._retentionMs;
1240
+ return retention === null ? intent : Math.min(intent, retention);
1241
+ }
1242
+ /**
1243
+ * Time-based, not count-based: the design specifies a rolling WINDOW.
1244
+ *
1245
+ * Evicts on the EFFECTIVE window, so a frame whose pixels native has already
1246
+ * dropped leaves the timeline at the same moment. Without this, `frameById`
1247
+ * would start returning null for frames still being listed.
1248
+ */
1249
+ evict() {
1250
+ const cutoff = Date.now() - this.effectiveWindowMs();
1251
+ this.frames = this.frames.filter((f) => f.timestamp >= cutoff);
1252
+ }
1253
+ timeline() {
1254
+ this.evict();
1255
+ return { windowMs: this.effectiveWindowMs(), frames: [...this.frames] };
1256
+ }
1257
+ latest() {
1258
+ this.evict();
1259
+ return this.frames.length ? this.frames[this.frames.length - 1] : null;
1260
+ }
1261
+ /**
1262
+ * Resolve a frame handle id (from a VisualFrameRef) back to the live frame so
1263
+ * the mechanical layer — native.vision — can read its on-device file. The
1264
+ * whole contract of that layer is that it takes an id, never pixels; this is
1265
+ * where an id becomes a `handle.uri` it can open. Returns null when the id is
1266
+ * unknown OR its frame has aged out of the rolling window (the cache file may
1267
+ * also be gone by then) — a real "I can't look at that frame" answer, which
1268
+ * the caller must treat as distinct from "I looked and found nothing".
1269
+ */
1270
+ frameById(id) {
1271
+ this.evict();
1272
+ return this.frames.find((f) => f.handle.id === id) ?? null;
1273
+ }
1274
+ /**
1275
+ * Record a MEASURED quality back onto a frame (native.vision.assessQuality).
1276
+ * Frames are minted with `quality: unknown` because nothing looks at the
1277
+ * pixels at capture time; this is how a real measurement replaces that so a
1278
+ * later read of VisualFrameRef.quality reflects it. No-op if the frame has
1279
+ * aged out — we never resurrect an evicted frame just to label it.
1280
+ */
1281
+ setFrameQuality(id, quality) {
1282
+ const f = this.frames.find((fr) => fr.handle.id === id);
1283
+ if (!f) return;
1284
+ f.quality = quality;
1285
+ this.notify();
1286
+ }
1287
+ // ── change notification ──
1288
+ subscribe(fn) {
1289
+ this.listeners.add(fn);
1290
+ return () => this.listeners.delete(fn);
1291
+ }
1292
+ notify() {
1293
+ for (const fn of this.listeners) {
1294
+ try {
1295
+ fn();
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ }
1300
+ };
1301
+ var visualEngine = new VisualEngine();
1302
+ function frameQuality(frame) {
1303
+ if (!frame) return { freshness: { state: "unknown", observedAt: null, ageMs: null } };
1304
+ const ageMs = Date.now() - frame.timestamp;
1305
+ return {
1306
+ freshness: {
1307
+ // Tighter than the other domains on purpose: a two-second-old frame of a
1308
+ // moving scene is not "fresh" in any useful sense.
1309
+ state: ageMs < 2e3 ? "fresh" : ageMs < 1e4 ? "aging" : "stale",
1310
+ observedAt: frame.timestamp,
1311
+ ageMs
1312
+ },
1313
+ confidence: { level: "unknown" }
1314
+ };
1315
+ }
1316
+
1317
+ // src/device-state/domains/visual.ts
1318
+ function cameraState() {
1319
+ const session = visualEngine.getSession();
1320
+ return {
1321
+ // `available` = the app could use a camera at all. `active` = one is
1322
+ // running RIGHT NOW. They differ exactly when it matters: a pocketed
1323
+ // phone is available but not active, and answering `true` to both
1324
+ // would let a guide speak about a scene nothing is watching.
1325
+ available: !!session,
1326
+ active: visualEngine.isCameraReady(),
1327
+ facing: session ? "back" : "unknown"
1328
+ };
1329
+ }
1330
+ function createVisualDomain() {
1331
+ return {
1332
+ async getSnapshot() {
1333
+ const latest2 = visualEngine.latest();
1334
+ const timeline = visualEngine.timeline();
1335
+ return {
1336
+ camera: cameraState(),
1337
+ // null, not a stale frame dressed as current. A caller with no usable
1338
+ // frame must see nothing rather than something old.
1339
+ latest: latest2,
1340
+ recentTimeline: timeline,
1341
+ quality: frameQuality(latest2)
1342
+ };
1343
+ },
1344
+ async start(options) {
1345
+ const s = visualEngine.startSession(options?.intent || "navigation");
1346
+ visualEngine.setContextProvider({ heading: currentHeading });
1347
+ void startHeading("visual");
1348
+ return { id: s.id, intent: s.intent, startedAt: s.startedAt };
1349
+ },
1350
+ async stop() {
1351
+ visualEngine.stopSession();
1352
+ visualEngine.setContextProvider(null);
1353
+ stopHeading("visual");
1354
+ },
1355
+ /**
1356
+ * A deliberate one-shot. Distinct from getSnapshot because it is an
1357
+ * OPERATION: it waits for a frame rather than serving the newest one.
1358
+ *
1359
+ * Throws when it cannot, rather than returning an empty result — the
1360
+ * design is explicit that capture is an operation and reads are state.
1361
+ */
1362
+ async capture() {
1363
+ if (!visualEngine.getSession()) {
1364
+ throw unsupported("visual.capture without an open session \u2014 call start({ intent }) first");
1365
+ }
1366
+ if (!visualEngine.isCameraReady()) {
1367
+ const err2 = new Error(
1368
+ "The camera is not running right now \u2014 the app may be in the background."
1369
+ );
1370
+ err2.code = "TEMPORARILY_UNAVAILABLE";
1371
+ throw err2;
1372
+ }
1373
+ const before = visualEngine.latest()?.timestamp ?? 0;
1374
+ const deadline = Date.now() + 4e3;
1375
+ while (Date.now() < deadline) {
1376
+ const f = visualEngine.latest();
1377
+ if (f && f.timestamp > before) return { frame: f };
1378
+ await new Promise((r) => setTimeout(r, 100));
1379
+ }
1380
+ const err = new Error("No frame arrived within 4s.");
1381
+ err.code = "TIMEOUT";
1382
+ throw err;
1383
+ },
1384
+ /**
1385
+ * Fires when the CAMERA block changes — a session opening or closing, the
1386
+ * camera coming up or going down. The engine notifies on every frame too,
1387
+ * and forwarding that made this a raw frame stream, which the freeze's core
1388
+ * rule 4 forbids. A frame arriving is what `getSnapshot()` is for.
1389
+ */
1390
+ subscribe(listener) {
1391
+ let last = JSON.stringify(cameraState());
1392
+ const off = visualEngine.subscribe(() => {
1393
+ const camera = cameraState();
1394
+ const key = JSON.stringify(camera);
1395
+ if (key === last) return;
1396
+ last = key;
1397
+ listener(stateEvent("visual", "visualStateChanged", { camera }));
1398
+ });
1399
+ return { unsubscribe: off };
1400
+ }
1401
+ };
1402
+ }
1403
+
1404
+ // src/device-state/activityEngine.ts
1405
+ var sensors = null;
1406
+ var sensorsMissing = false;
1407
+ function loadSensors() {
1408
+ if (sensors || sensorsMissing) return sensors;
1409
+ try {
1410
+ sensors = require("expo-sensors");
1411
+ } catch {
1412
+ sensorsMissing = true;
1413
+ sensors = null;
1414
+ }
1415
+ return sensors;
1416
+ }
1417
+ var ACCEL_INTERVAL_MS = 200;
1418
+ var MOTION_WINDOW_MS = 3e3;
1419
+ var STEP_WINDOW_MS = 2e4;
1420
+ var GRAVITY_ALPHA = 0.1;
1421
+ var ActivityEngine = class {
1422
+ constructor() {
1423
+ this.accelSub = null;
1424
+ this.stepSub = null;
1425
+ this.magnitudes = [];
1426
+ this.gravity = null;
1427
+ this.steps = [];
1428
+ this.sessionId = null;
1429
+ this.startedAt = 0;
1430
+ /** When the CURRENT reading last changed — feeds `human.since`. */
1431
+ this.pedometerAvailable = null;
1432
+ // ── transitions ──
1433
+ //
1434
+ // Tracked on a timer inside the engine rather than computed when someone
1435
+ // reads, because a transition is an EVENT: "you started walking" happens
1436
+ // between two reads, and a domain that only classifies on demand reports the
1437
+ // state and loses the moment it changed. `since` has the same problem.
1438
+ this.current = "unknown";
1439
+ this.since = null;
1440
+ this.transitions = [];
1441
+ this.ticker = null;
1442
+ }
1443
+ isRunning() {
1444
+ return this.sessionId !== null;
1445
+ }
1446
+ session() {
1447
+ return this.sessionId ? { id: this.sessionId, startedAt: this.startedAt } : null;
1448
+ }
1449
+ /**
1450
+ * @param classify how to name the current activity — injected so the engine
1451
+ * keeps no opinion of its own and the classifier stays pure and testable.
1452
+ */
1453
+ async start(classify) {
1454
+ if (this.sessionId) return this.session();
1455
+ this.sessionId = `activity_${Date.now()}`;
1456
+ this.startedAt = Date.now();
1457
+ const s = loadSensors();
1458
+ if (!s) {
1459
+ this.pedometerAvailable = false;
1460
+ this.startTicking(classify);
1461
+ return this.session();
1462
+ }
1463
+ const { Accelerometer, Pedometer } = s;
1464
+ Accelerometer.setUpdateInterval(ACCEL_INTERVAL_MS);
1465
+ this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
1466
+ const now = Date.now();
1467
+ this.magnitudes.push({ m: Math.sqrt(x * x + y * y + z * z), t: now });
1468
+ const cutoff = now - MOTION_WINDOW_MS;
1469
+ if (this.magnitudes.length > 8 && this.magnitudes[0].t < cutoff) {
1470
+ this.magnitudes = this.magnitudes.filter((s2) => s2.t >= cutoff);
1471
+ }
1472
+ this.gravity = this.gravity ? {
1473
+ x: this.gravity.x + GRAVITY_ALPHA * (x - this.gravity.x),
1474
+ y: this.gravity.y + GRAVITY_ALPHA * (y - this.gravity.y),
1475
+ z: this.gravity.z + GRAVITY_ALPHA * (z - this.gravity.z)
1476
+ } : { x, y, z };
1477
+ });
1478
+ try {
1479
+ this.pedometerAvailable = await Pedometer.isAvailableAsync();
1480
+ } catch {
1481
+ this.pedometerAvailable = false;
1482
+ }
1483
+ if (this.pedometerAvailable) {
1484
+ try {
1485
+ this.stepSub = Pedometer.watchStepCount(({ steps }) => {
1486
+ this.steps.push({ steps, t: Date.now() });
1487
+ const cutoff = Date.now() - STEP_WINDOW_MS;
1488
+ if (this.steps.length > 2 && this.steps[0].t < cutoff) {
1489
+ this.steps = this.steps.filter((s2) => s2.t >= cutoff);
1490
+ }
1491
+ });
1492
+ } catch {
1493
+ this.stepSub = null;
1494
+ this.pedometerAvailable = false;
1495
+ }
1496
+ }
1497
+ this.startTicking(classify);
1498
+ return this.session();
1499
+ }
1500
+ stop() {
1501
+ this.accelSub?.remove();
1502
+ this.stepSub?.remove();
1503
+ this.accelSub = null;
1504
+ this.stepSub = null;
1505
+ this.sessionId = null;
1506
+ this.stopTicking();
1507
+ this.magnitudes = [];
1508
+ this.steps = [];
1509
+ this.gravity = null;
1510
+ }
1511
+ /** Variance of accelerometer magnitude over the motion window, in g². */
1512
+ accelVariance() {
1513
+ const cutoff = Date.now() - MOTION_WINDOW_MS;
1514
+ const window = this.magnitudes.filter((s) => s.t >= cutoff);
1515
+ if (window.length < 5) return null;
1516
+ const mean = window.reduce((a, s) => a + s.m, 0) / window.length;
1517
+ return window.reduce((a, s) => a + (s.m - mean) ** 2, 0) / window.length;
1518
+ }
1519
+ currentGravity() {
1520
+ return this.gravity;
1521
+ }
1522
+ /**
1523
+ * Steps per minute over the step window.
1524
+ *
1525
+ * Computed from the DIFFERENCE between the first and last cumulative counts,
1526
+ * so it does not matter whether the platform counts from subscription start
1527
+ * or from midnight.
1528
+ *
1529
+ * Returns null — not 0 — when the window is too short to divide by. Zero
1530
+ * steps per minute is a real claim ("standing still") and must only be made
1531
+ * once there is a window long enough to support it.
1532
+ */
1533
+ cadenceSpm() {
1534
+ const cutoff = Date.now() - STEP_WINDOW_MS;
1535
+ const window = this.steps.filter((s) => s.t >= cutoff);
1536
+ if (window.length < 2) {
1537
+ if (this.pedometerAvailable && Date.now() - this.startedAt > STEP_WINDOW_MS) return 0;
1538
+ return null;
1539
+ }
1540
+ const first = window[0], last = window[window.length - 1];
1541
+ const minutes = (last.t - first.t) / 6e4;
1542
+ if (minutes <= 0) return null;
1543
+ return (last.steps - first.steps) / minutes;
1544
+ }
1545
+ /** Whether a pedometer exists and is permitted. Null before `start()`. */
1546
+ hasPedometer() {
1547
+ return this.pedometerAvailable;
1548
+ }
1549
+ /** When the newest sample arrived — what freshness is actually about. */
1550
+ observedAt() {
1551
+ const ts = [
1552
+ this.magnitudes[this.magnitudes.length - 1]?.t,
1553
+ this.steps[this.steps.length - 1]?.t
1554
+ ].filter((t) => typeof t === "number");
1555
+ return ts.length ? Math.max(...ts) : null;
1556
+ }
1557
+ /** Feed the engine its own classification once a second. */
1558
+ startTicking(classify) {
1559
+ this.ticker = setInterval(() => {
1560
+ const next = classify();
1561
+ if (next === this.current) return;
1562
+ this.current = next;
1563
+ this.since = Date.now();
1564
+ this.transitions = [...this.transitions, { name: next, timestamp: this.since }].slice(-10);
1565
+ }, 1e3);
1566
+ }
1567
+ stopTicking() {
1568
+ if (this.ticker) clearInterval(this.ticker);
1569
+ this.ticker = null;
1570
+ this.current = "unknown";
1571
+ this.since = null;
1572
+ this.transitions = [];
1573
+ }
1574
+ /** When the current activity began, or null if it has not been established. */
1575
+ currentSince() {
1576
+ return this.since;
1577
+ }
1578
+ /** Newest last, bounded. Empty is a real answer: nothing has changed yet. */
1579
+ recentTransitions() {
1580
+ return [...this.transitions];
1581
+ }
1582
+ };
1583
+ var activityEngine = new ActivityEngine();
1584
+
1585
+ // src/device-state/activityClassifier.ts
1586
+ var RUN_SPM = 140;
1587
+ var WALK_SPM = 20;
1588
+ var STILL_VAR = 2e-3;
1589
+ var ACTIVE_VAR = 0.02;
1590
+ var VEHICLE_MPS = 3;
1591
+ var AXIS_DOMINANT = 0.75;
1592
+ function classifyActivity(input) {
1593
+ return { human: classifyHuman(input), pose: classifyPose(input) };
1594
+ }
1595
+ function classifyHuman({ cadenceSpm, accelVariance, speedMps }) {
1596
+ if (cadenceSpm !== null && cadenceSpm >= RUN_SPM) {
1597
+ return { state: "running", confidence: { level: "high" } };
1598
+ }
1599
+ if (cadenceSpm !== null && cadenceSpm >= WALK_SPM) {
1600
+ return { state: "walking", confidence: { level: "high" } };
1601
+ }
1602
+ if (speedMps !== null && speedMps >= VEHICLE_MPS) {
1603
+ return { state: "unknown", confidence: { level: "low" } };
1604
+ }
1605
+ if (accelVariance !== null && accelVariance < STILL_VAR && (cadenceSpm === null || cadenceSpm < WALK_SPM)) {
1606
+ return { state: "stationary", confidence: { level: "medium" } };
1607
+ }
1608
+ return { state: "unknown", confidence: { level: "unknown" } };
1609
+ }
1610
+ function classifyPose({ gravity, accelVariance }) {
1611
+ if (!gravity) {
1612
+ return { placement: "unknown", orientation: "unknown", confidence: { level: "unknown" } };
1613
+ }
1614
+ const { x, y, z } = gravity;
1615
+ const orientation = Math.abs(z) >= AXIS_DOMINANT ? "flat" : Math.abs(y) >= AXIS_DOMINANT ? "portrait" : Math.abs(x) >= AXIS_DOMINANT ? "landscape" : "unknown";
1616
+ if (orientation === "flat" && accelVariance !== null && accelVariance < STILL_VAR) {
1617
+ return { placement: "table", orientation, confidence: { level: "high" } };
1618
+ }
1619
+ if (orientation !== "flat" && accelVariance !== null && accelVariance >= ACTIVE_VAR) {
1620
+ return { placement: "handheld", orientation, confidence: { level: "medium" } };
1621
+ }
1622
+ return { placement: "unknown", orientation, confidence: { level: "unknown" } };
1623
+ }
1624
+
1625
+ // src/device-state/domains/activity.ts
1626
+ function createActivityDomain(speedRef) {
1627
+ const SPEED_MAX_AGE_MS = 15e3;
1628
+ const input = () => ({
1629
+ cadenceSpm: activityEngine.cadenceSpm(),
1630
+ accelVariance: activityEngine.accelVariance(),
1631
+ gravity: activityEngine.currentGravity(),
1632
+ speedMps: speedRef.at && Date.now() - speedRef.at < SPEED_MAX_AGE_MS ? speedRef.mps : null
1633
+ });
1634
+ return {
1635
+ async getSnapshot() {
1636
+ if (!activityEngine.isRunning()) {
1637
+ return {
1638
+ human: { state: "unknown", since: null, confidence: { level: "unknown" } },
1639
+ devicePose: { placement: "unknown", orientation: "unknown", confidence: { level: "unknown" } },
1640
+ recentTransitions: [],
1641
+ quality: qualityUnknown()
1642
+ };
1643
+ }
1644
+ const verdict = classifyActivity(input());
1645
+ const observedAt = activityEngine.observedAt();
1646
+ const transitions = activityEngine.recentTransitions();
1647
+ return {
1648
+ human: {
1649
+ state: verdict.human.state,
1650
+ since: activityEngine.currentSince(),
1651
+ confidence: verdict.human.confidence
1652
+ },
1653
+ devicePose: {
1654
+ placement: verdict.pose.placement,
1655
+ orientation: verdict.pose.orientation,
1656
+ confidence: verdict.pose.confidence
1657
+ },
1658
+ transition: {
1659
+ current: verdict.human.state,
1660
+ // A hint, and we have no basis for one. The freeze calls `likelyNext`
1661
+ // advisory; advisory is not licence to invent it.
1662
+ likelyNext: null
1663
+ },
1664
+ recentTransitions: transitions,
1665
+ quality: observedAt === null ? qualityUnknown() : qualityFrom(observedAt, verdict.human.confidence)
1666
+ };
1667
+ },
1668
+ /**
1669
+ * Start sampling. The intent is recorded but does not yet change strategy —
1670
+ * there is no native power-tuning layer for it to reach.
1671
+ */
1672
+ async start(options) {
1673
+ const s = await activityEngine.start(() => classifyActivity(input()).human.state);
1674
+ return { id: s.id, intent: options.intent, startedAt: s.startedAt };
1675
+ },
1676
+ async stop() {
1677
+ activityEngine.stop();
1678
+ },
1679
+ // No activity event vocabulary is frozen — the design notes there is no
1680
+ // "Initial Activity Events" list at all — so naming events here would put
1681
+ // words in plugins that the freeze may contradict.
1682
+ subscribe() {
1683
+ throw unsupported("activity.subscribe");
1684
+ }
1685
+ };
1686
+ }
1687
+
1688
+ // src/device-state/index.ts
1689
+ function createDeviceState(collectors = {}) {
1690
+ const speedRef = { mps: null, at: 0 };
1691
+ return {
1692
+ deviceContext: createDeviceContextDomain(),
1693
+ connectivity: createConnectivityDomain(collectors),
1694
+ capability: createCapabilityDomain(collectors),
1695
+ interaction: createInteractionDomain(),
1696
+ spatial: createSpatialDomain(collectors, speedRef),
1697
+ visual: createVisualDomain(),
1698
+ activity: createActivityDomain(speedRef)
1699
+ };
1700
+ }
1701
+
1702
+ // src/camera/CameraTaskHost.tsx
1703
+ var import_react = require("react");
1704
+ var import_react_native2 = require("react-native");
1705
+ var import_expo_camera = require("expo-camera");
1706
+
1707
+ // src/camera/cameraTasks.ts
1708
+ function cameraError(code, message) {
1709
+ const err = new Error(message);
1710
+ err.code = code;
1711
+ return err;
1712
+ }
1713
+ var CameraTaskBroker = class {
1714
+ constructor() {
1715
+ this.task = null;
1716
+ this.listeners = /* @__PURE__ */ new Set();
1717
+ /** Set by the host while a camera is mounted and ready. */
1718
+ this.hostMounted = false;
1719
+ }
1720
+ current() {
1721
+ return this.task;
1722
+ }
1723
+ subscribe(fn) {
1724
+ this.listeners.add(fn);
1725
+ return () => this.listeners.delete(fn);
1726
+ }
1727
+ /**
1728
+ * Declare that a `CameraTaskHost` exists.
1729
+ *
1730
+ * Without it nothing will ever settle the promise, so a request must fail
1731
+ * IMMEDIATELY and say why. The alternative is a plugin awaiting forever,
1732
+ * which is the worst failure of the set: no error, no timeout, no camera,
1733
+ * and a conversation that simply stops.
1734
+ */
1735
+ setHostMounted(mounted) {
1736
+ this.hostMounted = mounted;
1737
+ if (!mounted) this.fail(cameraError("UNAVAILABLE", "The camera surface was dismissed before the task completed."));
1738
+ this.notify();
1739
+ }
1740
+ isHostMounted() {
1741
+ return this.hostMounted;
1742
+ }
1743
+ /** Enqueue and hand back the promise the plugin awaits. */
1744
+ run(kind, opts) {
1745
+ if (!this.hostMounted) {
1746
+ return Promise.reject(cameraError(
1747
+ "UNAVAILABLE",
1748
+ "No camera surface is mounted in this app, so the camera cannot be shown. The host must render <CameraTaskHost /> once, near the app root."
1749
+ ));
1750
+ }
1751
+ if (this.task) {
1752
+ return Promise.reject(cameraError(
1753
+ "BUSY",
1754
+ `The camera is already in use (${this.task.kind}). Only one camera task can run at a time.`
1755
+ ));
1756
+ }
1757
+ return new Promise((resolve, reject) => {
1758
+ this.task = { kind, opts, resolve, reject };
1759
+ this.notify();
1760
+ });
1761
+ }
1762
+ /** Settle the running task successfully and clear it. */
1763
+ complete(value) {
1764
+ const t = this.task;
1765
+ if (!t) return;
1766
+ this.task = null;
1767
+ this.notify();
1768
+ t.resolve(value);
1769
+ }
1770
+ /** Settle the running task with a failure and clear it. */
1771
+ fail(err) {
1772
+ const t = this.task;
1773
+ if (!t) return;
1774
+ this.task = null;
1775
+ this.notify();
1776
+ t.reject(err);
1777
+ }
1778
+ /**
1779
+ * The user backed out.
1780
+ *
1781
+ * A distinct code, because "the user chose not to" is not a malfunction and
1782
+ * a guide should say something different about it than about a broken lens.
1783
+ */
1784
+ cancel() {
1785
+ this.fail(cameraError("CANCELLED", "Cancelled \u2014 the camera was dismissed before anything was captured."));
1786
+ }
1787
+ notify() {
1788
+ for (const fn of this.listeners) {
1789
+ try {
1790
+ fn();
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ }
1795
+ };
1796
+ var cameraTasks = new CameraTaskBroker();
1797
+
1798
+ // src/camera/CameraTaskHost.tsx
1799
+ var import_jsx_runtime = require("react/jsx-runtime");
1800
+ var styles = import_react_native2.StyleSheet.create({
1801
+ overlay: { ...import_react_native2.StyleSheet.absoluteFillObject, backgroundColor: "#000", zIndex: 9999 },
1802
+ hud: { flex: 1, justifyContent: "space-between", paddingVertical: 64, alignItems: "center" },
1803
+ hint: { color: "#fff", fontSize: 16, textAlign: "center", paddingHorizontal: 24 },
1804
+ cancel: { paddingHorizontal: 28, paddingVertical: 14, borderRadius: 24, backgroundColor: "rgba(255,255,255,0.18)" },
1805
+ cancelText: { color: "#fff", fontSize: 16, fontWeight: "600" }
1806
+ });
1807
+
1808
+ // src/index.ts
1809
+ function setupExpoProviders(opts) {
1810
+ (0, import_plugin_runtime2.setHapticsProvider)(expoHaptics);
1811
+ if (opts?.deviceProvider) (0, import_plugin_runtime2.setPluginNativeProvider)(opts.deviceProvider);
1812
+ (0, import_plugin_runtime2.setBundleStorage)(new ExpoBundleStorage({ indexKey: opts?.bundleIndexKey }));
1813
+ (0, import_ui.setClipboardProvider)({
1814
+ setString: (text2) => ExpoClipboard.setStringAsync(text2).then(() => {
1815
+ })
1816
+ });
1817
+ (0, import_plugin_runtime2.registerBundleModules)({
1818
+ "expo-web-browser": ExpoWebBrowser,
1819
+ "expo-linking": ExpoLinking,
1820
+ ...opts?.bundleModules || {}
1821
+ });
1822
+ (0, import_core.setAcsCacheStorage)({
1823
+ getItem: (k) => import_async_storage3.default.getItem(k),
1824
+ setItem: (k, v) => import_async_storage3.default.setItem(k, v)
1825
+ });
1826
+ return { attachmentCache: new ExpoAttachmentCache({ indexKey: opts?.attachmentIndexKey }) };
1827
+ }
1828
+
1829
+ // src/device/createDeviceProvider.ts
1830
+ var Haptics2 = __toESM(require("expo-haptics"));
1831
+ var ExpoLocation4 = __toESM(require("expo-location"));
1832
+ var ImagePicker = __toESM(require("expo-image-picker"));
1833
+ var Notifications = __toESM(require("expo-notifications"));
1834
+ var DocumentPicker = __toESM(require("expo-document-picker"));
1835
+ var FS = __toESM(require("expo-file-system"));
1836
+ var SQLite = __toESM(require("expo-sqlite"));
1837
+ var import_async_storage6 = __toESM(require("@react-native-async-storage/async-storage"));
1838
+
1839
+ // src/vision/index.ts
1840
+ var import_expo_modules_core = require("expo-modules-core");
1841
+ var import_expo = require("expo");
1842
+ var CameraPreview = (() => {
1843
+ try {
1844
+ return (0, import_expo.requireNativeView)("AdaVision");
1845
+ } catch {
1846
+ return null;
1847
+ }
1848
+ })();
1849
+ var native = (0, import_expo_modules_core.requireOptionalNativeModule)("AdaVision");
1850
+ function isSupported() {
1851
+ try {
1852
+ return native?.isSupported() ?? false;
1853
+ } catch {
1854
+ return false;
1855
+ }
1856
+ }
1857
+ function qualityById(id) {
1858
+ return native?.qualityById?.(id) ?? null;
1859
+ }
1860
+ async function readText(id, languages) {
1861
+ if (!native) throw new Error("ada-vision native module not present in this build");
1862
+ return native.readText(id, languages ?? null);
1863
+ }
1864
+ async function describe(id, maxLabels = 5) {
1865
+ if (!native) throw new Error("ada-vision native module not present in this build");
1866
+ return native.describe(id, maxLabels);
1867
+ }
1868
+ async function detectObjects(id, maxItems = 10, minConfidence = 0.25) {
1869
+ if (!native) throw new Error("ada-vision native module not present in this build");
1870
+ return native.detectObjects(id, maxItems, minConfidence);
1871
+ }
1872
+
1873
+ // src/maps/index.ts
1874
+ var import_react_native3 = require("react-native");
1875
+ var import_expo_modules_core2 = require("expo-modules-core");
1876
+ var ExpoLocation2 = __toESM(require("expo-location"));
1877
+
1878
+ // src/args.ts
1879
+ var shown = (v) => {
1880
+ let s;
1881
+ try {
1882
+ s = JSON.stringify(v, (_k, x) => typeof x === "number" && !Number.isFinite(x) ? `${x}` : x)?.replace(/"\\u0001(NaN|-?Infinity)\\u0001"/g, "$1");
1883
+ } catch {
1884
+ }
1885
+ s = s ?? String(v);
1886
+ return s.length > 80 ? `${s.slice(0, 77)}...` : s;
1887
+ };
1888
+ function refuseArgument(capability, verb, problem, nothing) {
1889
+ const e = new Error(`${capability}.${verb}: ${problem}; ${nothing}.`);
1890
+ e.code = "INVALID_ARGUMENT";
1891
+ return e;
1892
+ }
1893
+ function countArg(capability, verb, name, value, def, max, nothing) {
1894
+ const v = value ?? def;
1895
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 1) {
1896
+ throw refuseArgument(capability, verb, `${name} must be a number from 1 (at most ${max} are used), not ${shown(value)}`, nothing);
1897
+ }
1898
+ return Math.min(max, Math.floor(v));
1899
+ }
1900
+ function fractionArg(capability, verb, name, value, def, nothing) {
1901
+ const v = value ?? def;
1902
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
1903
+ throw refuseArgument(capability, verb, `${name} must be a number from 0 to 1, not ${shown(value)}`, nothing);
1904
+ }
1905
+ return v;
1906
+ }
1907
+
1908
+ // src/maps/normalize.ts
1909
+ var SEARCH_RADIUS_DEFAULT_M = 3e3;
1910
+ var SEARCH_RADIUS_MAX_M = 1e5;
1911
+ var SEARCH_LIMIT_DEFAULT = 8;
1912
+ var SEARCH_LIMIT_MAX = 25;
1913
+ var text = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
1914
+ var refuseArgument2 = (verb, problem, nothing) => refuseArgument("maps", verb, problem, nothing);
1915
+ function coordinateArg(verb, name, value, nothing) {
1916
+ const c = value;
1917
+ const latitude = c?.latitude, longitude = c?.longitude;
1918
+ if (typeof latitude === "number" && Number.isFinite(latitude) && Math.abs(latitude) <= 90 && typeof longitude === "number" && Number.isFinite(longitude) && Math.abs(longitude) <= 180) {
1919
+ return { latitude, longitude };
1920
+ }
1921
+ throw refuseArgument2(
1922
+ verb,
1923
+ `${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,
1924
+ nothing
1925
+ );
1926
+ }
1927
+ function searchArgs(query, opts) {
1928
+ const nothing = "nothing was searched";
1929
+ if (typeof query !== "string" || query.trim() === "") {
1930
+ throw refuseArgument2("search", `query must be a non-empty string, not ${shown(query)}`, nothing);
1931
+ }
1932
+ const near = opts?.near == null ? null : coordinateArg("search", "near", opts.near, nothing);
1933
+ const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;
1934
+ if (typeof radiusM !== "number" || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {
1935
+ throw refuseArgument2(
1936
+ "search",
1937
+ `radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`,
1938
+ nothing
1939
+ );
1940
+ }
1941
+ const limit = countArg("maps", "search", "limit", opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);
1942
+ return { query, near, radiusM, limit };
1943
+ }
1944
+ var point = (c) => ({
1945
+ latitude: Number(c.latitude),
1946
+ longitude: Number(c.longitude)
1947
+ });
1948
+ function toMapsAddress(raw) {
1949
+ const r = raw ?? {};
1950
+ return {
1951
+ name: text(r.name),
1952
+ street: text(r.street),
1953
+ streetNumber: text(r.streetNumber),
1954
+ district: text(r.district),
1955
+ city: text(r.city),
1956
+ region: text(r.region),
1957
+ postalCode: text(r.postalCode),
1958
+ country: text(r.country),
1959
+ isoCountryCode: text(r.isoCountryCode)
1960
+ };
1961
+ }
1962
+ function toMapsCategory(raw) {
1963
+ const c = text(raw);
1964
+ return c ? c.replace(/^MKPOICategory/, "") || null : null;
1965
+ }
1966
+ function toMapsPlaces(raw, near) {
1967
+ return (raw ?? []).map((r) => {
1968
+ const coordinate = point(r);
1969
+ return {
1970
+ name: text(r.name),
1971
+ coordinate,
1972
+ address: toMapsAddress(r.address),
1973
+ category: toMapsCategory(r.category),
1974
+ distanceM: near ? distanceOf([near, coordinate]) : null
1975
+ };
1976
+ });
1977
+ }
1978
+ function toReverseGeocodedPlace(at, address) {
1979
+ return {
1980
+ name: address.name,
1981
+ coordinate: point(at),
1982
+ address,
1983
+ category: null,
1984
+ distanceM: null
1985
+ };
1986
+ }
1987
+ function toMapsRoute(raw) {
1988
+ const steps = (raw.steps ?? []).map((s) => ({
1989
+ instructions: typeof s.instructions === "string" ? s.instructions : "",
1990
+ distanceM: Number(s.distance),
1991
+ notice: text(s.notice),
1992
+ polyline: (s.polyline ?? []).map(point)
1993
+ }));
1994
+ return {
1995
+ mode: "walking",
1996
+ distanceM: Number(raw.distance),
1997
+ expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1e3),
1998
+ steps,
1999
+ polyline: (raw.polyline ?? []).map(point)
2000
+ };
2001
+ }
2002
+ function mapsFailure(verb, err) {
2003
+ const nativeCode = String(err?.code ?? "");
2004
+ const detail = String(err?.message ?? err ?? "").trim();
2005
+ let code;
2006
+ let why;
2007
+ if (nativeCode === "THROTTLED") {
2008
+ code = "THROTTLED";
2009
+ why = "Apple is rate-limiting map requests from this app \u2014 wait before retrying";
2010
+ } else if (nativeCode === "OFFLINE") {
2011
+ code = "OFFLINE";
2012
+ why = "the map service could not be reached \u2014 the phone may be offline";
2013
+ } else if (nativeCode === "ERR_GEOCODING_NETWORK") {
2014
+ code = "OFFLINE";
2015
+ why = "the geocoder could not be reached \u2014 the phone may be offline, or Apple is rate-limiting requests (iOS reports both the same way)";
2016
+ } else {
2017
+ code = "UNAVAILABLE";
2018
+ why = "the map service failed";
2019
+ }
2020
+ const failure = new Error(
2021
+ `maps.${verb} could not look: ${why}. This is not "found nothing".` + (detail ? ` (${nativeCode ? nativeCode + ": " : ""}${detail})` : "")
2022
+ );
2023
+ failure.code = code;
2024
+ return failure;
2025
+ }
2026
+
2027
+ // src/maps/index.ts
2028
+ var native2 = (0, import_expo_modules_core2.requireOptionalNativeModule)("AdaMaps");
2029
+ function missingReason() {
2030
+ if (import_react_native3.Platform.OS !== "ios") return `iOS (Apple MapKit) \u2014 ${import_react_native3.Platform.OS} has no implementation yet`;
2031
+ if (!native2) return "the app build with the AdaMaps module (this build predates it \u2014 a native rebuild is needed)";
2032
+ return null;
2033
+ }
2034
+ function isSupported2() {
2035
+ try {
2036
+ return missingReason() === null && (native2?.isSupported() ?? false);
2037
+ } catch {
2038
+ return false;
2039
+ }
2040
+ }
2041
+ async function search(query, near, radiusM, limit) {
2042
+ if (!native2) throw new Error("ada-maps native module not present in this build");
2043
+ return near ? native2.search(query, near.latitude, near.longitude, true, radiusM, limit) : native2.search(query, 0, 0, false, radiusM, limit);
2044
+ }
2045
+ async function route(from, to) {
2046
+ if (!native2) throw new Error("ada-maps native module not present in this build");
2047
+ return native2.route(from.latitude, from.longitude, to.latitude, to.longitude);
2048
+ }
2049
+ async function reverseGeocode(at) {
2050
+ const [first] = await ExpoLocation2.reverseGeocodeAsync({ latitude: at.latitude, longitude: at.longitude });
2051
+ return first ? toMapsAddress(first) : null;
2052
+ }
2053
+
2054
+ // src/device-bridge/actions/messaging.ts
2055
+ var SMS = __toESM(require("expo-sms"));
2056
+
2057
+ // src/device-bridge/actions/calendar-actions.ts
2058
+ var ExpoCalendar = __toESM(require("expo-calendar"));
2059
+
2060
+ // src/device-bridge/actions/navigation.ts
2061
+ var Linking = __toESM(require("expo-linking"));
2062
+ var import_react_native4 = require("react-native");
2063
+
2064
+ // src/device-bridge/collectors/health.ts
2065
+ var import_react_native5 = require("react-native");
2066
+
2067
+ // src/device-bridge/background.ts
2068
+ var TaskManager = __toESM(require("expo-task-manager"));
2069
+ var BackgroundFetch = __toESM(require("expo-background-fetch"));
2070
+
2071
+ // src/device-bridge/collectors/calendar.ts
2072
+ var ExpoCalendar2 = __toESM(require("expo-calendar"));
2073
+
2074
+ // src/device-bridge/collectors/contacts.ts
2075
+ var ExpoContacts = __toESM(require("expo-contacts"));
2076
+ var import_async_storage4 = __toESM(require("@react-native-async-storage/async-storage"));
2077
+
2078
+ // src/device-bridge/collectors/location.ts
2079
+ var location_exports = {};
2080
+ __export(location_exports, {
2081
+ collectCurrent: () => collectCurrent,
2082
+ getPosition: () => getPosition,
2083
+ hasPermission: () => hasPermission,
2084
+ requestPermission: () => requestPermission,
2085
+ watchPosition: () => watchPosition
2086
+ });
2087
+ var ExpoLocation3 = __toESM(require("expo-location"));
2088
+ async function requestPermission() {
2089
+ const { status: fg } = await ExpoLocation3.requestForegroundPermissionsAsync();
2090
+ if (fg !== "granted") return false;
2091
+ try {
2092
+ await ExpoLocation3.requestBackgroundPermissionsAsync();
2093
+ } catch {
2094
+ }
2095
+ return fg === "granted";
2096
+ }
2097
+ async function hasPermission() {
2098
+ const { status } = await ExpoLocation3.getForegroundPermissionsAsync();
2099
+ return status === "granted";
2100
+ }
2101
+ async function collectCurrent() {
2102
+ const location = await ExpoLocation3.getCurrentPositionAsync({
2103
+ accuracy: ExpoLocation3.Accuracy.Balanced
2104
+ });
2105
+ let place = "Current Location";
2106
+ let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;
2107
+ let locationType = "unknown";
2108
+ try {
2109
+ const geocoded = await reverseGeocode({
2110
+ latitude: location.coords.latitude,
2111
+ longitude: location.coords.longitude
2112
+ });
2113
+ if (geocoded) {
2114
+ place = geocoded.name || geocoded.street || place;
2115
+ address = [
2116
+ geocoded.streetNumber,
2117
+ geocoded.street,
2118
+ geocoded.city,
2119
+ geocoded.region
2120
+ ].filter(Boolean).join(", ") || address;
2121
+ locationType = classifyLocation(geocoded);
2122
+ }
2123
+ } catch {
2124
+ }
2125
+ return {
2126
+ ok: true,
2127
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2128
+ location: {
2129
+ place,
2130
+ address,
2131
+ lat: location.coords.latitude,
2132
+ lng: location.coords.longitude,
2133
+ type: locationType
2134
+ }
2135
+ };
2136
+ }
2137
+ async function ensurePermission() {
2138
+ if (await hasPermission()) return;
2139
+ const granted = await requestPermission();
2140
+ if (!granted) {
2141
+ throw new Error(
2142
+ "Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services."
2143
+ );
2144
+ }
2145
+ }
2146
+ async function getPosition(opts = {}) {
2147
+ await ensurePermission();
2148
+ const fix = await ExpoLocation3.getCurrentPositionAsync({
2149
+ accuracy: opts.accuracy === "high" ? ExpoLocation3.Accuracy.High : ExpoLocation3.Accuracy.Balanced
2150
+ });
2151
+ return toPosition(fix);
2152
+ }
2153
+ function toPosition(fix) {
2154
+ return {
2155
+ latitude: fix.coords.latitude,
2156
+ longitude: fix.coords.longitude,
2157
+ altitude: fix.coords.altitude ?? void 0,
2158
+ accuracy: nonNegative(fix.coords.accuracy),
2159
+ timestamp: fix.timestamp ?? Date.now(),
2160
+ speed: nonNegative(fix.coords.speed),
2161
+ course: nonNegative(fix.coords.heading)
2162
+ };
2163
+ }
2164
+ function nonNegative(v) {
2165
+ return typeof v === "number" && v >= 0 ? v : void 0;
2166
+ }
2167
+ var WATCH_TIER = {
2168
+ navigation: ExpoLocation3.Accuracy.BestForNavigation,
2169
+ high: ExpoLocation3.Accuracy.High,
2170
+ balanced: ExpoLocation3.Accuracy.Balanced,
2171
+ low: ExpoLocation3.Accuracy.Low
2172
+ };
2173
+ function watchPosition(cb, opts) {
2174
+ let subscription = null;
2175
+ let cancelled = false;
2176
+ (async () => {
2177
+ try {
2178
+ await ensurePermission();
2179
+ if (cancelled) return;
2180
+ subscription = await ExpoLocation3.watchPositionAsync(
2181
+ { accuracy: WATCH_TIER[opts?.accuracy ?? "balanced"] },
2182
+ (fix) => cb(toPosition(fix))
2183
+ );
2184
+ if (cancelled) {
2185
+ subscription?.remove();
2186
+ subscription = null;
2187
+ }
2188
+ } catch (err) {
2189
+ console.warn("[location] watchPosition failed:", err?.message || String(err));
2190
+ }
2191
+ })();
2192
+ return () => {
2193
+ cancelled = true;
2194
+ subscription?.remove();
2195
+ subscription = null;
2196
+ };
2197
+ }
2198
+ function classifyLocation(geocoded) {
2199
+ const name = (geocoded.name || "").toLowerCase();
2200
+ const street = (geocoded.street || "").toLowerCase();
2201
+ if (name.includes("home") || name.includes("residence")) return "home";
2202
+ if (name.includes("office") || name.includes("workplace") || name.includes("tech park")) return "work";
2203
+ if (name.includes("school") || name.includes("elementary") || name.includes("university")) return "school";
2204
+ if (name.includes("gym") || name.includes("fitness")) return "fitness";
2205
+ if (name.includes("hospital") || name.includes("clinic") || name.includes("dental")) return "medical";
2206
+ return "other";
2207
+ }
2208
+
2209
+ // src/device-bridge/collectors/battery.ts
2210
+ var ExpoBattery2 = __toESM(require("expo-battery"));
2211
+
2212
+ // src/device-bridge/collectors/connectivity.ts
2213
+ var connectivity_exports = {};
2214
+ __export(connectivity_exports, {
2215
+ collectConnectivity: () => collectConnectivity
2216
+ });
2217
+ var ExpoNetwork = __toESM(require("expo-network"));
2218
+ async function collectConnectivity() {
2219
+ const networkState = await ExpoNetwork.getNetworkStateAsync();
2220
+ let type = "none";
2221
+ let wifiName;
2222
+ let cellularType;
2223
+ if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {
2224
+ type = "wifi";
2225
+ try {
2226
+ wifiName = await getWifiName();
2227
+ } catch {
2228
+ wifiName = void 0;
2229
+ }
2230
+ } else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {
2231
+ type = "cellular";
2232
+ cellularType = getCellularGeneration();
2233
+ }
2234
+ return {
2235
+ ok: true,
2236
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2237
+ connectivity: {
2238
+ type,
2239
+ is_connected: networkState.isConnected ?? false,
2240
+ ...wifiName && { wifi_name: wifiName },
2241
+ ...cellularType && { cellular_type: cellularType }
2242
+ }
2243
+ };
2244
+ }
2245
+ async function getWifiName() {
2246
+ return void 0;
2247
+ }
2248
+ function getCellularGeneration() {
2249
+ return "LTE";
2250
+ }
2251
+
2252
+ // src/device-bridge/collectors/notifications.ts
2253
+ var ExpoNotifications = __toESM(require("expo-notifications"));
2254
+
2255
+ // src/device-bridge/collectors/appEngagement.ts
2256
+ var import_react_native6 = require("react-native");
2257
+ var import_async_storage5 = __toESM(require("@react-native-async-storage/async-storage"));
2258
+
2259
+ // src/device-bridge/collectors/camera.ts
2260
+ var import_expo_camera2 = require("expo-camera");
2261
+
2262
+ // src/device-bridge/biometrics.ts
2263
+ var biometrics_exports = {};
2264
+ __export(biometrics_exports, {
2265
+ authenticate: () => authenticate,
2266
+ capabilities: () => capabilities
2267
+ });
2268
+ var LocalAuthentication = __toESM(require("expo-local-authentication"));
2269
+ async function capabilities() {
2270
+ const [hasHardware, isEnrolled, types] = await Promise.all([
2271
+ LocalAuthentication.hasHardwareAsync(),
2272
+ LocalAuthentication.isEnrolledAsync(),
2273
+ LocalAuthentication.supportedAuthenticationTypesAsync()
2274
+ ]);
2275
+ const supportedTypes = types.map((t) => {
2276
+ if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return "fingerprint";
2277
+ if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return "face";
2278
+ if (t === LocalAuthentication.AuthenticationType.IRIS) return "iris";
2279
+ return "none";
2280
+ }).filter((t) => t !== "none");
2281
+ return { hasHardware, isEnrolled, supportedTypes };
2282
+ }
2283
+ async function authenticate(opts) {
2284
+ try {
2285
+ const res = await LocalAuthentication.authenticateAsync({
2286
+ promptMessage: opts.reason,
2287
+ cancelLabel: opts.cancelLabel,
2288
+ disableDeviceFallback: opts.fallbackToPasscode === false
2289
+ });
2290
+ if (res.success) return { success: true };
2291
+ return { success: false, error: res.error || "unknown" };
2292
+ } catch (e) {
2293
+ return { success: false, error: String(e?.message || e) };
2294
+ }
2295
+ }
2296
+
2297
+ // src/device-bridge/sharing.ts
2298
+ var sharing_exports = {};
2299
+ __export(sharing_exports, {
2300
+ isAvailable: () => isAvailable,
2301
+ shareFile: () => shareFile
2302
+ });
2303
+ var Sharing = __toESM(require("expo-sharing"));
2304
+ async function shareFile(fileUri, opts = {}) {
2305
+ try {
2306
+ const available = await Sharing.isAvailableAsync();
2307
+ if (!available) return { ok: false, error: "sharing_unavailable" };
2308
+ await Sharing.shareAsync(fileUri, opts);
2309
+ return { ok: true };
2310
+ } catch (e) {
2311
+ return { ok: false, error: String(e?.message || e) };
2312
+ }
2313
+ }
2314
+ async function isAvailable() {
2315
+ return Sharing.isAvailableAsync();
2316
+ }
2317
+
2318
+ // src/device-bridge/scanner.ts
2319
+ var scanner_exports = {};
2320
+ __export(scanner_exports, {
2321
+ CameraView: () => import_expo_camera3.CameraView,
2322
+ normalizeResult: () => normalizeResult,
2323
+ requestPermissions: () => requestPermissions,
2324
+ useCameraPermissions: () => import_expo_camera3.useCameraPermissions
2325
+ });
2326
+ var import_expo_camera3 = require("expo-camera");
2327
+ async function requestPermissions() {
2328
+ const { Camera: Camera2 } = await import("expo-camera");
2329
+ const { status } = await Camera2.requestCameraPermissionsAsync();
2330
+ return { granted: status === "granted", status };
2331
+ }
2332
+ function normalizeResult(r) {
2333
+ return { type: r.type, data: r.data };
2334
+ }
2335
+
2336
+ // src/device/createDeviceProvider.ts
2337
+ function getDeviceCookies() {
2338
+ try {
2339
+ return init_cookies(), __toCommonJS(cookies_exports);
2340
+ } catch {
2341
+ return null;
2342
+ }
2343
+ }
2344
+ function getDeviceNfc() {
2345
+ try {
2346
+ return init_nfc(), __toCommonJS(nfc_exports);
2347
+ } catch {
2348
+ return null;
2349
+ }
2350
+ }
2351
+ var CapabilityUnavailableError = class extends Error {
2352
+ constructor(capability, reason) {
2353
+ super(reason || `${capability} is not yet implemented. Coming in Phase 2.`);
2354
+ this.code = "UNAVAILABLE";
2355
+ this.name = "CapabilityUnavailableError";
2356
+ }
2357
+ };
2358
+ function stubApi(capability) {
2359
+ return new Proxy({}, {
2360
+ get(_target, prop) {
2361
+ if (typeof prop === "string") {
2362
+ return () => {
2363
+ throw new CapabilityUnavailableError(capability);
2364
+ };
2365
+ }
2366
+ }
2367
+ });
2368
+ }
2369
+ function createLocationAPI() {
2370
+ const ensurePermission2 = async () => {
2371
+ const existing = await ExpoLocation4.getForegroundPermissionsAsync();
2372
+ if (existing.status === "granted") return;
2373
+ const asked = await ExpoLocation4.requestForegroundPermissionsAsync();
2374
+ if (asked.status !== "granted") {
2375
+ throw new Error(
2376
+ "Location permission was not granted. Enable it for this app in Settings > Privacy > Location Services."
2377
+ );
2378
+ }
2379
+ };
2380
+ return {
2381
+ async getCurrent(opts) {
2382
+ await ensurePermission2();
2383
+ const fix = await ExpoLocation4.getCurrentPositionAsync({
2384
+ accuracy: opts?.accuracy === "high" ? ExpoLocation4.Accuracy.High : ExpoLocation4.Accuracy.Balanced
2385
+ });
2386
+ return {
2387
+ latitude: fix.coords.latitude,
2388
+ longitude: fix.coords.longitude,
2389
+ altitude: fix.coords.altitude ?? void 0
2390
+ };
2391
+ },
2392
+ watchPosition(cb) {
2393
+ let subscription = null;
2394
+ let cancelled = false;
2395
+ void (async () => {
2396
+ try {
2397
+ await ensurePermission2();
2398
+ if (cancelled) return;
2399
+ subscription = await ExpoLocation4.watchPositionAsync(
2400
+ { accuracy: ExpoLocation4.Accuracy.Balanced },
2401
+ (fix) => cb({ latitude: fix.coords.latitude, longitude: fix.coords.longitude })
2402
+ );
2403
+ if (cancelled) {
2404
+ subscription?.remove();
2405
+ subscription = null;
2406
+ }
2407
+ } catch (err) {
2408
+ console.warn("[location] watchPosition failed:", err?.message || String(err));
2409
+ }
2410
+ })();
2411
+ return () => {
2412
+ cancelled = true;
2413
+ subscription?.remove();
2414
+ subscription = null;
2415
+ };
2416
+ }
2417
+ };
2418
+ }
2419
+ function createCameraAPI() {
2420
+ const ensurePermission2 = async () => {
2421
+ const existing = await ImagePicker.getCameraPermissionsAsync();
2422
+ if (existing.granted) return;
2423
+ const asked = await ImagePicker.requestCameraPermissionsAsync();
2424
+ if (!asked.granted) {
2425
+ throw new Error(
2426
+ "Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera."
2427
+ );
2428
+ }
2429
+ };
2430
+ return {
2431
+ async takePhoto(opts) {
2432
+ await ensurePermission2();
2433
+ const res = await ImagePicker.launchCameraAsync({
2434
+ quality: opts?.quality ?? 0.7,
2435
+ cameraType: opts?.facing === "front" ? ImagePicker.CameraType.front : ImagePicker.CameraType.back
2436
+ });
2437
+ if (res.canceled || !res.assets?.[0]) {
2438
+ throw new Error("No photo taken \u2014 the camera was dismissed.");
2439
+ }
2440
+ const a = res.assets[0];
2441
+ return { uri: a.uri, width: a.width, height: a.height };
2442
+ },
2443
+ async scanBarcode(opts) {
2444
+ throw new CapabilityUnavailableError(
2445
+ "camera.scanBarcode",
2446
+ "Use native.scanner.scanOnce() instead \u2014 scanning needs a mounted camera, which this call cannot provide. Same arguments, and it returns { data, format }." + (opts?.formats?.length ? " Pass your formats through to scanOnce." : "")
2447
+ );
2448
+ },
2449
+ async recordVideo(opts) {
2450
+ return cameraTasks.run("record", {
2451
+ maxDurationMs: opts?.maxDurationMs,
2452
+ withAudio: opts?.withAudio ?? false
2453
+ });
2454
+ },
2455
+ async pickImage(opts) {
2456
+ const existing = await ImagePicker.getMediaLibraryPermissionsAsync();
2457
+ if (!existing.granted) {
2458
+ const asked = await ImagePicker.requestMediaLibraryPermissionsAsync();
2459
+ if (!asked.granted) {
2460
+ throw new Error(
2461
+ "Photo library permission was not granted. Enable it for this app in Settings > Privacy > Photos."
2462
+ );
2463
+ }
2464
+ }
2465
+ const res = await ImagePicker.launchImageLibraryAsync({
2466
+ allowsMultipleSelection: !!opts?.multiple
2467
+ });
2468
+ if (res.canceled) return [];
2469
+ return (res.assets || []).map((a) => ({ uri: a.uri }));
2470
+ }
2471
+ };
2472
+ }
2473
+ var _pluginDbs = /* @__PURE__ */ new Map();
2474
+ function openPluginDb(file) {
2475
+ const existing = _pluginDbs.get(file);
2476
+ if (existing) return existing;
2477
+ const opened = SQLite.openDatabaseAsync(file);
2478
+ _pluginDbs.set(file, opened);
2479
+ return opened;
2480
+ }
2481
+ function sqlLeadingKeyword(sql) {
2482
+ let s = String(sql);
2483
+ for (; ; ) {
2484
+ const before = s;
2485
+ s = s.replace(/^\s+/, "");
2486
+ if (s.startsWith("--")) {
2487
+ const nl = s.indexOf("\n");
2488
+ s = nl === -1 ? "" : s.slice(nl + 1);
2489
+ } else if (s.startsWith("/*")) {
2490
+ const end = s.indexOf("*/");
2491
+ s = end === -1 ? "" : s.slice(end + 2);
2492
+ }
2493
+ if (s === before) break;
2494
+ }
2495
+ const m = s.match(/^[A-Za-z]+/);
2496
+ return m ? m[0].toUpperCase() : "";
2497
+ }
2498
+ function createStorageAPI(pluginId) {
2499
+ const ns = encodeURIComponent(pluginId);
2500
+ const key = (k) => `@plugin:${ns}:${k}`;
2501
+ const dbFile = `plugin-storage-${ns}.db`;
2502
+ return {
2503
+ get: (k) => import_async_storage6.default.getItem(key(k)),
2504
+ set: (k, v) => import_async_storage6.default.setItem(key(k), v),
2505
+ delete: (k) => import_async_storage6.default.removeItem(key(k)),
2506
+ db: {
2507
+ async execute(sql, params) {
2508
+ const verb = sqlLeadingKeyword(sql);
2509
+ if (verb === "ATTACH" || verb === "DETACH" || verb === "VACUUM") {
2510
+ throw new CapabilityUnavailableError(
2511
+ "storage.db",
2512
+ `SQL '${verb}' is not permitted: a plugin's storage is confined to its own database and cannot reach another file.`
2513
+ );
2514
+ }
2515
+ const db = await openPluginDb(dbFile);
2516
+ const returnsRows = verb === "SELECT" || verb === "PRAGMA" || verb === "WITH";
2517
+ if (returnsRows) {
2518
+ const rows = await db.getAllAsync(sql, params ?? []);
2519
+ return { rows, changes: 0 };
2520
+ }
2521
+ const res = await db.runAsync(sql, params ?? []);
2522
+ return { rows: [], changes: res.changes ?? 0 };
2523
+ }
2524
+ }
2525
+ };
2526
+ }
2527
+ function createNotificationsAPI() {
2528
+ return {
2529
+ async schedule({ title, body, trigger }) {
2530
+ const perm = await Notifications.getPermissionsAsync();
2531
+ if (!perm.granted) {
2532
+ const asked = await Notifications.requestPermissionsAsync();
2533
+ if (!asked.granted) {
2534
+ throw new Error(
2535
+ "Notification permission was not granted. Enable it for this app in Settings > Notifications."
2536
+ );
2537
+ }
2538
+ }
2539
+ const when = typeof trigger?.seconds === "number" ? { type: "timeInterval", seconds: trigger.seconds, repeats: false } : trigger?.date ? { type: "date", date: new Date(trigger.date) } : null;
2540
+ if (!when) {
2541
+ throw new Error("schedule() needs trigger.seconds or trigger.date.");
2542
+ }
2543
+ return Notifications.scheduleNotificationAsync({
2544
+ content: { title, body },
2545
+ trigger: when
2546
+ });
2547
+ },
2548
+ cancel: (id) => Notifications.cancelScheduledNotificationAsync(id),
2549
+ onReceived(cb) {
2550
+ const sub2 = Notifications.addNotificationReceivedListener((n) => {
2551
+ cb({
2552
+ id: n.request.identifier,
2553
+ title: n.request.content.title || "",
2554
+ body: n.request.content.body || "",
2555
+ data: n.request.content.data
2556
+ });
2557
+ });
2558
+ return () => sub2.remove();
2559
+ }
2560
+ };
2561
+ }
2562
+ function createFileSystemAPI(pluginId) {
2563
+ let _root = null;
2564
+ const rootDir = () => _root ?? (_root = new FS.Directory(FS.Paths.document, "plugin-files", pluginId));
2565
+ const resolve = (p) => {
2566
+ const root = rootDir();
2567
+ const file = new FS.File(root, p);
2568
+ if (!file.uri.startsWith(root.uri)) {
2569
+ throw new CapabilityUnavailableError(
2570
+ "fileSystem",
2571
+ `Path escapes the plugin's storage area: ${p}. Plugin files live under plugin-files/ and paths cannot traverse out of it.`
2572
+ );
2573
+ }
2574
+ return file;
2575
+ };
2576
+ return {
2577
+ async readFile(path) {
2578
+ const file = resolve(path);
2579
+ if (!file.exists) {
2580
+ throw new CapabilityUnavailableError(
2581
+ "fileSystem.readFile",
2582
+ `No such file: ${path}. Nothing was read.`
2583
+ );
2584
+ }
2585
+ return file.text();
2586
+ },
2587
+ async writeFile(path, content) {
2588
+ const file = resolve(path);
2589
+ const parent = file.parentDirectory;
2590
+ if (!parent.exists) parent.create({ intermediates: true });
2591
+ if (!file.exists) file.create();
2592
+ file.write(content);
2593
+ },
2594
+ async pickDocument(opts) {
2595
+ const res = await DocumentPicker.getDocumentAsync({
2596
+ type: opts?.types?.length ? opts.types : "*/*",
2597
+ copyToCacheDirectory: true
2598
+ });
2599
+ if (res.canceled || !res.assets?.[0]) {
2600
+ throw new Error("No document chosen \u2014 the picker was dismissed.");
2601
+ }
2602
+ const a = res.assets[0];
2603
+ return { uri: a.uri, name: a.name, size: a.size ?? 0 };
2604
+ }
2605
+ };
2606
+ }
2607
+ function createHapticsAPI() {
2608
+ return {
2609
+ impact(style) {
2610
+ const map = {
2611
+ light: Haptics2.ImpactFeedbackStyle.Light,
2612
+ medium: Haptics2.ImpactFeedbackStyle.Medium,
2613
+ heavy: Haptics2.ImpactFeedbackStyle.Heavy
2614
+ };
2615
+ Haptics2.impactAsync(map[style]);
2616
+ },
2617
+ notification(type) {
2618
+ const map = {
2619
+ success: Haptics2.NotificationFeedbackType.Success,
2620
+ warning: Haptics2.NotificationFeedbackType.Warning,
2621
+ error: Haptics2.NotificationFeedbackType.Error
2622
+ };
2623
+ Haptics2.notificationAsync(map[type]);
2624
+ },
2625
+ // Shorthand for notification('error'). Plugins call this; it was never
2626
+ // implemented, and optional chaining meant the failure the user was meant
2627
+ // to FEEL simply did not happen.
2628
+ error() {
2629
+ Haptics2.notificationAsync(Haptics2.NotificationFeedbackType.Error);
2630
+ },
2631
+ selection() {
2632
+ Haptics2.selectionAsync();
2633
+ }
2634
+ };
2635
+ }
2636
+ function createConnectivityAPI(reachabilityUrl) {
2637
+ let online = true;
2638
+ const listeners = /* @__PURE__ */ new Set();
2639
+ let interval = null;
2640
+ const checkOnline = async () => {
2641
+ if (!reachabilityUrl) return true;
2642
+ try {
2643
+ const res = await fetch(reachabilityUrl, {
2644
+ method: "HEAD",
2645
+ signal: AbortSignal.timeout(5e3)
2646
+ });
2647
+ return res.ok;
2648
+ } catch {
2649
+ return false;
2650
+ }
2651
+ };
2652
+ const startPolling = () => {
2653
+ if (!reachabilityUrl) return;
2654
+ if (interval) return;
2655
+ interval = setInterval(async () => {
2656
+ const newState = await checkOnline();
2657
+ if (newState !== online) {
2658
+ online = newState;
2659
+ listeners.forEach((cb) => cb(online));
2660
+ }
2661
+ }, 3e4);
2662
+ };
2663
+ return {
2664
+ isOnline() {
2665
+ return online;
2666
+ },
2667
+ onChange(cb) {
2668
+ listeners.add(cb);
2669
+ startPolling();
2670
+ return () => {
2671
+ listeners.delete(cb);
2672
+ if (listeners.size === 0 && interval) {
2673
+ clearInterval(interval);
2674
+ interval = null;
2675
+ }
2676
+ };
2677
+ }
2678
+ };
2679
+ }
2680
+ function createCookiesAPI() {
2681
+ return {
2682
+ async get(url) {
2683
+ const mod = getDeviceCookies();
2684
+ if (!mod) throw new CapabilityUnavailableError("cookies (native module not available \u2014 use a dev build)");
2685
+ const list = await mod.get(url, true);
2686
+ return list;
2687
+ },
2688
+ async clear(_url) {
2689
+ const mod = getDeviceCookies();
2690
+ if (!mod) throw new CapabilityUnavailableError("cookies (native module not available \u2014 use a dev build)");
2691
+ const r = await mod.clearAll();
2692
+ return { ok: r.ok };
2693
+ }
2694
+ };
2695
+ }
2696
+ function createBiometricsAPIv2() {
2697
+ return {
2698
+ capabilities: () => biometrics_exports.capabilities(),
2699
+ authenticate: (opts) => biometrics_exports.authenticate(opts)
2700
+ };
2701
+ }
2702
+ function createSharingAPI() {
2703
+ return {
2704
+ isAvailable: () => sharing_exports.isAvailable(),
2705
+ shareFile: (fileUri, opts) => sharing_exports.shareFile(fileUri, opts)
2706
+ };
2707
+ }
2708
+ function createScannerAPI() {
2709
+ return {
2710
+ requestPermissions: () => scanner_exports.requestPermissions(),
2711
+ // Raises the shared camera surface and settles on the first code read.
2712
+ // The plugin mounts nothing — that lifecycle is owned once, by
2713
+ // CameraTaskHost, instead of by every plugin that wants to scan.
2714
+ scanOnce: (opts) => cameraTasks.run("scan", {
2715
+ formats: opts?.formats,
2716
+ timeoutMs: opts?.timeoutMs
2717
+ })
2718
+ };
2719
+ }
2720
+ function createNfcAPI() {
2721
+ return {
2722
+ async isSupported() {
2723
+ const mod = getDeviceNfc();
2724
+ if (!mod) return false;
2725
+ return mod.isSupported();
2726
+ },
2727
+ async isEnabled() {
2728
+ const mod = getDeviceNfc();
2729
+ if (!mod) return false;
2730
+ return mod.isEnabled();
2731
+ },
2732
+ readOnce(timeoutMs) {
2733
+ const mod = getDeviceNfc();
2734
+ if (!mod) return Promise.resolve({ ok: false, error: "nfc_unavailable" });
2735
+ return mod.readOnce(timeoutMs);
2736
+ }
2737
+ };
2738
+ }
2739
+ function createBluetoothAPI() {
2740
+ const refuse = (method) => new CapabilityUnavailableError(
2741
+ `bluetooth.${method}`,
2742
+ "Bluetooth is not implemented in this build \u2014 no BLE native module is installed. The capability is declared so it fails by name rather than silently; nothing was scanned."
2743
+ );
2744
+ return {
2745
+ // These two answer `false` rather than throwing, because "is BLE usable?"
2746
+ // is exactly the question a plugin should be allowed to ask before
2747
+ // committing to a flow, and `false` is the true answer here.
2748
+ async isSupported() {
2749
+ return false;
2750
+ },
2751
+ async isEnabled() {
2752
+ return false;
2753
+ },
2754
+ // scan() THROWS, because there is no honest empty result: an empty device
2755
+ // list is a real finding ("nothing is advertising nearby") and returning
2756
+ // one would report a scan that never happened.
2757
+ async scan() {
2758
+ throw refuse("scan");
2759
+ }
2760
+ };
2761
+ }
2762
+ function createVisionAPI() {
2763
+ return {
2764
+ // "Can I see?" — true once the native module is present in the binary. On a
2765
+ // build without it, adaVision.isSupported() answers false, and assessQuality
2766
+ // below refuses cleanly (no crash), exactly the pre-module behaviour.
2767
+ async isSupported() {
2768
+ return isSupported();
2769
+ },
2770
+ // readText: on-device OCR on a published frame (native resolves the id →
2771
+ // pixels). [] = looked, found no text (a real finding); throws = could not
2772
+ // look (unknown/aged-out id). describe (VNClassifyImage) is the next pass.
2773
+ async readText(frameId, opts) {
2774
+ const langs = opts?.languages;
2775
+ if (langs != null && (!Array.isArray(langs) || langs.some((l) => typeof l !== "string"))) {
2776
+ throw refuseArgument("vision", "readText", `languages must be an array of language codes, not ${shown(langs)}`, "nothing was read");
2777
+ }
2778
+ return readText(frameId, langs);
2779
+ },
2780
+ // describe: on-device labels (VNClassifyImage), not prose — the sentence is
2781
+ // the skill's job. [] = looked, recognised nothing.
2782
+ async describe(frameId, opts) {
2783
+ return describe(frameId, countArg("vision", "describe", "maxLabels", opts?.maxLabels, 5, 50, "nothing was labelled"));
2784
+ },
2785
+ // detectObjects: on-device located objects (bundled detector), each with a
2786
+ // box — what a guidance loop reads to say WHERE a thing is, not just that the
2787
+ // scene contains it. [] = looked, found nothing; throws = could not look.
2788
+ async detectObjects(frameId, opts) {
2789
+ const nothing = "nothing was detected";
2790
+ return detectObjects(
2791
+ frameId,
2792
+ countArg("vision", "detectObjects", "maxItems", opts?.maxItems, 10, 50, nothing),
2793
+ fractionArg("vision", "detectObjects", "minConfidence", opts?.minConfidence, 0.25, nothing)
2794
+ );
2795
+ },
2796
+ // assessQuality: resolve the frame id to its LOCAL file, measure on-device,
2797
+ // and record the result back onto the frame so a later read of
2798
+ // VisualFrameRef.quality reflects it. Throwing here means "could not look"
2799
+ // (unknown/aged-out id, or no native module) — never a measured verdict.
2800
+ async assessQuality(frameId) {
2801
+ const q = qualityById(frameId);
2802
+ if (!q) {
2803
+ throw new CapabilityUnavailableError(
2804
+ "vision.assessQuality",
2805
+ `No frame for id "${frameId}" \u2014 unknown or aged out of the ring. This is "could not look", not a measurement.`
2806
+ );
2807
+ }
2808
+ return q;
2809
+ }
2810
+ };
2811
+ }
2812
+ function createMapsAPI() {
2813
+ const refuseIfMissing = (verb, nothing) => {
2814
+ const missing = missingReason();
2815
+ if (missing) {
2816
+ throw new CapabilityUnavailableError(`maps.${verb}`, `maps.${verb} needs ${missing}; ${nothing}.`);
2817
+ }
2818
+ };
2819
+ return {
2820
+ async isSupported() {
2821
+ return isSupported2();
2822
+ },
2823
+ // [] = Apple looked and found nothing (a real finding); throws = could not
2824
+ // look. The defaults (radiusM 3000, limit 8) live in searchArgs.
2825
+ async search(query, opts) {
2826
+ const args = searchArgs(query, opts);
2827
+ refuseIfMissing("search", "nothing was searched");
2828
+ const raw = await search(args.query, args.near, args.radiusM, args.limit).catch((err) => {
2829
+ throw mapsFailure("search", err);
2830
+ });
2831
+ return toMapsPlaces(raw, args.near);
2832
+ },
2833
+ // null = nothing at that point; throws = could not look.
2834
+ async reverseGeocode(at) {
2835
+ const point2 = coordinateArg("reverseGeocode", "at", at, "nothing was looked up");
2836
+ const address = await reverseGeocode(point2).catch((err) => {
2837
+ throw mapsFailure("reverseGeocode", err);
2838
+ });
2839
+ return address ? toReverseGeocodedPlace(point2, address) : null;
2840
+ },
2841
+ // null = Apple found no route. Never an empty 0 m route, which would read as
2842
+ // "you have arrived". A remote bundle can pass any string as `mode`, so a
2843
+ // mode this build cannot honour refuses rather than silently walking.
2844
+ async route(from, to, opts) {
2845
+ if (opts?.mode !== void 0 && opts.mode !== "walking") {
2846
+ throw new CapabilityUnavailableError(
2847
+ "maps.route",
2848
+ `maps.route supports walking only, not "${String(opts.mode)}"; no route was requested.`
2849
+ );
2850
+ }
2851
+ const a = coordinateArg("route", "from", from, "no route was requested");
2852
+ const b = coordinateArg("route", "to", to, "no route was requested");
2853
+ refuseIfMissing("route", "no route was requested");
2854
+ const raw = await route(a, b).catch((err) => {
2855
+ throw mapsFailure("route", err);
2856
+ });
2857
+ return raw ? toMapsRoute(raw) : null;
2858
+ }
2859
+ };
2860
+ }
2861
+ function createDeviceProvider(config) {
2862
+ const connectivity = createConnectivityAPI(config?.reachabilityUrl);
2863
+ return {
2864
+ camera: createCameraAPI(),
2865
+ location: createLocationAPI(),
2866
+ // Only the per-plugin storageFor/fileSystemFor are exposed — NO shared
2867
+ // `storage`/`fileSystem` bucket. The gate scopes by pluginId and fails
2868
+ // closed without one, so a shared instance would be an unreachable
2869
+ // (and dangerous) fallback. Each plugin gets its own namespace + db file /
2870
+ // sandbox directory, or nothing.
2871
+ storageFor: (pluginId) => createStorageAPI(pluginId),
2872
+ biometrics: stubApi("biometrics"),
2873
+ biometricsV2: createBiometricsAPIv2(),
2874
+ notifications: createNotificationsAPI(),
2875
+ haptics: createHapticsAPI(),
2876
+ connectivity,
2877
+ fileSystemFor: (pluginId) => createFileSystemAPI(pluginId),
2878
+ cookies: createCookiesAPI(),
2879
+ sharing: createSharingAPI(),
2880
+ scanner: createScannerAPI(),
2881
+ nfc: createNfcAPI(),
2882
+ bluetooth: createBluetoothAPI(),
2883
+ vision: createVisionAPI(),
2884
+ maps: createMapsAPI()
2885
+ };
2886
+ }
2887
+
2888
+ // src/oauth/expoOAuth.tsx
2889
+ var import_react_native7 = require("react-native");
2890
+ var import_google_signin = require("@react-native-google-signin/google-signin");
2891
+ var AppleAuthentication = __toESM(require("expo-apple-authentication"));
2892
+ var import_jsx_runtime2 = require("react/jsx-runtime");
2893
+ var NOOP = () => {
2894
+ };
2895
+ function createExpoOAuthProvider(host = {}) {
2896
+ return {
2897
+ async signInWithGoogle(cfg) {
2898
+ const webClientId = cfg?.webClientId || host.googleWebClientId || "";
2899
+ const iosClientId = cfg?.iosClientId || host.googleIosClientId || "";
2900
+ const params = { webClientId };
2901
+ if (iosClientId) params.iosClientId = iosClientId;
2902
+ import_google_signin.GoogleSignin.configure(params);
2903
+ try {
2904
+ await import_google_signin.GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
2905
+ const result = await import_google_signin.GoogleSignin.signIn();
2906
+ return result?.data?.idToken || result?.idToken || null;
2907
+ } catch (err) {
2908
+ const code = err?.code;
2909
+ if (code === import_google_signin.statusCodes.SIGN_IN_CANCELLED || code === import_google_signin.statusCodes.IN_PROGRESS) {
2910
+ return null;
2911
+ }
2912
+ throw err;
2913
+ }
2914
+ },
2915
+ async signInWithApple() {
2916
+ try {
2917
+ const credential = await AppleAuthentication.signInAsync({
2918
+ requestedScopes: [
2919
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
2920
+ AppleAuthentication.AppleAuthenticationScope.EMAIL
2921
+ ]
2922
+ });
2923
+ return credential.identityToken || null;
2924
+ } catch (err) {
2925
+ if (err?.code === "ERR_REQUEST_CANCELED") return null;
2926
+ throw err;
2927
+ }
2928
+ },
2929
+ async isAppleAvailable() {
2930
+ if (import_react_native7.Platform.OS !== "ios") return false;
2931
+ try {
2932
+ return await AppleAuthentication.isAvailableAsync();
2933
+ } catch {
2934
+ return false;
2935
+ }
2936
+ },
2937
+ renderAppleButton({ onPress, disabled, style }) {
2938
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2939
+ AppleAuthentication.AppleAuthenticationButton,
2940
+ {
2941
+ buttonType: AppleAuthentication.AppleAuthenticationButtonType.CONTINUE,
2942
+ buttonStyle: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK,
2943
+ cornerRadius: 999,
2944
+ style,
2945
+ onPress: disabled ? NOOP : onPress
2946
+ }
2947
+ );
2948
+ }
2949
+ };
2950
+ }
2951
+
2952
+ // src/bootstrap.ts
2953
+ function bootstrapExpoAdas(opts) {
2954
+ const { attachmentCache } = setupExpoProviders({
2955
+ bundleModules: opts.bundleModules,
2956
+ attachmentIndexKey: opts.storageKeys?.attachmentIndex,
2957
+ bundleIndexKey: opts.storageKeys?.bundleIndex,
2958
+ deviceProvider: createDeviceProvider({ reachabilityUrl: opts.reachabilityUrl })
2959
+ });
2960
+ for (const plugin of opts.nativePlugins ?? []) {
2961
+ for (const id of plugin.ids) (0, import_plugin_runtime3.registerNativePlugin)(id, plugin.resolver);
2962
+ }
2963
+ const relay = opts.urls.relay;
2964
+ (0, import_ui2.setPluginWebViewExtraHosts)(() => relay ? [relay()] : []);
2965
+ (0, import_ui2.setOAuthProvider)(createExpoOAuthProvider({ googleIosClientId: opts.googleIosClientId }));
2966
+ if (opts.onReachabilityChange) {
2967
+ const onReach = opts.onReachabilityChange;
2968
+ (0, import_core2.setAcsReachabilityReporter)({ up: () => onReach(true), down: () => onReach(false) });
2969
+ }
2970
+ (0, import_plugin_runtime3.setDeviceStateProvider)(
2971
+ createDeviceState({ connectivity: connectivity_exports, location: location_exports })
2972
+ );
2973
+ (0, import_plugin_runtime3.registerPlatformDeviceTools)();
2974
+ (0, import_ui2.setUIActionNavigator)(opts.navigateToPlugin);
2975
+ (0, import_core2.initAdas)({
2976
+ coreUrl: opts.urls.core,
2977
+ ...opts.urls.voice ? { voiceUrl: opts.urls.voice } : {},
2978
+ ...opts.urls.consumerAuth ? { consumerAuthUrl: opts.urls.consumerAuth } : {},
2979
+ ...opts.onSession ? { onSession: opts.onSession } : {},
2980
+ ...opts.tenant ? { tenant: opts.tenant } : {},
2981
+ getUser: opts.getUser,
2982
+ tokenProvider: opts.tokenProvider,
2983
+ attachmentCache
2984
+ });
2985
+ return { attachmentCache };
2986
+ }
2987
+ // Annotate the CommonJS export names for ESM import in node:
2988
+ 0 && (module.exports = {
2989
+ bootstrapExpoAdas
2990
+ });
2991
+ //# sourceMappingURL=bootstrap.js.map