@reverbia/sdk 1.0.0-next.20260110155148 → 1.0.0-next.20260110214809

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.
@@ -3191,17 +3191,18 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
3191
3191
  if (existingKey) {
3192
3192
  return;
3193
3193
  }
3194
+ const signOptions = { showWalletUIs: false };
3194
3195
  let signature;
3195
3196
  try {
3196
3197
  if (embeddedWalletSigner) {
3197
- signature = await embeddedWalletSigner(SIGN_MESSAGE);
3198
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
3198
3199
  } else {
3199
- signature = await signMessage(SIGN_MESSAGE);
3200
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
3200
3201
  }
3201
3202
  } catch (error) {
3202
3203
  if (embeddedWalletSigner && error instanceof Error) {
3203
3204
  console.warn("Embedded wallet signing failed, falling back to standard signMessage:", error.message);
3204
- signature = await signMessage(SIGN_MESSAGE);
3205
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
3205
3206
  } else {
3206
3207
  throw error;
3207
3208
  }
@@ -695,17 +695,24 @@ interface MemoryExtractionResult {
695
695
  items: MemoryItem$1[];
696
696
  }
697
697
 
698
+ /**
699
+ * Options for signing messages.
700
+ */
701
+ interface SignMessageOptions {
702
+ /** Whether to show wallet UI during signing. Default: true */
703
+ showWalletUIs?: boolean;
704
+ }
698
705
  /**
699
706
  * Type for the signMessage function that client must provide.
700
- * This is typically from Privy's useSignMessage hook and may show a confirmation modal.
707
+ * This is typically from Privy's useSignMessage hook.
701
708
  */
702
- type SignMessageFn = (message: string) => Promise<string>;
709
+ type SignMessageFn = (message: string, options?: SignMessageOptions) => Promise<string>;
703
710
  /**
704
711
  * Type for embedded wallet signer function that enables silent signing.
705
712
  * For Privy embedded wallets, this can sign programmatically without user interaction
706
713
  * when configured correctly in the Privy dashboard.
707
714
  */
708
- type EmbeddedWalletSignerFn = (message: string) => Promise<string>;
715
+ type EmbeddedWalletSignerFn = (message: string, options?: SignMessageOptions) => Promise<string>;
709
716
 
710
717
  type MemoryType = "identity" | "preference" | "project" | "skill" | "constraint";
711
718
  interface MemoryItem {
@@ -695,17 +695,24 @@ interface MemoryExtractionResult {
695
695
  items: MemoryItem$1[];
696
696
  }
697
697
 
698
+ /**
699
+ * Options for signing messages.
700
+ */
701
+ interface SignMessageOptions {
702
+ /** Whether to show wallet UI during signing. Default: true */
703
+ showWalletUIs?: boolean;
704
+ }
698
705
  /**
699
706
  * Type for the signMessage function that client must provide.
700
- * This is typically from Privy's useSignMessage hook and may show a confirmation modal.
707
+ * This is typically from Privy's useSignMessage hook.
701
708
  */
702
- type SignMessageFn = (message: string) => Promise<string>;
709
+ type SignMessageFn = (message: string, options?: SignMessageOptions) => Promise<string>;
703
710
  /**
704
711
  * Type for embedded wallet signer function that enables silent signing.
705
712
  * For Privy embedded wallets, this can sign programmatically without user interaction
706
713
  * when configured correctly in the Privy dashboard.
707
714
  */
708
- type EmbeddedWalletSignerFn = (message: string) => Promise<string>;
715
+ type EmbeddedWalletSignerFn = (message: string, options?: SignMessageOptions) => Promise<string>;
709
716
 
710
717
  type MemoryType = "identity" | "preference" | "project" | "skill" | "constraint";
711
718
  interface MemoryItem {
@@ -3155,17 +3155,18 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
3155
3155
  if (existingKey) {
3156
3156
  return;
3157
3157
  }
3158
+ const signOptions = { showWalletUIs: false };
3158
3159
  let signature;
3159
3160
  try {
3160
3161
  if (embeddedWalletSigner) {
3161
- signature = await embeddedWalletSigner(SIGN_MESSAGE);
3162
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
3162
3163
  } else {
3163
- signature = await signMessage(SIGN_MESSAGE);
3164
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
3164
3165
  }
3165
3166
  } catch (error) {
3166
3167
  if (embeddedWalletSigner && error instanceof Error) {
3167
3168
  console.warn("Embedded wallet signing failed, falling back to standard signMessage:", error.message);
3168
- signature = await signMessage(SIGN_MESSAGE);
3169
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
3169
3170
  } else {
3170
3171
  throw error;
3171
3172
  }
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  BACKUP_DRIVE_ROOT_FOLDER: () => DEFAULT_ROOT_FOLDER,
43
43
  BACKUP_ICLOUD_FOLDER: () => DEFAULT_BACKUP_FOLDER2,
44
44
  BackupAuthProvider: () => BackupAuthProvider,
45
+ BlobUrlManager: () => BlobUrlManager,
45
46
  ChatConversation: () => Conversation,
46
47
  ChatMessage: () => Message,
47
48
  DEFAULT_BACKUP_FOLDER: () => DEFAULT_BACKUP_FOLDER,
@@ -71,9 +72,11 @@ __export(index_exports, {
71
72
  createMemoryContextSystemMessage: () => createMemoryContextSystemMessage,
72
73
  decryptData: () => decryptData,
73
74
  decryptDataBytes: () => decryptDataBytes,
75
+ deleteEncryptedFile: () => deleteEncryptedFile,
74
76
  encryptData: () => encryptData,
75
77
  exportPublicKey: () => exportPublicKey,
76
78
  extractConversationContext: () => extractConversationContext,
79
+ fileExists: () => fileExists,
77
80
  findFileIdBySourceUrl: () => findFileIdBySourceUrl,
78
81
  formatMemoriesForChat: () => formatMemoriesForChat,
79
82
  generateCompositeKey: () => generateCompositeKey,
@@ -85,6 +88,7 @@ __export(index_exports, {
85
88
  getAndClearDriveReturnUrl: () => getAndClearDriveReturnUrl,
86
89
  getCalendarAccessToken: () => getCalendarAccessToken,
87
90
  getDriveAccessToken: () => getDriveAccessToken,
91
+ getEncryptionKey: () => getEncryptionKey,
88
92
  getGoogleDriveStoredToken: () => getGoogleDriveStoredToken,
89
93
  getValidCalendarToken: () => getValidCalendarToken,
90
94
  getValidDriveToken: () => getValidDriveToken,
@@ -99,7 +103,9 @@ __export(index_exports, {
99
103
  hasKeyPair: () => hasKeyPair,
100
104
  isCalendarCallback: () => isCalendarCallback,
101
105
  isDriveCallback: () => isDriveCallback,
106
+ isOPFSSupported: () => isOPFSSupported,
102
107
  memoryStorageSchema: () => memoryStorageSchema,
108
+ readEncryptedFile: () => readEncryptedFile,
103
109
  refreshCalendarToken: () => refreshCalendarToken,
104
110
  refreshDriveToken: () => refreshDriveToken,
105
111
  replaceUrlWithMCPPlaceholder: () => replaceUrlWithMCPPlaceholder,
@@ -137,7 +143,8 @@ __export(index_exports, {
137
143
  usePdf: () => usePdf,
138
144
  useSearch: () => useSearch,
139
145
  useSettings: () => useSettings,
140
- userPreferencesStorageSchema: () => userPreferencesStorageSchema
146
+ userPreferencesStorageSchema: () => userPreferencesStorageSchema,
147
+ writeEncryptedFile: () => writeEncryptedFile
141
148
  });
142
149
  module.exports = __toCommonJS(index_exports);
143
150
 
@@ -2162,17 +2169,18 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
2162
2169
  if (existingKey) {
2163
2170
  return;
2164
2171
  }
2172
+ const signOptions = { showWalletUIs: false };
2165
2173
  let signature;
2166
2174
  try {
2167
2175
  if (embeddedWalletSigner) {
2168
- signature = await embeddedWalletSigner(SIGN_MESSAGE);
2176
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
2169
2177
  } else {
2170
- signature = await signMessage(SIGN_MESSAGE);
2178
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
2171
2179
  }
2172
2180
  } catch (error) {
2173
2181
  if (embeddedWalletSigner && error instanceof Error) {
2174
2182
  console.warn("Embedded wallet signing failed, falling back to standard signMessage:", error.message);
2175
- signature = await signMessage(SIGN_MESSAGE);
2183
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
2176
2184
  } else {
2177
2185
  throw error;
2178
2186
  }
@@ -2332,17 +2340,18 @@ async function requestKeyPair(walletAddress, signMessage, embeddedWalletSigner)
2332
2340
  `Failed to load persisted keypair, generating new one: ${error instanceof Error ? error.message : String(error)}`
2333
2341
  );
2334
2342
  }
2343
+ const signOptions = { showWalletUIs: false };
2335
2344
  let signature;
2336
2345
  try {
2337
2346
  if (embeddedWalletSigner) {
2338
- signature = await embeddedWalletSigner(SIGN_MESSAGE);
2347
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
2339
2348
  } else {
2340
- signature = await signMessage(SIGN_MESSAGE);
2349
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
2341
2350
  }
2342
2351
  } catch (error) {
2343
2352
  if (embeddedWalletSigner && error instanceof Error) {
2344
2353
  console.warn("Embedded wallet signing failed, falling back to standard signMessage:", error.message);
2345
- signature = await signMessage(SIGN_MESSAGE);
2354
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
2346
2355
  } else {
2347
2356
  throw error;
2348
2357
  }
@@ -2829,6 +2838,178 @@ async function searchMessagesOp(ctx, queryVector, options) {
2829
2838
  return resultsWithSimilarity.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
2830
2839
  }
2831
2840
 
2841
+ // src/lib/storage/opfs.ts
2842
+ var FILE_PLACEHOLDER_PREFIX = "__SDKFILE__";
2843
+ var FILE_PLACEHOLDER_SUFFIX = "__";
2844
+ var FILE_PLACEHOLDER_REGEX = /__SDKFILE__([a-f0-9-]+)__/g;
2845
+ function createFilePlaceholder(fileId) {
2846
+ return `${FILE_PLACEHOLDER_PREFIX}${fileId}${FILE_PLACEHOLDER_SUFFIX}`;
2847
+ }
2848
+ function extractFileIds(content) {
2849
+ const matches = content.matchAll(FILE_PLACEHOLDER_REGEX);
2850
+ return Array.from(matches, (m) => m[1]);
2851
+ }
2852
+ function isOPFSSupported() {
2853
+ return typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage;
2854
+ }
2855
+ async function getSDKDirectory() {
2856
+ const root = await navigator.storage.getDirectory();
2857
+ return root.getDirectoryHandle("reverbia-sdk-files", { create: true });
2858
+ }
2859
+ function bytesToHex2(bytes) {
2860
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2861
+ }
2862
+ function hexToBytes2(hex) {
2863
+ const bytes = new Uint8Array(hex.length / 2);
2864
+ for (let i = 0; i < hex.length; i += 2) {
2865
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
2866
+ }
2867
+ return bytes;
2868
+ }
2869
+ async function encryptBlob(blob, encryptionKey) {
2870
+ const arrayBuffer = await blob.arrayBuffer();
2871
+ const plaintext = new Uint8Array(arrayBuffer);
2872
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2873
+ const ciphertext = await crypto.subtle.encrypt(
2874
+ { name: "AES-GCM", iv },
2875
+ encryptionKey,
2876
+ plaintext
2877
+ );
2878
+ const combined = new Uint8Array(iv.length + ciphertext.byteLength);
2879
+ combined.set(iv, 0);
2880
+ combined.set(new Uint8Array(ciphertext), iv.length);
2881
+ return bytesToHex2(combined);
2882
+ }
2883
+ async function decryptToBytes(encryptedHex, encryptionKey) {
2884
+ const combined = hexToBytes2(encryptedHex);
2885
+ const iv = combined.slice(0, 12);
2886
+ const ciphertext = combined.slice(12);
2887
+ const decrypted = await crypto.subtle.decrypt(
2888
+ { name: "AES-GCM", iv },
2889
+ encryptionKey,
2890
+ ciphertext
2891
+ );
2892
+ return new Uint8Array(decrypted);
2893
+ }
2894
+ async function writeEncryptedFile(fileId, blob, encryptionKey, metadata) {
2895
+ if (!isOPFSSupported()) {
2896
+ throw new Error("OPFS is not supported in this browser");
2897
+ }
2898
+ const dir = await getSDKDirectory();
2899
+ const encryptedHex = await encryptBlob(blob, encryptionKey);
2900
+ const contentHandle = await dir.getFileHandle(`${fileId}.enc`, {
2901
+ create: true
2902
+ });
2903
+ const contentWritable = await contentHandle.createWritable();
2904
+ await contentWritable.write(encryptedHex);
2905
+ await contentWritable.close();
2906
+ const fileMetadata = {
2907
+ id: fileId,
2908
+ name: metadata?.name || `file-${fileId}`,
2909
+ type: blob.type || "application/octet-stream",
2910
+ size: blob.size,
2911
+ sourceUrl: metadata?.sourceUrl,
2912
+ createdAt: Date.now()
2913
+ };
2914
+ const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`, {
2915
+ create: true
2916
+ });
2917
+ const metaWritable = await metaHandle.createWritable();
2918
+ await metaWritable.write(JSON.stringify(fileMetadata));
2919
+ await metaWritable.close();
2920
+ }
2921
+ async function readEncryptedFile(fileId, encryptionKey) {
2922
+ if (!isOPFSSupported()) {
2923
+ throw new Error("OPFS is not supported in this browser");
2924
+ }
2925
+ const dir = await getSDKDirectory();
2926
+ try {
2927
+ const contentHandle = await dir.getFileHandle(`${fileId}.enc`);
2928
+ const contentFile = await contentHandle.getFile();
2929
+ const encryptedHex = await contentFile.text();
2930
+ const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`);
2931
+ const metaFile = await metaHandle.getFile();
2932
+ const metadata = JSON.parse(await metaFile.text());
2933
+ const decryptedBytes = await decryptToBytes(encryptedHex, encryptionKey);
2934
+ const blob = new Blob([decryptedBytes.buffer], { type: metadata.type });
2935
+ return { blob, metadata };
2936
+ } catch (error) {
2937
+ if (error instanceof DOMException && error.name === "NotFoundError") {
2938
+ return null;
2939
+ }
2940
+ throw error;
2941
+ }
2942
+ }
2943
+ async function deleteEncryptedFile(fileId) {
2944
+ if (!isOPFSSupported()) {
2945
+ throw new Error("OPFS is not supported in this browser");
2946
+ }
2947
+ const dir = await getSDKDirectory();
2948
+ try {
2949
+ await dir.removeEntry(`${fileId}.enc`);
2950
+ await dir.removeEntry(`${fileId}.meta.json`);
2951
+ } catch {
2952
+ }
2953
+ }
2954
+ async function fileExists(fileId) {
2955
+ if (!isOPFSSupported()) {
2956
+ return false;
2957
+ }
2958
+ const dir = await getSDKDirectory();
2959
+ try {
2960
+ await dir.getFileHandle(`${fileId}.enc`);
2961
+ return true;
2962
+ } catch {
2963
+ return false;
2964
+ }
2965
+ }
2966
+ var BlobUrlManager = class {
2967
+ constructor() {
2968
+ this.activeUrls = /* @__PURE__ */ new Map();
2969
+ }
2970
+ // fileId -> blobUrl
2971
+ /**
2972
+ * Creates a blob URL for a file and tracks it.
2973
+ */
2974
+ createUrl(fileId, blob) {
2975
+ this.revokeUrl(fileId);
2976
+ const url = URL.createObjectURL(blob);
2977
+ this.activeUrls.set(fileId, url);
2978
+ return url;
2979
+ }
2980
+ /**
2981
+ * Gets the active blob URL for a file, if any.
2982
+ */
2983
+ getUrl(fileId) {
2984
+ return this.activeUrls.get(fileId);
2985
+ }
2986
+ /**
2987
+ * Revokes a blob URL and removes it from tracking.
2988
+ */
2989
+ revokeUrl(fileId) {
2990
+ const url = this.activeUrls.get(fileId);
2991
+ if (url) {
2992
+ URL.revokeObjectURL(url);
2993
+ this.activeUrls.delete(fileId);
2994
+ }
2995
+ }
2996
+ /**
2997
+ * Revokes all tracked blob URLs.
2998
+ */
2999
+ revokeAll() {
3000
+ for (const url of this.activeUrls.values()) {
3001
+ URL.revokeObjectURL(url);
3002
+ }
3003
+ this.activeUrls.clear();
3004
+ }
3005
+ /**
3006
+ * Gets the count of active blob URLs.
3007
+ */
3008
+ get size() {
3009
+ return this.activeUrls.size;
3010
+ }
3011
+ };
3012
+
2832
3013
  // src/react/useChatStorage.ts
2833
3014
  function replaceUrlWithMCPPlaceholder(content, url, fileId) {
2834
3015
  const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -2850,20 +3031,69 @@ function replaceUrlWithMCPPlaceholder(content, url, fileId) {
2850
3031
  function findFileIdBySourceUrl(files, sourceUrl) {
2851
3032
  return files?.find((f) => f.sourceUrl === sourceUrl)?.id;
2852
3033
  }
2853
- function storedToLlmapiMessage(stored) {
2854
- const content = [
2855
- { type: "text", text: stored.content }
2856
- ];
3034
+ async function isUrlValid(url, timeoutMs = 5e3) {
3035
+ try {
3036
+ const controller = new AbortController();
3037
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3038
+ const response = await fetch(url, {
3039
+ method: "GET",
3040
+ headers: { Range: "bytes=0-0" },
3041
+ signal: controller.signal
3042
+ });
3043
+ clearTimeout(timeoutId);
3044
+ return response.ok || response.status === 206;
3045
+ } catch {
3046
+ return false;
3047
+ }
3048
+ }
3049
+ async function storedToLlmapiMessage(stored) {
3050
+ let textContent = stored.content;
3051
+ const fileUrlMap = /* @__PURE__ */ new Map();
3052
+ const imageParts = [];
2857
3053
  if (stored.files?.length) {
2858
3054
  for (const file of stored.files) {
2859
3055
  if (file.url) {
2860
- content.push({
3056
+ imageParts.push({
2861
3057
  type: "image_url",
2862
3058
  image_url: { url: file.url }
2863
3059
  });
3060
+ } else if (file.sourceUrl) {
3061
+ const isValid = await isUrlValid(file.sourceUrl);
3062
+ if (isValid) {
3063
+ imageParts.push({
3064
+ type: "image_url",
3065
+ image_url: { url: file.sourceUrl }
3066
+ });
3067
+ fileUrlMap.set(file.id, file.sourceUrl);
3068
+ }
2864
3069
  }
2865
3070
  }
2866
3071
  }
3072
+ textContent = textContent.replace(
3073
+ /__SDKFILE__([a-f0-9-]+)__/g,
3074
+ (match, fileId) => {
3075
+ const sourceUrl = fileUrlMap.get(fileId);
3076
+ if (sourceUrl) {
3077
+ return `![image](${sourceUrl})`;
3078
+ }
3079
+ return "";
3080
+ }
3081
+ );
3082
+ textContent = textContent.replace(
3083
+ /!\[MCP_IMAGE:([a-f0-9-]+)\]/g,
3084
+ (match, fileId) => {
3085
+ const sourceUrl = fileUrlMap.get(fileId);
3086
+ if (sourceUrl) {
3087
+ return `![image](${sourceUrl})`;
3088
+ }
3089
+ return "";
3090
+ }
3091
+ );
3092
+ textContent = textContent.replace(/\n{3,}/g, "\n\n").trim();
3093
+ const content = [
3094
+ { type: "text", text: textContent },
3095
+ ...imageParts
3096
+ ];
2867
3097
  return {
2868
3098
  role: stored.role,
2869
3099
  content
@@ -2880,9 +3110,17 @@ function useChatStorage(options) {
2880
3110
  onData,
2881
3111
  onFinish,
2882
3112
  onError,
2883
- apiType
3113
+ apiType,
3114
+ walletAddress
2884
3115
  } = options;
2885
3116
  const [currentConversationId, setCurrentConversationId] = (0, import_react2.useState)(initialConversationId || null);
3117
+ const blobManagerRef = (0, import_react2.useRef)(new BlobUrlManager());
3118
+ (0, import_react2.useEffect)(() => {
3119
+ const manager = blobManagerRef.current;
3120
+ return () => {
3121
+ manager.revokeAll();
3122
+ };
3123
+ }, []);
2886
3124
  const messagesCollection = (0, import_react2.useMemo)(
2887
3125
  () => database.get("history"),
2888
3126
  [database]
@@ -2950,9 +3188,49 @@ function useChatStorage(options) {
2950
3188
  );
2951
3189
  const getMessages = (0, import_react2.useCallback)(
2952
3190
  async (convId) => {
2953
- return getMessagesOp(storageCtx, convId);
3191
+ const messages = await getMessagesOp(storageCtx, convId);
3192
+ if (walletAddress && hasEncryptionKey(walletAddress) && isOPFSSupported()) {
3193
+ try {
3194
+ const encryptionKey = await getEncryptionKey(walletAddress);
3195
+ const blobManager = blobManagerRef.current;
3196
+ const resolvedMessages = await Promise.all(
3197
+ messages.map(async (msg) => {
3198
+ const fileIds = extractFileIds(msg.content);
3199
+ if (fileIds.length === 0) {
3200
+ return msg;
3201
+ }
3202
+ let resolvedContent = msg.content;
3203
+ for (const fileId of fileIds) {
3204
+ let url = blobManager.getUrl(fileId);
3205
+ if (!url) {
3206
+ const result = await readEncryptedFile(fileId, encryptionKey);
3207
+ if (result) {
3208
+ url = blobManager.createUrl(fileId, result.blob);
3209
+ }
3210
+ }
3211
+ if (url) {
3212
+ const placeholder = createFilePlaceholder(fileId);
3213
+ resolvedContent = resolvedContent.replace(
3214
+ new RegExp(
3215
+ placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
3216
+ "g"
3217
+ ),
3218
+ `![image](${url})`
3219
+ );
3220
+ }
3221
+ }
3222
+ return { ...msg, content: resolvedContent };
3223
+ })
3224
+ );
3225
+ return resolvedMessages;
3226
+ } catch (error) {
3227
+ console.error("[useChatStorage] Failed to resolve file placeholders:", error);
3228
+ return messages;
3229
+ }
3230
+ }
3231
+ return messages;
2954
3232
  },
2955
- [storageCtx]
3233
+ [storageCtx, walletAddress]
2956
3234
  );
2957
3235
  const getMessageCount = (0, import_react2.useCallback)(
2958
3236
  async (convId) => {
@@ -3176,6 +3454,89 @@ function useChatStorage(options) {
3176
3454
  },
3177
3455
  []
3178
3456
  );
3457
+ const extractAndStoreEncryptedMCPImages = (0, import_react2.useCallback)(
3458
+ async (content, address) => {
3459
+ try {
3460
+ if (!isOPFSSupported()) {
3461
+ console.warn("[extractAndStoreEncryptedMCPImages] OPFS not supported");
3462
+ return { processedFiles: [], cleanedContent: content };
3463
+ }
3464
+ if (!hasEncryptionKey(address)) {
3465
+ console.warn("[extractAndStoreEncryptedMCPImages] Encryption key not available");
3466
+ return { processedFiles: [], cleanedContent: content };
3467
+ }
3468
+ const MCP_IMAGE_URL_PATTERN = new RegExp(
3469
+ `https://${MCP_R2_DOMAIN.replace(/\./g, "\\.")}[^\\s)]*`,
3470
+ "g"
3471
+ );
3472
+ const urlMatches = content.match(MCP_IMAGE_URL_PATTERN);
3473
+ if (!urlMatches || urlMatches.length === 0) {
3474
+ return { processedFiles: [], cleanedContent: content };
3475
+ }
3476
+ const uniqueUrls = [...new Set(urlMatches)];
3477
+ const encryptionKey = await getEncryptionKey(address);
3478
+ const processedFiles = [];
3479
+ let cleanedContent = content;
3480
+ const results = await Promise.allSettled(
3481
+ uniqueUrls.map(async (imageUrl) => {
3482
+ const controller = new AbortController();
3483
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
3484
+ try {
3485
+ const response = await fetch(imageUrl, {
3486
+ signal: controller.signal,
3487
+ cache: "no-store"
3488
+ });
3489
+ if (!response.ok) {
3490
+ throw new Error(`Failed to fetch image: ${response.status}`);
3491
+ }
3492
+ const blob = await response.blob();
3493
+ const fileId = crypto.randomUUID();
3494
+ const urlPath = imageUrl.split("?")[0] ?? imageUrl;
3495
+ const extension = urlPath.match(/\.([a-zA-Z0-9]+)$/)?.[1] || "png";
3496
+ const mimeType = blob.type || `image/${extension}`;
3497
+ const fileName = `mcp-image-${Date.now()}-${fileId.slice(0, 8)}.${extension}`;
3498
+ await writeEncryptedFile(fileId, blob, encryptionKey, {
3499
+ name: fileName,
3500
+ sourceUrl: imageUrl
3501
+ });
3502
+ return { fileId, fileName, mimeType, size: blob.size, imageUrl };
3503
+ } finally {
3504
+ clearTimeout(timeoutId);
3505
+ }
3506
+ })
3507
+ );
3508
+ results.forEach((result, i) => {
3509
+ const imageUrl = uniqueUrls[i];
3510
+ if (result.status === "fulfilled") {
3511
+ const { fileId, fileName, mimeType, size } = result.value;
3512
+ processedFiles.push({
3513
+ id: fileId,
3514
+ name: fileName,
3515
+ type: mimeType,
3516
+ size,
3517
+ sourceUrl: imageUrl
3518
+ });
3519
+ const placeholder = createFilePlaceholder(fileId);
3520
+ const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3521
+ const markdownImagePattern = new RegExp(
3522
+ `!\\[[^\\]]*\\]\\([\\s]*${escapedUrl}[\\s]*\\)`,
3523
+ "g"
3524
+ );
3525
+ cleanedContent = cleanedContent.replace(markdownImagePattern, placeholder);
3526
+ cleanedContent = cleanedContent.replace(new RegExp(escapedUrl, "g"), placeholder);
3527
+ } else {
3528
+ console.error("[extractAndStoreEncryptedMCPImages] Failed:", result.reason);
3529
+ }
3530
+ });
3531
+ cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n").trim();
3532
+ return { processedFiles, cleanedContent };
3533
+ } catch (err) {
3534
+ console.error("[extractAndStoreEncryptedMCPImages] Unexpected error:", err);
3535
+ return { processedFiles: [], cleanedContent: content };
3536
+ }
3537
+ },
3538
+ []
3539
+ );
3179
3540
  const sendMessage = (0, import_react2.useCallback)(
3180
3541
  async (args) => {
3181
3542
  const {
@@ -3227,10 +3588,10 @@ function useChatStorage(options) {
3227
3588
  const storedMessages = await getMessages(convId);
3228
3589
  const validMessages = storedMessages.filter((msg) => !msg.error);
3229
3590
  const limitedMessages = validMessages.slice(-maxHistoryMessages);
3230
- messagesToSend = [
3231
- ...limitedMessages.map(storedToLlmapiMessage),
3232
- ...messages
3233
- ];
3591
+ const historyMessages = await Promise.all(
3592
+ limitedMessages.map(storedToLlmapiMessage)
3593
+ );
3594
+ messagesToSend = [...historyMessages, ...messages];
3234
3595
  } else {
3235
3596
  messagesToSend = [...messages];
3236
3597
  }
@@ -3368,7 +3729,14 @@ function useChatStorage(options) {
3368
3729
  let cleanedContent = assistantContent.replace(jsonSourcesBlockRegex, "").trim();
3369
3730
  cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n");
3370
3731
  let processedFiles = [];
3371
- if (writeFile) {
3732
+ if (walletAddress) {
3733
+ const result2 = await extractAndStoreEncryptedMCPImages(
3734
+ cleanedContent,
3735
+ walletAddress
3736
+ );
3737
+ processedFiles = result2.processedFiles;
3738
+ cleanedContent = result2.cleanedContent;
3739
+ } else if (writeFile) {
3372
3740
  const result2 = await extractAndStoreMCPImages(
3373
3741
  cleanedContent,
3374
3742
  writeFile
@@ -3404,7 +3772,7 @@ function useChatStorage(options) {
3404
3772
  assistantMessage: storedAssistantMessage
3405
3773
  };
3406
3774
  },
3407
- [ensureConversation, getMessages, storageCtx, baseSendMessage]
3775
+ [ensureConversation, getMessages, storageCtx, baseSendMessage, walletAddress, extractAndStoreEncryptedMCPImages]
3408
3776
  );
3409
3777
  const searchMessages = (0, import_react2.useCallback)(
3410
3778
  async (queryVector, options2) => {
@@ -9179,6 +9547,7 @@ function hasDriveCredentials() {
9179
9547
  BACKUP_DRIVE_ROOT_FOLDER,
9180
9548
  BACKUP_ICLOUD_FOLDER,
9181
9549
  BackupAuthProvider,
9550
+ BlobUrlManager,
9182
9551
  ChatConversation,
9183
9552
  ChatMessage,
9184
9553
  DEFAULT_BACKUP_FOLDER,
@@ -9208,9 +9577,11 @@ function hasDriveCredentials() {
9208
9577
  createMemoryContextSystemMessage,
9209
9578
  decryptData,
9210
9579
  decryptDataBytes,
9580
+ deleteEncryptedFile,
9211
9581
  encryptData,
9212
9582
  exportPublicKey,
9213
9583
  extractConversationContext,
9584
+ fileExists,
9214
9585
  findFileIdBySourceUrl,
9215
9586
  formatMemoriesForChat,
9216
9587
  generateCompositeKey,
@@ -9222,6 +9593,7 @@ function hasDriveCredentials() {
9222
9593
  getAndClearDriveReturnUrl,
9223
9594
  getCalendarAccessToken,
9224
9595
  getDriveAccessToken,
9596
+ getEncryptionKey,
9225
9597
  getGoogleDriveStoredToken,
9226
9598
  getValidCalendarToken,
9227
9599
  getValidDriveToken,
@@ -9236,7 +9608,9 @@ function hasDriveCredentials() {
9236
9608
  hasKeyPair,
9237
9609
  isCalendarCallback,
9238
9610
  isDriveCallback,
9611
+ isOPFSSupported,
9239
9612
  memoryStorageSchema,
9613
+ readEncryptedFile,
9240
9614
  refreshCalendarToken,
9241
9615
  refreshDriveToken,
9242
9616
  replaceUrlWithMCPPlaceholder,
@@ -9274,5 +9648,6 @@ function hasDriveCredentials() {
9274
9648
  usePdf,
9275
9649
  useSearch,
9276
9650
  useSettings,
9277
- userPreferencesStorageSchema
9651
+ userPreferencesStorageSchema,
9652
+ writeEncryptedFile
9278
9653
  });