openmates 0.11.0-alpha.14 → 0.11.0-alpha.16

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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-SFTCIVE2.js";
4
4
 
5
5
  // src/client.ts
6
- import { randomUUID } from "crypto";
6
+ import { randomUUID as randomUUID2 } from "crypto";
7
7
  import { platform as platform2, release } from "os";
8
8
  import { createInterface } from "readline/promises";
9
9
  import { stdin, stdout } from "process";
@@ -726,6 +726,12 @@ function loadSyncCache() {
726
726
  const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
727
727
  return readJsonFile(filePath);
728
728
  }
729
+ function clearSyncCache() {
730
+ const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
731
+ if (existsSync2(filePath)) {
732
+ rmSync(filePath);
733
+ }
734
+ }
729
735
  function isSyncCacheFresh(maxAgeMs = 3e5) {
730
736
  const cache = loadSyncCache();
731
737
  if (!cache) return false;
@@ -733,7 +739,9 @@ function isSyncCacheFresh(maxAgeMs = 3e5) {
733
739
  }
734
740
 
735
741
  // src/ws.ts
736
- import WebSocket from "ws";
742
+ import { createRequire } from "module";
743
+ var require2 = createRequire(import.meta.url);
744
+ var WebSocket = require2("ws");
737
745
  var OpenMatesWsClient = class {
738
746
  socket;
739
747
  constructor(options) {
@@ -793,6 +801,14 @@ var OpenMatesWsClient = class {
793
801
  send(type, payload) {
794
802
  this.socket.send(JSON.stringify({ type, payload }));
795
803
  }
804
+ sendAsync(type, payload) {
805
+ return new Promise((resolve4, reject) => {
806
+ this.socket.send(JSON.stringify({ type, payload }), (error) => {
807
+ if (error) reject(error);
808
+ else resolve4();
809
+ });
810
+ });
811
+ }
796
812
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
797
813
  return new Promise((resolve4, reject) => {
798
814
  const onMessage = (rawData) => {
@@ -895,35 +911,83 @@ var OpenMatesWsClient = class {
895
911
  collectAiResponse(userMessageId, chatId, options) {
896
912
  const timeoutMs = options?.timeoutMs ?? 9e4;
897
913
  const onStream = options?.onStream;
914
+ const asyncEmbedWaitMs = options?.asyncEmbedWaitMs ?? 12e4;
898
915
  return new Promise((resolve4, reject) => {
899
916
  let latestContent = "";
917
+ let messageId = null;
918
+ let taskId = null;
900
919
  let category = null;
901
920
  let modelName = null;
902
921
  let followUpSuggestions = [];
922
+ const embeds = /* @__PURE__ */ new Map();
923
+ const processingEmbedIds = /* @__PURE__ */ new Set();
903
924
  let aiResponseDone = false;
925
+ let postProcessingDone = false;
904
926
  const POST_PROCESSING_WINDOW_MS = 12e3;
905
927
  let postProcessingTimer = null;
928
+ let asyncEmbedTimer = null;
906
929
  const timeout = setTimeout(() => {
907
930
  cleanup();
908
931
  reject(new Error("Timed out waiting for AI response"));
909
932
  }, timeoutMs);
910
933
  const capture = (p) => {
934
+ if (typeof p.message_id === "string" && p.message_id)
935
+ messageId = p.message_id;
936
+ if (typeof p.task_id === "string" && p.task_id) taskId = p.task_id;
911
937
  if (typeof p.category === "string" && p.category) category = p.category;
912
938
  if (typeof p.model_name === "string" && p.model_name)
913
939
  modelName = p.model_name;
914
940
  };
941
+ const extractMessageContent = (message) => {
942
+ if (typeof message.content === "string") return message.content;
943
+ const content = message.content;
944
+ if (!content || typeof content !== "object") return "";
945
+ try {
946
+ const root = content;
947
+ const text = root.content?.[0]?.content?.[0]?.text;
948
+ return typeof text === "string" ? text : "";
949
+ } catch {
950
+ return "";
951
+ }
952
+ };
953
+ const finishPostProcessingWait = () => {
954
+ postProcessingDone = true;
955
+ maybeResolve();
956
+ };
957
+ const maybeResolve = () => {
958
+ if (!aiResponseDone || !postProcessingDone) return;
959
+ if (processingEmbedIds.size > 0 && !asyncEmbedTimer) {
960
+ asyncEmbedTimer = setTimeout(() => {
961
+ cleanup();
962
+ resolve4({
963
+ messageId,
964
+ taskId,
965
+ content: latestContent,
966
+ category,
967
+ modelName,
968
+ followUpSuggestions,
969
+ embeds: [...embeds.values()]
970
+ });
971
+ }, asyncEmbedWaitMs);
972
+ return;
973
+ }
974
+ if (processingEmbedIds.size > 0) return;
975
+ cleanup();
976
+ resolve4({
977
+ messageId,
978
+ taskId,
979
+ content: latestContent,
980
+ category,
981
+ modelName,
982
+ followUpSuggestions,
983
+ embeds: [...embeds.values()]
984
+ });
985
+ };
915
986
  const scheduleResolve = (content) => {
916
987
  aiResponseDone = true;
917
988
  latestContent = content;
918
- postProcessingTimer = setTimeout(() => {
919
- cleanup();
920
- resolve4({
921
- content: latestContent,
922
- category,
923
- modelName,
924
- followUpSuggestions
925
- });
926
- }, POST_PROCESSING_WINDOW_MS);
989
+ clearTimeout(timeout);
990
+ postProcessingTimer = setTimeout(finishPostProcessingWait, POST_PROCESSING_WINDOW_MS);
927
991
  };
928
992
  const onMessage = (rawData) => {
929
993
  try {
@@ -939,9 +1003,27 @@ var OpenMatesWsClient = class {
939
1003
  );
940
1004
  return;
941
1005
  }
1006
+ if (type === "send_embed_data") {
1007
+ const embedPayload = p.payload && typeof p.payload === "object" ? p.payload : p;
1008
+ if (typeof embedPayload.chat_id === "string" && embedPayload.chat_id !== chatId) {
1009
+ return;
1010
+ }
1011
+ const embedId = embedPayload.embed_id;
1012
+ if (typeof embedId === "string" && embedId) {
1013
+ const status = typeof embedPayload.status === "string" ? embedPayload.status : "finished";
1014
+ embeds.set(embedId, embedPayload);
1015
+ if (status === "processing") {
1016
+ processingEmbedIds.add(embedId);
1017
+ } else {
1018
+ processingEmbedIds.delete(embedId);
1019
+ }
1020
+ maybeResolve();
1021
+ }
1022
+ return;
1023
+ }
942
1024
  if (type === "ai_message_update") {
943
1025
  const msgId = p.user_message_id ?? p.userMessageId;
944
- if (msgId !== userMessageId) return;
1026
+ if (msgId !== userMessageId && p.chat_id !== chatId) return;
945
1027
  capture(p);
946
1028
  if (typeof p.full_content_so_far === "string") {
947
1029
  latestContent = p.full_content_so_far;
@@ -966,7 +1048,7 @@ var OpenMatesWsClient = class {
966
1048
  }
967
1049
  if (type === "ai_background_response_completed") {
968
1050
  const msgId = p.user_message_id ?? p.userMessageId;
969
- if (msgId && msgId !== userMessageId) return;
1051
+ if (msgId && msgId !== userMessageId && p.chat_id !== chatId) return;
970
1052
  if (!msgId && p.chat_id !== chatId) return;
971
1053
  capture(p);
972
1054
  const content = typeof p.full_content === "string" ? p.full_content : latestContent;
@@ -974,6 +1056,25 @@ var OpenMatesWsClient = class {
974
1056
  scheduleResolve(content);
975
1057
  return;
976
1058
  }
1059
+ if (type === "chat_message_added") {
1060
+ if (p.chat_id !== chatId) return;
1061
+ const rawMessage = p.message;
1062
+ if (!rawMessage || typeof rawMessage !== "object") return;
1063
+ const message = rawMessage;
1064
+ if (message.role !== "assistant") return;
1065
+ const content = extractMessageContent(message);
1066
+ if (!content) return;
1067
+ capture(message);
1068
+ if (typeof message.category === "string" && message.category) {
1069
+ category = message.category;
1070
+ }
1071
+ if (typeof message.model_name === "string" && message.model_name) {
1072
+ modelName = message.model_name;
1073
+ }
1074
+ onStream?.({ kind: "done", content, category, modelName });
1075
+ scheduleResolve(content);
1076
+ return;
1077
+ }
977
1078
  if (type === "ai_typing_started") {
978
1079
  capture(p);
979
1080
  onStream?.({
@@ -997,13 +1098,7 @@ var OpenMatesWsClient = class {
997
1098
  clearTimeout(postProcessingTimer);
998
1099
  postProcessingTimer = null;
999
1100
  }
1000
- cleanup();
1001
- resolve4({
1002
- content: latestContent,
1003
- category,
1004
- modelName,
1005
- followUpSuggestions
1006
- });
1101
+ finishPostProcessingWait();
1007
1102
  }
1008
1103
  return;
1009
1104
  }
@@ -1018,10 +1113,13 @@ var OpenMatesWsClient = class {
1018
1113
  if (aiResponseDone) {
1019
1114
  cleanup();
1020
1115
  resolve4({
1116
+ messageId,
1117
+ taskId,
1021
1118
  content: latestContent,
1022
1119
  category,
1023
1120
  modelName,
1024
- followUpSuggestions
1121
+ followUpSuggestions,
1122
+ embeds: [...embeds.values()]
1025
1123
  });
1026
1124
  return;
1027
1125
  }
@@ -1034,6 +1132,10 @@ var OpenMatesWsClient = class {
1034
1132
  clearTimeout(postProcessingTimer);
1035
1133
  postProcessingTimer = null;
1036
1134
  }
1135
+ if (asyncEmbedTimer) {
1136
+ clearTimeout(asyncEmbedTimer);
1137
+ asyncEmbedTimer = null;
1138
+ }
1037
1139
  this.socket.off("message", onMessage);
1038
1140
  this.socket.off("error", onError);
1039
1141
  this.socket.off("close", onClose);
@@ -1345,9 +1447,139 @@ function escapeRegExp(s) {
1345
1447
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1346
1448
  }
1347
1449
 
1450
+ // src/embedCreator.ts
1451
+ import { randomUUID, createHash as createHash3, randomBytes, webcrypto as webcrypto3 } from "crypto";
1452
+ import { encode as toonEncode } from "@toon-format/toon";
1453
+ var cryptoApi3 = webcrypto3;
1454
+ var AES_GCM_IV_LENGTH3 = 12;
1455
+ function bytesToBase642(input) {
1456
+ return Buffer.from(input).toString("base64");
1457
+ }
1458
+ function toArrayBuffer2(input) {
1459
+ return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
1460
+ }
1461
+ async function encryptAesGcm(plaintext, rawKeyBytes) {
1462
+ const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
1463
+ const key = await cryptoApi3.subtle.importKey(
1464
+ "raw",
1465
+ toArrayBuffer2(rawKeyBytes),
1466
+ { name: "AES-GCM" },
1467
+ false,
1468
+ ["encrypt"]
1469
+ );
1470
+ const encrypted = await cryptoApi3.subtle.encrypt(
1471
+ { name: "AES-GCM", iv: toArrayBuffer2(iv) },
1472
+ key,
1473
+ new TextEncoder().encode(plaintext)
1474
+ );
1475
+ const cipherBytes = new Uint8Array(encrypted);
1476
+ const combined = new Uint8Array(iv.length + cipherBytes.length);
1477
+ combined.set(iv);
1478
+ combined.set(cipherBytes, iv.length);
1479
+ return bytesToBase642(combined);
1480
+ }
1481
+ async function wrapKey(embedKey, wrappingKey) {
1482
+ const cryptoKey = await cryptoApi3.subtle.importKey(
1483
+ "raw",
1484
+ toArrayBuffer2(wrappingKey),
1485
+ { name: "AES-GCM" },
1486
+ false,
1487
+ ["encrypt"]
1488
+ );
1489
+ const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
1490
+ const encrypted = await cryptoApi3.subtle.encrypt(
1491
+ { name: "AES-GCM", iv: toArrayBuffer2(iv) },
1492
+ cryptoKey,
1493
+ toArrayBuffer2(embedKey)
1494
+ );
1495
+ const cipherBytes = new Uint8Array(encrypted);
1496
+ const combined = new Uint8Array(iv.length + cipherBytes.length);
1497
+ combined.set(iv);
1498
+ combined.set(cipherBytes, iv.length);
1499
+ return bytesToBase642(combined);
1500
+ }
1501
+ function generateEmbedKey() {
1502
+ return new Uint8Array(randomBytes(32));
1503
+ }
1504
+ function computeSHA256(content) {
1505
+ return createHash3("sha256").update(content).digest("hex");
1506
+ }
1507
+ function toonEncodeContent(data) {
1508
+ try {
1509
+ return toonEncode(data);
1510
+ } catch {
1511
+ return JSON.stringify(data);
1512
+ }
1513
+ }
1514
+ function generateEmbedId() {
1515
+ return randomUUID();
1516
+ }
1517
+ function createEmbedReferenceBlock(type, embedId, url) {
1518
+ const data = { type, embed_id: embedId };
1519
+ if (url) data.url = url;
1520
+ const ref = JSON.stringify(data, null, 2);
1521
+ return "```json\n" + ref + "\n```";
1522
+ }
1523
+ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId) {
1524
+ try {
1525
+ const embedKey = generateEmbedKey();
1526
+ const encryptedContent = await encryptAesGcm(embed.content, embedKey);
1527
+ const encryptedType = await encryptAesGcm(embed.type, embedKey);
1528
+ const encryptedTextPreview = await encryptAesGcm(embed.textPreview, embedKey);
1529
+ const hashedEmbedId = computeSHA256(embed.embedId);
1530
+ const hashedChatId = computeSHA256(chatId);
1531
+ const hashedMessageId = computeSHA256(messageId);
1532
+ const hashedUserId = computeSHA256(userId);
1533
+ const wrappedWithMaster = await wrapKey(embedKey, masterKey);
1534
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1535
+ const embedKeys = [
1536
+ {
1537
+ hashed_embed_id: hashedEmbedId,
1538
+ key_type: "master",
1539
+ hashed_chat_id: null,
1540
+ encrypted_embed_key: wrappedWithMaster,
1541
+ hashed_user_id: hashedUserId,
1542
+ created_at: nowSeconds
1543
+ }
1544
+ ];
1545
+ if (chatKey) {
1546
+ const wrappedWithChat = await wrapKey(embedKey, chatKey);
1547
+ embedKeys.push({
1548
+ hashed_embed_id: hashedEmbedId,
1549
+ key_type: "chat",
1550
+ hashed_chat_id: hashedChatId,
1551
+ encrypted_embed_key: wrappedWithChat,
1552
+ hashed_user_id: hashedUserId,
1553
+ created_at: nowSeconds
1554
+ });
1555
+ }
1556
+ return {
1557
+ embed_id: embed.embedId,
1558
+ encrypted_type: encryptedType,
1559
+ encrypted_content: encryptedContent,
1560
+ encrypted_text_preview: encryptedTextPreview,
1561
+ status: embed.status,
1562
+ hashed_chat_id: hashedChatId,
1563
+ hashed_message_id: hashedMessageId,
1564
+ hashed_user_id: hashedUserId,
1565
+ file_path: embed.filePath,
1566
+ content_hash: embed.contentHash,
1567
+ text_length_chars: embed.textLengthChars,
1568
+ created_at: nowSeconds,
1569
+ updated_at: nowSeconds,
1570
+ embed_keys: embedKeys
1571
+ };
1572
+ } catch (error) {
1573
+ const msg = error instanceof Error ? error.message : String(error);
1574
+ process.stderr.write(`\x1B[31mError:\x1B[0m Failed to encrypt embed: ${msg}
1575
+ `);
1576
+ return null;
1577
+ }
1578
+ }
1579
+
1348
1580
  // src/shareEncryption.ts
1349
- import { webcrypto as webcrypto3 } from "crypto";
1350
- var crypto = webcrypto3;
1581
+ import { webcrypto as webcrypto4 } from "crypto";
1582
+ var crypto = webcrypto4;
1351
1583
  function base64UrlEncode(data) {
1352
1584
  return Buffer.from(data).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1353
1585
  }
@@ -1459,6 +1691,12 @@ function buildEmbedShareUrl(origin, embedId, blob) {
1459
1691
  }
1460
1692
 
1461
1693
  // src/client.ts
1694
+ function normalizeUnixSeconds(value, fallback) {
1695
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1696
+ return fallback;
1697
+ }
1698
+ return value > 1e10 ? Math.floor(value / 1e3) : Math.floor(value);
1699
+ }
1462
1700
  var MEMORY_TYPE_REGISTRY = {
1463
1701
  "ai/communication_style": {
1464
1702
  appId: "ai",
@@ -1892,7 +2130,7 @@ var OpenMatesClient = class _OpenMatesClient {
1892
2130
  pin: pin.trim().toUpperCase(),
1893
2131
  token
1894
2132
  });
1895
- const sessionId = randomUUID();
2133
+ const sessionId = randomUUID2();
1896
2134
  const login = await this.http.post(
1897
2135
  "/v1/auth/login",
1898
2136
  {
@@ -2387,7 +2625,7 @@ var OpenMatesClient = class _OpenMatesClient {
2387
2625
  async sendMessage(params) {
2388
2626
  let chatId;
2389
2627
  if (!params.chatId) {
2390
- chatId = randomUUID();
2628
+ chatId = randomUUID2();
2391
2629
  } else if (params.chatId.length < 36) {
2392
2630
  const resolved = await this.resolveFullChatId(params.chatId);
2393
2631
  if (!resolved) {
@@ -2400,7 +2638,7 @@ var OpenMatesClient = class _OpenMatesClient {
2400
2638
  chatId = params.chatId;
2401
2639
  }
2402
2640
  const { ws, session } = await this.openWsClient();
2403
- const messageId = randomUUID();
2641
+ const messageId = randomUUID2();
2404
2642
  const createdAt = Math.floor(Date.now() / 1e3);
2405
2643
  const isNewChat = !params.chatId;
2406
2644
  ws.send("set_active_chat", { chat_id: chatId });
@@ -2420,6 +2658,7 @@ var OpenMatesClient = class _OpenMatesClient {
2420
2658
  };
2421
2659
  let chatKeyBytes = null;
2422
2660
  let encryptedChatKey = null;
2661
+ let baselineMessagesV = 0;
2423
2662
  if (!params.incognito) {
2424
2663
  const masterKey = this.getMasterKeyBytes();
2425
2664
  if (isNewChat) {
@@ -2439,6 +2678,7 @@ var OpenMatesClient = class _OpenMatesClient {
2439
2678
  (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
2440
2679
  );
2441
2680
  if (chat) {
2681
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
2442
2682
  const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
2443
2683
  if (encKey) {
2444
2684
  chatKeyBytes = await decryptBytesWithAesGcm(encKey, masterKey);
@@ -2447,8 +2687,23 @@ var OpenMatesClient = class _OpenMatesClient {
2447
2687
  }
2448
2688
  }
2449
2689
  }
2450
- if (params.encryptedEmbeds && params.encryptedEmbeds.length > 0) {
2451
- messagePayload.encrypted_embeds = params.encryptedEmbeds;
2690
+ const encryptedEmbeds = [...params.encryptedEmbeds ?? []];
2691
+ if (!params.incognito && params.preparedEmbeds && params.preparedEmbeds.length > 0) {
2692
+ const masterKey = this.getMasterKeyBytes();
2693
+ for (const embed of params.preparedEmbeds) {
2694
+ const encrypted = await encryptEmbed(
2695
+ embed,
2696
+ masterKey,
2697
+ chatKeyBytes,
2698
+ chatId,
2699
+ messageId,
2700
+ session.hashedEmail
2701
+ );
2702
+ if (encrypted) encryptedEmbeds.push(encrypted);
2703
+ }
2704
+ }
2705
+ if (encryptedEmbeds.length > 0) {
2706
+ messagePayload.encrypted_embeds = encryptedEmbeds;
2452
2707
  }
2453
2708
  ws.send("chat_message_added", messagePayload);
2454
2709
  if (!params.incognito && chatKeyBytes) {
@@ -2476,6 +2731,7 @@ var OpenMatesClient = class _OpenMatesClient {
2476
2731
  ws.send("encrypted_chat_metadata", metadataPayload);
2477
2732
  }
2478
2733
  let assistant = "";
2734
+ let assistantMessageId = null;
2479
2735
  let category = null;
2480
2736
  let modelName = null;
2481
2737
  let followUpSuggestions = [];
@@ -2483,6 +2739,7 @@ var OpenMatesClient = class _OpenMatesClient {
2483
2739
  if (params.incognito) {
2484
2740
  try {
2485
2741
  const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2742
+ assistantMessageId = resp.messageId;
2486
2743
  assistant = resp.content;
2487
2744
  category = resp.category;
2488
2745
  modelName = resp.modelName;
@@ -2492,10 +2749,55 @@ var OpenMatesClient = class _OpenMatesClient {
2492
2749
  } else {
2493
2750
  try {
2494
2751
  const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2752
+ assistantMessageId = resp.messageId;
2495
2753
  assistant = resp.content;
2496
2754
  category = resp.category;
2497
2755
  modelName = resp.modelName;
2498
2756
  followUpSuggestions = resp.followUpSuggestions;
2757
+ if (chatKeyBytes && assistant) {
2758
+ const completedAt = Math.floor(Date.now() / 1e3);
2759
+ const encryptedAssistantContent = await encryptWithAesGcmCombined(
2760
+ assistant,
2761
+ chatKeyBytes
2762
+ );
2763
+ const encryptedCategory = category ? await encryptWithAesGcmCombined(category, chatKeyBytes) : void 0;
2764
+ const encryptedModelName = modelName ? await encryptWithAesGcmCombined(modelName, chatKeyBytes) : void 0;
2765
+ const persistedAssistantMessageId = assistantMessageId ?? randomUUID2();
2766
+ ws.send("ai_response_completed", {
2767
+ chat_id: chatId,
2768
+ message: {
2769
+ message_id: persistedAssistantMessageId,
2770
+ chat_id: chatId,
2771
+ role: "assistant",
2772
+ created_at: completedAt,
2773
+ status: "synced",
2774
+ user_message_id: messageId,
2775
+ encrypted_content: encryptedAssistantContent,
2776
+ encrypted_category: encryptedCategory,
2777
+ encrypted_model_name: encryptedModelName
2778
+ },
2779
+ versions: {
2780
+ messages_v: baselineMessagesV + 2,
2781
+ last_edited_overall_timestamp: completedAt
2782
+ }
2783
+ });
2784
+ await ws.waitForMessage(
2785
+ "ai_response_storage_confirmed",
2786
+ (payload) => {
2787
+ const p = payload;
2788
+ return p.message_id === persistedAssistantMessageId;
2789
+ },
2790
+ 2e4
2791
+ );
2792
+ await this.persistStreamedEmbeds({
2793
+ ws,
2794
+ embeds: resp.embeds,
2795
+ chatId,
2796
+ chatKeyBytes,
2797
+ fallbackMessageId: persistedAssistantMessageId
2798
+ });
2799
+ clearSyncCache();
2800
+ }
2499
2801
  } finally {
2500
2802
  ws.close();
2501
2803
  }
@@ -2510,6 +2812,110 @@ var OpenMatesClient = class _OpenMatesClient {
2510
2812
  followUpSuggestions
2511
2813
  };
2512
2814
  }
2815
+ async persistStreamedEmbeds(params) {
2816
+ const finalized = new Map(
2817
+ params.embeds.filter((embed) => {
2818
+ const status = embed.status ?? "finished";
2819
+ return embed.embed_id && embed.content && status !== "processing" && status !== "error" && status !== "cancelled";
2820
+ }).map((embed) => [embed.embed_id, embed])
2821
+ );
2822
+ if (finalized.size === 0) return;
2823
+ const session = this.requireSession();
2824
+ const masterKey = this.getMasterKeyBytes();
2825
+ const parentKeys = /* @__PURE__ */ new Map();
2826
+ const processed = /* @__PURE__ */ new Set();
2827
+ const makeKey = () => {
2828
+ const key = new Uint8Array(32);
2829
+ globalThis.crypto.getRandomValues(key);
2830
+ return key;
2831
+ };
2832
+ const persistOne = async (embed, embedKey, isChild) => {
2833
+ const now = Math.floor(Date.now() / 1e3);
2834
+ const createdAt = normalizeUnixSeconds(embed.createdAt, now);
2835
+ const updatedAt = normalizeUnixSeconds(embed.updatedAt, now);
2836
+ const messageId = embed.message_id || params.fallbackMessageId;
2837
+ const userId = embed.user_id || session.hashedEmail;
2838
+ const hashedChatId = computeSHA256(params.chatId);
2839
+ const hashedMessageId = computeSHA256(messageId);
2840
+ const hashedUserId = computeSHA256(userId);
2841
+ const hashedEmbedId = computeSHA256(embed.embed_id);
2842
+ const encryptedContent = await encryptWithAesGcmCombined(
2843
+ embed.content ?? "",
2844
+ embedKey
2845
+ );
2846
+ const encryptedType = await encryptWithAesGcmCombined(
2847
+ embed.type || "app_skill_use",
2848
+ embedKey
2849
+ );
2850
+ const encryptedTextPreview = embed.text_preview ? await encryptWithAesGcmCombined(embed.text_preview, embedKey) : void 0;
2851
+ await params.ws.sendAsync("store_embed", {
2852
+ embed_id: embed.embed_id,
2853
+ encrypted_type: encryptedType,
2854
+ encrypted_content: encryptedContent,
2855
+ encrypted_text_preview: encryptedTextPreview,
2856
+ status: embed.status || "finished",
2857
+ hashed_chat_id: hashedChatId,
2858
+ hashed_message_id: hashedMessageId,
2859
+ hashed_task_id: embed.task_id ? computeSHA256(embed.task_id) : void 0,
2860
+ hashed_user_id: hashedUserId,
2861
+ embed_ids: embed.embed_ids,
2862
+ parent_embed_id: embed.parent_embed_id || void 0,
2863
+ version_number: embed.version_number,
2864
+ file_path: embed.file_path,
2865
+ content_hash: embed.content_hash,
2866
+ text_length_chars: embed.text_length_chars,
2867
+ is_private: embed.is_private ?? false,
2868
+ is_shared: embed.is_shared ?? false,
2869
+ created_at: createdAt,
2870
+ updated_at: updatedAt
2871
+ });
2872
+ if (!isChild) {
2873
+ const keys = [
2874
+ {
2875
+ hashed_embed_id: hashedEmbedId,
2876
+ key_type: "master",
2877
+ hashed_chat_id: null,
2878
+ encrypted_embed_key: await encryptBytesWithAesGcm(embedKey, masterKey),
2879
+ hashed_user_id: hashedUserId,
2880
+ created_at: now
2881
+ },
2882
+ {
2883
+ hashed_embed_id: hashedEmbedId,
2884
+ key_type: "chat",
2885
+ hashed_chat_id: hashedChatId,
2886
+ encrypted_embed_key: await encryptBytesWithAesGcm(
2887
+ embedKey,
2888
+ params.chatKeyBytes
2889
+ ),
2890
+ hashed_user_id: hashedUserId,
2891
+ created_at: now
2892
+ }
2893
+ ];
2894
+ await params.ws.sendAsync("store_embed_keys", { keys });
2895
+ parentKeys.set(embed.embed_id, embedKey);
2896
+ }
2897
+ };
2898
+ let madeProgress = true;
2899
+ while (processed.size < finalized.size && madeProgress) {
2900
+ madeProgress = false;
2901
+ for (const embed of finalized.values()) {
2902
+ if (processed.has(embed.embed_id)) continue;
2903
+ const parentId = embed.parent_embed_id || null;
2904
+ if (parentId && finalized.has(parentId) && !parentKeys.has(parentId)) {
2905
+ continue;
2906
+ }
2907
+ const parentKey = parentId ? parentKeys.get(parentId) : void 0;
2908
+ await persistOne(embed, parentKey ?? makeKey(), Boolean(parentKey));
2909
+ processed.add(embed.embed_id);
2910
+ madeProgress = true;
2911
+ }
2912
+ }
2913
+ for (const embed of finalized.values()) {
2914
+ if (processed.has(embed.embed_id)) continue;
2915
+ await persistOne(embed, makeKey(), false);
2916
+ processed.add(embed.embed_id);
2917
+ }
2918
+ }
2513
2919
  /**
2514
2920
  * Delete a chat by ID.
2515
2921
  *
@@ -3284,7 +3690,7 @@ Required: ${schema.required.join(", ")}`
3284
3690
  );
3285
3691
  }
3286
3692
  }
3287
- const entryId = params.entryId ?? randomUUID();
3693
+ const entryId = params.entryId ?? randomUUID2();
3288
3694
  const now = Math.floor(Date.now() / 1e3);
3289
3695
  const hashedKey = hashItemKey(params.appId, params.itemType);
3290
3696
  const plaintextPayload = {
@@ -3556,7 +3962,7 @@ Required: ${schema.required.join(", ")}`
3556
3962
  makeWsClient(session) {
3557
3963
  return new OpenMatesWsClient({
3558
3964
  apiUrl: session.apiUrl,
3559
- sessionId: randomUUID(),
3965
+ sessionId: randomUUID2(),
3560
3966
  wsToken: session.wsToken,
3561
3967
  refreshToken: session.cookies.auth_refresh_token ?? null,
3562
3968
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -3928,7 +4334,7 @@ function printLogo() {
3928
4334
  }
3929
4335
 
3930
4336
  // ../secret-scanner/src/registry.ts
3931
- import { createRequire } from "module";
4337
+ import { createRequire as createRequire2 } from "module";
3932
4338
 
3933
4339
  // ../secret-scanner/src/types.ts
3934
4340
  var SECRET_ENV_PATTERNS = [
@@ -4062,8 +4468,8 @@ var SECRET_PATTERNS = [
4062
4468
  ];
4063
4469
 
4064
4470
  // ../secret-scanner/src/registry.ts
4065
- var require2 = createRequire(import.meta.url);
4066
- var AhoCorasick = require2("ahocorasick");
4471
+ var require3 = createRequire2(import.meta.url);
4472
+ var AhoCorasick = require3("ahocorasick");
4067
4473
  var MIN_SECRET_LENGTH = 8;
4068
4474
  var SecretRegistry = class {
4069
4475
  /** Map from plaintext value → registry entry */
@@ -4605,136 +5011,6 @@ import { readFileSync as readFileSync3, statSync, existsSync as existsSync3 } fr
4605
5011
  import { basename, extname, resolve } from "path";
4606
5012
  import { homedir as homedir3 } from "os";
4607
5013
  import { createHash as createHash4 } from "crypto";
4608
-
4609
- // src/embedCreator.ts
4610
- import { randomUUID as randomUUID2, createHash as createHash3, randomBytes, webcrypto as webcrypto4 } from "crypto";
4611
- import { encode as toonEncode } from "@toon-format/toon";
4612
- var cryptoApi3 = webcrypto4;
4613
- var AES_GCM_IV_LENGTH3 = 12;
4614
- function bytesToBase643(input) {
4615
- return Buffer.from(input).toString("base64");
4616
- }
4617
- function toArrayBuffer2(input) {
4618
- return input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
4619
- }
4620
- async function encryptAesGcm(plaintext, rawKeyBytes) {
4621
- const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
4622
- const key = await cryptoApi3.subtle.importKey(
4623
- "raw",
4624
- toArrayBuffer2(rawKeyBytes),
4625
- { name: "AES-GCM" },
4626
- false,
4627
- ["encrypt"]
4628
- );
4629
- const encrypted = await cryptoApi3.subtle.encrypt(
4630
- { name: "AES-GCM", iv: toArrayBuffer2(iv) },
4631
- key,
4632
- new TextEncoder().encode(plaintext)
4633
- );
4634
- const cipherBytes = new Uint8Array(encrypted);
4635
- const combined = new Uint8Array(iv.length + cipherBytes.length);
4636
- combined.set(iv);
4637
- combined.set(cipherBytes, iv.length);
4638
- return bytesToBase643(combined);
4639
- }
4640
- async function wrapKey(embedKey, wrappingKey) {
4641
- const cryptoKey = await cryptoApi3.subtle.importKey(
4642
- "raw",
4643
- toArrayBuffer2(wrappingKey),
4644
- { name: "AES-GCM" },
4645
- false,
4646
- ["encrypt"]
4647
- );
4648
- const iv = cryptoApi3.getRandomValues(new Uint8Array(AES_GCM_IV_LENGTH3));
4649
- const encrypted = await cryptoApi3.subtle.encrypt(
4650
- { name: "AES-GCM", iv: toArrayBuffer2(iv) },
4651
- cryptoKey,
4652
- toArrayBuffer2(embedKey)
4653
- );
4654
- const cipherBytes = new Uint8Array(encrypted);
4655
- const combined = new Uint8Array(iv.length + cipherBytes.length);
4656
- combined.set(iv);
4657
- combined.set(cipherBytes, iv.length);
4658
- return bytesToBase643(combined);
4659
- }
4660
- function generateEmbedKey() {
4661
- return new Uint8Array(randomBytes(32));
4662
- }
4663
- function computeSHA256(content) {
4664
- return createHash3("sha256").update(content).digest("hex");
4665
- }
4666
- function toonEncodeContent(data) {
4667
- try {
4668
- return toonEncode(data);
4669
- } catch {
4670
- return JSON.stringify(data);
4671
- }
4672
- }
4673
- function generateEmbedId() {
4674
- return randomUUID2();
4675
- }
4676
- function createEmbedReferenceBlock(type, embedId) {
4677
- const ref = JSON.stringify({ type, embed_id: embedId }, null, 2);
4678
- return "```json\n" + ref + "\n```";
4679
- }
4680
- async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId) {
4681
- try {
4682
- const embedKey = generateEmbedKey();
4683
- const encryptedContent = await encryptAesGcm(embed.content, embedKey);
4684
- const encryptedType = await encryptAesGcm(embed.type, embedKey);
4685
- const encryptedTextPreview = await encryptAesGcm(embed.textPreview, embedKey);
4686
- const hashedEmbedId = computeSHA256(embed.embedId);
4687
- const hashedChatId = computeSHA256(chatId);
4688
- const hashedMessageId = computeSHA256(messageId);
4689
- const hashedUserId = computeSHA256(userId);
4690
- const wrappedWithMaster = await wrapKey(embedKey, masterKey);
4691
- const nowSeconds = Math.floor(Date.now() / 1e3);
4692
- const embedKeys = [
4693
- {
4694
- hashed_embed_id: hashedEmbedId,
4695
- key_type: "master",
4696
- hashed_chat_id: null,
4697
- encrypted_embed_key: wrappedWithMaster,
4698
- hashed_user_id: hashedUserId,
4699
- created_at: nowSeconds
4700
- }
4701
- ];
4702
- if (chatKey) {
4703
- const wrappedWithChat = await wrapKey(embedKey, chatKey);
4704
- embedKeys.push({
4705
- hashed_embed_id: hashedEmbedId,
4706
- key_type: "chat",
4707
- hashed_chat_id: hashedChatId,
4708
- encrypted_embed_key: wrappedWithChat,
4709
- hashed_user_id: hashedUserId,
4710
- created_at: nowSeconds
4711
- });
4712
- }
4713
- return {
4714
- embed_id: embed.embedId,
4715
- encrypted_type: encryptedType,
4716
- encrypted_content: encryptedContent,
4717
- encrypted_text_preview: encryptedTextPreview,
4718
- status: embed.status,
4719
- hashed_chat_id: hashedChatId,
4720
- hashed_message_id: hashedMessageId,
4721
- hashed_user_id: hashedUserId,
4722
- file_path: embed.filePath,
4723
- content_hash: embed.contentHash,
4724
- text_length_chars: embed.textLengthChars,
4725
- created_at: nowSeconds,
4726
- updated_at: nowSeconds,
4727
- embed_keys: embedKeys
4728
- };
4729
- } catch (error) {
4730
- const msg = error instanceof Error ? error.message : String(error);
4731
- process.stderr.write(`\x1B[31mError:\x1B[0m Failed to encrypt embed: ${msg}
4732
- `);
4733
- return null;
4734
- }
4735
- }
4736
-
4737
- // src/fileEmbed.ts
4738
5014
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
4739
5015
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
4740
5016
  ".pem",
@@ -5125,6 +5401,70 @@ function formatEmbedsForMessage(embeds) {
5125
5401
  return "\n" + embeds.map((e) => e.referenceBlock).join("\n");
5126
5402
  }
5127
5403
 
5404
+ // src/urlEmbed.ts
5405
+ var URL_PATTERN = /\bhttps?:\/\/[^\s<>()`"']+/gi;
5406
+ var FENCED_BLOCK_PATTERN = /(```[\s\S]*?```)/g;
5407
+ var TRAILING_URL_PUNCTUATION = /[.,!?;:]+$/;
5408
+ function trimTrailingUrlPunctuation(rawUrl) {
5409
+ let url = rawUrl;
5410
+ let suffix = "";
5411
+ const punctuation = url.match(TRAILING_URL_PUNCTUATION)?.[0] ?? "";
5412
+ if (punctuation) {
5413
+ url = url.slice(0, -punctuation.length);
5414
+ suffix = punctuation + suffix;
5415
+ }
5416
+ while (url.endsWith(")")) {
5417
+ const openCount = (url.match(/\(/g) ?? []).length;
5418
+ const closeCount = (url.match(/\)/g) ?? []).length;
5419
+ if (closeCount <= openCount) break;
5420
+ url = url.slice(0, -1);
5421
+ suffix = `)${suffix}`;
5422
+ }
5423
+ return { url, suffix };
5424
+ }
5425
+ function createWebsiteEmbed(url) {
5426
+ const embedId = generateEmbedId();
5427
+ const content = toonEncodeContent({
5428
+ url,
5429
+ title: null,
5430
+ description: null,
5431
+ favicon: null,
5432
+ image: null,
5433
+ site_name: null,
5434
+ fetched_at: (/* @__PURE__ */ new Date()).toISOString()
5435
+ });
5436
+ return {
5437
+ embedId,
5438
+ type: "web-website",
5439
+ content,
5440
+ textPreview: url,
5441
+ status: "finished"
5442
+ };
5443
+ }
5444
+ function replaceUrlsInText(text, embeds) {
5445
+ return text.replace(URL_PATTERN, (rawUrl) => {
5446
+ const { url, suffix } = trimTrailingUrlPunctuation(rawUrl);
5447
+ if (!url) return rawUrl;
5448
+ try {
5449
+ new URL(url);
5450
+ } catch {
5451
+ return rawUrl;
5452
+ }
5453
+ const embed = createWebsiteEmbed(url);
5454
+ embeds.push(embed);
5455
+ return `${createEmbedReferenceBlock("website", embed.embedId, url)}${suffix}`;
5456
+ });
5457
+ }
5458
+ function prepareUrlEmbeds(message) {
5459
+ const embeds = [];
5460
+ const parts = message.split(FENCED_BLOCK_PATTERN);
5461
+ const processed = parts.map((part, index) => {
5462
+ const isFence = index % 2 === 1 && part.startsWith("```");
5463
+ return isFence ? part : replaceUrlsInText(part, embeds);
5464
+ }).join("");
5465
+ return { message: processed, embeds };
5466
+ }
5467
+
5128
5468
  // src/embedRenderers.ts
5129
5469
  var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
5130
5470
  var DIRECT_TYPES = /* @__PURE__ */ new Set([
@@ -8938,7 +9278,7 @@ async function sendMessageStreaming(client, params, redactor) {
8938
9278
  processedRawLength = raw.length;
8939
9279
  }
8940
9280
  };
8941
- const encryptedEmbeds = [];
9281
+ const preparedEmbeds = [];
8942
9282
  let finalMessage = params.message;
8943
9283
  if (params.message.includes("@")) {
8944
9284
  try {
@@ -9045,31 +9385,7 @@ async function sendMessageStreaming(client, params, redactor) {
9045
9385
  if (embedRefs) {
9046
9386
  finalMessage += embedRefs;
9047
9387
  }
9048
- try {
9049
- const { masterKey, userId } = client.getEmbedEncryptionKeys();
9050
- for (const fe of fileResult.embeds) {
9051
- const encrypted = await encryptEmbed(
9052
- fe.embed,
9053
- masterKey,
9054
- null,
9055
- // chatKey — not available in CLI yet for new chats
9056
- params.chatId || "new",
9057
- // will be replaced by server
9058
- "pending",
9059
- // messageId set during send
9060
- userId
9061
- );
9062
- if (encrypted) {
9063
- encryptedEmbeds.push(encrypted);
9064
- }
9065
- }
9066
- } catch (err) {
9067
- const msg = err instanceof Error ? err.message : String(err);
9068
- process.stderr.write(
9069
- `\x1B[33mWarning:\x1B[0m Embed encryption failed: ${msg}. Files sent without encryption.
9070
- `
9071
- );
9072
- }
9388
+ preparedEmbeds.push(...fileResult.embeds.map((fileEmbed) => fileEmbed.embed));
9073
9389
  }
9074
9390
  if (parsed.resolved.length > 0 && !params.json) {
9075
9391
  clearTyping();
@@ -9084,12 +9400,15 @@ async function sendMessageStreaming(client, params, redactor) {
9084
9400
  } catch {
9085
9401
  }
9086
9402
  }
9403
+ const urlResult = prepareUrlEmbeds(finalMessage);
9404
+ finalMessage = urlResult.message;
9405
+ preparedEmbeds.push(...urlResult.embeds);
9087
9406
  const result = await client.sendMessage({
9088
9407
  message: finalMessage,
9089
9408
  chatId: params.chatId,
9090
9409
  incognito: params.incognito,
9091
9410
  onStream,
9092
- encryptedEmbeds: encryptedEmbeds.length > 0 ? encryptedEmbeds : void 0
9411
+ preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0
9093
9412
  });
9094
9413
  clearTyping();
9095
9414
  if (!params.json) {
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-NQ3NCRIL.js";
5
+ } from "./chunk-TUPR2SL3.js";
6
6
  import "./chunk-SFTCIVE2.js";
7
7
  export {
8
8
  getExtForLang,
package/dist/index.d.ts CHANGED
@@ -105,6 +105,35 @@ interface MentionContext {
105
105
  memoryEntries: MemoryEntryInfo[];
106
106
  }
107
107
 
108
+ /**
109
+ * @file Embed creation pipeline for the CLI — generates encrypted embeds
110
+ * with proper key wrapping, matching the web app's zero-knowledge architecture.
111
+ *
112
+ * For each embed:
113
+ * 1. Content is TOON-encoded (or JSON-fallback)
114
+ * 2. A random AES-256 embed key is generated
115
+ * 3. Content, type, and text_preview are encrypted with the embed key
116
+ * 4. The embed key is wrapped with both master key and chat key
117
+ * 5. SHA-256 hashes are computed for all IDs
118
+ * 6. The encrypted embed + wrapped keys are attached to chat_message_added
119
+ *
120
+ * Mirrors: cryptoService.ts (encryptWithEmbedKey, wrapEmbedKeyWithMasterKey,
121
+ * wrapEmbedKeyWithChatKey, generateEmbedKey)
122
+ * chatSyncServiceSenders.ts (encrypted_embeds construction)
123
+ *
124
+ * Architecture: docs/architecture/embeds.md
125
+ */
126
+ /** A prepared embed ready for encryption and sending */
127
+ interface PreparedEmbed {
128
+ embedId: string;
129
+ type: string;
130
+ content: string;
131
+ textPreview: string;
132
+ status: string;
133
+ filePath?: string;
134
+ contentHash?: string;
135
+ textLengthChars?: number;
136
+ }
108
137
  /** A fully encrypted embed ready for the WebSocket payload */
109
138
  interface EncryptedEmbed {
110
139
  embed_id: string;
@@ -424,6 +453,8 @@ declare class OpenMatesClient {
424
453
  onStream?: (event: StreamEvent) => void;
425
454
  /** Encrypted file embeds to attach to the message (code, images, PDFs). */
426
455
  encryptedEmbeds?: EncryptedEmbed[];
456
+ /** Prepared embeds to encrypt after the real chat/message IDs are known. */
457
+ preparedEmbeds?: PreparedEmbed[];
427
458
  }): Promise<{
428
459
  chatId: string;
429
460
  assistant: string;
@@ -433,6 +464,7 @@ declare class OpenMatesClient {
433
464
  /** Follow-up suggestions from post-processing (may be empty for incognito chats). */
434
465
  followUpSuggestions: string[];
435
466
  }>;
467
+ private persistStreamedEmbeds;
436
468
  /**
437
469
  * Delete a chat by ID.
438
470
  *
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getExtForLang,
8
8
  parseNewChatSuggestionText,
9
9
  serializeToYaml
10
- } from "./chunk-NQ3NCRIL.js";
10
+ } from "./chunk-TUPR2SL3.js";
11
11
  import "./chunk-SFTCIVE2.js";
12
12
  export {
13
13
  MATE_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.11.0-alpha.14",
3
+ "version": "0.11.0-alpha.16",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,9 +18,11 @@
18
18
  "test:unit:crypto": "node --test --experimental-strip-types tests/crypto.test.ts",
19
19
  "test:unit:storage": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/storage.test.ts",
20
20
  "test:unit:keychain": "node --test --experimental-strip-types tests/keychain.test.ts",
21
+ "test:unit:ws": "node --test --experimental-strip-types tests/ws.test.ts",
22
+ "test:unit:url-embed": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/urlEmbed.test.ts",
21
23
  "test:unit:server": "node --test --experimental-strip-types tests/server.test.ts",
22
24
  "test:unit:cli": "node --test tests/cli.test.ts",
23
- "test": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/crypto.test.ts tests/storage.test.ts tests/keychain.test.ts tests/mentions.test.ts tests/outputRedactor.test.ts tests/fileEmbed.test.ts tests/embedCreator.test.ts tests/shareEncryption.test.ts tests/server.test.ts && node --test tests/cli.test.ts"
25
+ "test": "node --test --experimental-strip-types --loader ./tests/loader.mjs tests/crypto.test.ts tests/storage.test.ts tests/keychain.test.ts tests/mentions.test.ts tests/outputRedactor.test.ts tests/fileEmbed.test.ts tests/embedCreator.test.ts tests/shareEncryption.test.ts tests/server.test.ts tests/ws.test.ts tests/urlEmbed.test.ts && node --test tests/cli.test.ts"
24
26
  },
25
27
  "keywords": [
26
28
  "openmates",