@ateam-ai-mobile/expo 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/android/build.gradle +16 -0
  2. package/android/src/main/java/expo/modules/adavision/AdaVisionModule.kt +75 -0
  3. package/dist/attachmentCache-DhQTqi2E.d.mts +18 -0
  4. package/dist/attachmentCache-DhQTqi2E.d.ts +18 -0
  5. package/dist/bootstrap.d.mts +95 -0
  6. package/dist/bootstrap.d.ts +95 -0
  7. package/dist/bootstrap.js +2473 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/bootstrap.mjs +64 -0
  10. package/dist/bootstrap.mjs.map +1 -0
  11. package/dist/chunk-4BMWVN25.mjs +91 -0
  12. package/dist/chunk-4BMWVN25.mjs.map +1 -0
  13. package/dist/chunk-4XYXJYN5.mjs +1513 -0
  14. package/dist/chunk-4XYXJYN5.mjs.map +1 -0
  15. package/dist/chunk-5HM6UA2K.mjs +571 -0
  16. package/dist/chunk-5HM6UA2K.mjs.map +1 -0
  17. package/dist/chunk-6LXF3JVX.mjs +96 -0
  18. package/dist/chunk-6LXF3JVX.mjs.map +1 -0
  19. package/dist/chunk-FF6ZZZXH.mjs +71 -0
  20. package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
  21. package/dist/chunk-KOXF2FZM.mjs +92 -0
  22. package/dist/chunk-KOXF2FZM.mjs.map +1 -0
  23. package/dist/chunk-L4V7OCJ3.mjs +71 -0
  24. package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
  25. package/dist/chunk-TZXWVYYR.mjs +67 -0
  26. package/dist/chunk-TZXWVYYR.mjs.map +1 -0
  27. package/dist/chunk-VQJPSLHP.mjs +1801 -0
  28. package/dist/chunk-VQJPSLHP.mjs.map +1 -0
  29. package/dist/chunk-WMJKH4XE.mjs +34 -0
  30. package/dist/chunk-WMJKH4XE.mjs.map +1 -0
  31. package/dist/chunk-XQSY6MHP.mjs +213 -0
  32. package/dist/chunk-XQSY6MHP.mjs.map +1 -0
  33. package/dist/createBridge.mjs +2 -0
  34. package/dist/createBridge.mjs.map +1 -1
  35. package/dist/device/createDeviceProvider.d.mts +23 -0
  36. package/dist/device/createDeviceProvider.d.ts +23 -0
  37. package/dist/device/createDeviceProvider.js +967 -0
  38. package/dist/device/createDeviceProvider.js.map +1 -0
  39. package/dist/device/createDeviceProvider.mjs +17 -0
  40. package/dist/device/createDeviceProvider.mjs.map +1 -0
  41. package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
  42. package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
  43. package/dist/device-bridge/collectors/appEngagement.js +128 -0
  44. package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
  45. package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
  46. package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
  47. package/dist/device-bridge/collectors/health.d.mts +24 -0
  48. package/dist/device-bridge/collectors/health.d.ts +24 -0
  49. package/dist/device-bridge/collectors/health.js +238 -0
  50. package/dist/device-bridge/collectors/health.js.map +1 -0
  51. package/dist/device-bridge/collectors/health.mjs +18 -0
  52. package/dist/device-bridge/collectors/health.mjs.map +1 -0
  53. package/dist/device-bridge/cookies.d.mts +49 -0
  54. package/dist/device-bridge/cookies.d.ts +49 -0
  55. package/dist/device-bridge/cookies.js +90 -0
  56. package/dist/device-bridge/cookies.js.map +1 -0
  57. package/dist/device-bridge/cookies.mjs +14 -0
  58. package/dist/device-bridge/cookies.mjs.map +1 -0
  59. package/dist/device-bridge/index.d.mts +483 -0
  60. package/dist/device-bridge/index.d.ts +483 -0
  61. package/dist/device-bridge/index.js +2084 -0
  62. package/dist/device-bridge/index.js.map +1 -0
  63. package/dist/device-bridge/index.mjs +32 -0
  64. package/dist/device-bridge/index.mjs.map +1 -0
  65. package/dist/device-bridge/nfc.d.mts +30 -0
  66. package/dist/device-bridge/nfc.d.ts +30 -0
  67. package/dist/device-bridge/nfc.js +109 -0
  68. package/dist/device-bridge/nfc.js.map +1 -0
  69. package/dist/device-bridge/nfc.mjs +14 -0
  70. package/dist/device-bridge/nfc.mjs.map +1 -0
  71. package/dist/index.d.mts +493 -18
  72. package/dist/index.d.ts +493 -18
  73. package/dist/index.js +1409 -6
  74. package/dist/index.js.map +1 -1
  75. package/dist/index.mjs +23 -194
  76. package/dist/index.mjs.map +1 -1
  77. package/dist/oauth/expoOAuth.d.mts +18 -0
  78. package/dist/oauth/expoOAuth.d.ts +18 -0
  79. package/dist/oauth/expoOAuth.js +102 -0
  80. package/dist/oauth/expoOAuth.js.map +1 -0
  81. package/dist/oauth/expoOAuth.mjs +8 -0
  82. package/dist/oauth/expoOAuth.mjs.map +1 -0
  83. package/dist/types-BRanJYTn.d.mts +138 -0
  84. package/dist/types-BRanJYTn.d.ts +138 -0
  85. package/dist/vision/index.d.mts +81 -0
  86. package/dist/vision/index.d.ts +81 -0
  87. package/dist/vision/index.js +101 -0
  88. package/dist/vision/index.js.map +1 -0
  89. package/dist/vision/index.mjs +28 -0
  90. package/dist/vision/index.mjs.map +1 -0
  91. package/expo-module.config.json +9 -0
  92. package/ios/AdaVision.podspec +28 -0
  93. package/ios/AdaVisionModule.swift +432 -0
  94. package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
  95. package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
  96. package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
  97. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
  98. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
  99. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
  100. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
  101. package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
  102. package/package.json +107 -7
  103. package/src/bootstrap.ts +159 -0
  104. package/src/camera/CameraTaskHost.tsx +175 -0
  105. package/src/camera/cameraTasks.ts +129 -0
  106. package/src/device/createDeviceProvider.ts +832 -0
  107. package/src/device-bridge/actions/calendar-actions.ts +217 -0
  108. package/src/device-bridge/actions/dnd.ts +74 -0
  109. package/src/device-bridge/actions/health-actions.ts +58 -0
  110. package/src/device-bridge/actions/index.ts +12 -0
  111. package/src/device-bridge/actions/messaging.ts +74 -0
  112. package/src/device-bridge/actions/navigation.ts +88 -0
  113. package/src/device-bridge/actions/queue-poller.ts +151 -0
  114. package/src/device-bridge/background.ts +137 -0
  115. package/src/device-bridge/biometrics.ts +64 -0
  116. package/src/device-bridge/capabilities.ts +218 -0
  117. package/src/device-bridge/collectors/appEngagement.ts +120 -0
  118. package/src/device-bridge/collectors/battery.ts +83 -0
  119. package/src/device-bridge/collectors/calendar.ts +162 -0
  120. package/src/device-bridge/collectors/camera.ts +27 -0
  121. package/src/device-bridge/collectors/connectivity.ts +60 -0
  122. package/src/device-bridge/collectors/contacts.ts +200 -0
  123. package/src/device-bridge/collectors/health.ts +299 -0
  124. package/src/device-bridge/collectors/index.ts +12 -0
  125. package/src/device-bridge/collectors/location.ts +224 -0
  126. package/src/device-bridge/collectors/mood.ts +141 -0
  127. package/src/device-bridge/collectors/notifications.ts +70 -0
  128. package/src/device-bridge/cookies.ts +89 -0
  129. package/src/device-bridge/dynamic-connector.ts +422 -0
  130. package/src/device-bridge/index.ts +467 -0
  131. package/src/device-bridge/nfc.ts +85 -0
  132. package/src/device-bridge/plugin.ts +71 -0
  133. package/src/device-bridge/relay-client.ts +237 -0
  134. package/src/device-bridge/scanner.ts +42 -0
  135. package/src/device-bridge/sharing.ts +35 -0
  136. package/src/device-bridge/types.ts +326 -0
  137. package/src/device-state/activityClassifier.ts +151 -0
  138. package/src/device-state/activityEngine.ts +252 -0
  139. package/src/device-state/collectors.ts +31 -0
  140. package/src/device-state/domains/activity.ts +93 -0
  141. package/src/device-state/domains/capability.ts +60 -0
  142. package/src/device-state/domains/connectivity.ts +60 -0
  143. package/src/device-state/domains/deviceContext.ts +55 -0
  144. package/src/device-state/domains/interaction.ts +59 -0
  145. package/src/device-state/domains/spatial.ts +283 -0
  146. package/src/device-state/domains/visual.ts +100 -0
  147. package/src/device-state/headingSource.ts +84 -0
  148. package/src/device-state/index.ts +77 -0
  149. package/src/device-state/quality.ts +60 -0
  150. package/src/device-state/replay.ts +220 -0
  151. package/src/device-state/turnDetector.ts +121 -0
  152. package/src/device-state/visualEngine.ts +341 -0
  153. package/src/haptics.ts +4 -0
  154. package/src/index.ts +37 -0
  155. package/src/oauth/expoOAuth.tsx +113 -0
  156. package/src/vision/analysis.js +80 -0
  157. package/src/vision/index.ts +144 -0
  158. package/src/vision/ring.js +35 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Notifications Collector — reads notification state via expo-notifications.
3
+ * Output format matches device-mock-mcp's notifications.list schema.
4
+ *
5
+ * Note: On iOS/Android, expo-notifications only gives access to notifications
6
+ * that YOUR app has posted or received. System-wide notification access
7
+ * requires platform-specific APIs (NotificationListenerService on Android,
8
+ * UNUserNotificationCenter on iOS with app extensions).
9
+ *
10
+ * For the PA, the relay approach means we capture notifications delivered
11
+ * TO our app (from the PA system), not FROM other apps.
12
+ * Other-app notifications would need a native module or accessibility service.
13
+ */
14
+
15
+ import * as ExpoNotifications from 'expo-notifications';
16
+ import type { Notification, NotificationsData } from '../types';
17
+
18
+ /**
19
+ * Request notification permissions.
20
+ */
21
+ export async function requestPermission(): Promise<boolean> {
22
+ const { status } = await ExpoNotifications.requestPermissionsAsync();
23
+ return status === 'granted';
24
+ }
25
+
26
+ /**
27
+ * Check if notification permission is granted.
28
+ */
29
+ export async function hasPermission(): Promise<boolean> {
30
+ const { status } = await ExpoNotifications.getPermissionsAsync();
31
+ return status === 'granted';
32
+ }
33
+
34
+ /**
35
+ * Collect delivered notifications from the notification tray.
36
+ * Only captures notifications delivered to THIS app.
37
+ * Matches the device.notifications.list tool output schema.
38
+ */
39
+ export async function collectNotifications(): Promise<NotificationsData> {
40
+ const presented = await ExpoNotifications.getPresentedNotificationsAsync();
41
+
42
+ const notifications: Notification[] = presented.map((n, i) => ({
43
+ id: n.request.identifier || `n${i}`,
44
+ app: (n.request.content.data?.app as string) || 'A-Team',
45
+ title: n.request.content.title || '',
46
+ body: n.request.content.body || '',
47
+ timestamp: n.date ? new Date(n.date).toISOString() : new Date().toISOString(),
48
+ sender: (n.request.content.data?.sender as string) || null,
49
+ priority: mapPriority(n.request.content.data?.priority as string),
50
+ read: false, // Presented notifications are unread by definition
51
+ }));
52
+
53
+ const unread = notifications.filter(n => !n.read).length;
54
+
55
+ return {
56
+ ok: true,
57
+ timestamp: new Date().toISOString(),
58
+ total: notifications.length,
59
+ unread,
60
+ notifications,
61
+ };
62
+ }
63
+
64
+ // ─── Helpers ───
65
+
66
+ function mapPriority(p: string | undefined): 'high' | 'medium' | 'low' {
67
+ if (p === 'high' || p === 'max') return 'high';
68
+ if (p === 'low' || p === 'min') return 'low';
69
+ return 'medium';
70
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Device-bridge: Cookies API
3
+ *
4
+ * Reads the platform's shared cookie jar (NSHTTPCookieStorage on iOS,
5
+ * CookieManager on Android) so upper layers (plugin-sdk → RN plugins) can
6
+ * extract session cookies captured during a native WebView login flow.
7
+ *
8
+ * Wraps `@react-native-cookies/cookies` so upper layers never import the
9
+ * native module directly. If we swap implementations, only this file changes.
10
+ *
11
+ * Why device-bridge owns this: cookies are a device-level capability (tied
12
+ * to the platform's shared HTTP stack). Plugins should consume a clean
13
+ * abstraction; the native wiring lives here.
14
+ */
15
+
16
+ import CookieManager from '@react-native-cookies/cookies';
17
+
18
+ export interface DeviceCookie {
19
+ name: string;
20
+ value: string;
21
+ domain?: string;
22
+ path?: string;
23
+ /** Unix seconds. Omitted for session cookies. */
24
+ expires?: number;
25
+ httpOnly?: boolean;
26
+ secure?: boolean;
27
+ sameSite?: 'Lax' | 'Strict' | 'None';
28
+ }
29
+
30
+ /**
31
+ * Get every cookie attached to the URL's effective domain.
32
+ * Set `includeHttpOnly` true to include HttpOnly cookies (required for
33
+ * LinkedIn/Google session tokens).
34
+ */
35
+ export async function get(url: string, includeHttpOnly: boolean = true): Promise<DeviceCookie[]> {
36
+ const raw = (await CookieManager.get(url, includeHttpOnly)) as Record<string, any>;
37
+ const out: DeviceCookie[] = [];
38
+ for (const [name, c] of Object.entries(raw || {})) {
39
+ if (!c || typeof c !== 'object') continue;
40
+ const value = typeof c.value === 'string' ? c.value : String(c.value ?? '');
41
+ const cookie: DeviceCookie = { name, value };
42
+ if (c.domain) cookie.domain = String(c.domain);
43
+ if (c.path) cookie.path = String(c.path);
44
+ if (c.expires) {
45
+ const t = Date.parse(c.expires);
46
+ if (!Number.isNaN(t)) cookie.expires = Math.floor(t / 1000);
47
+ }
48
+ if (typeof c.httpOnly === 'boolean') cookie.httpOnly = c.httpOnly;
49
+ if (typeof c.secure === 'boolean') cookie.secure = c.secure;
50
+ if (c.sameSite === 'Lax' || c.sameSite === 'Strict' || c.sameSite === 'None') {
51
+ cookie.sameSite = c.sameSite;
52
+ }
53
+ out.push(cookie);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Wipe the shared cookie jar. Cross-platform scoped clear isn't supported
60
+ * uniformly; `clearAll` is the safe operation on both iOS and Android.
61
+ */
62
+ export async function clearAll(): Promise<{ ok: boolean; error?: string }> {
63
+ try {
64
+ await CookieManager.clearAll(true);
65
+ return { ok: true };
66
+ } catch (e: any) {
67
+ return { ok: false, error: String(e?.message || e) };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Set a cookie programmatically. Useful when replaying a saved session on
73
+ * the device's shared jar before opening a WebView.
74
+ */
75
+ export async function set(url: string, cookie: DeviceCookie): Promise<{ ok: boolean; error?: string }> {
76
+ try {
77
+ await CookieManager.set(url, {
78
+ name: cookie.name,
79
+ value: cookie.value,
80
+ domain: cookie.domain,
81
+ path: cookie.path || '/',
82
+ httpOnly: cookie.httpOnly,
83
+ secure: cookie.secure,
84
+ } as any, true);
85
+ return { ok: true };
86
+ } catch (e: any) {
87
+ return { ok: false, error: String(e?.message || e) };
88
+ }
89
+ }
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Device Bridge — Dynamic Connector
3
+ *
4
+ * Pure JavaScript implementation (zero native imports).
5
+ * Uses bridge APIs for all device operations.
6
+ * Can be downloaded and executed at runtime.
7
+ *
8
+ * This file compiles to pure JS that gets bundled as device-bridge-connector.js
9
+ */
10
+
11
+ // Structural Bridge type. The old import pointed at '@ateam-mobile/bridge',
12
+ // a package that does not exist here — it resolved to `any` and hid whatever
13
+ // this file actually needs from the host.
14
+ type Bridge = Record<string, any>;
15
+
16
+ interface PendingAction {
17
+ id: string;
18
+ type: string;
19
+ params: Record<string, any>;
20
+ created_at: string;
21
+ }
22
+
23
+ interface ActionResult {
24
+ id: string;
25
+ ok: boolean;
26
+ result?: Record<string, any>;
27
+ error?: string;
28
+ }
29
+
30
+ interface RelayPushResult {
31
+ ok: boolean;
32
+ error?: string;
33
+ }
34
+
35
+ /**
36
+ * Main dynamic connector export
37
+ */
38
+ export default {
39
+ id: 'device-bridge',
40
+ name: 'Device Bridge',
41
+ version: '1.0.0',
42
+
43
+ // State (persisted across syncs)
44
+ _config: {} as Record<string, string>,
45
+ // ReturnType<typeof setInterval>, not NodeJS.Timeout: this runs in React
46
+ // Native, where setInterval returns a number. @types/node is present in the
47
+ // workspace and made the Node type look right, so the annotation disagreed
48
+ // with every value ever assigned to it.
49
+ _syncTimers: new Map<string, ReturnType<typeof setInterval>>(),
50
+ _actionPollTimer: undefined as ReturnType<typeof setInterval> | undefined,
51
+ _lastSync: new Map<string, string>(),
52
+ _isRunning: false,
53
+
54
+ // ─────────────────────────────────────────────────────────────────────
55
+ // LIFECYCLE: onStart — Request permissions and start syncing
56
+ // ─────────────────────────────────────────────────────────────────────
57
+
58
+ async onStart(bridge: Bridge, config: Record<string, string>) {
59
+ this._config = config;
60
+ this._isRunning = true;
61
+
62
+ bridge.log.info('device-bridge starting', {
63
+ relay: config.relay_url,
64
+ deviceId: config.device_id,
65
+ });
66
+
67
+ // Request permissions for all capabilities
68
+ const capabilities = ['calendar', 'contacts', 'location', 'notifications'];
69
+ for (const cap of capabilities) {
70
+ try {
71
+ const granted = await bridge.permissions.request(cap);
72
+ bridge.log.info(`Permission requested: ${cap} = ${granted ? 'granted' : 'denied'}`);
73
+ } catch (err) {
74
+ bridge.log.warn(`Permission request failed for ${cap}:`, err);
75
+ }
76
+ }
77
+
78
+ // Do initial sync
79
+ await this.onSync!(bridge);
80
+
81
+ // Start periodic sync
82
+ this._setupSyncTimers(bridge);
83
+
84
+ // Start action polling
85
+ this._startActionPoll(bridge);
86
+
87
+ bridge.log.info('device-bridge started');
88
+ },
89
+
90
+ // ─────────────────────────────────────────────────────────────────────
91
+ // LIFECYCLE: onSync — Collect all device data and push to relay
92
+ // ─────────────────────────────────────────────────────────────────────
93
+
94
+ async onSync(bridge: Bridge) {
95
+ if (!this._isRunning) return;
96
+
97
+ bridge.log.info('device-bridge syncing');
98
+
99
+ try {
100
+ // Collect all domains in parallel
101
+ const results = await Promise.allSettled([
102
+ this._collectCalendar(bridge),
103
+ this._collectContacts(bridge),
104
+ this._collectLocation(bridge),
105
+ this._collectBattery(bridge),
106
+ this._collectNetwork(bridge),
107
+ this._collectNotifications(bridge),
108
+ ]);
109
+
110
+ // Push each domain to relay
111
+ const domains = [
112
+ { name: 'calendar', result: results[0] },
113
+ { name: 'contacts', result: results[1] },
114
+ { name: 'location', result: results[2] },
115
+ { name: 'battery', result: results[3] },
116
+ { name: 'connectivity', result: results[4] },
117
+ { name: 'notifications', result: results[5] },
118
+ ];
119
+
120
+ for (const { name, result } of domains) {
121
+ if (result.status !== 'fulfilled' || !result.value) {
122
+ bridge.log.warn(`Skipping ${name} (collection failed)`);
123
+ continue;
124
+ }
125
+
126
+ const pushResult = await this._pushDomain(bridge, name, result.value);
127
+ if (pushResult.ok) {
128
+ this._lastSync.set(name, new Date().toISOString());
129
+ bridge.log.info(`Synced ${name}`);
130
+ } else {
131
+ bridge.log.error(`Failed to sync ${name}: ${pushResult.error}`);
132
+ }
133
+ }
134
+ } catch (err) {
135
+ bridge.log.error('Sync failed', err);
136
+ }
137
+ },
138
+
139
+ // ─────────────────────────────────────────────────────────────────────
140
+ // LIFECYCLE: onAction — Execute inbound actions from relay
141
+ // ─────────────────────────────────────────────────────────────────────
142
+
143
+ async onAction(bridge: Bridge, action: PendingAction): Promise<ActionResult> {
144
+ try {
145
+ bridge.log.info(`Executing action: ${action.type}`, action.params);
146
+
147
+ switch (action.type) {
148
+ // Messaging
149
+ case 'messaging.send': {
150
+ const { phone, message } = action.params;
151
+ const result = await bridge.sms.send(phone, message);
152
+ return {
153
+ id: action.id,
154
+ ok: result.ok,
155
+ result: { status: result.status },
156
+ };
157
+ }
158
+
159
+ // Calendar — Create
160
+ case 'calendar.create': {
161
+ const { title, date, start_time, end_time, location, notes } = action.params;
162
+ const result = await bridge.calendar.create({
163
+ title,
164
+ date,
165
+ start_time,
166
+ end_time: end_time || undefined,
167
+ location: location || undefined,
168
+ notes: notes || undefined,
169
+ });
170
+ return {
171
+ id: action.id,
172
+ ok: true,
173
+ result,
174
+ };
175
+ }
176
+
177
+ // Calendar — Reschedule
178
+ case 'calendar.reschedule': {
179
+ const result = await bridge.calendar.reschedule(action.params as any);
180
+ return {
181
+ id: action.id,
182
+ ok: result.ok,
183
+ };
184
+ }
185
+
186
+ // Calendar — Cancel
187
+ case 'calendar.cancel': {
188
+ const { event_id } = action.params;
189
+ const result = await bridge.calendar.cancel(event_id);
190
+ return {
191
+ id: action.id,
192
+ ok: result.ok,
193
+ };
194
+ }
195
+
196
+ // Navigation
197
+ case 'navigation.start': {
198
+ const { destination, mode } = action.params;
199
+ const result = await bridge.maps.navigate(destination, mode || 'driving');
200
+ return {
201
+ id: action.id,
202
+ ok: result.ok,
203
+ result: { status: result.status },
204
+ };
205
+ }
206
+
207
+ // DND — Store as preference (app-level, not system-level)
208
+ case 'dnd.set': {
209
+ await bridge.storage.set('dnd-state', JSON.stringify(action.params));
210
+ return {
211
+ id: action.id,
212
+ ok: true,
213
+ result: { stored: true },
214
+ };
215
+ }
216
+
217
+ default:
218
+ return {
219
+ id: action.id,
220
+ ok: false,
221
+ error: `Unknown action type: ${action.type}`,
222
+ };
223
+ }
224
+ } catch (err) {
225
+ const message = err instanceof Error ? err.message : String(err);
226
+ return {
227
+ id: action.id,
228
+ ok: false,
229
+ error: message,
230
+ };
231
+ }
232
+ },
233
+
234
+ // ─────────────────────────────────────────────────────────────────────
235
+ // LIFECYCLE: onStop — Cleanup
236
+ // ─────────────────────────────────────────────────────────────────────
237
+
238
+ async onStop(bridge: Bridge) {
239
+ bridge.log.info('device-bridge stopping');
240
+ this._isRunning = false;
241
+
242
+ // Clear all timers
243
+ for (const timer of this._syncTimers.values()) {
244
+ clearInterval(timer);
245
+ }
246
+ this._syncTimers.clear();
247
+
248
+ if (this._actionPollTimer) {
249
+ clearInterval(this._actionPollTimer);
250
+ this._actionPollTimer = undefined;
251
+ }
252
+
253
+ bridge.log.info('device-bridge stopped');
254
+ },
255
+
256
+ // ─────────────────────────────────────────────────────────────────────
257
+ // LIFECYCLE: onForeground — Immediate sync on app wake
258
+ // ─────────────────────────────────────────────────────────────────────
259
+
260
+ async onForeground(bridge: Bridge) {
261
+ bridge.log.info('device-bridge foreground — immediate sync');
262
+ await this.onSync!(bridge);
263
+ },
264
+
265
+ // ─────────────────────────────────────────────────────────────────────
266
+ // INTERNAL: Data Collection (uses bridge APIs only)
267
+ // ─────────────────────────────────────────────────────────────────────
268
+
269
+ async _collectCalendar(bridge: Bridge) {
270
+ try {
271
+ const [today, upcoming] = await Promise.all([
272
+ bridge.calendar.getToday(),
273
+ bridge.calendar.getUpcoming(7),
274
+ ]);
275
+ return { today, upcoming };
276
+ } catch (err) {
277
+ return null;
278
+ }
279
+ },
280
+
281
+ async _collectContacts(bridge: Bridge) {
282
+ try {
283
+ const result = await bridge.contacts.getAll();
284
+ return { ok: true, contacts: result.contacts, count: result.count };
285
+ } catch (err) {
286
+ return null;
287
+ }
288
+ },
289
+
290
+ async _collectLocation(bridge: Bridge) {
291
+ try {
292
+ return await bridge.location.getCurrent();
293
+ } catch (err) {
294
+ return null;
295
+ }
296
+ },
297
+
298
+ async _collectBattery(bridge: Bridge) {
299
+ try {
300
+ return await bridge.battery.getStatus();
301
+ } catch (err) {
302
+ return null;
303
+ }
304
+ },
305
+
306
+ async _collectNetwork(bridge: Bridge) {
307
+ try {
308
+ return await bridge.network.getStatus();
309
+ } catch (err) {
310
+ return null;
311
+ }
312
+ },
313
+
314
+ async _collectNotifications(bridge: Bridge) {
315
+ try {
316
+ const result = await bridge.notifications.list();
317
+ return result;
318
+ } catch (err) {
319
+ return null;
320
+ }
321
+ },
322
+
323
+ // ─────────────────────────────────────────────────────────────────────
324
+ // INTERNAL: Relay Communication (uses bridge.http only)
325
+ // ─────────────────────────────────────────────────────────────────────
326
+
327
+ async _pushDomain(bridge: Bridge, domain: string, data: any): Promise<RelayPushResult> {
328
+ try {
329
+ const url = `${this._config.relay_url}/api/device/${this._config.device_id}/sync/${domain}`;
330
+ const response = await bridge.http.post(
331
+ url,
332
+ { data, timestamp: new Date().toISOString() },
333
+ { 'X-Device-Key': this._config.api_key }
334
+ );
335
+
336
+ if (!response.ok) {
337
+ return { ok: false, error: `HTTP ${response.status}` };
338
+ }
339
+
340
+ return { ok: true };
341
+ } catch (err) {
342
+ const message = err instanceof Error ? err.message : String(err);
343
+ return { ok: false, error: message };
344
+ }
345
+ },
346
+
347
+ async _getActions(bridge: Bridge): Promise<PendingAction[]> {
348
+ try {
349
+ const url = `${this._config.relay_url}/api/device/${this._config.device_id}/actions`;
350
+ const response = await bridge.http.get(url, {
351
+ 'X-Device-Key': this._config.api_key,
352
+ });
353
+
354
+ if (!response.ok || !response.data?.actions) {
355
+ return [];
356
+ }
357
+
358
+ return response.data.actions.filter((a: any) => a.status === 'pending');
359
+ } catch (err) {
360
+ return [];
361
+ }
362
+ },
363
+
364
+ async _reportActionResult(
365
+ bridge: Bridge,
366
+ result: ActionResult
367
+ ): Promise<void> {
368
+ try {
369
+ const url = `${this._config.relay_url}/api/device/${this._config.device_id}/actions/${result.id}`;
370
+ await bridge.http.delete(
371
+ url,
372
+ { ...result, executed_at: new Date().toISOString() },
373
+ { 'X-Device-Key': this._config.api_key }
374
+ );
375
+ } catch (err) {
376
+ // Silently fail — action was executed, just couldn't report back
377
+ }
378
+ },
379
+
380
+ // ─────────────────────────────────────────────────────────────────────
381
+ // INTERNAL: Timers and Polling
382
+ // ─────────────────────────────────────────────────────────────────────
383
+
384
+ _setupSyncTimers(bridge: Bridge) {
385
+ const intervals = this._config.sync_intervals
386
+ ? JSON.parse(this._config.sync_intervals)
387
+ : {};
388
+
389
+ // Default intervals (ms)
390
+ const defaultIntervals: Record<string, number> = {
391
+ calendar: 60_000,
392
+ contacts: 300_000,
393
+ location: 30_000,
394
+ battery: 60_000,
395
+ connectivity: 30_000,
396
+ notifications: 15_000,
397
+ };
398
+
399
+ for (const [domain, defaultInterval] of Object.entries(defaultIntervals)) {
400
+ const interval = intervals[domain] || defaultInterval;
401
+ const timer = setInterval(() => this.onSync!(bridge), interval);
402
+ this._syncTimers.set(domain, timer);
403
+ }
404
+ },
405
+
406
+ _startActionPoll(bridge: Bridge) {
407
+ const pollInterval = 5_000; // 5 seconds
408
+
409
+ this._actionPollTimer = setInterval(async () => {
410
+ try {
411
+ const actions = await this._getActions(bridge);
412
+
413
+ for (const action of actions) {
414
+ const result = await this.onAction!(bridge, action);
415
+ await this._reportActionResult(bridge, result);
416
+ }
417
+ } catch (err) {
418
+ bridge.log.warn('Action poll failed', err);
419
+ }
420
+ }, pollInterval);
421
+ },
422
+ };