@mentra/miniapp 0.3.0-dev.0 → 0.3.0-dev.1

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 (55) hide show
  1. package/README.md +8 -6
  2. package/dist/background/index.d.ts +4 -3
  3. package/dist/background/index.d.ts.map +1 -1
  4. package/dist/background/index.js.map +1 -1
  5. package/dist/background/register.d.ts +3 -2
  6. package/dist/background/register.d.ts.map +1 -1
  7. package/dist/background/register.js.map +1 -1
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/modules/camera.d.ts +50 -4
  11. package/dist/modules/camera.d.ts.map +1 -1
  12. package/dist/modules/camera.js +20 -5
  13. package/dist/modules/camera.js.map +1 -1
  14. package/dist/modules/events.d.ts +10 -17
  15. package/dist/modules/events.d.ts.map +1 -1
  16. package/dist/modules/events.js +56 -11
  17. package/dist/modules/events.js.map +1 -1
  18. package/dist/modules/phone.d.ts +38 -4
  19. package/dist/modules/phone.d.ts.map +1 -1
  20. package/dist/modules/phone.js +22 -6
  21. package/dist/modules/phone.js.map +1 -1
  22. package/dist/modules/speaker.d.ts +7 -0
  23. package/dist/modules/speaker.d.ts.map +1 -1
  24. package/dist/modules/speaker.js +8 -8
  25. package/dist/modules/speaker.js.map +1 -1
  26. package/dist/modules/transcription.d.ts +10 -2
  27. package/dist/modules/transcription.d.ts.map +1 -1
  28. package/dist/modules/transcription.js +4 -4
  29. package/dist/modules/transcription.js.map +1 -1
  30. package/dist/protocol.d.ts +6 -1
  31. package/dist/protocol.d.ts.map +1 -1
  32. package/dist/protocol.js +6 -1
  33. package/dist/protocol.js.map +1 -1
  34. package/dist/session.d.ts +5 -3
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +7 -3
  37. package/dist/session.js.map +1 -1
  38. package/dist/sub-path-types.test-d.d.ts +12 -0
  39. package/dist/sub-path-types.test-d.d.ts.map +1 -1
  40. package/dist/sub-path-types.test-d.js.map +1 -1
  41. package/dist/transport/mock.js +2 -0
  42. package/dist/transport/mock.js.map +1 -1
  43. package/package.json +11 -2
  44. package/src/background/index.ts +3 -3
  45. package/src/background/register.ts +10 -3
  46. package/src/index.ts +2 -2
  47. package/src/modules/camera.ts +75 -13
  48. package/src/modules/events.ts +54 -27
  49. package/src/modules/phone.ts +59 -24
  50. package/src/modules/speaker.ts +17 -2
  51. package/src/modules/transcription.ts +21 -4
  52. package/src/protocol.ts +9 -2
  53. package/src/session.ts +19 -7
  54. package/src/sub-path-types.test-d.ts +12 -1
  55. package/src/transport/mock.ts +3 -0
@@ -4,7 +4,7 @@
4
4
  * Mirrors cloud SDK v3's PhoneManager structure. Sub-namespaced by concern:
5
5
  *
6
6
  * session.phone.notifications.{on, hasPermission, stop}
7
- * session.phone.calendar.{on, hasPermission, stop}
7
+ * session.phone.calendar.{listEvents, hasPermission}
8
8
  * session.phone.onBattery(...) // stays flat
9
9
  *
10
10
  * Imperative phone-OS calls (share, openUrl, copyToClipboard, download) live
@@ -16,15 +16,9 @@
16
16
  * subscribing on iOS is a no-op even though the API is present.
17
17
  */
18
18
 
19
- import {MiniappStreamType} from "../protocol"
19
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
20
20
  import {MiniappSession} from "../session"
21
- import type {
22
- BatteryData,
23
- CalendarEventData,
24
- NotificationDismissedData,
25
- PhoneNotificationData,
26
- UnsubscribeFn,
27
- } from "./events"
21
+ import type {BatteryData, NotificationDismissedData, PhoneNotificationData, UnsubscribeFn} from "./events"
28
22
 
29
23
  class TrackedSubs {
30
24
  private readonly unsubs = new Set<UnsubscribeFn>()
@@ -55,9 +49,7 @@ export class PhoneNotificationsModule extends TrackedSubs {
55
49
  }
56
50
 
57
51
  on(handler: (data: PhoneNotificationData) => void): UnsubscribeFn {
58
- return this.track(
59
- this.session._subscribe(MiniappStreamType.PHONE_NOTIFICATION, handler as (data: unknown) => void),
60
- )
52
+ return this.track(this.session._subscribe(MiniappStreamType.PHONE_NOTIFICATION, handler as (data: unknown) => void))
61
53
  }
62
54
 
63
55
  /**
@@ -71,10 +63,7 @@ export class PhoneNotificationsModule extends TrackedSubs {
71
63
  */
72
64
  onDismissed(handler: (data: NotificationDismissedData) => void): UnsubscribeFn {
73
65
  return this.track(
74
- this.session._subscribe(
75
- MiniappStreamType.PHONE_NOTIFICATION_DISMISSED,
76
- handler as (data: unknown) => void,
77
- ),
66
+ this.session._subscribe(MiniappStreamType.PHONE_NOTIFICATION_DISMISSED, handler as (data: unknown) => void),
78
67
  )
79
68
  }
80
69
 
@@ -84,15 +73,55 @@ export class PhoneNotificationsModule extends TrackedSubs {
84
73
  }
85
74
  }
86
75
 
87
- export class PhoneCalendarModule extends TrackedSubs {
88
- constructor(private readonly session: MiniappSession) {
89
- super()
90
- }
76
+ export interface CalendarListOptions {
77
+ /** Inclusive start of the query window. */
78
+ startsAt: string | Date
79
+ /** Exclusive end of the query window. Must be after startsAt. */
80
+ endsAt: string | Date
81
+ /** Maximum events to return. Defaults to 50 and may not exceed 100. */
82
+ limit?: number
83
+ }
91
84
 
92
- on(handler: (data: CalendarEventData) => void): UnsubscribeFn {
93
- return this.track(
94
- this.session._subscribe(MiniappStreamType.CALENDAR_EVENT, handler as (data: unknown) => void),
95
- )
85
+ export interface CalendarEvent {
86
+ /** Stable occurrence id, unique across calendars and recurring instances. */
87
+ id: string
88
+ calendarId: string
89
+ title: string
90
+ /** ISO 8601 start time. All-day events preserve their calendar timezone offset. */
91
+ startsAt: string
92
+ /** ISO 8601 end time. All-day events preserve their calendar timezone offset. */
93
+ endsAt: string
94
+ timezone?: string
95
+ allDay: boolean
96
+ location?: string
97
+ notes?: string
98
+ url?: string
99
+ /** Deduplicated HTTPS links found in the event URL, location, and notes. */
100
+ links: string[]
101
+ }
102
+
103
+ export interface CalendarListResult {
104
+ events: CalendarEvent[]
105
+ /** True when more matching events existed than the requested limit. */
106
+ truncated: boolean
107
+ }
108
+
109
+ export class PhoneCalendarModule {
110
+ constructor(private readonly session: MiniappSession) {}
111
+
112
+ /**
113
+ * Read calendar events in a bounded window. CALENDAR must be declared in
114
+ * miniapp.json and granted before the miniapp opens.
115
+ */
116
+ listEvents(options: CalendarListOptions): Promise<CalendarListResult> {
117
+ const startsAt = normalizeCalendarDate(options?.startsAt, "startsAt")
118
+ const endsAt = normalizeCalendarDate(options?.endsAt, "endsAt")
119
+ return this.session.sendRequest<CalendarListResult>({
120
+ type: MiniappRequestType.CALENDAR_LIST_EVENTS,
121
+ startsAt,
122
+ endsAt,
123
+ ...(options.limit === undefined ? {} : {limit: options.limit}),
124
+ })
96
125
  }
97
126
 
98
127
  /** True iff `CALENDAR` is declared in the miniapp's manifest. */
@@ -101,6 +130,12 @@ export class PhoneCalendarModule extends TrackedSubs {
101
130
  }
102
131
  }
103
132
 
133
+ function normalizeCalendarDate(value: string | Date | undefined, field: string): string {
134
+ const date = value instanceof Date ? value : new Date(value ?? "")
135
+ if (Number.isNaN(date.getTime())) throw new TypeError(`${field} must be a valid Date or ISO 8601 string`)
136
+ return date.toISOString()
137
+ }
138
+
104
139
  export class PhoneModule {
105
140
  public readonly notifications: PhoneNotificationsModule
106
141
  public readonly calendar: PhoneCalendarModule
@@ -50,6 +50,12 @@ export interface SpeakOptions {
50
50
  voice_settings?: Record<string, unknown>
51
51
  volume?: number
52
52
  stopOtherAudio?: boolean
53
+ /**
54
+ * Force this call to use on-device offline TTS, skipping cloud TTS
55
+ * entirely — even when cloud is connected. Rejects with `TTS_LOCAL_UNAVAILABLE`
56
+ * if the offline model isn't ready instead of falling back to cloud.
57
+ */
58
+ forceLocal?: boolean
53
59
  }
54
60
 
55
61
  export interface SpeakResult {
@@ -244,7 +250,15 @@ export class SpeakerModule {
244
250
  * Rejects with a MiniappRequestError containing a `code` field on cloud-side
245
251
  * TTS failures: `TTS_TEXT_TOO_LONG`, `TTS_INVALID_VOICE`, `TTS_UPSTREAM_ERROR`.
246
252
  */
247
- async speak(text: string, options: SpeakOptions = {}): Promise<SpeakResult> {
253
+ async speak(text: string, options?: SpeakOptions): Promise<SpeakResult>
254
+ async speak(sentences: string[], options?: SpeakOptions): Promise<SpeakResult>
255
+ async speak(text: string | string[], options: SpeakOptions = {}): Promise<SpeakResult> {
256
+ const normalized = Array.isArray(text)
257
+ ? text.map((sentence) => sentence.trim()).filter(Boolean)
258
+ : text.trim()
259
+ if ((Array.isArray(normalized) && normalized.length === 0) || normalized === "") {
260
+ throw {code: MiniappErrorCode.INTERNAL, message: "speak requires at least one non-empty sentence"}
261
+ }
248
262
  try {
249
263
  // Like play(): resolves only when TTS playback completes, so opt out of the
250
264
  // default request timeout (long text can outlast it). Settled by the host
@@ -252,11 +266,12 @@ export class SpeakerModule {
252
266
  const result = await this.session.sendRequest<SpeakResult | null>(
253
267
  {
254
268
  type: MiniappRequestType.SPEAK,
255
- text,
269
+ text: normalized,
256
270
  voice_id: options.voice_id,
257
271
  voice_settings: options.voice_settings,
258
272
  volume: options.volume,
259
273
  stopOtherAudio: options.stopOtherAudio ?? false,
274
+ forceLocal: options.forceLocal ?? false,
260
275
  },
261
276
  {timeoutMs: 0},
262
277
  )
@@ -32,6 +32,15 @@ export interface TranscriptionConfig {
32
32
  diarization?: boolean
33
33
  }
34
34
 
35
+ export interface TranscriptionOptions {
36
+ /**
37
+ * Use only on-device STT for this listener; never deliver cloud transcripts.
38
+ * Other listeners on the same stream keep their own routing choice. Requires
39
+ * a downloaded local STT model.
40
+ */
41
+ forceLocal?: boolean
42
+ }
43
+
35
44
  export class TranscriptionModule {
36
45
  private readonly unsubs = new Set<UnsubscribeFn>()
37
46
  private currentConfig: TranscriptionConfig | null = null
@@ -46,9 +55,9 @@ export class TranscriptionModule {
46
55
  * (handlers on `transcription:auto` receive any `transcription:<lang>`
47
56
  * event) preserves existing semantics.
48
57
  */
49
- on(handler: (data: TranscriptionData) => void): UnsubscribeFn {
58
+ on(handler: (data: TranscriptionData) => void, options: TranscriptionOptions = {}): UnsubscribeFn {
50
59
  return this.track(
51
- this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:auto`, handler as (data: unknown) => void),
60
+ this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:auto`, handler as (data: unknown) => void, options),
52
61
  )
53
62
  }
54
63
 
@@ -59,14 +68,22 @@ export class TranscriptionModule {
59
68
  * @param language - BCP-47 tag(s), e.g. `"en-US"` or `["en-US", "es-ES"]`.
60
69
  * @param handler - Called for every event in any of the listed languages.
61
70
  */
62
- forLanguage(language: string | string[], handler: (data: TranscriptionData) => void): UnsubscribeFn {
71
+ forLanguage(
72
+ language: string | string[],
73
+ handler: (data: TranscriptionData) => void,
74
+ options: TranscriptionOptions = {},
75
+ ): UnsubscribeFn {
63
76
  const langs = Array.isArray(language) ? language : [language]
64
77
  if (langs.length === 0) return () => {}
65
78
 
66
79
  const unsubs: UnsubscribeFn[] = []
67
80
  for (const lang of langs) {
68
81
  unsubs.push(
69
- this.session._subscribe(`${MiniappStreamType.TRANSCRIPTION}:${lang}`, handler as (data: unknown) => void),
82
+ this.session._subscribe(
83
+ `${MiniappStreamType.TRANSCRIPTION}:${lang}`,
84
+ handler as (data: unknown) => void,
85
+ options,
86
+ ),
70
87
  )
71
88
  }
72
89
  const combined: UnsubscribeFn = () => {
package/src/protocol.ts CHANGED
@@ -60,6 +60,9 @@ export enum MiniappRequestType {
60
60
  /** One-shot location poll. */
61
61
  LOCATION_POLL = "miniapp_location_poll",
62
62
 
63
+ /** Read a bounded snapshot of phone calendar events. */
64
+ CALENDAR_LIST_EVENTS = "miniapp_calendar_list_events",
65
+
63
66
  /** Start a turn-by-turn navigation trip. Android only. */
64
67
  NAVIGATION_START = "miniapp_navigation_start",
65
68
  /** Stop the active navigation trip (if any). */
@@ -300,8 +303,6 @@ export enum MiniappStreamType {
300
303
  * fire. See agents/miniapp-speaker-state-and-notif-dismissed-plan.md.
301
304
  */
302
305
  PHONE_NOTIFICATION_DISMISSED = "phone_notification_dismissed",
303
- CALENDAR_EVENT = "calendar_event",
304
-
305
306
  // Photos, streaming
306
307
  PHOTO_TAKEN = "photo_taken",
307
308
  STREAM_STATUS = "stream_status",
@@ -318,6 +319,9 @@ export enum MiniappErrorCode {
318
319
  /** The miniapp subscribed to a stream whose required permission wasn't in its manifest. */
319
320
  PERMISSION_NOT_DECLARED = "PERMISSION_NOT_DECLARED",
320
321
 
322
+ /** The required phone OS permission is not currently granted. */
323
+ PERMISSION_DENIED = "PERMISSION_DENIED",
324
+
321
325
  /** Request routed to a method that isn't supported yet. */
322
326
  NOT_IMPLEMENTED = "NOT_IMPLEMENTED",
323
327
 
@@ -332,6 +336,9 @@ export enum MiniappErrorCode {
332
336
  TTS_INVALID_VOICE = "TTS_INVALID_VOICE",
333
337
  TTS_UPSTREAM_ERROR = "TTS_UPSTREAM_ERROR",
334
338
 
339
+ /** `speak({forceLocal: true})` but the on-device offline TTS model isn't downloaded/ready. */
340
+ TTS_LOCAL_UNAVAILABLE = "TTS_LOCAL_UNAVAILABLE",
341
+
335
342
  /** Not connected / pre-ACK and transport closed. */
336
343
  NOT_CONNECTED = "NOT_CONNECTED",
337
344
 
package/src/session.ts CHANGED
@@ -24,7 +24,7 @@ import {AuthModule} from "./modules/auth"
24
24
  import {CloudModule} from "./modules/cloud"
25
25
  import {DashboardAPI} from "./modules/dashboard"
26
26
  import {DisplayManager} from "./modules/display"
27
- import {EventManager, type UnsubscribeFn} from "./modules/events"
27
+ import {EventManager, type TranscriptionEventRoute, type UnsubscribeFn} from "./modules/events"
28
28
  import {GlassesModule} from "./modules/glasses"
29
29
  import {HeadingModule} from "./modules/heading"
30
30
  import {ImuModule} from "./modules/imu"
@@ -194,7 +194,11 @@ type SessionEmitterEvents = {
194
194
  auth: (auth: MiniappAuthState) => void
195
195
  }
196
196
 
197
- export class MiniappSession {
197
+ // The default preserves the pre-channel-registry behavior for code that creates
198
+ // or accepts a bare MiniappSession. `registerMiniapp<Channels>` supplies the
199
+ // concrete mapping for scaffolded miniapps.
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ export class MiniappSession<TChannels extends object = any> {
198
202
  public readonly auth: AuthModule
199
203
  public readonly display: DisplayManager
200
204
  /**
@@ -238,7 +242,7 @@ export class MiniappSession {
238
242
  * with inverted buffering policy (background drops when no WebView is
239
243
  * bound; the WebView buffers until ready).
240
244
  */
241
- public readonly ui: UIModule
245
+ public readonly ui: UIModule<TChannels>
242
246
  /**
243
247
  * Inter-miniapp lifecycle + discovery (list / start / stop). SYSTEM-only —
244
248
  * calls reject with NOT_PERMITTED unless this miniapp is a system app.
@@ -324,7 +328,7 @@ export class MiniappSession {
324
328
  this.system = new SystemModule(this)
325
329
  this.transcription = new TranscriptionModule(this)
326
330
  this.translation = new TranslationModule(this)
327
- this.ui = new UIModuleImpl(this)
331
+ this.ui = new UIModuleImpl<TChannels>(this)
328
332
  this.miniapps = new MiniappsModule(this)
329
333
  this.actions = new ActionsModule(this)
330
334
  }
@@ -392,8 +396,12 @@ export class MiniappSession {
392
396
  * session.transcription.on(...)
393
397
  * etc. instead."
394
398
  */
395
- _subscribe(streamType: string, handler: (data: unknown) => void): UnsubscribeFn {
396
- return this.events.subscribe(streamType, handler)
399
+ _subscribe(
400
+ streamType: string,
401
+ handler: (data: unknown) => void,
402
+ options: {forceLocal?: boolean} = {},
403
+ ): UnsubscribeFn {
404
+ return this.events.subscribe(streamType, handler, options)
397
405
  }
398
406
 
399
407
  // -------------------------------------------------------------------------
@@ -677,7 +685,11 @@ export class MiniappSession {
677
685
  case MiniappResponseType.EVENT: {
678
686
  const streamType = payload.streamType as string | undefined
679
687
  if (!streamType) return
680
- this.events._forwardEvent(streamType, payload.data)
688
+ this.events._forwardEvent(
689
+ streamType,
690
+ payload.data,
691
+ payload.transcriptionRoute as TranscriptionEventRoute | undefined,
692
+ )
681
693
  return
682
694
  }
683
695
 
@@ -23,12 +23,23 @@
23
23
  */
24
24
 
25
25
  // ----- @mentra/miniapp/background ---------------------------------------
26
- import type {MiniappSession} from "./background/index"
26
+ import {registerMiniapp} from "./background/index"
27
+ import type {MiniappSession, UIModule} from "./background/index"
27
28
 
28
29
  // ✅ MiniappSession is reachable from the background entry.
29
30
  const _bg: MiniappSession | undefined = undefined
30
31
  void _bg
31
32
 
33
+ // ✅ registerMiniapp<Channels> carries the shared channel registry into
34
+ // session.ui on the background side.
35
+ interface _TestChannels {
36
+ ping: {at: number}
37
+ }
38
+ type _RegisteredHandler = Parameters<typeof registerMiniapp<_TestChannels>>[0]
39
+ type _RegisteredSession = Parameters<_RegisteredHandler>[0]
40
+ type _Assert<T extends true> = T
41
+ export type _TypedBackground = _Assert<_RegisteredSession["ui"] extends UIModule<_TestChannels> ? true : false>
42
+
32
43
  // ----- @mentra/miniapp/ui -----------------------------------------------
33
44
  import type {MentraUiGlobal, MentraTyped} from "./ui/index"
34
45
 
@@ -226,6 +226,9 @@ function syntheticDataFor(requestId: string, requestType: string, requestPayload
226
226
  // Atlantic, which makes maps/navigation unusable in the WebView.
227
227
  return {lat: 37.7956, lng: -122.3933, accuracy: 0, timestamp: Date.now()}
228
228
 
229
+ case MiniappRequestType.CALENDAR_LIST_EVENTS:
230
+ return {events: [], truncated: false}
231
+
229
232
  case MiniappRequestType.STORAGE_GET:
230
233
  return {value: null}
231
234