@elizaos/plugin-facewear 2.0.3-beta.6 → 2.0.3-beta.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 (191) hide show
  1. package/dist/actions/display-text.d.ts +4 -0
  2. package/dist/actions/display-text.d.ts.map +1 -0
  3. package/dist/actions/display-text.js +90 -0
  4. package/dist/actions/display-text.js.map +1 -0
  5. package/dist/actions/facewear-connect.d.ts +3 -0
  6. package/dist/actions/facewear-connect.d.ts.map +1 -0
  7. package/dist/actions/facewear-connect.js +70 -0
  8. package/dist/actions/facewear-connect.js.map +1 -0
  9. package/dist/actions/facewear-control.d.ts +4 -0
  10. package/dist/actions/facewear-control.d.ts.map +1 -0
  11. package/dist/actions/facewear-control.js +627 -0
  12. package/dist/actions/facewear-control.js.map +1 -0
  13. package/dist/actions/facewear-debug.d.ts +3 -0
  14. package/dist/actions/facewear-debug.d.ts.map +1 -0
  15. package/dist/actions/facewear-debug.js +62 -0
  16. package/dist/actions/facewear-debug.js.map +1 -0
  17. package/dist/actions/facewear-status.d.ts +4 -0
  18. package/dist/actions/facewear-status.d.ts.map +1 -0
  19. package/dist/actions/facewear-status.js +66 -0
  20. package/dist/actions/facewear-status.js.map +1 -0
  21. package/dist/actions/microphone.d.ts +4 -0
  22. package/dist/actions/microphone.d.ts.map +1 -0
  23. package/dist/actions/microphone.js +63 -0
  24. package/dist/actions/microphone.js.map +1 -0
  25. package/dist/actions/view-actions.d.ts +23 -0
  26. package/dist/actions/view-actions.d.ts.map +1 -0
  27. package/dist/actions/view-actions.js +314 -0
  28. package/dist/actions/view-actions.js.map +1 -0
  29. package/dist/actions/vision-query.d.ts +4 -0
  30. package/dist/actions/vision-query.d.ts.map +1 -0
  31. package/dist/actions/vision-query.js +41 -0
  32. package/dist/actions/vision-query.js.map +1 -0
  33. package/dist/actions/xr-view-actions.d.ts +14 -0
  34. package/dist/actions/xr-view-actions.d.ts.map +1 -0
  35. package/dist/actions/xr-view-actions.js +29 -0
  36. package/dist/actions/xr-view-actions.js.map +1 -0
  37. package/dist/components/FacewearSpatialView.d.ts +50 -0
  38. package/dist/components/FacewearSpatialView.d.ts.map +1 -0
  39. package/dist/components/FacewearSpatialView.js +129 -0
  40. package/dist/components/FacewearSpatialView.js.map +1 -0
  41. package/dist/components/FacewearView.d.ts +17 -0
  42. package/dist/components/FacewearView.d.ts.map +1 -0
  43. package/dist/components/FacewearView.js +88 -0
  44. package/dist/components/FacewearView.js.map +1 -0
  45. package/dist/components/SmartglassesPanelView.d.ts +22 -0
  46. package/dist/components/SmartglassesPanelView.d.ts.map +1 -0
  47. package/dist/components/SmartglassesPanelView.js +140 -0
  48. package/dist/components/SmartglassesPanelView.js.map +1 -0
  49. package/dist/components/SmartglassesSpatialView.d.ts +46 -0
  50. package/dist/components/SmartglassesSpatialView.d.ts.map +1 -0
  51. package/dist/components/SmartglassesSpatialView.js +240 -0
  52. package/dist/components/SmartglassesSpatialView.js.map +1 -0
  53. package/dist/components/facewear-profiles.d.ts +27 -0
  54. package/dist/components/facewear-profiles.d.ts.map +1 -0
  55. package/dist/components/facewear-profiles.js +40 -0
  56. package/dist/components/facewear-profiles.js.map +1 -0
  57. package/dist/devices/apple-vision-pro.d.ts +7 -0
  58. package/dist/devices/apple-vision-pro.d.ts.map +1 -0
  59. package/dist/devices/apple-vision-pro.js +21 -0
  60. package/dist/devices/apple-vision-pro.js.map +1 -0
  61. package/dist/devices/even-realities.d.ts +7 -0
  62. package/dist/devices/even-realities.d.ts.map +1 -0
  63. package/dist/devices/even-realities.js +13 -0
  64. package/dist/devices/even-realities.js.map +1 -0
  65. package/dist/devices/meta-quest.d.ts +5 -0
  66. package/dist/devices/meta-quest.d.ts.map +1 -0
  67. package/dist/devices/meta-quest.js +21 -0
  68. package/dist/devices/meta-quest.js.map +1 -0
  69. package/dist/devices/registry.d.ts +19 -0
  70. package/dist/devices/registry.d.ts.map +1 -0
  71. package/dist/devices/registry.js +96 -0
  72. package/dist/devices/registry.js.map +1 -0
  73. package/dist/devices/xreal.d.ts +7 -0
  74. package/dist/devices/xreal.d.ts.map +1 -0
  75. package/dist/devices/xreal.js +19 -0
  76. package/dist/devices/xreal.js.map +1 -0
  77. package/dist/index.d.ts +28 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +260 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/protocol/smartglasses.d.ts +306 -0
  82. package/dist/protocol/smartglasses.d.ts.map +1 -0
  83. package/dist/protocol/smartglasses.js +1485 -0
  84. package/dist/protocol/smartglasses.js.map +1 -0
  85. package/dist/protocol/xr.d.ts +137 -0
  86. package/dist/protocol/xr.d.ts.map +1 -0
  87. package/dist/protocol/xr.js +18 -0
  88. package/dist/protocol/xr.js.map +1 -0
  89. package/dist/providers/facewear-context.d.ts +3 -0
  90. package/dist/providers/facewear-context.d.ts.map +1 -0
  91. package/dist/providers/facewear-context.js +59 -0
  92. package/dist/providers/facewear-context.js.map +1 -0
  93. package/dist/providers/smartglasses-status.d.ts +3 -0
  94. package/dist/providers/smartglasses-status.d.ts.map +1 -0
  95. package/dist/providers/smartglasses-status.js +33 -0
  96. package/dist/providers/smartglasses-status.js.map +1 -0
  97. package/dist/register-terminal-view.d.ts +21 -0
  98. package/dist/register-terminal-view.d.ts.map +1 -0
  99. package/dist/register-terminal-view.js +70 -0
  100. package/dist/register-terminal-view.js.map +1 -0
  101. package/dist/register.d.ts +8 -0
  102. package/dist/register.d.ts.map +1 -0
  103. package/dist/register.js +116 -0
  104. package/dist/register.js.map +1 -0
  105. package/dist/routes/connect.d.ts +3 -0
  106. package/dist/routes/connect.d.ts.map +1 -0
  107. package/dist/routes/connect.js +86 -0
  108. package/dist/routes/connect.js.map +1 -0
  109. package/dist/routes/device-config.d.ts +5 -0
  110. package/dist/routes/device-config.d.ts.map +1 -0
  111. package/dist/routes/device-config.js +56 -0
  112. package/dist/routes/device-config.js.map +1 -0
  113. package/dist/routes/simulator-route.d.ts +8 -0
  114. package/dist/routes/simulator-route.d.ts.map +1 -0
  115. package/dist/routes/simulator-route.js +32 -0
  116. package/dist/routes/simulator-route.js.map +1 -0
  117. package/dist/routes/status.d.ts +3 -0
  118. package/dist/routes/status.d.ts.map +1 -0
  119. package/dist/routes/status.js +34 -0
  120. package/dist/routes/status.js.map +1 -0
  121. package/dist/routes/view-host.d.ts +24 -0
  122. package/dist/routes/view-host.d.ts.map +1 -0
  123. package/dist/routes/view-host.js +339 -0
  124. package/dist/routes/view-host.js.map +1 -0
  125. package/dist/routes/views.d.ts +8 -0
  126. package/dist/routes/views.d.ts.map +1 -0
  127. package/dist/routes/views.js +31 -0
  128. package/dist/routes/views.js.map +1 -0
  129. package/dist/services/audio-pipeline.d.ts +20 -0
  130. package/dist/services/audio-pipeline.d.ts.map +1 -0
  131. package/dist/services/audio-pipeline.js +87 -0
  132. package/dist/services/audio-pipeline.js.map +1 -0
  133. package/dist/services/facewear-service.d.ts +26 -0
  134. package/dist/services/facewear-service.d.ts.map +1 -0
  135. package/dist/services/facewear-service.js +45 -0
  136. package/dist/services/facewear-service.js.map +1 -0
  137. package/dist/services/smartglasses-service.d.ts +244 -0
  138. package/dist/services/smartglasses-service.d.ts.map +1 -0
  139. package/dist/services/smartglasses-service.js +821 -0
  140. package/dist/services/smartglasses-service.js.map +1 -0
  141. package/dist/services/vision-pipeline.d.ts +16 -0
  142. package/dist/services/vision-pipeline.d.ts.map +1 -0
  143. package/dist/services/vision-pipeline.js +39 -0
  144. package/dist/services/vision-pipeline.js.map +1 -0
  145. package/dist/services/xr-session-service.d.ts +54 -0
  146. package/dist/services/xr-session-service.d.ts.map +1 -0
  147. package/dist/services/xr-session-service.js +345 -0
  148. package/dist/services/xr-session-service.js.map +1 -0
  149. package/dist/status-format.d.ts +15 -0
  150. package/dist/status-format.d.ts.map +1 -0
  151. package/dist/status-format.js +89 -0
  152. package/dist/status-format.js.map +1 -0
  153. package/dist/transport/even-bridge.d.ts +69 -0
  154. package/dist/transport/even-bridge.d.ts.map +1 -0
  155. package/dist/transport/even-bridge.js +510 -0
  156. package/dist/transport/even-bridge.js.map +1 -0
  157. package/dist/transport/mock.d.ts +42 -0
  158. package/dist/transport/mock.d.ts.map +1 -0
  159. package/dist/transport/mock.js +124 -0
  160. package/dist/transport/mock.js.map +1 -0
  161. package/dist/transport/noble.d.ts +62 -0
  162. package/dist/transport/noble.d.ts.map +1 -0
  163. package/dist/transport/noble.js +256 -0
  164. package/dist/transport/noble.js.map +1 -0
  165. package/dist/transport/types.d.ts +36 -0
  166. package/dist/transport/types.d.ts.map +1 -0
  167. package/dist/transport/types.js +1 -0
  168. package/dist/transport/types.js.map +1 -0
  169. package/dist/transport/web-bluetooth.d.ts +58 -0
  170. package/dist/transport/web-bluetooth.d.ts.map +1 -0
  171. package/dist/transport/web-bluetooth.js +164 -0
  172. package/dist/transport/web-bluetooth.js.map +1 -0
  173. package/dist/ui/FacewearAppView.d.ts +4 -0
  174. package/dist/ui/FacewearAppView.d.ts.map +1 -0
  175. package/dist/ui/FacewearAppView.js +257 -0
  176. package/dist/ui/FacewearAppView.js.map +1 -0
  177. package/dist/ui/SmartglassesView.d.ts +10 -0
  178. package/dist/ui/SmartglassesView.d.ts.map +1 -0
  179. package/dist/ui/SmartglassesView.helpers.d.ts +104 -0
  180. package/dist/ui/SmartglassesView.helpers.d.ts.map +1 -0
  181. package/dist/ui/SmartglassesView.helpers.js +261 -0
  182. package/dist/ui/SmartglassesView.helpers.js.map +1 -0
  183. package/dist/ui/SmartglassesView.js +1189 -0
  184. package/dist/ui/SmartglassesView.js.map +1 -0
  185. package/dist/ui/facewear-view-bundle.d.ts +5 -0
  186. package/dist/ui/facewear-view-bundle.d.ts.map +1 -0
  187. package/dist/ui/facewear-view-bundle.js +17 -0
  188. package/dist/ui/facewear-view-bundle.js.map +1 -0
  189. package/dist/views/bundle.js +2950 -0
  190. package/dist/views/bundle.js.map +1 -0
  191. package/package.json +5 -5
@@ -0,0 +1,339 @@
1
+ const viewHostRoute = {
2
+ type: "GET",
3
+ path: "/xr/view-host/:id",
4
+ description: "Serves a self-contained XR-friendly HTML host page for a registered view",
5
+ routeHandler: async (ctx) => {
6
+ const viewId = ctx.params?.id ?? "";
7
+ if (!viewId) {
8
+ return { status: 400, body: { error: "Missing view id" } };
9
+ }
10
+ const agentPort = ctx.runtime.port ?? 31337;
11
+ const agentOrigin = process.env.XR_AGENT_URL ?? `http://localhost:${agentPort}`;
12
+ const bundleUrl = `${agentOrigin}/api/views/${viewId}/bundle.js`;
13
+ const viewsApiUrl = `${agentOrigin}/api/views`;
14
+ const html = buildHostPage(viewId, bundleUrl, viewsApiUrl, agentOrigin);
15
+ return {
16
+ status: 200,
17
+ headers: {
18
+ "Content-Type": "text/html; charset=utf-8",
19
+ // Relax CSP for dynamic imports of view bundles (same agent origin only)
20
+ "Content-Security-Policy": `default-src 'self' ${agentOrigin} https://esm.sh https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' 'unsafe-eval' ${agentOrigin} https://esm.sh https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; connect-src 'self' ${agentOrigin} ws://localhost:*;`
21
+ },
22
+ body: html
23
+ };
24
+ }
25
+ };
26
+ function buildHostPage(viewId, bundleUrl, viewsApiUrl, agentOrigin) {
27
+ return `<!DOCTYPE html>
28
+ <html lang="en" data-view-id="${viewId}">
29
+ <head>
30
+ <meta charset="utf-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
32
+ <title>XR \u2013 ${viewId}</title>
33
+ <style>
34
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
35
+
36
+ :root {
37
+ --bg: #0d0d0f;
38
+ --surface: #18181b;
39
+ --border: rgba(255,255,255,0.08);
40
+ --text: #f4f4f5;
41
+ --muted: #a1a1aa;
42
+ --accent: #6366f1;
43
+ --radius: 12px;
44
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
45
+ }
46
+
47
+ html, body {
48
+ width: 100%; height: 100%;
49
+ background: var(--bg);
50
+ color: var(--text);
51
+ font-family: var(--font);
52
+ font-size: 18px; /* large for XR readability */
53
+ line-height: 1.5;
54
+ overflow: hidden;
55
+ }
56
+
57
+ #xr-shell {
58
+ display: flex;
59
+ flex-direction: column;
60
+ height: 100%;
61
+ position: relative;
62
+ }
63
+
64
+ /* XR header bar */
65
+ #xr-bar {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 10px;
69
+ padding: 8px 16px;
70
+ background: var(--surface);
71
+ border-bottom: 1px solid var(--border);
72
+ flex-shrink: 0;
73
+ user-select: none;
74
+ }
75
+ #xr-bar-title {
76
+ flex: 1;
77
+ font-weight: 600;
78
+ font-size: 1rem;
79
+ color: var(--text);
80
+ }
81
+ .xr-btn {
82
+ background: var(--border);
83
+ border: none;
84
+ border-radius: 8px;
85
+ color: var(--text);
86
+ cursor: pointer;
87
+ font-size: 1rem;
88
+ padding: 6px 12px;
89
+ transition: background 0.15s;
90
+ }
91
+ .xr-btn:hover { background: rgba(255,255,255,0.15) }
92
+
93
+ /* Voice indicator */
94
+ #voice-indicator {
95
+ display: none;
96
+ align-items: center;
97
+ gap: 6px;
98
+ padding: 4px 10px;
99
+ border-radius: 20px;
100
+ background: var(--accent);
101
+ font-size: 0.8rem;
102
+ font-weight: 600;
103
+ }
104
+ #voice-indicator.active { display: flex }
105
+ .voice-dot {
106
+ width: 8px; height: 8px;
107
+ border-radius: 50%;
108
+ background: #fff;
109
+ animation: pulse 1s ease-in-out infinite;
110
+ }
111
+ @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.4 } }
112
+
113
+ /* Content area */
114
+ #view-mount {
115
+ flex: 1;
116
+ overflow: auto;
117
+ position: relative;
118
+ }
119
+
120
+ /* Loading / error states */
121
+ #view-loader {
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: center;
125
+ justify-content: center;
126
+ height: 100%;
127
+ gap: 12px;
128
+ color: var(--muted);
129
+ }
130
+ .spinner {
131
+ width: 36px; height: 36px;
132
+ border: 3px solid var(--border);
133
+ border-top-color: var(--accent);
134
+ border-radius: 50%;
135
+ animation: spin 0.8s linear infinite;
136
+ }
137
+ @keyframes spin { to { transform: rotate(360deg) } }
138
+
139
+ /* XR-friendly form overrides for injected views */
140
+ #view-mount input, #view-mount textarea, #view-mount select {
141
+ font-size: 1rem !important;
142
+ min-height: 44px;
143
+ }
144
+ #view-mount button {
145
+ min-height: 44px;
146
+ min-width: 44px;
147
+ }
148
+
149
+ /* Transcript toast */
150
+ #transcript-toast {
151
+ position: fixed;
152
+ bottom: 12px; left: 50%;
153
+ transform: translateX(-50%);
154
+ background: rgba(99,102,241,0.9);
155
+ color: #fff;
156
+ border-radius: 20px;
157
+ padding: 6px 16px;
158
+ font-size: 0.85rem;
159
+ pointer-events: none;
160
+ opacity: 0;
161
+ transition: opacity 0.2s;
162
+ white-space: nowrap;
163
+ max-width: 90vw;
164
+ overflow: hidden;
165
+ text-overflow: ellipsis;
166
+ }
167
+ #transcript-toast.show { opacity: 1 }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <div id="xr-shell">
172
+ <div id="xr-bar">
173
+ <span id="xr-bar-title">${viewId}</span>
174
+ <div id="voice-indicator">
175
+ <div class="voice-dot"></div>
176
+ <span>Listening</span>
177
+ </div>
178
+ <button class="xr-btn" id="btn-close" title="Close panel">\u2715</button>
179
+ </div>
180
+
181
+ <div id="view-mount">
182
+ <div id="view-loader">
183
+ <div class="spinner"></div>
184
+ <span>Loading ${viewId}\u2026</span>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <div id="transcript-toast"></div>
190
+
191
+ <!-- React from CDN \u2014 must match the version view bundles are built against -->
192
+ <script type="importmap">
193
+ {
194
+ "imports": {
195
+ "react": "https://esm.sh/react@18",
196
+ "react-dom": "https://esm.sh/react-dom@18",
197
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
198
+ "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime"
199
+ }
200
+ }
201
+ </script>
202
+
203
+ <script type="module">
204
+ const VIEW_ID = "${viewId}";
205
+ const BUNDLE_URL = "${bundleUrl}";
206
+ const VIEWS_API = "${viewsApiUrl}";
207
+ const AGENT_ORIGIN = "${agentOrigin}";
208
+
209
+ // \u2500\u2500 postMessage bridge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
210
+
211
+ function notifyParent(msg) {
212
+ window.parent.postMessage(msg, "*");
213
+ }
214
+
215
+ window.addEventListener("message", (ev) => {
216
+ if (ev.data?.type === "xr:transcript") fillFocusedInput(ev.data.text);
217
+ if (ev.data?.type === "xr:focus-next") focusNext();
218
+ if (ev.data?.type === "xr:voice-start") showVoiceIndicator(true);
219
+ if (ev.data?.type === "xr:voice-end") showVoiceIndicator(false);
220
+ });
221
+
222
+ // \u2500\u2500 Voice input helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
223
+
224
+ function fillFocusedInput(text) {
225
+ const el = document.activeElement;
226
+ if (!el) return;
227
+ const tag = el.tagName;
228
+ if (tag === "INPUT" || tag === "TEXTAREA") {
229
+ const native = Object.getOwnPropertyDescriptor(window[tag === "INPUT" ? "HTMLInputElement" : "HTMLTextAreaElement"].prototype, "value");
230
+ native.set.call(el, text);
231
+ el.dispatchEvent(new Event("input", { bubbles: true }));
232
+ el.dispatchEvent(new Event("change", { bubbles: true }));
233
+ showTranscript(text);
234
+ } else if (tag === "SELECT") {
235
+ // Voice select: find option whose text matches transcript (case-insensitive)
236
+ const select = /** @type {HTMLSelectElement} */ (el);
237
+ const lower = text.toLowerCase();
238
+ for (const opt of select.options) {
239
+ if (opt.text.toLowerCase().includes(lower) || opt.value.toLowerCase().includes(lower)) {
240
+ const native = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value");
241
+ native.set.call(select, opt.value);
242
+ select.dispatchEvent(new Event("change", { bubbles: true }));
243
+ showTranscript(text);
244
+ break;
245
+ }
246
+ }
247
+ } else {
248
+ // ARIA combobox / listbox \u2014 set aria-activedescendant or dispatch custom event
249
+ const role = el.getAttribute("role");
250
+ if (role === "combobox" || role === "listbox" || role === "searchbox") {
251
+ el.dispatchEvent(new CustomEvent("xr:transcript", { detail: { text }, bubbles: true }));
252
+ showTranscript(text);
253
+ }
254
+ }
255
+ }
256
+
257
+ function focusNext() {
258
+ const all = Array.from(document.querySelectorAll("input,textarea,button,select,[tabindex]"))
259
+ .filter(el => !el.disabled && !el.closest("[hidden]"));
260
+ const idx = all.indexOf(document.activeElement);
261
+ const next = all[idx + 1] ?? all[0];
262
+ next?.focus();
263
+ }
264
+
265
+ function showVoiceIndicator(active) {
266
+ const el = document.getElementById("voice-indicator");
267
+ if (el) el.classList.toggle("active", active);
268
+ }
269
+
270
+ function showTranscript(text) {
271
+ const toast = document.getElementById("transcript-toast");
272
+ if (!toast) return;
273
+ toast.textContent = text;
274
+ toast.classList.add("show");
275
+ clearTimeout(toast._t);
276
+ toast._t = setTimeout(() => toast.classList.remove("show"), 3000);
277
+ }
278
+
279
+ // \u2500\u2500 Close button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
280
+
281
+ document.getElementById("btn-close")?.addEventListener("click", () => {
282
+ notifyParent({ type: "xr:close", viewId: VIEW_ID });
283
+ });
284
+
285
+ // \u2500\u2500 Mount the view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
286
+
287
+ async function mountView() {
288
+ const mount = document.getElementById("view-mount");
289
+ const loader = document.getElementById("view-loader");
290
+
291
+ try {
292
+ // Provide minimal elizaOS-like context so views can render
293
+ window.__elizaXRContext = {
294
+ agentBaseUrl: AGENT_ORIGIN,
295
+ viewId: VIEW_ID,
296
+ fetchViews: () => fetch(VIEWS_API).then(r => r.json()),
297
+ navigate: (id) => notifyParent({ type: "xr:navigate", viewId: id }),
298
+ };
299
+
300
+ const mod = await import(/* @vite-ignore */ BUNDLE_URL);
301
+ const component = mod.default ?? mod[Object.keys(mod)[0]];
302
+
303
+ if (!component) throw new Error("No component export found in bundle");
304
+
305
+ // Dynamically import React + ReactDOM
306
+ const [React, ReactDOMClient] = await Promise.all([
307
+ import("react"),
308
+ import("react-dom/client"),
309
+ ]);
310
+
311
+ if (loader) loader.style.display = "none";
312
+
313
+ // Render the view component
314
+ const root = ReactDOMClient.createRoot(mount);
315
+ root.render(React.createElement(component));
316
+
317
+ notifyParent({ type: "xr:view-ready", viewId: VIEW_ID });
318
+
319
+ } catch (err) {
320
+ console.error("[xr-host] Failed to mount view:", err);
321
+ if (loader) loader.innerHTML =
322
+ \`<div style="color:#f87171;text-align:center;padding:24px">
323
+ <div style="font-size:1.5rem;margin-bottom:8px">\u26A0 Load error</div>
324
+ <div style="font-size:0.85rem;color:#a1a1aa">\${err.message}</div>
325
+ <button class="xr-btn" style="margin-top:16px" onclick="mountView()">Retry</button>
326
+ </div>\`;
327
+ notifyParent({ type: "xr:view-error", viewId: VIEW_ID, error: err.message });
328
+ }
329
+ }
330
+
331
+ mountView();
332
+ </script>
333
+ </body>
334
+ </html>`;
335
+ }
336
+ export {
337
+ viewHostRoute
338
+ };
339
+ //# sourceMappingURL=view-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/view-host.ts"],"sourcesContent":["import type { Route } from \"@elizaos/core\";\n\n/**\n * GET /api/xr/view-host/:id\n *\n * Serves a self-contained HTML page that loads and renders a registered\n * elizaOS view bundle inside an XR-optimised shell. App-xr opens this URL\n * in an iframe that is overlaid on the WebXR scene.\n *\n * The host page:\n * 1. Loads React + ReactDOM from the CDN (same versions as the view bundles).\n * 2. Dynamically imports the view bundle from the agent's /api/views/:id/bundle.js.\n * 3. Mounts the view component inside an XR-friendly dark-theme container.\n * 4. Provides a minimal elizaOS context (agentBaseUrl, viewId, postMessage bridge).\n * 5. Routes voice transcript text to the focused form input.\n *\n * Inter-frame communication (postMessage):\n * Parent → host: { type:\"xr:transcript\", text:\"...\" } — fill focused input\n * { type:\"xr:focus-next\" } — tab to next field\n * Host → parent: { type:\"xr:view-ready\", viewId:\"...\" }\n * { type:\"xr:navigate\", viewId:\"...\" }\n * { type:\"xr:close\" }\n */\nexport const viewHostRoute: Route = {\n type: \"GET\",\n path: \"/xr/view-host/:id\",\n description:\n \"Serves a self-contained XR-friendly HTML host page for a registered view\",\n routeHandler: async (ctx) => {\n const viewId = (ctx.params as Record<string, string>)?.id ?? \"\";\n if (!viewId) {\n return { status: 400, body: { error: \"Missing view id\" } };\n }\n\n // Resolve the agent origin so the page can load the bundle\n const agentPort = (ctx.runtime as { port?: number }).port ?? 31337;\n const agentOrigin =\n process.env.XR_AGENT_URL ?? `http://localhost:${agentPort}`;\n const bundleUrl = `${agentOrigin}/api/views/${viewId}/bundle.js`;\n const viewsApiUrl = `${agentOrigin}/api/views`;\n\n const html = buildHostPage(viewId, bundleUrl, viewsApiUrl, agentOrigin);\n\n return {\n status: 200,\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n // Relax CSP for dynamic imports of view bundles (same agent origin only)\n \"Content-Security-Policy\":\n `default-src 'self' ${agentOrigin} https://esm.sh https://cdn.jsdelivr.net; ` +\n `script-src 'self' 'unsafe-inline' 'unsafe-eval' ${agentOrigin} https://esm.sh https://cdn.jsdelivr.net; ` +\n `style-src 'self' 'unsafe-inline'; ` +\n `connect-src 'self' ${agentOrigin} ws://localhost:*;`,\n },\n body: html,\n };\n },\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction buildHostPage(\n viewId: string,\n bundleUrl: string,\n viewsApiUrl: string,\n agentOrigin: string,\n): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" data-view-id=\"${viewId}\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" />\n <title>XR – ${viewId}</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }\n\n :root {\n --bg: #0d0d0f;\n --surface: #18181b;\n --border: rgba(255,255,255,0.08);\n --text: #f4f4f5;\n --muted: #a1a1aa;\n --accent: #6366f1;\n --radius: 12px;\n --font: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n }\n\n html, body {\n width: 100%; height: 100%;\n background: var(--bg);\n color: var(--text);\n font-family: var(--font);\n font-size: 18px; /* large for XR readability */\n line-height: 1.5;\n overflow: hidden;\n }\n\n #xr-shell {\n display: flex;\n flex-direction: column;\n height: 100%;\n position: relative;\n }\n\n /* XR header bar */\n #xr-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n flex-shrink: 0;\n user-select: none;\n }\n #xr-bar-title {\n flex: 1;\n font-weight: 600;\n font-size: 1rem;\n color: var(--text);\n }\n .xr-btn {\n background: var(--border);\n border: none;\n border-radius: 8px;\n color: var(--text);\n cursor: pointer;\n font-size: 1rem;\n padding: 6px 12px;\n transition: background 0.15s;\n }\n .xr-btn:hover { background: rgba(255,255,255,0.15) }\n\n /* Voice indicator */\n #voice-indicator {\n display: none;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: 20px;\n background: var(--accent);\n font-size: 0.8rem;\n font-weight: 600;\n }\n #voice-indicator.active { display: flex }\n .voice-dot {\n width: 8px; height: 8px;\n border-radius: 50%;\n background: #fff;\n animation: pulse 1s ease-in-out infinite;\n }\n @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.4 } }\n\n /* Content area */\n #view-mount {\n flex: 1;\n overflow: auto;\n position: relative;\n }\n\n /* Loading / error states */\n #view-loader {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n gap: 12px;\n color: var(--muted);\n }\n .spinner {\n width: 36px; height: 36px;\n border: 3px solid var(--border);\n border-top-color: var(--accent);\n border-radius: 50%;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg) } }\n\n /* XR-friendly form overrides for injected views */\n #view-mount input, #view-mount textarea, #view-mount select {\n font-size: 1rem !important;\n min-height: 44px;\n }\n #view-mount button {\n min-height: 44px;\n min-width: 44px;\n }\n\n /* Transcript toast */\n #transcript-toast {\n position: fixed;\n bottom: 12px; left: 50%;\n transform: translateX(-50%);\n background: rgba(99,102,241,0.9);\n color: #fff;\n border-radius: 20px;\n padding: 6px 16px;\n font-size: 0.85rem;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s;\n white-space: nowrap;\n max-width: 90vw;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n #transcript-toast.show { opacity: 1 }\n </style>\n</head>\n<body>\n <div id=\"xr-shell\">\n <div id=\"xr-bar\">\n <span id=\"xr-bar-title\">${viewId}</span>\n <div id=\"voice-indicator\">\n <div class=\"voice-dot\"></div>\n <span>Listening</span>\n </div>\n <button class=\"xr-btn\" id=\"btn-close\" title=\"Close panel\">✕</button>\n </div>\n\n <div id=\"view-mount\">\n <div id=\"view-loader\">\n <div class=\"spinner\"></div>\n <span>Loading ${viewId}…</span>\n </div>\n </div>\n </div>\n\n <div id=\"transcript-toast\"></div>\n\n <!-- React from CDN — must match the version view bundles are built against -->\n <script type=\"importmap\">\n {\n \"imports\": {\n \"react\": \"https://esm.sh/react@18\",\n \"react-dom\": \"https://esm.sh/react-dom@18\",\n \"react-dom/client\": \"https://esm.sh/react-dom@18/client\",\n \"react/jsx-runtime\": \"https://esm.sh/react@18/jsx-runtime\"\n }\n }\n </script>\n\n <script type=\"module\">\n const VIEW_ID = \"${viewId}\";\n const BUNDLE_URL = \"${bundleUrl}\";\n const VIEWS_API = \"${viewsApiUrl}\";\n const AGENT_ORIGIN = \"${agentOrigin}\";\n\n // ── postMessage bridge ───────────────────────────────────────────────────\n\n function notifyParent(msg) {\n window.parent.postMessage(msg, \"*\");\n }\n\n window.addEventListener(\"message\", (ev) => {\n if (ev.data?.type === \"xr:transcript\") fillFocusedInput(ev.data.text);\n if (ev.data?.type === \"xr:focus-next\") focusNext();\n if (ev.data?.type === \"xr:voice-start\") showVoiceIndicator(true);\n if (ev.data?.type === \"xr:voice-end\") showVoiceIndicator(false);\n });\n\n // ── Voice input helpers ──────────────────────────────────────────────────\n\n function fillFocusedInput(text) {\n const el = document.activeElement;\n if (!el) return;\n const tag = el.tagName;\n if (tag === \"INPUT\" || tag === \"TEXTAREA\") {\n const native = Object.getOwnPropertyDescriptor(window[tag === \"INPUT\" ? \"HTMLInputElement\" : \"HTMLTextAreaElement\"].prototype, \"value\");\n native.set.call(el, text);\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n showTranscript(text);\n } else if (tag === \"SELECT\") {\n // Voice select: find option whose text matches transcript (case-insensitive)\n const select = /** @type {HTMLSelectElement} */ (el);\n const lower = text.toLowerCase();\n for (const opt of select.options) {\n if (opt.text.toLowerCase().includes(lower) || opt.value.toLowerCase().includes(lower)) {\n const native = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, \"value\");\n native.set.call(select, opt.value);\n select.dispatchEvent(new Event(\"change\", { bubbles: true }));\n showTranscript(text);\n break;\n }\n }\n } else {\n // ARIA combobox / listbox — set aria-activedescendant or dispatch custom event\n const role = el.getAttribute(\"role\");\n if (role === \"combobox\" || role === \"listbox\" || role === \"searchbox\") {\n el.dispatchEvent(new CustomEvent(\"xr:transcript\", { detail: { text }, bubbles: true }));\n showTranscript(text);\n }\n }\n }\n\n function focusNext() {\n const all = Array.from(document.querySelectorAll(\"input,textarea,button,select,[tabindex]\"))\n .filter(el => !el.disabled && !el.closest(\"[hidden]\"));\n const idx = all.indexOf(document.activeElement);\n const next = all[idx + 1] ?? all[0];\n next?.focus();\n }\n\n function showVoiceIndicator(active) {\n const el = document.getElementById(\"voice-indicator\");\n if (el) el.classList.toggle(\"active\", active);\n }\n\n function showTranscript(text) {\n const toast = document.getElementById(\"transcript-toast\");\n if (!toast) return;\n toast.textContent = text;\n toast.classList.add(\"show\");\n clearTimeout(toast._t);\n toast._t = setTimeout(() => toast.classList.remove(\"show\"), 3000);\n }\n\n // ── Close button ─────────────────────────────────────────────────────────\n\n document.getElementById(\"btn-close\")?.addEventListener(\"click\", () => {\n notifyParent({ type: \"xr:close\", viewId: VIEW_ID });\n });\n\n // ── Mount the view ───────────────────────────────────────────────────────\n\n async function mountView() {\n const mount = document.getElementById(\"view-mount\");\n const loader = document.getElementById(\"view-loader\");\n\n try {\n // Provide minimal elizaOS-like context so views can render\n window.__elizaXRContext = {\n agentBaseUrl: AGENT_ORIGIN,\n viewId: VIEW_ID,\n fetchViews: () => fetch(VIEWS_API).then(r => r.json()),\n navigate: (id) => notifyParent({ type: \"xr:navigate\", viewId: id }),\n };\n\n const mod = await import(/* @vite-ignore */ BUNDLE_URL);\n const component = mod.default ?? mod[Object.keys(mod)[0]];\n\n if (!component) throw new Error(\"No component export found in bundle\");\n\n // Dynamically import React + ReactDOM\n const [React, ReactDOMClient] = await Promise.all([\n import(\"react\"),\n import(\"react-dom/client\"),\n ]);\n\n if (loader) loader.style.display = \"none\";\n\n // Render the view component\n const root = ReactDOMClient.createRoot(mount);\n root.render(React.createElement(component));\n\n notifyParent({ type: \"xr:view-ready\", viewId: VIEW_ID });\n\n } catch (err) {\n console.error(\"[xr-host] Failed to mount view:\", err);\n if (loader) loader.innerHTML =\n \\`<div style=\"color:#f87171;text-align:center;padding:24px\">\n <div style=\"font-size:1.5rem;margin-bottom:8px\">⚠ Load error</div>\n <div style=\"font-size:0.85rem;color:#a1a1aa\">\\${err.message}</div>\n <button class=\"xr-btn\" style=\"margin-top:16px\" onclick=\"mountView()\">Retry</button>\n </div>\\`;\n notifyParent({ type: \"xr:view-error\", viewId: VIEW_ID, error: err.message });\n }\n }\n\n mountView();\n </script>\n</body>\n</html>`;\n}\n"],"mappings":"AAuBO,MAAM,gBAAuB;AAAA,EAClC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aACE;AAAA,EACF,cAAc,OAAO,QAAQ;AAC3B,UAAM,SAAU,IAAI,QAAmC,MAAM;AAC7D,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,kBAAkB,EAAE;AAAA,IAC3D;AAGA,UAAM,YAAa,IAAI,QAA8B,QAAQ;AAC7D,UAAM,cACJ,QAAQ,IAAI,gBAAgB,oBAAoB,SAAS;AAC3D,UAAM,YAAY,GAAG,WAAW,cAAc,MAAM;AACpD,UAAM,cAAc,GAAG,WAAW;AAElC,UAAM,OAAO,cAAc,QAAQ,WAAW,aAAa,WAAW;AAEtE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA;AAAA,QAEhB,2BACE,sBAAsB,WAAW,6FACkB,WAAW,kGAExC,WAAW;AAAA,MACrC;AAAA,MACA,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAIA,SAAS,cACP,QACA,WACA,aACA,aACQ;AACR,SAAO;AAAA,gCACuB,MAAM;AAAA;AAAA;AAAA;AAAA,qBAItB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gCA6IU,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAWd,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAoBP,MAAM;AAAA,0BACH,SAAS;AAAA,yBACV,WAAW;AAAA,4BACR,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgIvC;","names":[]}
@@ -0,0 +1,8 @@
1
+ import type { Route } from "@elizaos/core";
2
+ /**
3
+ * GET /api/xr/views
4
+ * Returns all XR-typed views from registered plugins.
5
+ * Used by app-xr to populate the view launcher.
6
+ */
7
+ export declare const viewsRoute: Route;
8
+ //# sourceMappingURL=views.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../../src/routes/views.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAM3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,KA8BxB,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { listViews } from "@elizaos/agent/api/views-registry";
2
+ import {
3
+ XR_SERVICE_TYPE
4
+ } from "../services/xr-session-service.js";
5
+ const viewsRoute = {
6
+ type: "GET",
7
+ path: "/xr/views",
8
+ description: "Lists all XR-capable views from registered plugins",
9
+ routeHandler: async (ctx) => {
10
+ const views = listViews({ developerMode: true, viewType: "xr" }).filter((v) => v.viewType === "xr").map((v) => ({
11
+ id: v.id,
12
+ label: v.label,
13
+ icon: v.icon,
14
+ description: v.description,
15
+ tags: v.tags,
16
+ xrOptions: v.xrOptions,
17
+ path: v.path,
18
+ pluginName: v.pluginName,
19
+ available: v.available
20
+ }));
21
+ const connections = ctx.runtime.getService(XR_SERVICE_TYPE)?.getConnections().map((c) => ({ id: c.id, deviceType: c.deviceType })) ?? [];
22
+ return {
23
+ status: 200,
24
+ body: { views, connections, count: views.length }
25
+ };
26
+ }
27
+ };
28
+ export {
29
+ viewsRoute
30
+ };
31
+ //# sourceMappingURL=views.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/views.ts"],"sourcesContent":["import { listViews } from \"@elizaos/agent/api/views-registry\";\nimport type { Route } from \"@elizaos/core\";\nimport {\n XR_SERVICE_TYPE,\n type XRSessionService,\n} from \"../services/xr-session-service.js\";\n\n/**\n * GET /api/xr/views\n * Returns all XR-typed views from registered plugins.\n * Used by app-xr to populate the view launcher.\n */\nexport const viewsRoute: Route = {\n type: \"GET\",\n path: \"/xr/views\",\n description: \"Lists all XR-capable views from registered plugins\",\n routeHandler: async (ctx) => {\n const views = listViews({ developerMode: true, viewType: \"xr\" })\n .filter((v) => v.viewType === \"xr\")\n .map((v) => ({\n id: v.id,\n label: v.label,\n icon: v.icon,\n description: v.description,\n tags: v.tags,\n xrOptions: v.xrOptions,\n path: v.path,\n pluginName: v.pluginName,\n available: v.available,\n }));\n\n const connections =\n ctx.runtime\n .getService<XRSessionService>(XR_SERVICE_TYPE)\n ?.getConnections()\n .map((c) => ({ id: c.id, deviceType: c.deviceType })) ?? [];\n\n return {\n status: 200,\n body: { views, connections, count: views.length },\n };\n },\n};\n"],"mappings":"AAAA,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,OAEK;AAOA,MAAM,aAAoB;AAAA,EAC/B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,cAAc,OAAO,QAAQ;AAC3B,UAAM,QAAQ,UAAU,EAAE,eAAe,MAAM,UAAU,KAAK,CAAC,EAC5D,OAAO,CAAC,MAAM,EAAE,aAAa,IAAI,EACjC,IAAI,CAAC,OAAO;AAAA,MACX,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,MAAM,EAAE;AAAA,MACR,aAAa,EAAE;AAAA,MACf,MAAM,EAAE;AAAA,MACR,WAAW,EAAE;AAAA,MACb,MAAM,EAAE;AAAA,MACR,YAAY,EAAE;AAAA,MACd,WAAW,EAAE;AAAA,IACf,EAAE;AAEJ,UAAM,cACJ,IAAI,QACD,WAA6B,eAAe,GAC3C,eAAe,EAChB,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,YAAY,EAAE,WAAW,EAAE,KAAK,CAAC;AAE9D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,aAAa,OAAO,MAAM,OAAO;AAAA,IAClD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ import type { IAgentRuntime } from "@elizaos/core";
2
+ import type { XRAudioHeader } from "../protocol/xr.js";
3
+ export interface PendingTranscription {
4
+ chunks: Buffer[];
5
+ firstTs: number;
6
+ lastTs: number;
7
+ encoding: XRAudioHeader["encoding"];
8
+ sampleRate: number;
9
+ silenceTimer?: ReturnType<typeof setTimeout>;
10
+ }
11
+ export declare class AudioPipeline {
12
+ private readonly runtime;
13
+ private readonly onTranscript;
14
+ private pending;
15
+ constructor(runtime: IAgentRuntime, onTranscript: (connectionId: string, text: string) => Promise<void>);
16
+ push(connectionId: string, header: XRAudioHeader, chunk: Buffer): void;
17
+ flush(connectionId: string): Promise<void>;
18
+ clear(connectionId: string): void;
19
+ }
20
+ //# sourceMappingURL=audio-pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-pipeline.d.ts","sourceRoot":"","sources":["../../src/services/audio-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA4BvD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;CAC9C;AAED,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAJ/B,OAAO,CAAC,OAAO,CAA2C;gBAGvC,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,CAC7B,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,KACT,OAAO,CAAC,IAAI,CAAC;IAGpB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IA6BhE,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmChD,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;CAKlC"}
@@ -0,0 +1,87 @@
1
+ import { ModelType } from "@elizaos/core";
2
+ function pcmF32ToWav(pcmData, sampleRate) {
3
+ const channels = 1;
4
+ const dataSize = pcmData.length;
5
+ const header = Buffer.alloc(44);
6
+ header.write("RIFF", 0);
7
+ header.writeUInt32LE(36 + dataSize, 4);
8
+ header.write("WAVE", 8);
9
+ header.write("fmt ", 12);
10
+ header.writeUInt32LE(16, 16);
11
+ header.writeUInt16LE(3, 20);
12
+ header.writeUInt16LE(channels, 22);
13
+ header.writeUInt32LE(sampleRate, 24);
14
+ header.writeUInt32LE(sampleRate * channels * 4, 28);
15
+ header.writeUInt16LE(channels * 4, 32);
16
+ header.writeUInt16LE(32, 34);
17
+ header.write("data", 36);
18
+ header.writeUInt32LE(dataSize, 40);
19
+ return Buffer.concat([header, pcmData]);
20
+ }
21
+ const FLUSH_AFTER_MS = 2e3;
22
+ const SILENCE_GAP_MS = 1500;
23
+ class AudioPipeline {
24
+ constructor(runtime, onTranscript) {
25
+ this.runtime = runtime;
26
+ this.onTranscript = onTranscript;
27
+ }
28
+ runtime;
29
+ onTranscript;
30
+ pending = /* @__PURE__ */ new Map();
31
+ push(connectionId, header, chunk) {
32
+ let state = this.pending.get(connectionId);
33
+ if (!state) {
34
+ state = {
35
+ chunks: [],
36
+ firstTs: header.ts,
37
+ lastTs: header.ts,
38
+ encoding: header.encoding,
39
+ sampleRate: header.sampleRate
40
+ };
41
+ this.pending.set(connectionId, state);
42
+ }
43
+ state.chunks.push(chunk);
44
+ state.lastTs = header.ts;
45
+ if (state.silenceTimer) clearTimeout(state.silenceTimer);
46
+ state.silenceTimer = setTimeout(
47
+ () => void this.flush(connectionId),
48
+ SILENCE_GAP_MS
49
+ );
50
+ if (state.lastTs - state.firstTs >= FLUSH_AFTER_MS) {
51
+ void this.flush(connectionId);
52
+ }
53
+ }
54
+ async flush(connectionId) {
55
+ const state = this.pending.get(connectionId);
56
+ if (!state || state.chunks.length === 0) return;
57
+ if (state.silenceTimer) {
58
+ clearTimeout(state.silenceTimer);
59
+ state.silenceTimer = void 0;
60
+ }
61
+ this.pending.delete(connectionId);
62
+ const combined = Buffer.concat(state.chunks);
63
+ if (combined.length < 512) return;
64
+ const audioBuffer = state.encoding === "pcm-f32" ? pcmF32ToWav(combined, state.sampleRate) : combined;
65
+ try {
66
+ const transcript = await this.runtime.useModel(
67
+ ModelType.TRANSCRIPTION,
68
+ audioBuffer
69
+ );
70
+ const text = typeof transcript === "string" ? transcript.trim() : "";
71
+ if (text.length > 0) {
72
+ await this.onTranscript(connectionId, text);
73
+ }
74
+ } catch (err) {
75
+ console.error("[plugin-facewear/xr] transcription error:", err);
76
+ }
77
+ }
78
+ clear(connectionId) {
79
+ const state = this.pending.get(connectionId);
80
+ if (state?.silenceTimer) clearTimeout(state.silenceTimer);
81
+ this.pending.delete(connectionId);
82
+ }
83
+ }
84
+ export {
85
+ AudioPipeline
86
+ };
87
+ //# sourceMappingURL=audio-pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/audio-pipeline.ts"],"sourcesContent":["import type { IAgentRuntime } from \"@elizaos/core\";\nimport { ModelType } from \"@elizaos/core\";\nimport type { XRAudioHeader } from \"../protocol/xr.js\";\n\n// Prepend a RIFF/WAV header so Whisper can decode raw Float32 PCM.\nfunction pcmF32ToWav(pcmData: Buffer, sampleRate: number): Buffer {\n const channels = 1; // ScriptProcessorNode fallback is always mono\n const dataSize = pcmData.length;\n const header = Buffer.alloc(44);\n header.write(\"RIFF\", 0);\n header.writeUInt32LE(36 + dataSize, 4);\n header.write(\"WAVE\", 8);\n header.write(\"fmt \", 12);\n header.writeUInt32LE(16, 16);\n header.writeUInt16LE(3, 20); // IEEE_FLOAT\n header.writeUInt16LE(channels, 22);\n header.writeUInt32LE(sampleRate, 24);\n header.writeUInt32LE(sampleRate * channels * 4, 28);\n header.writeUInt16LE(channels * 4, 32);\n header.writeUInt16LE(32, 34);\n header.write(\"data\", 36);\n header.writeUInt32LE(dataSize, 40);\n return Buffer.concat([header, pcmData]);\n}\n\n// Accumulate up to FLUSH_AFTER_MS of audio then transcribe.\n// Also flush if no chunk arrives within SILENCE_GAP_MS (end-of-utterance).\nconst FLUSH_AFTER_MS = 2000;\nconst SILENCE_GAP_MS = 1500;\n\nexport interface PendingTranscription {\n chunks: Buffer[];\n firstTs: number;\n lastTs: number;\n encoding: XRAudioHeader[\"encoding\"];\n sampleRate: number;\n silenceTimer?: ReturnType<typeof setTimeout>;\n}\n\nexport class AudioPipeline {\n private pending = new Map<string, PendingTranscription>();\n\n constructor(\n private readonly runtime: IAgentRuntime,\n private readonly onTranscript: (\n connectionId: string,\n text: string,\n ) => Promise<void>,\n ) {}\n\n push(connectionId: string, header: XRAudioHeader, chunk: Buffer): void {\n let state = this.pending.get(connectionId);\n if (!state) {\n state = {\n chunks: [],\n firstTs: header.ts,\n lastTs: header.ts,\n encoding: header.encoding,\n sampleRate: header.sampleRate,\n };\n this.pending.set(connectionId, state);\n }\n\n state.chunks.push(chunk);\n state.lastTs = header.ts;\n\n // Reset silence timer on each incoming chunk\n if (state.silenceTimer) clearTimeout(state.silenceTimer);\n state.silenceTimer = setTimeout(\n () => void this.flush(connectionId),\n SILENCE_GAP_MS,\n );\n\n // Also flush if we've accumulated enough audio\n if (state.lastTs - state.firstTs >= FLUSH_AFTER_MS) {\n void this.flush(connectionId);\n }\n }\n\n async flush(connectionId: string): Promise<void> {\n const state = this.pending.get(connectionId);\n if (!state || state.chunks.length === 0) return;\n\n if (state.silenceTimer) {\n clearTimeout(state.silenceTimer);\n state.silenceTimer = undefined;\n }\n this.pending.delete(connectionId);\n\n const combined = Buffer.concat(state.chunks);\n if (combined.length < 512) return; // too small to be real speech\n\n // pcm-f32 is raw Float32 samples (mono, from ScriptProcessorNode fallback).\n // Whisper expects a valid audio container — wrap with a WAV header.\n const audioBuffer =\n state.encoding === \"pcm-f32\"\n ? pcmF32ToWav(combined, state.sampleRate)\n : combined;\n\n try {\n const transcript = await this.runtime.useModel(\n ModelType.TRANSCRIPTION,\n audioBuffer,\n );\n const text = typeof transcript === \"string\" ? transcript.trim() : \"\";\n if (text.length > 0) {\n await this.onTranscript(connectionId, text);\n }\n } catch (err) {\n // log but don't crash the pipeline\n console.error(\"[plugin-facewear/xr] transcription error:\", err);\n }\n }\n\n clear(connectionId: string): void {\n const state = this.pending.get(connectionId);\n if (state?.silenceTimer) clearTimeout(state.silenceTimer);\n this.pending.delete(connectionId);\n }\n}\n"],"mappings":"AACA,SAAS,iBAAiB;AAI1B,SAAS,YAAY,SAAiB,YAA4B;AAChE,QAAM,WAAW;AACjB,QAAM,WAAW,QAAQ;AACzB,QAAM,SAAS,OAAO,MAAM,EAAE;AAC9B,SAAO,MAAM,QAAQ,CAAC;AACtB,SAAO,cAAc,KAAK,UAAU,CAAC;AACrC,SAAO,MAAM,QAAQ,CAAC;AACtB,SAAO,MAAM,QAAQ,EAAE;AACvB,SAAO,cAAc,IAAI,EAAE;AAC3B,SAAO,cAAc,GAAG,EAAE;AAC1B,SAAO,cAAc,UAAU,EAAE;AACjC,SAAO,cAAc,YAAY,EAAE;AACnC,SAAO,cAAc,aAAa,WAAW,GAAG,EAAE;AAClD,SAAO,cAAc,WAAW,GAAG,EAAE;AACrC,SAAO,cAAc,IAAI,EAAE;AAC3B,SAAO,MAAM,QAAQ,EAAE;AACvB,SAAO,cAAc,UAAU,EAAE;AACjC,SAAO,OAAO,OAAO,CAAC,QAAQ,OAAO,CAAC;AACxC;AAIA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AAWhB,MAAM,cAAc;AAAA,EAGzB,YACmB,SACA,cAIjB;AALiB;AACA;AAAA,EAIhB;AAAA,EALgB;AAAA,EACA;AAAA,EAJX,UAAU,oBAAI,IAAkC;AAAA,EAUxD,KAAK,cAAsB,QAAuB,OAAqB;AACrE,QAAI,QAAQ,KAAK,QAAQ,IAAI,YAAY;AACzC,QAAI,CAAC,OAAO;AACV,cAAQ;AAAA,QACN,QAAQ,CAAC;AAAA,QACT,SAAS,OAAO;AAAA,QAChB,QAAQ,OAAO;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,MACrB;AACA,WAAK,QAAQ,IAAI,cAAc,KAAK;AAAA,IACtC;AAEA,UAAM,OAAO,KAAK,KAAK;AACvB,UAAM,SAAS,OAAO;AAGtB,QAAI,MAAM,aAAc,cAAa,MAAM,YAAY;AACvD,UAAM,eAAe;AAAA,MACnB,MAAM,KAAK,KAAK,MAAM,YAAY;AAAA,MAClC;AAAA,IACF;AAGA,QAAI,MAAM,SAAS,MAAM,WAAW,gBAAgB;AAClD,WAAK,KAAK,MAAM,YAAY;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,cAAqC;AAC/C,UAAM,QAAQ,KAAK,QAAQ,IAAI,YAAY;AAC3C,QAAI,CAAC,SAAS,MAAM,OAAO,WAAW,EAAG;AAEzC,QAAI,MAAM,cAAc;AACtB,mBAAa,MAAM,YAAY;AAC/B,YAAM,eAAe;AAAA,IACvB;AACA,SAAK,QAAQ,OAAO,YAAY;AAEhC,UAAM,WAAW,OAAO,OAAO,MAAM,MAAM;AAC3C,QAAI,SAAS,SAAS,IAAK;AAI3B,UAAM,cACJ,MAAM,aAAa,YACf,YAAY,UAAU,MAAM,UAAU,IACtC;AAEN,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,QAAQ;AAAA,QACpC,UAAU;AAAA,QACV;AAAA,MACF;AACA,YAAM,OAAO,OAAO,eAAe,WAAW,WAAW,KAAK,IAAI;AAClE,UAAI,KAAK,SAAS,GAAG;AACnB,cAAM,KAAK,aAAa,cAAc,IAAI;AAAA,MAC5C;AAAA,IACF,SAAS,KAAK;AAEZ,cAAQ,MAAM,6CAA6C,GAAG;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,MAAM,cAA4B;AAChC,UAAM,QAAQ,KAAK,QAAQ,IAAI,YAAY;AAC3C,QAAI,OAAO,aAAc,cAAa,MAAM,YAAY;AACxD,SAAK,QAAQ,OAAO,YAAY;AAAA,EAClC;AACF;","names":[]}
@@ -0,0 +1,26 @@
1
+ import type { IAgentRuntime } from "@elizaos/core";
2
+ import { Service } from "@elizaos/core";
3
+ import type { SmartglassesService } from "./smartglasses-service.js";
4
+ import { type XRConnection, type XRSessionService } from "./xr-session-service.js";
5
+ export declare const FACEWEAR_SERVICE_TYPE = "facewear";
6
+ export type FacewearActiveDevice = {
7
+ kind: "xr";
8
+ connection: XRConnection;
9
+ } | {
10
+ kind: "smartglasses";
11
+ } | null;
12
+ export declare class FacewearService extends Service {
13
+ static serviceType: string;
14
+ readonly capabilityDescription = "Unified facewear service \u2014 coordinates XR streaming and smartglasses BLE for Meta Quest, XReal, Even Realities G1/G2, and Apple Vision Pro.";
15
+ static start(runtime: IAgentRuntime): Promise<Service>;
16
+ stop(): Promise<void>;
17
+ getXRService(): XRSessionService | undefined;
18
+ getSmartglassesService(): SmartglassesService | undefined;
19
+ getConnectedDevices(): Array<{
20
+ id: string;
21
+ kind: "xr" | "smartglasses";
22
+ deviceType?: string;
23
+ }>;
24
+ hasActiveDevice(): boolean;
25
+ }
26
+ //# sourceMappingURL=facewear-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facewear-service.d.ts","sourceRoot":"","sources":["../../src/services/facewear-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB,IAAI,CAAC;AAET,qBAAa,eAAgB,SAAQ,OAAO;IAC1C,OAAgB,WAAW,SAAyB;IAEpD,QAAQ,CAAC,qBAAqB,sJACkH;WAE1H,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAEpC,YAAY,IAAI,gBAAgB,GAAG,SAAS;IAM5C,sBAAsB,IAAI,mBAAmB,GAAG,SAAS;IAMzD,mBAAmB,IAAI,KAAK,CAAC;QAC3B,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,IAAI,GAAG,cAAc,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAmBF,eAAe,IAAI,OAAO;CAG3B"}
@@ -0,0 +1,45 @@
1
+ import { Service } from "@elizaos/core";
2
+ import { SMARTGLASSES_SERVICE_NAME } from "./smartglasses-service.js";
3
+ import {
4
+ XR_SERVICE_TYPE
5
+ } from "./xr-session-service.js";
6
+ const FACEWEAR_SERVICE_TYPE = "facewear";
7
+ class FacewearService extends Service {
8
+ static serviceType = FACEWEAR_SERVICE_TYPE;
9
+ capabilityDescription = "Unified facewear service \u2014 coordinates XR streaming and smartglasses BLE for Meta Quest, XReal, Even Realities G1/G2, and Apple Vision Pro.";
10
+ static async start(runtime) {
11
+ return new FacewearService(runtime);
12
+ }
13
+ async stop() {
14
+ }
15
+ getXRService() {
16
+ return this.runtime.getService(XR_SERVICE_TYPE) ?? void 0;
17
+ }
18
+ getSmartglassesService() {
19
+ return this.runtime.getService(
20
+ SMARTGLASSES_SERVICE_NAME
21
+ ) ?? void 0;
22
+ }
23
+ getConnectedDevices() {
24
+ const devices = [];
25
+ const xrSvc = this.getXRService();
26
+ if (xrSvc) {
27
+ for (const conn of xrSvc.getConnections()) {
28
+ devices.push({ id: conn.id, kind: "xr", deviceType: conn.deviceType });
29
+ }
30
+ }
31
+ const sgSvc = this.getSmartglassesService();
32
+ if (sgSvc?.getStatus().connected) {
33
+ devices.push({ id: "smartglasses", kind: "smartglasses" });
34
+ }
35
+ return devices;
36
+ }
37
+ hasActiveDevice() {
38
+ return this.getConnectedDevices().length > 0;
39
+ }
40
+ }
41
+ export {
42
+ FACEWEAR_SERVICE_TYPE,
43
+ FacewearService
44
+ };
45
+ //# sourceMappingURL=facewear-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/facewear-service.ts"],"sourcesContent":["import type { IAgentRuntime } from \"@elizaos/core\";\nimport { Service } from \"@elizaos/core\";\nimport type { SmartglassesService } from \"./smartglasses-service.js\";\nimport { SMARTGLASSES_SERVICE_NAME } from \"./smartglasses-service.js\";\nimport {\n XR_SERVICE_TYPE,\n type XRConnection,\n type XRSessionService,\n} from \"./xr-session-service.js\";\n\nexport const FACEWEAR_SERVICE_TYPE = \"facewear\";\n\nexport type FacewearActiveDevice =\n | { kind: \"xr\"; connection: XRConnection }\n | { kind: \"smartglasses\" }\n | null;\n\nexport class FacewearService extends Service {\n static override serviceType = FACEWEAR_SERVICE_TYPE;\n\n readonly capabilityDescription =\n \"Unified facewear service — coordinates XR streaming and smartglasses BLE for Meta Quest, XReal, Even Realities G1/G2, and Apple Vision Pro.\";\n\n static override async start(runtime: IAgentRuntime): Promise<Service> {\n return new FacewearService(runtime);\n }\n\n override async stop(): Promise<void> {}\n\n getXRService(): XRSessionService | undefined {\n return (\n this.runtime.getService<XRSessionService>(XR_SERVICE_TYPE) ?? undefined\n );\n }\n\n getSmartglassesService(): SmartglassesService | undefined {\n return (this.runtime.getService<SmartglassesService>(\n SMARTGLASSES_SERVICE_NAME,\n ) ?? undefined) as SmartglassesService | undefined;\n }\n\n getConnectedDevices(): Array<{\n id: string;\n kind: \"xr\" | \"smartglasses\";\n deviceType?: string;\n }> {\n const devices: Array<{\n id: string;\n kind: \"xr\" | \"smartglasses\";\n deviceType?: string;\n }> = [];\n const xrSvc = this.getXRService();\n if (xrSvc) {\n for (const conn of xrSvc.getConnections()) {\n devices.push({ id: conn.id, kind: \"xr\", deviceType: conn.deviceType });\n }\n }\n const sgSvc = this.getSmartglassesService();\n if (sgSvc?.getStatus().connected) {\n devices.push({ id: \"smartglasses\", kind: \"smartglasses\" });\n }\n return devices;\n }\n\n hasActiveDevice(): boolean {\n return this.getConnectedDevices().length > 0;\n }\n}\n"],"mappings":"AACA,SAAS,eAAe;AAExB,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,OAGK;AAEA,MAAM,wBAAwB;AAO9B,MAAM,wBAAwB,QAAQ;AAAA,EAC3C,OAAgB,cAAc;AAAA,EAErB,wBACP;AAAA,EAEF,aAAsB,MAAM,SAA0C;AACpE,WAAO,IAAI,gBAAgB,OAAO;AAAA,EACpC;AAAA,EAEA,MAAe,OAAsB;AAAA,EAAC;AAAA,EAEtC,eAA6C;AAC3C,WACE,KAAK,QAAQ,WAA6B,eAAe,KAAK;AAAA,EAElE;AAAA,EAEA,yBAA0D;AACxD,WAAQ,KAAK,QAAQ;AAAA,MACnB;AAAA,IACF,KAAK;AAAA,EACP;AAAA,EAEA,sBAIG;AACD,UAAM,UAID,CAAC;AACN,UAAM,QAAQ,KAAK,aAAa;AAChC,QAAI,OAAO;AACT,iBAAW,QAAQ,MAAM,eAAe,GAAG;AACzC,gBAAQ,KAAK,EAAE,IAAI,KAAK,IAAI,MAAM,MAAM,YAAY,KAAK,WAAW,CAAC;AAAA,MACvE;AAAA,IACF;AACA,UAAM,QAAQ,KAAK,uBAAuB;AAC1C,QAAI,OAAO,UAAU,EAAE,WAAW;AAChC,cAAQ,KAAK,EAAE,IAAI,gBAAgB,MAAM,eAAe,CAAC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAAA,EAEA,kBAA2B;AACzB,WAAO,KAAK,oBAAoB,EAAE,SAAS;AAAA,EAC7C;AACF;","names":[]}