@omnikit-ai/sdk 2.2.0 → 2.2.2

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.
package/dist/index.js CHANGED
@@ -660,6 +660,20 @@ var APIClient = class {
660
660
  description: "Create a signed URL for a private file",
661
661
  method: "POST",
662
662
  params: { file_uri: "string" }
663
+ },
664
+ {
665
+ name: "DownloadPrivateFile",
666
+ path: "/services/files/private/download",
667
+ description: "Download a private file via backend proxy (solves CORS issues)",
668
+ method: "POST",
669
+ params: { file_uri: "string" }
670
+ },
671
+ {
672
+ name: "DownloadFile",
673
+ path: "/services/files/{file_id}/download",
674
+ description: "Download a public file via backend proxy (like Supabase ?download)",
675
+ method: "GET",
676
+ params: { file_id: "string", download: "boolean", filename: "string" }
663
677
  }
664
678
  ];
665
679
  builtInServices.forEach((service) => {
@@ -869,6 +883,25 @@ var APIClient = class {
869
883
  }
870
884
  return this._connectors;
871
885
  }
886
+ /**
887
+ * Resolve a return URL to an absolute URL.
888
+ * Handles relative paths like "/profile" by combining with current location.
889
+ * This fixes the OAuth redirect bug where relative URLs like "/profile" become
890
+ * "https://omnikit.ai/profile" instead of "https://omnikit.ai/app-builder/{id}/preview/profile"
891
+ */
892
+ _resolveReturnUrl(returnUrl) {
893
+ if (typeof window === "undefined") return returnUrl || "/";
894
+ if (!returnUrl) return window.location.href;
895
+ if (returnUrl.startsWith("http://") || returnUrl.startsWith("https://")) {
896
+ return returnUrl;
897
+ }
898
+ if (returnUrl.startsWith("/")) {
899
+ const basePath = window.location.pathname.endsWith("/") ? window.location.pathname.slice(0, -1) : window.location.pathname;
900
+ return window.location.origin + basePath + returnUrl;
901
+ }
902
+ const base = window.location.origin + window.location.pathname;
903
+ return base.endsWith("/") ? base + returnUrl : base + "/" + returnUrl;
904
+ }
872
905
  /**
873
906
  * Create auth proxy that auto-initializes
874
907
  */
@@ -896,31 +929,42 @@ var APIClient = class {
896
929
  client.emitUserChange(response);
897
930
  return response;
898
931
  },
899
- login(returnUrl) {
900
- const fullReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
901
- if (typeof window !== "undefined") {
902
- if (window.__omnikit_openLoginModal) {
903
- window.__omnikit_openLoginModal(fullReturnUrl);
904
- return;
905
- }
906
- const encodedReturnUrl = encodeURIComponent(fullReturnUrl);
907
- const currentPath = window.location.pathname;
908
- const apiSitesMatch = currentPath.match(/^\/api\/sites\/([^\/]+)/);
909
- if (apiSitesMatch) {
910
- window.location.href = `/api/sites/${client.appId}/login?return_url=${encodedReturnUrl}`;
911
- } else {
912
- window.location.href = `/login?return_url=${encodedReturnUrl}`;
932
+ async login(returnUrl) {
933
+ if (typeof window === "undefined") return;
934
+ const fullReturnUrl = client._resolveReturnUrl(returnUrl);
935
+ const token = getAccessToken();
936
+ if (token) {
937
+ try {
938
+ const isAuth = await this.isAuthenticated();
939
+ if (isAuth && returnUrl) {
940
+ window.location.href = fullReturnUrl;
941
+ return;
942
+ }
943
+ } catch {
913
944
  }
914
945
  }
946
+ if (window.__omnikit_openLoginModal) {
947
+ window.__omnikit_openLoginModal(fullReturnUrl);
948
+ return;
949
+ }
950
+ const encodedReturnUrl = encodeURIComponent(fullReturnUrl);
951
+ const currentPath = window.location.pathname;
952
+ const apiSitesMatch = currentPath.match(/^\/api\/sites\/([^\/]+)/);
953
+ if (apiSitesMatch) {
954
+ window.location.href = `/api/sites/${client.appId}/login?return_url=${encodedReturnUrl}`;
955
+ } else {
956
+ window.location.href = `/login?return_url=${encodedReturnUrl}`;
957
+ }
915
958
  },
916
959
  /**
917
960
  * Request a passwordless login code to email
918
961
  */
919
962
  async requestLoginCode(email, returnUrl) {
963
+ const fullReturnUrl = returnUrl ? client._resolveReturnUrl(returnUrl) : void 0;
920
964
  return await client.makeRequest(
921
965
  `${client.baseUrl}/auth/email/request-code`,
922
966
  "POST",
923
- { email, return_url: returnUrl }
967
+ { email, return_url: fullReturnUrl }
924
968
  );
925
969
  },
926
970
  /**
@@ -968,44 +1012,43 @@ var APIClient = class {
968
1012
  * Redirects to the backend OAuth endpoint for the specified provider
969
1013
  */
970
1014
  loginWithProvider(providerId, returnUrl) {
971
- const currentUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
972
- const redirectUrl = `${client.baseUrl}/auth/${providerId}?redirect_url=${encodeURIComponent(currentUrl)}`;
973
- if (typeof window !== "undefined") {
974
- const inIframe = window.self !== window.top;
975
- if (inIframe) {
976
- const width = 600;
977
- const height = 700;
978
- const left = window.screen.width / 2 - width / 2;
979
- const top = window.screen.height / 2 - height / 2;
980
- const popup = window.open(
981
- redirectUrl,
982
- "oauth_popup",
983
- `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=yes`
984
- );
985
- if (popup) {
986
- const checkTimer = setInterval(() => {
987
- const token = getAccessToken();
988
- if (token) {
989
- clearInterval(checkTimer);
990
- popup.close();
1015
+ if (typeof window === "undefined") {
1016
+ throw new OmnikitError("loginWithProvider() can only be called in browser environment", 400, "NOT_BROWSER");
1017
+ }
1018
+ const fullReturnUrl = client._resolveReturnUrl(returnUrl);
1019
+ const redirectUrl = `${client.baseUrl}/auth/${providerId}?redirect_url=${encodeURIComponent(fullReturnUrl)}`;
1020
+ const inIframe = window.self !== window.top;
1021
+ if (inIframe) {
1022
+ const width = 600;
1023
+ const height = 700;
1024
+ const left = window.screen.width / 2 - width / 2;
1025
+ const top = window.screen.height / 2 - height / 2;
1026
+ const popup = window.open(
1027
+ redirectUrl,
1028
+ "oauth_popup",
1029
+ `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=yes`
1030
+ );
1031
+ if (popup) {
1032
+ const checkTimer = setInterval(() => {
1033
+ const token = getAccessToken();
1034
+ if (token) {
1035
+ clearInterval(checkTimer);
1036
+ popup.close();
1037
+ window.location.reload();
1038
+ return;
1039
+ }
1040
+ if (popup.closed) {
1041
+ clearInterval(checkTimer);
1042
+ const finalToken = getAccessToken();
1043
+ if (finalToken) {
991
1044
  window.location.reload();
992
- return;
993
1045
  }
994
- if (popup.closed) {
995
- clearInterval(checkTimer);
996
- const finalToken = getAccessToken();
997
- if (finalToken) {
998
- window.location.reload();
999
- }
1000
- }
1001
- }, 1e3);
1002
- return;
1003
- }
1046
+ }
1047
+ }, 1e3);
1048
+ return;
1004
1049
  }
1005
- window.location.href = redirectUrl;
1006
- } else {
1007
- throw new OmnikitError("loginWithProvider() can only be called in browser environment", 400, "NOT_BROWSER");
1008
1050
  }
1051
+ window.location.href = redirectUrl;
1009
1052
  },
1010
1053
  /**
1011
1054
  * Initiate Google OAuth Login
@@ -1217,7 +1260,8 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1217
1260
  // Remove callback functions from request body
1218
1261
  onToken: void 0,
1219
1262
  onComplete: void 0,
1220
- onError: void 0
1263
+ onError: void 0,
1264
+ onToolCall: void 0
1221
1265
  };
1222
1266
  Object.keys(requestBody).forEach((key) => {
1223
1267
  if (requestBody[key] === void 0) {
@@ -1268,6 +1312,14 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1268
1312
  if (event.type === "token" && event.content) {
1269
1313
  fullResponse += event.content;
1270
1314
  params.onToken?.(event.content);
1315
+ } else if (event.type === "tool_call") {
1316
+ if (params.onToolCall && event.id && event.name) {
1317
+ params.onToolCall({
1318
+ id: event.id,
1319
+ name: event.name,
1320
+ arguments: event.arguments || {}
1321
+ });
1322
+ }
1271
1323
  } else if (event.type === "done") {
1272
1324
  params.onComplete?.({
1273
1325
  result: event.result || fullResponse,
@@ -1326,6 +1378,56 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1326
1378
  );
1327
1379
  };
1328
1380
  }
1381
+ if (normalizedName === "DownloadFile") {
1382
+ return async function(params) {
1383
+ await client.ensureInitialized();
1384
+ if (!params?.file_id) {
1385
+ throw new OmnikitError(
1386
+ "file_id is required for DownloadFile",
1387
+ 400,
1388
+ "MISSING_FILE_ID"
1389
+ );
1390
+ }
1391
+ const downloadUrl = new URL(`${client.baseUrl}/apps/${client.appId}/services/files/${params.file_id}/download`);
1392
+ if (params.download !== void 0) {
1393
+ downloadUrl.searchParams.set("download", String(params.download));
1394
+ }
1395
+ if (params.filename) {
1396
+ downloadUrl.searchParams.set("filename", params.filename);
1397
+ }
1398
+ const response = await fetch(downloadUrl.toString(), {
1399
+ method: "GET"
1400
+ });
1401
+ if (!response.ok) {
1402
+ const error = await response.json().catch(() => ({ detail: "Download failed" }));
1403
+ throw new OmnikitError(
1404
+ error.detail || "Download failed",
1405
+ response.status,
1406
+ "DOWNLOAD_FAILED"
1407
+ );
1408
+ }
1409
+ const blob = await response.blob();
1410
+ const blobUrl = URL.createObjectURL(blob);
1411
+ let filename = params.filename;
1412
+ if (!filename) {
1413
+ const contentDisposition = response.headers.get("Content-Disposition");
1414
+ if (contentDisposition) {
1415
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
1416
+ if (match) filename = match[1];
1417
+ }
1418
+ }
1419
+ if (!filename) {
1420
+ filename = `file-${params.file_id}`;
1421
+ }
1422
+ const link = document.createElement("a");
1423
+ link.href = blobUrl;
1424
+ link.download = filename;
1425
+ document.body.appendChild(link);
1426
+ link.click();
1427
+ document.body.removeChild(link);
1428
+ URL.revokeObjectURL(blobUrl);
1429
+ };
1430
+ }
1329
1431
  if (normalizedName === "DownloadPrivateFile") {
1330
1432
  return async function(params) {
1331
1433
  await client.ensureInitialized();
@@ -1336,16 +1438,47 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1336
1438
  "MISSING_FILE_URI"
1337
1439
  );
1338
1440
  }
1339
- const downloadUrl = new URL(`${client.baseUrl}/apps/${client.appId}/services/files/download`);
1340
- downloadUrl.searchParams.set("file_uri", params.file_uri);
1341
- if (params.filename) {
1342
- downloadUrl.searchParams.set("filename", params.filename);
1343
- }
1441
+ const downloadUrl = `${client.baseUrl}/apps/${client.appId}/services/files/private/download`;
1344
1442
  const token = client.getAuthToken();
1345
- if (token) {
1346
- downloadUrl.searchParams.set("token", token);
1443
+ const response = await fetch(downloadUrl, {
1444
+ method: "POST",
1445
+ headers: {
1446
+ "Content-Type": "application/json",
1447
+ ...token ? { "Authorization": `Bearer ${token}` } : {}
1448
+ },
1449
+ body: JSON.stringify({ file_uri: params.file_uri })
1450
+ });
1451
+ if (!response.ok) {
1452
+ const error = await response.json().catch(() => ({ detail: "Download failed" }));
1453
+ throw new OmnikitError(
1454
+ error.detail || "Download failed",
1455
+ response.status,
1456
+ "DOWNLOAD_FAILED"
1457
+ );
1458
+ }
1459
+ const blob = await response.blob();
1460
+ const blobUrl = URL.createObjectURL(blob);
1461
+ let filename = params.filename;
1462
+ if (!filename) {
1463
+ const contentDisposition = response.headers.get("Content-Disposition");
1464
+ if (contentDisposition) {
1465
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
1466
+ if (match) filename = match[1];
1467
+ }
1347
1468
  }
1348
- window.open(downloadUrl.toString(), "_blank");
1469
+ if (!filename) {
1470
+ const uriParts = params.file_uri.split("/");
1471
+ const lastPart = uriParts[uriParts.length - 1];
1472
+ const underscoreIdx = lastPart.indexOf("_");
1473
+ filename = underscoreIdx > 0 ? lastPart.slice(underscoreIdx + 1) : lastPart;
1474
+ }
1475
+ const link = document.createElement("a");
1476
+ link.href = blobUrl;
1477
+ link.download = filename;
1478
+ document.body.appendChild(link);
1479
+ link.click();
1480
+ document.body.removeChild(link);
1481
+ URL.revokeObjectURL(blobUrl);
1349
1482
  };
1350
1483
  }
1351
1484
  return async function(params, asyncOptions) {
@@ -1364,7 +1497,7 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1364
1497
  response = await method(params, useServiceToken);
1365
1498
  } else {
1366
1499
  throw new OmnikitError(
1367
- `Service '${serviceName}' not found. Known services: SendEmail, InvokeLLM, GenerateImage, GenerateSpeech, GenerateVideo, ExtractData, SendSMS, UploadFile, UploadPrivateFile, CreateFileSignedUrl (camelCase also supported)`,
1500
+ `Service '${serviceName}' not found. Known services: SendEmail, InvokeLLM, GenerateImage, GenerateSpeech, GenerateVideo, ExtractData, SendSMS, UploadFile, UploadPrivateFile, CreateFileSignedUrl, DownloadFile, DownloadPrivateFile (camelCase also supported)`,
1368
1501
  404,
1369
1502
  "SERVICE_NOT_FOUND"
1370
1503
  );
@@ -1417,6 +1550,18 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1417
1550
  if (params?.model) {
1418
1551
  params = { ...params, model: mapLLMModel(params.model) };
1419
1552
  }
1553
+ if (params?.saveResult) {
1554
+ params = {
1555
+ ...params,
1556
+ save_result: {
1557
+ collection: params.saveResult.collection,
1558
+ document_id: params.saveResult.documentId,
1559
+ field: params.saveResult.field,
1560
+ additional_fields: params.saveResult.additionalFields
1561
+ }
1562
+ };
1563
+ delete params.saveResult;
1564
+ }
1420
1565
  }
1421
1566
  const integrationPackage = client._integrations[packageName];
1422
1567
  if (!integrationPackage) {
@@ -1531,7 +1676,7 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1531
1676
  );
1532
1677
  return response;
1533
1678
  },
1534
- // List all records with MongoDB/Mongoose-style filtering
1679
+ // List all records with MongoDB/Mongoose-style filtering and cursor pagination
1535
1680
  async list(...args) {
1536
1681
  const queryParams = new URLSearchParams();
1537
1682
  let filter;
@@ -1576,11 +1721,25 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1576
1721
  if (options.offset !== void 0) {
1577
1722
  queryParams.append("offset", String(options.offset));
1578
1723
  }
1724
+ if (options.after !== void 0) {
1725
+ queryParams.append("after", options.after);
1726
+ }
1579
1727
  }
1580
1728
  }
1581
1729
  const url = queryParams.toString() ? `${client.baseUrl}/apps/${client.appId}/collections/${collectionName}?${queryParams}` : `${client.baseUrl}/apps/${client.appId}/collections/${collectionName}`;
1582
1730
  const response = await client.makeRequest(url, "GET");
1583
- return makeArrayForgiving(Array.isArray(response) ? response : []);
1731
+ if (response && typeof response === "object" && "data" in response && "hasMore" in response) {
1732
+ return {
1733
+ data: makeArrayForgiving(response.data),
1734
+ hasMore: response.hasMore,
1735
+ nextCursor: response.nextCursor
1736
+ };
1737
+ }
1738
+ return {
1739
+ data: makeArrayForgiving(Array.isArray(response) ? response : []),
1740
+ hasMore: false,
1741
+ nextCursor: null
1742
+ };
1584
1743
  },
1585
1744
  // Filter records by query (alias for list)
1586
1745
  async filter(...args) {
@@ -1589,21 +1748,21 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1589
1748
  // Find single record matching query
1590
1749
  async findOne(...args) {
1591
1750
  if (args.length === 0) {
1592
- const results = await this.list({}, { limit: 1 });
1593
- return results[0] || null;
1751
+ const result = await this.list({}, { limit: 1 });
1752
+ return result.data[0] || null;
1594
1753
  } else if (args.length === 1) {
1595
1754
  const arg = args[0];
1596
1755
  if (arg && (arg.q !== void 0 || arg._count !== void 0)) {
1597
- const results = await this.list({ ...arg, limit: 1 });
1598
- return results[0] || null;
1756
+ const result = await this.list({ ...arg, limit: 1 });
1757
+ return result.data[0] || null;
1599
1758
  } else {
1600
- const results = await this.list(arg, { limit: 1 });
1601
- return results[0] || null;
1759
+ const result = await this.list(arg, { limit: 1 });
1760
+ return result.data[0] || null;
1602
1761
  }
1603
1762
  } else {
1604
1763
  const [filter, options] = args;
1605
- const results = await this.list(filter, { ...options, limit: 1 });
1606
- return results[0] || null;
1764
+ const result = await this.list(filter, { ...options, limit: 1 });
1765
+ return result.data[0] || null;
1607
1766
  }
1608
1767
  },
1609
1768
  // Create new record
@@ -1880,6 +2039,8 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
1880
2039
  "files": "UploadFile",
1881
2040
  "files/private": "UploadPrivateFile",
1882
2041
  "files/signed-url": "CreateFileSignedUrl",
2042
+ "files/private/download": "DownloadPrivateFile",
2043
+ "files/download": "DownloadFile",
1883
2044
  "images": "GenerateImage",
1884
2045
  "speech": "GenerateSpeech",
1885
2046
  "video": "GenerateVideo",
@@ -2227,6 +2388,266 @@ Example: await ${collectionName}.list({ limit: 100, sort: '-created_at' })`,
2227
2388
  config
2228
2389
  );
2229
2390
  }
2391
+ /**
2392
+ * Run an assistant with streaming response.
2393
+ *
2394
+ * Assistants are server-side AI agents with custom instructions and built-in actions.
2395
+ * They can create/update/delete records, send emails, make HTTP requests, etc.
2396
+ *
2397
+ * @example
2398
+ * ```typescript
2399
+ * const result = await omnikit.runAssistant('customer_support', {
2400
+ * messages: [{ role: 'user', content: 'Help me track my order #12345' }],
2401
+ * onToken: (token) => setResponse(prev => prev + token),
2402
+ * onActionStart: (action) => console.log(`Starting: ${action.name}`),
2403
+ * onActionComplete: (action) => console.log(`Completed: ${action.name}`),
2404
+ * });
2405
+ *
2406
+ * // Continue the conversation
2407
+ * await omnikit.runAssistant('customer_support', {
2408
+ * messages: [{ role: 'user', content: 'When will it arrive?' }],
2409
+ * threadId: result.threadId,
2410
+ * onToken: (token) => setResponse(prev => prev + token),
2411
+ * });
2412
+ * ```
2413
+ *
2414
+ * @param assistantName - Name of the assistant to run
2415
+ * @param params - Run parameters including messages and callbacks
2416
+ * @returns Promise resolving to the run result with threadId
2417
+ */
2418
+ async runAssistant(assistantName, params) {
2419
+ await this.ensureInitialized();
2420
+ const headers = {
2421
+ "Content-Type": "application/json"
2422
+ };
2423
+ const token = this.getAuthToken();
2424
+ if (token) {
2425
+ headers["Authorization"] = `Bearer ${token}`;
2426
+ }
2427
+ if (this._serviceToken) {
2428
+ headers["X-Service-Token"] = this._serviceToken;
2429
+ }
2430
+ if (this._apiKey) {
2431
+ headers["X-API-Key"] = this._apiKey;
2432
+ }
2433
+ const requestBody = {
2434
+ messages: params.messages,
2435
+ thread_id: params.threadId,
2436
+ stream: true
2437
+ };
2438
+ const response = await fetch(
2439
+ `${this.baseUrl}/apps/${this.appId}/assistants/${assistantName}/runs`,
2440
+ {
2441
+ method: "POST",
2442
+ headers,
2443
+ body: JSON.stringify(requestBody)
2444
+ }
2445
+ );
2446
+ if (!response.ok) {
2447
+ const errorText = await response.text();
2448
+ let errorMessage = `Assistant run failed: ${response.statusText}`;
2449
+ try {
2450
+ const errorJson = JSON.parse(errorText);
2451
+ errorMessage = errorJson.detail || errorJson.message || errorMessage;
2452
+ } catch {
2453
+ errorMessage = errorText || errorMessage;
2454
+ }
2455
+ const err = new OmnikitError(errorMessage, response.status, "ASSISTANT_ERROR");
2456
+ params.onError?.(err);
2457
+ throw err;
2458
+ }
2459
+ const reader = response.body?.getReader();
2460
+ if (!reader) {
2461
+ const err = new OmnikitError("No response body", 500, "NO_BODY");
2462
+ params.onError?.(err);
2463
+ throw err;
2464
+ }
2465
+ const decoder = new TextDecoder();
2466
+ let fullResponse = "";
2467
+ let buffer = "";
2468
+ let threadId = params.threadId || "";
2469
+ let messageCount = 0;
2470
+ try {
2471
+ while (true) {
2472
+ const { done, value } = await reader.read();
2473
+ if (done) break;
2474
+ buffer += decoder.decode(value, { stream: true });
2475
+ const lines = buffer.split("\n");
2476
+ buffer = lines.pop() || "";
2477
+ for (const line of lines) {
2478
+ if (line.startsWith("data: ")) {
2479
+ try {
2480
+ const event = JSON.parse(line.slice(6));
2481
+ if (event.type === "token" && event.content) {
2482
+ fullResponse += event.content;
2483
+ params.onToken?.(event.content);
2484
+ } else if (event.type === "action_start") {
2485
+ params.onActionStart?.({
2486
+ id: event.id || "",
2487
+ name: event.name || "",
2488
+ arguments: event.arguments || {}
2489
+ });
2490
+ } else if (event.type === "action_complete") {
2491
+ params.onActionComplete?.({
2492
+ id: event.id || "",
2493
+ name: event.name || "",
2494
+ output: event.output
2495
+ });
2496
+ } else if (event.type === "action_failed") {
2497
+ params.onActionFailed?.({
2498
+ id: event.id || "",
2499
+ name: event.name || "",
2500
+ error: event.error || "Action failed"
2501
+ });
2502
+ } else if (event.type === "done") {
2503
+ threadId = event.thread_id || threadId;
2504
+ messageCount = event.message_count || 0;
2505
+ const result = {
2506
+ threadId,
2507
+ messageCount,
2508
+ response: fullResponse
2509
+ };
2510
+ params.onComplete?.(result);
2511
+ } else if (event.type === "error") {
2512
+ const err = new OmnikitError(
2513
+ event.message || "Assistant error",
2514
+ 500,
2515
+ "ASSISTANT_ERROR"
2516
+ );
2517
+ params.onError?.(err);
2518
+ throw err;
2519
+ }
2520
+ } catch (parseError) {
2521
+ if (parseError instanceof OmnikitError) {
2522
+ throw parseError;
2523
+ }
2524
+ }
2525
+ }
2526
+ }
2527
+ }
2528
+ } finally {
2529
+ reader.releaseLock();
2530
+ }
2531
+ return {
2532
+ threadId,
2533
+ messageCount,
2534
+ response: fullResponse
2535
+ };
2536
+ }
2537
+ /**
2538
+ * Subscribe to a thread via WebSocket for real-time updates.
2539
+ *
2540
+ * Use this to watch a thread without triggering a run. Useful for:
2541
+ * - Showing live updates when another client runs the assistant
2542
+ * - Reconnecting after page navigation
2543
+ * - Observing thread activity in real-time
2544
+ *
2545
+ * @example
2546
+ * ```typescript
2547
+ * // Subscribe to a thread
2548
+ * const unsubscribe = omnikit.subscribeToThread(threadId, {
2549
+ * onConnected: (info) => console.log(`Connected to ${info.assistantName}`),
2550
+ * onToken: (token) => setResponse(prev => prev + token),
2551
+ * onActionStart: (action) => console.log(`Action: ${action.name}`),
2552
+ * onActionComplete: (action) => console.log(`Result: ${JSON.stringify(action.output)}`),
2553
+ * onRunComplete: (result) => console.log(`Run complete, ${result.messageCount} messages`),
2554
+ * onError: (error) => console.error(error),
2555
+ * });
2556
+ *
2557
+ * // Later: disconnect
2558
+ * unsubscribe();
2559
+ * ```
2560
+ *
2561
+ * @param threadId - Thread ID to subscribe to
2562
+ * @param callbacks - Event callbacks for real-time updates
2563
+ * @returns Unsubscribe function to close the WebSocket
2564
+ */
2565
+ subscribeToThread(threadId, callbacks) {
2566
+ const wsProtocol = this.baseUrl.startsWith("https") ? "wss" : "ws";
2567
+ const wsBaseUrl = this.baseUrl.replace(/^https?:\/\//, "");
2568
+ let wsUrl = `${wsProtocol}://${wsBaseUrl}/apps/${this.appId}/threads/${threadId}/subscribe`;
2569
+ const token = this.getAuthToken();
2570
+ if (token) {
2571
+ wsUrl += `?token=${encodeURIComponent(token)}`;
2572
+ }
2573
+ const ws = new WebSocket(wsUrl);
2574
+ let pingInterval = null;
2575
+ ws.onopen = () => {
2576
+ pingInterval = setInterval(() => {
2577
+ if (ws.readyState === WebSocket.OPEN) {
2578
+ ws.send(JSON.stringify({ type: "ping" }));
2579
+ }
2580
+ }, 3e4);
2581
+ };
2582
+ ws.onmessage = (event) => {
2583
+ try {
2584
+ const data = JSON.parse(event.data);
2585
+ switch (data.type) {
2586
+ case "connected":
2587
+ callbacks.onConnected?.({
2588
+ threadId: data.thread_id,
2589
+ assistantName: data.assistant_name,
2590
+ messageCount: data.message_count
2591
+ });
2592
+ break;
2593
+ case "token":
2594
+ callbacks.onToken?.(data.content || "");
2595
+ break;
2596
+ case "action_start":
2597
+ callbacks.onActionStart?.({
2598
+ id: data.id || "",
2599
+ name: data.name || "",
2600
+ arguments: data.arguments || {}
2601
+ });
2602
+ break;
2603
+ case "action_complete":
2604
+ callbacks.onActionComplete?.({
2605
+ id: data.id || "",
2606
+ name: data.name || "",
2607
+ output: data.output
2608
+ });
2609
+ break;
2610
+ case "action_failed":
2611
+ callbacks.onActionFailed?.({
2612
+ id: data.id || "",
2613
+ name: data.name || "",
2614
+ error: data.error || "Action failed"
2615
+ });
2616
+ break;
2617
+ case "run_complete":
2618
+ case "done":
2619
+ callbacks.onRunComplete?.({
2620
+ threadId: data.thread_id || threadId,
2621
+ messageCount: data.message_count || 0
2622
+ });
2623
+ break;
2624
+ case "error":
2625
+ callbacks.onError?.(new Error(data.message || "WebSocket error"));
2626
+ break;
2627
+ case "pong":
2628
+ break;
2629
+ }
2630
+ } catch (parseError) {
2631
+ }
2632
+ };
2633
+ ws.onerror = (event) => {
2634
+ callbacks.onError?.(new Error("WebSocket connection error"));
2635
+ };
2636
+ ws.onclose = () => {
2637
+ if (pingInterval) {
2638
+ clearInterval(pingInterval);
2639
+ }
2640
+ callbacks.onDisconnect?.();
2641
+ };
2642
+ return () => {
2643
+ if (pingInterval) {
2644
+ clearInterval(pingInterval);
2645
+ }
2646
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
2647
+ ws.close();
2648
+ }
2649
+ };
2650
+ }
2230
2651
  /**
2231
2652
  * Invoke a backend function by name.
2232
2653
  *