@mentra/sdk 2.1.12 โ†’ 2.1.14

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.
@@ -4,10 +4,11 @@
4
4
  * Creates and manages a server for Apps in the MentraOS ecosystem.
5
5
  * Handles webhook endpoints, session management, and cleanup.
6
6
  */
7
- import { type Express } from 'express';
8
- import { AppSession } from '../session/index';
9
- import { ToolCall } from '../../types';
10
- import { Logger } from 'pino';
7
+ import { type Express } from "express";
8
+ import { AppSession } from "../session/index";
9
+ import { ToolCall } from "../../types";
10
+ import { Logger } from "pino";
11
+ export declare const GIVE_APP_CONTROL_OF_TOOL_RESPONSE: string;
11
12
  /**
12
13
  * ๐Ÿ”ง Configuration options for App Server
13
14
  *
@@ -81,6 +82,8 @@ export declare class AppServer {
81
82
  private app;
82
83
  /** Map of active user sessions by sessionId */
83
84
  private activeSessions;
85
+ /** Map of active user sessions by userId */
86
+ private activeSessionsByUserId;
84
87
  /** Array of cleanup handlers to run on shutdown */
85
88
  private cleanupHandlers;
86
89
  /** App instructions string shown to the user */
@@ -130,14 +133,14 @@ export declare class AppServer {
130
133
  */
131
134
  stop(): void;
132
135
  /**
133
- * ๐Ÿ” Generate a App token for a user
134
- * This should be called when handling a session webhook request.
135
- *
136
- * @param userId - User identifier
137
- * @param sessionId - Session identifier
138
- * @param secretKey - Secret key for signing the token
139
- * @returns JWT token string
140
- */
136
+ * ๐Ÿ” Generate a App token for a user
137
+ * This should be called when handling a session webhook request.
138
+ *
139
+ * @param userId - User identifier
140
+ * @param sessionId - Session identifier
141
+ * @param secretKey - Secret key for signing the token
142
+ * @returns JWT token string
143
+ */
141
144
  protected generateToken(userId: string, sessionId: string, secretKey: string): string;
142
145
  /**
143
146
  * ๐Ÿงน Add Cleanup Handler
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/app/server/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAgB,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAQL,QAAQ,EACT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAI9B;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,oIAAoI;IACpI,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,+FAA+F;IAC/F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAE3B,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,SAAS;IAYR,OAAO,CAAC,MAAM;IAX1B,2BAA2B;IAC3B,OAAO,CAAC,GAAG,CAAU;IACrB,+CAA+C;IAC/C,OAAO,CAAC,cAAc,CAAiC;IACvD,mDAAmD;IACnD,OAAO,CAAC,eAAe,CAAyB;IAChD,gDAAgD;IAChD,OAAO,CAAC,eAAe,CAAuB;IAE9C,SAAgB,MAAM,EAAE,MAAM,CAAC;gBAEX,MAAM,EAAE,eAAe;IAwCpC,aAAa,IAAI,OAAO;IAI/B;;;;;;;;OAQG;cACa,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhG;;;;;;;;OAQG;cACa,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxF;;;;;;;OAOG;cACa,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAM3E;;;;;OAKG;IACI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY7B;;;OAGG;IACI,IAAI,IAAI,IAAI;IAMnB;;;;;;;;KAQC;IACD,SAAS,CAAC,aAAa,CACrB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM;IAYT;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAItD;;;OAGG;IACH,OAAO,CAAC,YAAY;IAoCpB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IA2B7B;;OAEG;YACW,oBAAoB;IA6DlC;;OAEG;YACW,iBAAiB;IAgB/B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAyD7B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAQtB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAKrB;;;OAGG;IACH,OAAO,CAAC,OAAO;IAYf;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAkFhC;;OAEG;IACH,OAAO,CAAC,2BAA2B;CAQpC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,eAAe,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,qBAAa,SAAU,SAAQ,SAAS;gBAC1B,MAAM,EAAE,eAAe;CASpC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/app/server/index.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TpaServer = exports.AppServer = void 0;
6
+ exports.TpaServer = exports.AppServer = exports.GIVE_APP_CONTROL_OF_TOOL_RESPONSE = void 0;
7
7
  /**
8
8
  * ๐Ÿš€ App Server Module
9
9
  *
@@ -16,6 +16,7 @@ const index_1 = require("../session/index");
16
16
  const webview_1 = require("../webview");
17
17
  const types_1 = require("../../types");
18
18
  const logger_1 = require("../../logging/logger");
19
+ exports.GIVE_APP_CONTROL_OF_TOOL_RESPONSE = "GIVE_APP_CONTROL_OF_TOOL_RESPONSE";
19
20
  /**
20
21
  * ๐ŸŽฏ App Server Implementation
21
22
  *
@@ -51,6 +52,8 @@ class AppServer {
51
52
  this.config = config;
52
53
  /** Map of active user sessions by sessionId */
53
54
  this.activeSessions = new Map();
55
+ /** Map of active user sessions by userId */
56
+ this.activeSessionsByUserId = new Map();
54
57
  /** Array of cleanup handlers to run on shutdown */
55
58
  this.cleanupHandlers = [];
56
59
  /** App instructions string shown to the user */
@@ -58,22 +61,31 @@ class AppServer {
58
61
  // Set defaults and merge with provided config
59
62
  this.config = {
60
63
  port: 7010,
61
- webhookPath: '/webhook',
64
+ webhookPath: "/webhook",
62
65
  publicDir: false,
63
66
  healthCheck: true,
64
- ...config
67
+ ...config,
65
68
  };
66
- this.logger = logger_1.logger.child({ app: this.config.packageName, packageName: this.config.packageName, service: 'app-server' });
69
+ this.logger = logger_1.logger.child({
70
+ app: this.config.packageName,
71
+ packageName: this.config.packageName,
72
+ service: "app-server",
73
+ });
67
74
  // Initialize Express app
68
75
  this.app = (0, express_1.default)();
69
76
  this.app.use(express_1.default.json());
70
- const cookieParser = require('cookie-parser');
71
- this.app.use(cookieParser(this.config.cookieSecret || `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`));
77
+ const cookieParser = require("cookie-parser");
78
+ this.app.use(cookieParser(this.config.cookieSecret ||
79
+ `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`));
72
80
  // Apply authentication middleware
73
81
  this.app.use((0, webview_1.createAuthMiddleware)({
74
82
  apiKey: this.config.apiKey,
75
83
  packageName: this.config.packageName,
76
- cookieSecret: this.config.cookieSecret || `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`
84
+ getAppSessionForUser: (userId) => {
85
+ return this.activeSessionsByUserId.get(userId) || null;
86
+ },
87
+ cookieSecret: this.config.cookieSecret ||
88
+ `AOS_${this.config.packageName}_${this.config.apiKey.substring(0, 8)}`,
77
89
  }));
78
90
  this.appInstructions = config.appInstructions || null;
79
91
  // Setup server features
@@ -120,6 +132,7 @@ class AppServer {
120
132
  if (session) {
121
133
  session.disconnect();
122
134
  this.activeSessions.delete(sessionId);
135
+ this.activeSessionsByUserId.delete(userId);
123
136
  }
124
137
  }
125
138
  /**
@@ -157,25 +170,25 @@ class AppServer {
157
170
  * Gracefully shuts down the server and cleans up all sessions.
158
171
  */
159
172
  stop() {
160
- this.logger.info('\n๐Ÿ›‘ Shutting down...');
173
+ this.logger.info("\n๐Ÿ›‘ Shutting down...");
161
174
  this.cleanup();
162
175
  process.exit(0);
163
176
  }
164
177
  /**
165
- * ๐Ÿ” Generate a App token for a user
166
- * This should be called when handling a session webhook request.
167
- *
168
- * @param userId - User identifier
169
- * @param sessionId - Session identifier
170
- * @param secretKey - Secret key for signing the token
171
- * @returns JWT token string
172
- */
178
+ * ๐Ÿ” Generate a App token for a user
179
+ * This should be called when handling a session webhook request.
180
+ *
181
+ * @param userId - User identifier
182
+ * @param sessionId - Session identifier
183
+ * @param secretKey - Secret key for signing the token
184
+ * @returns JWT token string
185
+ */
173
186
  generateToken(userId, sessionId, secretKey) {
174
- const { createToken } = require('../token/utils');
187
+ const { createToken } = require("../token/utils");
175
188
  return createToken({
176
189
  userId,
177
190
  packageName: this.config.packageName,
178
- sessionId
191
+ sessionId,
179
192
  }, { secretKey });
180
193
  }
181
194
  /**
@@ -193,8 +206,8 @@ class AppServer {
193
206
  */
194
207
  setupWebhook() {
195
208
  if (!this.config.webhookPath) {
196
- this.logger.error('โŒ Webhook path not set');
197
- throw new Error('Webhook path not set');
209
+ this.logger.error("โŒ Webhook path not set");
210
+ throw new Error("Webhook path not set");
198
211
  }
199
212
  this.app.post(this.config.webhookPath, async (req, res) => {
200
213
  try {
@@ -209,18 +222,18 @@ class AppServer {
209
222
  }
210
223
  // Unknown webhook type
211
224
  else {
212
- this.logger.error('โŒ Unknown webhook request type');
225
+ this.logger.error("โŒ Unknown webhook request type");
213
226
  res.status(400).json({
214
- status: 'error',
215
- message: 'Unknown webhook request type'
227
+ status: "error",
228
+ message: "Unknown webhook request type",
216
229
  });
217
230
  }
218
231
  }
219
232
  catch (error) {
220
- this.logger.error(error, 'โŒ Error handling webhook: ' + error.message);
233
+ this.logger.error(error, "โŒ Error handling webhook: " + error.message);
221
234
  res.status(500).json({
222
- status: 'error',
223
- message: 'Error handling webhook: ' + error.message
235
+ status: "error",
236
+ message: "Error handling webhook: " + error.message,
224
237
  });
225
238
  }
226
239
  });
@@ -230,30 +243,39 @@ class AppServer {
230
243
  * Creates a /tool endpoint for handling tool calls from MentraOS Cloud.
231
244
  */
232
245
  setupToolCallEndpoint() {
233
- this.app.post('/tool', async (req, res) => {
246
+ this.app.post("/tool", async (req, res) => {
234
247
  try {
235
248
  const toolCall = req.body;
249
+ if (this.activeSessionsByUserId.has(toolCall.userId)) {
250
+ toolCall.activeSession =
251
+ this.activeSessionsByUserId.get(toolCall.userId) || null;
252
+ }
253
+ else {
254
+ toolCall.activeSession = null;
255
+ }
236
256
  this.logger.info({ body: req.body }, `๐Ÿ”ง Received tool call: ${toolCall.toolId}`);
237
257
  // Call the onToolCall handler and get the response
238
258
  const response = await this.onToolCall(toolCall);
239
259
  // Send back the response if one was provided
240
260
  if (response !== undefined) {
241
- res.json({ status: 'success', reply: response });
261
+ res.json({ status: "success", reply: response });
242
262
  }
243
263
  else {
244
- res.json({ status: 'success', reply: null });
264
+ res.json({ status: "success", reply: null });
245
265
  }
246
266
  }
247
267
  catch (error) {
248
- this.logger.error(error, 'โŒ Error handling tool call:');
268
+ this.logger.error(error, "โŒ Error handling tool call:");
249
269
  res.status(500).json({
250
- status: 'error',
251
- message: error instanceof Error ? error.message : 'Unknown error occurred calling tool'
270
+ status: "error",
271
+ message: error instanceof Error
272
+ ? error.message
273
+ : "Unknown error occurred calling tool",
252
274
  });
253
275
  }
254
276
  });
255
- this.app.get('/tool', async (req, res) => {
256
- res.json({ status: 'success', reply: 'Hello, world!' });
277
+ this.app.get("/tool", async (req, res) => {
278
+ res.json({ status: "success", reply: "Hello, world!" });
257
279
  });
258
280
  }
259
281
  /**
@@ -273,7 +295,7 @@ class AppServer {
273
295
  // Setup session event handlers
274
296
  const cleanupDisconnect = session.events.onDisconnected((info) => {
275
297
  // Handle different disconnect info formats (string or object)
276
- if (typeof info === 'string') {
298
+ if (typeof info === "string") {
277
299
  this.logger.info(`๐Ÿ‘‹ Session ${sessionId} disconnected: ${info}`);
278
300
  }
279
301
  else {
@@ -285,13 +307,14 @@ class AppServer {
285
307
  // Keep track of the original session before removal
286
308
  const session = this.activeSessions.get(sessionId);
287
309
  // Call onStop with a reconnection failure reason
288
- this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch(error => {
310
+ this.onStop(sessionId, userId, `Connection permanently lost: ${info.reason}`).catch((error) => {
289
311
  this.logger.error(error, `โŒ Error in onStop handler for permanent disconnection:`);
290
312
  });
291
313
  }
292
314
  }
293
315
  // Remove the session from active sessions in all cases
294
316
  this.activeSessions.delete(sessionId);
317
+ this.activeSessionsByUserId.delete(userId);
295
318
  });
296
319
  const cleanupError = session.events.onError((error) => {
297
320
  this.logger.error(error, `โŒ [Session ${sessionId}] Error:`);
@@ -300,16 +323,17 @@ class AppServer {
300
323
  try {
301
324
  await session.connect(sessionId);
302
325
  this.activeSessions.set(sessionId, session);
326
+ this.activeSessionsByUserId.set(userId, session);
303
327
  await this.onSession(session, sessionId, userId);
304
- res.status(200).json({ status: 'success' });
328
+ res.status(200).json({ status: "success" });
305
329
  }
306
330
  catch (error) {
307
- this.logger.error(error, 'โŒ Failed to connect:');
331
+ this.logger.error(error, "โŒ Failed to connect:");
308
332
  cleanupDisconnect();
309
333
  cleanupError();
310
334
  res.status(500).json({
311
- status: 'error',
312
- message: 'Failed to connect'
335
+ status: "error",
336
+ message: "Failed to connect",
313
337
  });
314
338
  }
315
339
  }
@@ -321,13 +345,13 @@ class AppServer {
321
345
  this.logger.info(`\n\n๐Ÿ›‘ Received stop request for user ${userId}, session ${sessionId}, reason: ${reason}\n\n`);
322
346
  try {
323
347
  await this.onStop(sessionId, userId, reason);
324
- res.status(200).json({ status: 'success' });
348
+ res.status(200).json({ status: "success" });
325
349
  }
326
350
  catch (error) {
327
- this.logger.error(error, 'โŒ Error handling stop request:');
351
+ this.logger.error(error, "โŒ Error handling stop request:");
328
352
  res.status(500).json({
329
- status: 'error',
330
- message: 'Failed to process stop request'
353
+ status: "error",
354
+ message: "Failed to process stop request",
331
355
  });
332
356
  }
333
357
  }
@@ -337,11 +361,11 @@ class AppServer {
337
361
  */
338
362
  setupHealthCheck() {
339
363
  if (this.config.healthCheck) {
340
- this.app.get('/health', (req, res) => {
364
+ this.app.get("/health", (req, res) => {
341
365
  res.json({
342
- status: 'healthy',
366
+ status: "healthy",
343
367
  app: this.config.packageName,
344
- activeSessions: this.activeSessions.size
368
+ activeSessions: this.activeSessions.size,
345
369
  });
346
370
  });
347
371
  }
@@ -351,13 +375,13 @@ class AppServer {
351
375
  * Creates a /settings endpoint that the MentraOS Cloud can use to update settings.
352
376
  */
353
377
  setupSettingsEndpoint() {
354
- this.app.post('/settings', async (req, res) => {
378
+ this.app.post("/settings", async (req, res) => {
355
379
  try {
356
380
  const { userIdForSettings, settings } = req.body;
357
381
  if (!userIdForSettings || !Array.isArray(settings)) {
358
382
  return res.status(400).json({
359
- status: 'error',
360
- message: 'Missing userId or settings array in request body'
383
+ status: "error",
384
+ message: "Missing userId or settings array in request body",
361
385
  });
362
386
  }
363
387
  this.logger.info(`โš™๏ธ Received settings update for user ${userIdForSettings}`);
@@ -367,7 +391,7 @@ class AppServer {
367
391
  this.activeSessions.forEach((session, sessionId) => {
368
392
  // Check if the session has this userId (not directly accessible)
369
393
  // We're relying on the webhook handler to have already verified this
370
- if (sessionId.includes(userIdForSettings)) {
394
+ if (session.userId === userIdForSettings) {
371
395
  userSessions.push(session);
372
396
  }
373
397
  });
@@ -382,20 +406,20 @@ class AppServer {
382
406
  session.updateSettingsForTesting(settings);
383
407
  }
384
408
  // Allow subclasses to handle settings updates if they implement the method
385
- if (typeof this.onSettingsUpdate === 'function') {
409
+ if (typeof this.onSettingsUpdate === "function") {
386
410
  await this.onSettingsUpdate(userIdForSettings, settings);
387
411
  }
388
412
  res.json({
389
- status: 'success',
390
- message: 'Settings updated successfully',
391
- sessionsUpdated: userSessions.length
413
+ status: "success",
414
+ message: "Settings updated successfully",
415
+ sessionsUpdated: userSessions.length,
392
416
  });
393
417
  }
394
418
  catch (error) {
395
- this.logger.error(error, 'โŒ Error handling settings update:');
419
+ this.logger.error(error, "โŒ Error handling settings update:");
396
420
  res.status(500).json({
397
- status: 'error',
398
- message: 'Internal server error processing settings update'
421
+ status: "error",
422
+ message: "Internal server error processing settings update",
399
423
  });
400
424
  }
401
425
  });
@@ -416,8 +440,8 @@ class AppServer {
416
440
  * Registers process signal handlers for graceful shutdown.
417
441
  */
418
442
  setupShutdown() {
419
- process.on('SIGTERM', () => this.stop());
420
- process.on('SIGINT', () => this.stop());
443
+ process.on("SIGTERM", () => this.stop());
444
+ process.on("SIGINT", () => this.stop());
421
445
  }
422
446
  /**
423
447
  * ๐Ÿงน Cleanup
@@ -430,15 +454,16 @@ class AppServer {
430
454
  session.disconnect();
431
455
  }
432
456
  this.activeSessions.clear();
457
+ this.activeSessionsByUserId.clear();
433
458
  // Run cleanup handlers
434
- this.cleanupHandlers.forEach(handler => handler());
459
+ this.cleanupHandlers.forEach((handler) => handler());
435
460
  }
436
461
  /**
437
462
  * ๐ŸŽฏ Setup Photo Upload Endpoint
438
463
  * Creates a /photo-upload endpoint for receiving photos directly from ASG glasses
439
464
  */
440
465
  setupPhotoUploadEndpoint() {
441
- const multer = require('multer');
466
+ const multer = require("multer");
442
467
  // Configure multer for handling multipart form data
443
468
  const upload = multer({
444
469
  storage: multer.memoryStorage(),
@@ -447,50 +472,50 @@ class AppServer {
447
472
  },
448
473
  fileFilter: (req, file, cb) => {
449
474
  // Accept image files only
450
- if (file.mimetype && file.mimetype.startsWith('image/')) {
475
+ if (file.mimetype && file.mimetype.startsWith("image/")) {
451
476
  cb(null, true);
452
477
  }
453
478
  else {
454
- cb(new Error('Only image files are allowed'), false);
479
+ cb(new Error("Only image files are allowed"), false);
455
480
  }
456
- }
481
+ },
457
482
  });
458
- this.app.post('/photo-upload', upload.single('photo'), async (req, res) => {
483
+ this.app.post("/photo-upload", upload.single("photo"), async (req, res) => {
459
484
  try {
460
485
  const { requestId, type } = req.body;
461
486
  const photoFile = req.file;
462
487
  this.logger.info({ requestId, type }, `๐Ÿ“ธ Received photo upload: ${requestId}`);
463
488
  if (!photoFile) {
464
- this.logger.error({ requestId }, 'No photo file in upload');
489
+ this.logger.error({ requestId }, "No photo file in upload");
465
490
  return res.status(400).json({
466
491
  success: false,
467
- error: 'No photo file provided'
492
+ error: "No photo file provided",
468
493
  });
469
494
  }
470
495
  if (!requestId) {
471
- this.logger.error('No requestId in photo upload');
496
+ this.logger.error("No requestId in photo upload");
472
497
  return res.status(400).json({
473
498
  success: false,
474
- error: 'No requestId provided'
499
+ error: "No requestId provided",
475
500
  });
476
501
  }
477
502
  // Find the corresponding session that made this photo request
478
503
  const session = this.findSessionByPhotoRequestId(requestId);
479
504
  if (!session) {
480
- this.logger.warn({ requestId }, 'No active session found for photo request');
505
+ this.logger.warn({ requestId }, "No active session found for photo request");
481
506
  return res.status(404).json({
482
507
  success: false,
483
- error: 'No active session found for this photo request'
508
+ error: "No active session found for this photo request",
484
509
  });
485
510
  }
486
511
  // Create photo data object
487
512
  const photoData = {
488
513
  buffer: photoFile.buffer,
489
514
  mimeType: photoFile.mimetype,
490
- filename: photoFile.originalname || 'photo.jpg',
515
+ filename: photoFile.originalname || "photo.jpg",
491
516
  requestId,
492
517
  size: photoFile.size,
493
- timestamp: new Date()
518
+ timestamp: new Date(),
494
519
  };
495
520
  // Deliver photo to the session
496
521
  session.camera.handlePhotoReceived(photoData);
@@ -498,14 +523,14 @@ class AppServer {
498
523
  res.json({
499
524
  success: true,
500
525
  requestId,
501
- message: 'Photo received successfully'
526
+ message: "Photo received successfully",
502
527
  });
503
528
  }
504
529
  catch (error) {
505
- this.logger.error(error, 'โŒ Error handling photo upload');
530
+ this.logger.error(error, "โŒ Error handling photo upload");
506
531
  res.status(500).json({
507
532
  success: false,
508
- error: 'Internal server error processing photo upload'
533
+ error: "Internal server error processing photo upload",
509
534
  });
510
535
  }
511
536
  });
@@ -540,8 +565,8 @@ class TpaServer extends AppServer {
540
565
  constructor(config) {
541
566
  super(config);
542
567
  // Emit a deprecation warning to help developers migrate
543
- console.warn('โš ๏ธ DEPRECATION WARNING: TpaServer is deprecated and will be removed in a future version. ' +
544
- 'Please use AppServer instead. ' +
568
+ console.warn("โš ๏ธ DEPRECATION WARNING: TpaServer is deprecated and will be removed in a future version. " +
569
+ "Please use AppServer instead. " +
545
570
  'Simply replace "TpaServer" with "AppServer" in your code.');
546
571
  }
547
572
  }
@@ -1,37 +1,42 @@
1
1
  /**
2
2
  * ๐ŸŽฎ Event Manager Module
3
3
  */
4
- import EventEmitter from 'events';
5
- import { StreamType, ExtendedStreamType, AppSettings, WebSocketError, ButtonPress, HeadPosition, PhoneNotification, TranscriptionData, TranslationData, GlassesBatteryUpdate, PhoneBatteryUpdate, GlassesConnectionState, LocationUpdate, Vad, AudioChunk, CalendarEvent, VpsCoordinates, CustomMessage, RtmpStreamStatus, PhotoTaken, ManagedStreamStatus, PhoneNotificationDismissed } from '../../types';
6
- import { DashboardMode } from '../../types/dashboard';
7
- import { PermissionErrorDetail } from '../../types/messages/cloud-to-app';
4
+ import EventEmitter from "events";
5
+ import { StreamType, ExtendedStreamType, AppSettings, WebSocketError, ButtonPress, HeadPosition, PhoneNotification, TranscriptionData, TranslationData, GlassesBatteryUpdate, PhoneBatteryUpdate, GlassesConnectionState, LocationUpdate, Vad, AudioChunk, CalendarEvent, VpsCoordinates, CustomMessage, RtmpStreamStatus, PhotoTaken, ManagedStreamStatus, PhoneNotificationDismissed, Capabilities } from "../../types";
6
+ import { DashboardMode } from "../../types/dashboard";
7
+ import { PermissionErrorDetail } from "../../types/messages/cloud-to-app";
8
8
  /** ๐ŸŽฏ Type-safe event handler function */
9
9
  type Handler<T> = (data: T) => void;
10
10
  /** ๐Ÿ”„ System events not tied to streams */
11
11
  interface SystemEvents {
12
- 'connected': AppSettings | undefined;
13
- 'disconnected': string | {
12
+ connected: AppSettings | undefined;
13
+ disconnected: string | {
14
14
  message: string;
15
15
  code: number;
16
16
  reason: string;
17
17
  wasClean: boolean;
18
18
  permanent?: boolean;
19
19
  };
20
- 'error': WebSocketError | Error;
21
- 'settings_update': AppSettings;
22
- 'dashboard_mode_change': {
23
- mode: DashboardMode | 'none';
20
+ error: WebSocketError | Error;
21
+ settings_update: AppSettings;
22
+ capabilities_update: {
23
+ capabilities: Capabilities | null;
24
+ modelName: string | null;
25
+ timestamp?: Date;
26
+ };
27
+ dashboard_mode_change: {
28
+ mode: DashboardMode | "none";
24
29
  };
25
- 'dashboard_always_on_change': {
30
+ dashboard_always_on_change: {
26
31
  enabled: boolean;
27
32
  };
28
- 'custom_message': CustomMessage;
29
- 'permission_error': {
33
+ custom_message: CustomMessage;
34
+ permission_error: {
30
35
  message: string;
31
36
  details: PermissionErrorDetail[];
32
37
  timestamp?: Date;
33
38
  };
34
- 'permission_denied': {
39
+ permission_denied: {
35
40
  stream: string;
36
41
  requiredPermission: string;
37
42
  message: string;
@@ -80,10 +85,11 @@ export declare class EventManager {
80
85
  * ๐ŸŽค Listen for transcription events in a specific language
81
86
  * @param language - Language code (e.g., "en-US")
82
87
  * @param handler - Function to handle transcription data
88
+ * @param disableLanguageIdentification - Optional flag to disable language identification (defaults to false/enabled)
83
89
  * @returns Cleanup function to remove the handler
84
90
  * @throws Error if language code is invalid
85
91
  */
86
- onTranscriptionForLanguage(language: string, handler: Handler<TranscriptionData>): () => void;
92
+ onTranscriptionForLanguage(language: string, handler: Handler<TranscriptionData>, disableLanguageIdentification?: boolean): () => void;
87
93
  /**
88
94
  * ๐ŸŒ Listen for translation events for a specific language pair
89
95
  * @param sourceLanguage - Source language code (e.g., "es-ES")
@@ -108,34 +114,40 @@ export declare class EventManager {
108
114
  * @returns Cleanup function to remove the handler
109
115
  */
110
116
  onAudioChunk(handler: Handler<AudioChunk>): () => void;
111
- onConnected(handler: Handler<SystemEvents['connected']>): () => EventEmitter<[never]>;
112
- onDisconnected(handler: Handler<SystemEvents['disconnected']>): () => EventEmitter<[never]>;
113
- onError(handler: Handler<SystemEvents['error']>): () => EventEmitter<[never]>;
114
- onSettingsUpdate(handler: Handler<SystemEvents['settings_update']>): () => EventEmitter<[never]>;
117
+ onConnected(handler: Handler<SystemEvents["connected"]>): () => EventEmitter<[never]>;
118
+ onDisconnected(handler: Handler<SystemEvents["disconnected"]>): () => EventEmitter<[never]>;
119
+ onError(handler: Handler<SystemEvents["error"]>): () => EventEmitter<[never]>;
120
+ onSettingsUpdate(handler: Handler<SystemEvents["settings_update"]>): () => EventEmitter<[never]>;
121
+ /**
122
+ * ๐Ÿ”ง Listen for device capabilities updates
123
+ * @param handler - Function to handle capabilities updates
124
+ * @returns Cleanup function to remove the handler
125
+ */
126
+ onCapabilitiesUpdate(handler: Handler<SystemEvents["capabilities_update"]>): () => EventEmitter<[never]>;
115
127
  /**
116
128
  * ๐ŸŒ Listen for dashboard mode changes
117
129
  * @param handler - Function to handle dashboard mode changes
118
130
  * @returns Cleanup function to remove the handler
119
131
  */
120
- onDashboardModeChange(handler: Handler<SystemEvents['dashboard_mode_change']>): () => EventEmitter<[never]>;
132
+ onDashboardModeChange(handler: Handler<SystemEvents["dashboard_mode_change"]>): () => EventEmitter<[never]>;
121
133
  /**
122
134
  * ๐ŸŒ Listen for dashboard always-on mode changes
123
135
  * @param handler - Function to handle dashboard always-on mode changes
124
136
  * @returns Cleanup function to remove the handler
125
137
  */
126
- onDashboardAlwaysOnChange(handler: Handler<SystemEvents['dashboard_always_on_change']>): () => EventEmitter<[never]>;
138
+ onDashboardAlwaysOnChange(handler: Handler<SystemEvents["dashboard_always_on_change"]>): () => EventEmitter<[never]>;
127
139
  /**
128
140
  * ๐Ÿšซ Listen for permission errors when subscriptions are rejected
129
141
  * @param handler - Function to handle permission errors
130
142
  * @returns Cleanup function to remove the handler
131
143
  */
132
- onPermissionError(handler: Handler<SystemEvents['permission_error']>): () => EventEmitter<[never]>;
144
+ onPermissionError(handler: Handler<SystemEvents["permission_error"]>): () => EventEmitter<[never]>;
133
145
  /**
134
146
  * ๐Ÿšซ Listen for individual permission denied events for specific streams
135
147
  * @param handler - Function to handle permission denied events
136
148
  * @returns Cleanup function to remove the handler
137
149
  */
138
- onPermissionDenied(handler: Handler<SystemEvents['permission_denied']>): () => EventEmitter<[never]>;
150
+ onPermissionDenied(handler: Handler<SystemEvents["permission_denied"]>): () => EventEmitter<[never]>;
139
151
  /**
140
152
  * ๐Ÿ”„ Listen for changes to a specific setting
141
153
  * @param key - Setting key to monitor