@reverbia/sdk 1.0.0-next.20260110032702 → 1.0.0-next.20260110210906

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.
@@ -777,12 +777,38 @@ declare function generateCompositeKey(namespace: string, key: string): string;
777
777
  declare function generateUniqueKey(namespace: string, key: string, value: string): string;
778
778
 
779
779
  type ChatRole = "user" | "assistant" | "system";
780
+ /**
781
+ * Metadata for files attached to messages.
782
+ *
783
+ * Note the distinction between `url` and `sourceUrl`:
784
+ * - `url`: Content URL that gets sent to the AI as part of the message (e.g., data URIs for user uploads)
785
+ * - `sourceUrl`: Original external URL for locally-cached files (for lookup only, never sent to AI)
786
+ */
780
787
  interface FileMetadata {
788
+ /** Unique identifier for the file (used as OPFS key for cached files) */
781
789
  id: string;
790
+ /** Display name of the file */
782
791
  name: string;
792
+ /** MIME type (e.g., "image/png") */
783
793
  type: string;
794
+ /** File size in bytes */
784
795
  size: number;
796
+ /**
797
+ * Content URL to include when sending this message to the AI.
798
+ * When present, this URL is added as an `image_url` content part.
799
+ * Typically used for user-uploaded files (data URIs) that should be sent with the message.
800
+ *
801
+ * NOT used for MCP-cached files - those use `sourceUrl` for lookup and render from OPFS.
802
+ */
785
803
  url?: string;
804
+ /**
805
+ * Original external URL for files downloaded and cached locally (e.g., from MCP R2).
806
+ * Used purely for URL→OPFS mapping to enable fallback when the source returns 404.
807
+ *
808
+ * This is metadata for local lookup only - it is NOT sent to the AI or rendered directly.
809
+ * The file content is served from OPFS using the `id` field.
810
+ */
811
+ sourceUrl?: string;
786
812
  }
787
813
  interface ChatCompletionUsage {
788
814
  promptTokens?: number;
@@ -777,12 +777,38 @@ declare function generateCompositeKey(namespace: string, key: string): string;
777
777
  declare function generateUniqueKey(namespace: string, key: string, value: string): string;
778
778
 
779
779
  type ChatRole = "user" | "assistant" | "system";
780
+ /**
781
+ * Metadata for files attached to messages.
782
+ *
783
+ * Note the distinction between `url` and `sourceUrl`:
784
+ * - `url`: Content URL that gets sent to the AI as part of the message (e.g., data URIs for user uploads)
785
+ * - `sourceUrl`: Original external URL for locally-cached files (for lookup only, never sent to AI)
786
+ */
780
787
  interface FileMetadata {
788
+ /** Unique identifier for the file (used as OPFS key for cached files) */
781
789
  id: string;
790
+ /** Display name of the file */
782
791
  name: string;
792
+ /** MIME type (e.g., "image/png") */
783
793
  type: string;
794
+ /** File size in bytes */
784
795
  size: number;
796
+ /**
797
+ * Content URL to include when sending this message to the AI.
798
+ * When present, this URL is added as an `image_url` content part.
799
+ * Typically used for user-uploaded files (data URIs) that should be sent with the message.
800
+ *
801
+ * NOT used for MCP-cached files - those use `sourceUrl` for lookup and render from OPFS.
802
+ */
785
803
  url?: string;
804
+ /**
805
+ * Original external URL for files downloaded and cached locally (e.g., from MCP R2).
806
+ * Used purely for URL→OPFS mapping to enable fallback when the source returns 404.
807
+ *
808
+ * This is metadata for local lookup only - it is NOT sent to the AI or rendered directly.
809
+ * The file content is served from OPFS using the `id` field.
810
+ */
811
+ sourceUrl?: string;
786
812
  }
787
813
  interface ChatCompletionUsage {
788
814
  promptTokens?: number;
@@ -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,12 @@ __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,
80
+ findFileIdBySourceUrl: () => findFileIdBySourceUrl,
77
81
  formatMemoriesForChat: () => formatMemoriesForChat,
78
82
  generateCompositeKey: () => generateCompositeKey,
79
83
  generateConversationId: () => generateConversationId,
@@ -84,6 +88,7 @@ __export(index_exports, {
84
88
  getAndClearDriveReturnUrl: () => getAndClearDriveReturnUrl,
85
89
  getCalendarAccessToken: () => getCalendarAccessToken,
86
90
  getDriveAccessToken: () => getDriveAccessToken,
91
+ getEncryptionKey: () => getEncryptionKey,
87
92
  getGoogleDriveStoredToken: () => getGoogleDriveStoredToken,
88
93
  getValidCalendarToken: () => getValidCalendarToken,
89
94
  getValidDriveToken: () => getValidDriveToken,
@@ -98,9 +103,12 @@ __export(index_exports, {
98
103
  hasKeyPair: () => hasKeyPair,
99
104
  isCalendarCallback: () => isCalendarCallback,
100
105
  isDriveCallback: () => isDriveCallback,
106
+ isOPFSSupported: () => isOPFSSupported,
101
107
  memoryStorageSchema: () => memoryStorageSchema,
108
+ readEncryptedFile: () => readEncryptedFile,
102
109
  refreshCalendarToken: () => refreshCalendarToken,
103
110
  refreshDriveToken: () => refreshDriveToken,
111
+ replaceUrlWithMCPPlaceholder: () => replaceUrlWithMCPPlaceholder,
104
112
  requestEncryptionKey: () => requestEncryptionKey,
105
113
  requestKeyPair: () => requestKeyPair,
106
114
  revokeCalendarToken: () => revokeCalendarToken,
@@ -135,7 +143,8 @@ __export(index_exports, {
135
143
  usePdf: () => usePdf,
136
144
  useSearch: () => useSearch,
137
145
  useSettings: () => useSettings,
138
- userPreferencesStorageSchema: () => userPreferencesStorageSchema
146
+ userPreferencesStorageSchema: () => userPreferencesStorageSchema,
147
+ writeEncryptedFile: () => writeEncryptedFile
139
148
  });
140
149
  module.exports = __toCommonJS(index_exports);
141
150
 
@@ -2827,21 +2836,262 @@ async function searchMessagesOp(ctx, queryVector, options) {
2827
2836
  return resultsWithSimilarity.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
2828
2837
  }
2829
2838
 
2839
+ // src/lib/storage/opfs.ts
2840
+ var FILE_PLACEHOLDER_PREFIX = "__SDKFILE__";
2841
+ var FILE_PLACEHOLDER_SUFFIX = "__";
2842
+ var FILE_PLACEHOLDER_REGEX = /__SDKFILE__([a-f0-9-]+)__/g;
2843
+ function createFilePlaceholder(fileId) {
2844
+ return `${FILE_PLACEHOLDER_PREFIX}${fileId}${FILE_PLACEHOLDER_SUFFIX}`;
2845
+ }
2846
+ function extractFileIds(content) {
2847
+ const matches = content.matchAll(FILE_PLACEHOLDER_REGEX);
2848
+ return Array.from(matches, (m) => m[1]);
2849
+ }
2850
+ function isOPFSSupported() {
2851
+ return typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage;
2852
+ }
2853
+ async function getSDKDirectory() {
2854
+ const root = await navigator.storage.getDirectory();
2855
+ return root.getDirectoryHandle("reverbia-sdk-files", { create: true });
2856
+ }
2857
+ function bytesToHex2(bytes) {
2858
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2859
+ }
2860
+ function hexToBytes2(hex) {
2861
+ const bytes = new Uint8Array(hex.length / 2);
2862
+ for (let i = 0; i < hex.length; i += 2) {
2863
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
2864
+ }
2865
+ return bytes;
2866
+ }
2867
+ async function encryptBlob(blob, encryptionKey) {
2868
+ const arrayBuffer = await blob.arrayBuffer();
2869
+ const plaintext = new Uint8Array(arrayBuffer);
2870
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2871
+ const ciphertext = await crypto.subtle.encrypt(
2872
+ { name: "AES-GCM", iv },
2873
+ encryptionKey,
2874
+ plaintext
2875
+ );
2876
+ const combined = new Uint8Array(iv.length + ciphertext.byteLength);
2877
+ combined.set(iv, 0);
2878
+ combined.set(new Uint8Array(ciphertext), iv.length);
2879
+ return bytesToHex2(combined);
2880
+ }
2881
+ async function decryptToBytes(encryptedHex, encryptionKey) {
2882
+ const combined = hexToBytes2(encryptedHex);
2883
+ const iv = combined.slice(0, 12);
2884
+ const ciphertext = combined.slice(12);
2885
+ const decrypted = await crypto.subtle.decrypt(
2886
+ { name: "AES-GCM", iv },
2887
+ encryptionKey,
2888
+ ciphertext
2889
+ );
2890
+ return new Uint8Array(decrypted);
2891
+ }
2892
+ async function writeEncryptedFile(fileId, blob, encryptionKey, metadata) {
2893
+ if (!isOPFSSupported()) {
2894
+ throw new Error("OPFS is not supported in this browser");
2895
+ }
2896
+ const dir = await getSDKDirectory();
2897
+ const encryptedHex = await encryptBlob(blob, encryptionKey);
2898
+ const contentHandle = await dir.getFileHandle(`${fileId}.enc`, {
2899
+ create: true
2900
+ });
2901
+ const contentWritable = await contentHandle.createWritable();
2902
+ await contentWritable.write(encryptedHex);
2903
+ await contentWritable.close();
2904
+ const fileMetadata = {
2905
+ id: fileId,
2906
+ name: metadata?.name || `file-${fileId}`,
2907
+ type: blob.type || "application/octet-stream",
2908
+ size: blob.size,
2909
+ sourceUrl: metadata?.sourceUrl,
2910
+ createdAt: Date.now()
2911
+ };
2912
+ const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`, {
2913
+ create: true
2914
+ });
2915
+ const metaWritable = await metaHandle.createWritable();
2916
+ await metaWritable.write(JSON.stringify(fileMetadata));
2917
+ await metaWritable.close();
2918
+ }
2919
+ async function readEncryptedFile(fileId, encryptionKey) {
2920
+ if (!isOPFSSupported()) {
2921
+ throw new Error("OPFS is not supported in this browser");
2922
+ }
2923
+ const dir = await getSDKDirectory();
2924
+ try {
2925
+ const contentHandle = await dir.getFileHandle(`${fileId}.enc`);
2926
+ const contentFile = await contentHandle.getFile();
2927
+ const encryptedHex = await contentFile.text();
2928
+ const metaHandle = await dir.getFileHandle(`${fileId}.meta.json`);
2929
+ const metaFile = await metaHandle.getFile();
2930
+ const metadata = JSON.parse(await metaFile.text());
2931
+ const decryptedBytes = await decryptToBytes(encryptedHex, encryptionKey);
2932
+ const blob = new Blob([decryptedBytes.buffer], { type: metadata.type });
2933
+ return { blob, metadata };
2934
+ } catch (error) {
2935
+ if (error instanceof DOMException && error.name === "NotFoundError") {
2936
+ return null;
2937
+ }
2938
+ throw error;
2939
+ }
2940
+ }
2941
+ async function deleteEncryptedFile(fileId) {
2942
+ if (!isOPFSSupported()) {
2943
+ throw new Error("OPFS is not supported in this browser");
2944
+ }
2945
+ const dir = await getSDKDirectory();
2946
+ try {
2947
+ await dir.removeEntry(`${fileId}.enc`);
2948
+ await dir.removeEntry(`${fileId}.meta.json`);
2949
+ } catch {
2950
+ }
2951
+ }
2952
+ async function fileExists(fileId) {
2953
+ if (!isOPFSSupported()) {
2954
+ return false;
2955
+ }
2956
+ const dir = await getSDKDirectory();
2957
+ try {
2958
+ await dir.getFileHandle(`${fileId}.enc`);
2959
+ return true;
2960
+ } catch {
2961
+ return false;
2962
+ }
2963
+ }
2964
+ var BlobUrlManager = class {
2965
+ constructor() {
2966
+ this.activeUrls = /* @__PURE__ */ new Map();
2967
+ }
2968
+ // fileId -> blobUrl
2969
+ /**
2970
+ * Creates a blob URL for a file and tracks it.
2971
+ */
2972
+ createUrl(fileId, blob) {
2973
+ this.revokeUrl(fileId);
2974
+ const url = URL.createObjectURL(blob);
2975
+ this.activeUrls.set(fileId, url);
2976
+ return url;
2977
+ }
2978
+ /**
2979
+ * Gets the active blob URL for a file, if any.
2980
+ */
2981
+ getUrl(fileId) {
2982
+ return this.activeUrls.get(fileId);
2983
+ }
2984
+ /**
2985
+ * Revokes a blob URL and removes it from tracking.
2986
+ */
2987
+ revokeUrl(fileId) {
2988
+ const url = this.activeUrls.get(fileId);
2989
+ if (url) {
2990
+ URL.revokeObjectURL(url);
2991
+ this.activeUrls.delete(fileId);
2992
+ }
2993
+ }
2994
+ /**
2995
+ * Revokes all tracked blob URLs.
2996
+ */
2997
+ revokeAll() {
2998
+ for (const url of this.activeUrls.values()) {
2999
+ URL.revokeObjectURL(url);
3000
+ }
3001
+ this.activeUrls.clear();
3002
+ }
3003
+ /**
3004
+ * Gets the count of active blob URLs.
3005
+ */
3006
+ get size() {
3007
+ return this.activeUrls.size;
3008
+ }
3009
+ };
3010
+
2830
3011
  // src/react/useChatStorage.ts
2831
- function storedToLlmapiMessage(stored) {
2832
- const content = [
2833
- { type: "text", text: stored.content }
2834
- ];
3012
+ function replaceUrlWithMCPPlaceholder(content, url, fileId) {
3013
+ const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3014
+ const placeholder = `![MCP_IMAGE:${fileId}]`;
3015
+ let result = content;
3016
+ const markdownImagePattern = new RegExp(
3017
+ `!\\[[^\\]]*\\]\\([\\s]*${escapedUrl}[\\s]*\\)`,
3018
+ "g"
3019
+ );
3020
+ result = result.replace(markdownImagePattern, placeholder);
3021
+ result = result.replace(new RegExp(escapedUrl, "g"), placeholder);
3022
+ const orphanedMarkdownPattern = new RegExp(
3023
+ `!\\[[^\\]]*\\]\\([\\s]*\\!\\[MCP_IMAGE:${fileId}\\][\\s]*\\)`,
3024
+ "g"
3025
+ );
3026
+ result = result.replace(orphanedMarkdownPattern, placeholder);
3027
+ return result;
3028
+ }
3029
+ function findFileIdBySourceUrl(files, sourceUrl) {
3030
+ return files?.find((f) => f.sourceUrl === sourceUrl)?.id;
3031
+ }
3032
+ async function isUrlValid(url, timeoutMs = 5e3) {
3033
+ try {
3034
+ const controller = new AbortController();
3035
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3036
+ const response = await fetch(url, {
3037
+ method: "GET",
3038
+ headers: { Range: "bytes=0-0" },
3039
+ signal: controller.signal
3040
+ });
3041
+ clearTimeout(timeoutId);
3042
+ return response.ok || response.status === 206;
3043
+ } catch {
3044
+ return false;
3045
+ }
3046
+ }
3047
+ async function storedToLlmapiMessage(stored) {
3048
+ let textContent = stored.content;
3049
+ const fileUrlMap = /* @__PURE__ */ new Map();
3050
+ const imageParts = [];
2835
3051
  if (stored.files?.length) {
2836
3052
  for (const file of stored.files) {
2837
3053
  if (file.url) {
2838
- content.push({
3054
+ imageParts.push({
2839
3055
  type: "image_url",
2840
3056
  image_url: { url: file.url }
2841
3057
  });
3058
+ } else if (file.sourceUrl) {
3059
+ const isValid = await isUrlValid(file.sourceUrl);
3060
+ if (isValid) {
3061
+ imageParts.push({
3062
+ type: "image_url",
3063
+ image_url: { url: file.sourceUrl }
3064
+ });
3065
+ fileUrlMap.set(file.id, file.sourceUrl);
3066
+ }
2842
3067
  }
2843
3068
  }
2844
3069
  }
3070
+ textContent = textContent.replace(
3071
+ /__SDKFILE__([a-f0-9-]+)__/g,
3072
+ (match, fileId) => {
3073
+ const sourceUrl = fileUrlMap.get(fileId);
3074
+ if (sourceUrl) {
3075
+ return `![image](${sourceUrl})`;
3076
+ }
3077
+ return "";
3078
+ }
3079
+ );
3080
+ textContent = textContent.replace(
3081
+ /!\[MCP_IMAGE:([a-f0-9-]+)\]/g,
3082
+ (match, fileId) => {
3083
+ const sourceUrl = fileUrlMap.get(fileId);
3084
+ if (sourceUrl) {
3085
+ return `![image](${sourceUrl})`;
3086
+ }
3087
+ return "";
3088
+ }
3089
+ );
3090
+ textContent = textContent.replace(/\n{3,}/g, "\n\n").trim();
3091
+ const content = [
3092
+ { type: "text", text: textContent },
3093
+ ...imageParts
3094
+ ];
2845
3095
  return {
2846
3096
  role: stored.role,
2847
3097
  content
@@ -2858,9 +3108,17 @@ function useChatStorage(options) {
2858
3108
  onData,
2859
3109
  onFinish,
2860
3110
  onError,
2861
- apiType
3111
+ apiType,
3112
+ walletAddress
2862
3113
  } = options;
2863
3114
  const [currentConversationId, setCurrentConversationId] = (0, import_react2.useState)(initialConversationId || null);
3115
+ const blobManagerRef = (0, import_react2.useRef)(new BlobUrlManager());
3116
+ (0, import_react2.useEffect)(() => {
3117
+ const manager = blobManagerRef.current;
3118
+ return () => {
3119
+ manager.revokeAll();
3120
+ };
3121
+ }, []);
2864
3122
  const messagesCollection = (0, import_react2.useMemo)(
2865
3123
  () => database.get("history"),
2866
3124
  [database]
@@ -2928,9 +3186,49 @@ function useChatStorage(options) {
2928
3186
  );
2929
3187
  const getMessages = (0, import_react2.useCallback)(
2930
3188
  async (convId) => {
2931
- return getMessagesOp(storageCtx, convId);
3189
+ const messages = await getMessagesOp(storageCtx, convId);
3190
+ if (walletAddress && hasEncryptionKey(walletAddress) && isOPFSSupported()) {
3191
+ try {
3192
+ const encryptionKey = await getEncryptionKey(walletAddress);
3193
+ const blobManager = blobManagerRef.current;
3194
+ const resolvedMessages = await Promise.all(
3195
+ messages.map(async (msg) => {
3196
+ const fileIds = extractFileIds(msg.content);
3197
+ if (fileIds.length === 0) {
3198
+ return msg;
3199
+ }
3200
+ let resolvedContent = msg.content;
3201
+ for (const fileId of fileIds) {
3202
+ let url = blobManager.getUrl(fileId);
3203
+ if (!url) {
3204
+ const result = await readEncryptedFile(fileId, encryptionKey);
3205
+ if (result) {
3206
+ url = blobManager.createUrl(fileId, result.blob);
3207
+ }
3208
+ }
3209
+ if (url) {
3210
+ const placeholder = createFilePlaceholder(fileId);
3211
+ resolvedContent = resolvedContent.replace(
3212
+ new RegExp(
3213
+ placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
3214
+ "g"
3215
+ ),
3216
+ `![image](${url})`
3217
+ );
3218
+ }
3219
+ }
3220
+ return { ...msg, content: resolvedContent };
3221
+ })
3222
+ );
3223
+ return resolvedMessages;
3224
+ } catch (error) {
3225
+ console.error("[useChatStorage] Failed to resolve file placeholders:", error);
3226
+ return messages;
3227
+ }
3228
+ }
3229
+ return messages;
2932
3230
  },
2933
- [storageCtx]
3231
+ [storageCtx, walletAddress]
2934
3232
  );
2935
3233
  const getMessageCount = (0, import_react2.useCallback)(
2936
3234
  async (convId) => {
@@ -3054,7 +3352,8 @@ function useChatStorage(options) {
3054
3352
  []
3055
3353
  );
3056
3354
  const extractAndStoreMCPImages = (0, import_react2.useCallback)(
3057
- async (content, writeFile) => {
3355
+ async (content, writeFile, options2) => {
3356
+ const { replaceUrls = true } = options2 ?? {};
3058
3357
  try {
3059
3358
  const MCP_IMAGE_URL_PATTERN = new RegExp(
3060
3359
  `https://${MCP_R2_DOMAIN.replace(/\./g, "\\.")}[^\\s)]*`,
@@ -3128,9 +3427,10 @@ function useChatStorage(options) {
3128
3427
  id: fileId,
3129
3428
  name: fileName,
3130
3429
  type: mimeType,
3131
- size
3430
+ size,
3431
+ sourceUrl: imageUrl
3132
3432
  });
3133
- if (imageUrl) {
3433
+ if (replaceUrls && imageUrl) {
3134
3434
  replaceUrlWithPlaceholder(imageUrl, fileId);
3135
3435
  }
3136
3436
  } else {
@@ -3152,6 +3452,89 @@ function useChatStorage(options) {
3152
3452
  },
3153
3453
  []
3154
3454
  );
3455
+ const extractAndStoreEncryptedMCPImages = (0, import_react2.useCallback)(
3456
+ async (content, address) => {
3457
+ try {
3458
+ if (!isOPFSSupported()) {
3459
+ console.warn("[extractAndStoreEncryptedMCPImages] OPFS not supported");
3460
+ return { processedFiles: [], cleanedContent: content };
3461
+ }
3462
+ if (!hasEncryptionKey(address)) {
3463
+ console.warn("[extractAndStoreEncryptedMCPImages] Encryption key not available");
3464
+ return { processedFiles: [], cleanedContent: content };
3465
+ }
3466
+ const MCP_IMAGE_URL_PATTERN = new RegExp(
3467
+ `https://${MCP_R2_DOMAIN.replace(/\./g, "\\.")}[^\\s)]*`,
3468
+ "g"
3469
+ );
3470
+ const urlMatches = content.match(MCP_IMAGE_URL_PATTERN);
3471
+ if (!urlMatches || urlMatches.length === 0) {
3472
+ return { processedFiles: [], cleanedContent: content };
3473
+ }
3474
+ const uniqueUrls = [...new Set(urlMatches)];
3475
+ const encryptionKey = await getEncryptionKey(address);
3476
+ const processedFiles = [];
3477
+ let cleanedContent = content;
3478
+ const results = await Promise.allSettled(
3479
+ uniqueUrls.map(async (imageUrl) => {
3480
+ const controller = new AbortController();
3481
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
3482
+ try {
3483
+ const response = await fetch(imageUrl, {
3484
+ signal: controller.signal,
3485
+ cache: "no-store"
3486
+ });
3487
+ if (!response.ok) {
3488
+ throw new Error(`Failed to fetch image: ${response.status}`);
3489
+ }
3490
+ const blob = await response.blob();
3491
+ const fileId = crypto.randomUUID();
3492
+ const urlPath = imageUrl.split("?")[0] ?? imageUrl;
3493
+ const extension = urlPath.match(/\.([a-zA-Z0-9]+)$/)?.[1] || "png";
3494
+ const mimeType = blob.type || `image/${extension}`;
3495
+ const fileName = `mcp-image-${Date.now()}-${fileId.slice(0, 8)}.${extension}`;
3496
+ await writeEncryptedFile(fileId, blob, encryptionKey, {
3497
+ name: fileName,
3498
+ sourceUrl: imageUrl
3499
+ });
3500
+ return { fileId, fileName, mimeType, size: blob.size, imageUrl };
3501
+ } finally {
3502
+ clearTimeout(timeoutId);
3503
+ }
3504
+ })
3505
+ );
3506
+ results.forEach((result, i) => {
3507
+ const imageUrl = uniqueUrls[i];
3508
+ if (result.status === "fulfilled") {
3509
+ const { fileId, fileName, mimeType, size } = result.value;
3510
+ processedFiles.push({
3511
+ id: fileId,
3512
+ name: fileName,
3513
+ type: mimeType,
3514
+ size,
3515
+ sourceUrl: imageUrl
3516
+ });
3517
+ const placeholder = createFilePlaceholder(fileId);
3518
+ const escapedUrl = imageUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3519
+ const markdownImagePattern = new RegExp(
3520
+ `!\\[[^\\]]*\\]\\([\\s]*${escapedUrl}[\\s]*\\)`,
3521
+ "g"
3522
+ );
3523
+ cleanedContent = cleanedContent.replace(markdownImagePattern, placeholder);
3524
+ cleanedContent = cleanedContent.replace(new RegExp(escapedUrl, "g"), placeholder);
3525
+ } else {
3526
+ console.error("[extractAndStoreEncryptedMCPImages] Failed:", result.reason);
3527
+ }
3528
+ });
3529
+ cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n").trim();
3530
+ return { processedFiles, cleanedContent };
3531
+ } catch (err) {
3532
+ console.error("[extractAndStoreEncryptedMCPImages] Unexpected error:", err);
3533
+ return { processedFiles: [], cleanedContent: content };
3534
+ }
3535
+ },
3536
+ []
3537
+ );
3155
3538
  const sendMessage = (0, import_react2.useCallback)(
3156
3539
  async (args) => {
3157
3540
  const {
@@ -3203,10 +3586,10 @@ function useChatStorage(options) {
3203
3586
  const storedMessages = await getMessages(convId);
3204
3587
  const validMessages = storedMessages.filter((msg) => !msg.error);
3205
3588
  const limitedMessages = validMessages.slice(-maxHistoryMessages);
3206
- messagesToSend = [
3207
- ...limitedMessages.map(storedToLlmapiMessage),
3208
- ...messages
3209
- ];
3589
+ const historyMessages = await Promise.all(
3590
+ limitedMessages.map(storedToLlmapiMessage)
3591
+ );
3592
+ messagesToSend = [...historyMessages, ...messages];
3210
3593
  } else {
3211
3594
  messagesToSend = [...messages];
3212
3595
  }
@@ -3344,7 +3727,14 @@ function useChatStorage(options) {
3344
3727
  let cleanedContent = assistantContent.replace(jsonSourcesBlockRegex, "").trim();
3345
3728
  cleanedContent = cleanedContent.replace(/\n{3,}/g, "\n\n");
3346
3729
  let processedFiles = [];
3347
- if (writeFile) {
3730
+ if (walletAddress) {
3731
+ const result2 = await extractAndStoreEncryptedMCPImages(
3732
+ cleanedContent,
3733
+ walletAddress
3734
+ );
3735
+ processedFiles = result2.processedFiles;
3736
+ cleanedContent = result2.cleanedContent;
3737
+ } else if (writeFile) {
3348
3738
  const result2 = await extractAndStoreMCPImages(
3349
3739
  cleanedContent,
3350
3740
  writeFile
@@ -3380,7 +3770,7 @@ function useChatStorage(options) {
3380
3770
  assistantMessage: storedAssistantMessage
3381
3771
  };
3382
3772
  },
3383
- [ensureConversation, getMessages, storageCtx, baseSendMessage]
3773
+ [ensureConversation, getMessages, storageCtx, baseSendMessage, walletAddress, extractAndStoreEncryptedMCPImages]
3384
3774
  );
3385
3775
  const searchMessages = (0, import_react2.useCallback)(
3386
3776
  async (queryVector, options2) => {
@@ -9155,6 +9545,7 @@ function hasDriveCredentials() {
9155
9545
  BACKUP_DRIVE_ROOT_FOLDER,
9156
9546
  BACKUP_ICLOUD_FOLDER,
9157
9547
  BackupAuthProvider,
9548
+ BlobUrlManager,
9158
9549
  ChatConversation,
9159
9550
  ChatMessage,
9160
9551
  DEFAULT_BACKUP_FOLDER,
@@ -9184,9 +9575,12 @@ function hasDriveCredentials() {
9184
9575
  createMemoryContextSystemMessage,
9185
9576
  decryptData,
9186
9577
  decryptDataBytes,
9578
+ deleteEncryptedFile,
9187
9579
  encryptData,
9188
9580
  exportPublicKey,
9189
9581
  extractConversationContext,
9582
+ fileExists,
9583
+ findFileIdBySourceUrl,
9190
9584
  formatMemoriesForChat,
9191
9585
  generateCompositeKey,
9192
9586
  generateConversationId,
@@ -9197,6 +9591,7 @@ function hasDriveCredentials() {
9197
9591
  getAndClearDriveReturnUrl,
9198
9592
  getCalendarAccessToken,
9199
9593
  getDriveAccessToken,
9594
+ getEncryptionKey,
9200
9595
  getGoogleDriveStoredToken,
9201
9596
  getValidCalendarToken,
9202
9597
  getValidDriveToken,
@@ -9211,9 +9606,12 @@ function hasDriveCredentials() {
9211
9606
  hasKeyPair,
9212
9607
  isCalendarCallback,
9213
9608
  isDriveCallback,
9609
+ isOPFSSupported,
9214
9610
  memoryStorageSchema,
9611
+ readEncryptedFile,
9215
9612
  refreshCalendarToken,
9216
9613
  refreshDriveToken,
9614
+ replaceUrlWithMCPPlaceholder,
9217
9615
  requestEncryptionKey,
9218
9616
  requestKeyPair,
9219
9617
  revokeCalendarToken,
@@ -9248,5 +9646,6 @@ function hasDriveCredentials() {
9248
9646
  usePdf,
9249
9647
  useSearch,
9250
9648
  useSettings,
9251
- userPreferencesStorageSchema
9649
+ userPreferencesStorageSchema,
9650
+ writeEncryptedFile
9252
9651
  });