bunnyquery 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bunnyquery.js CHANGED
@@ -166,6 +166,7 @@ ${lines.join("\n")}`;
166
166
  }
167
167
  let composedForLlm = composed;
168
168
  let extractContent;
169
+ let fileUrls;
169
170
  if (attachmentUrls.length > 0) {
170
171
  const extractFiles = attachmentUrls.filter((u) => isServerExtractable(u.name));
171
172
  if (extractFiles.length > 0) {
@@ -186,8 +187,12 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
186
187
 
187
188
  ` + sections.join("\n\n");
188
189
  }
190
+ const urlFiles = attachmentUrls.filter((u) => u.url && !isServerExtractable(u.name));
191
+ if (urlFiles.length > 0) {
192
+ fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
193
+ }
189
194
  }
190
- return { composed, composedForLlm, extractContent };
195
+ return { composed, composedForLlm, extractContent, fileUrls };
191
196
  }
192
197
 
193
198
  // src/engine/attachments.ts
@@ -249,7 +254,7 @@ Project description: """${serviceDescription}"""`;
249
254
  - Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly and do NOT call web_fetch for those files. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
250
255
  - For any file given to you as a temporary URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
251
256
  - Whatever the file type, use the file's storage path (the "storage path" metadata line) as the "src::" unique_id - never the inline content or a temporary URL.
252
- - TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet. If a sheet has many rows, make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. (You may ALSO save one file-level summary record, but the per-row records are mandatory.)
257
+ - TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet. If a sheet has many rows, make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. ALSO save one file-level summary record (file name, sheet name(s), column headers, total row count, overall summary) - this is the record that carries the file's "src::" unique_id - and link EVERY per-row record to it via reference (set each row record's reference to that src:: file record; the row records themselves do NOT carry a src:: unique_id). The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed.
253
258
  - EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in a dedicated table (e.g. "book_chapters") - never collapse the whole book into a single record. Each chapter record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO save one book-level record (title, author, language, overall summary, chapter list / table of contents, genre/subjects) and link each chapter record to it via reference. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
254
259
  - This is a ONE-SHOT background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions or invite back-and-forth. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). Never store raw or binary bytes (base64, blobs); describe them in human-readable text instead.
255
260
  - Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
@@ -432,13 +437,26 @@ ${options.inlineContentPlaceholder}
432
437
  function buildDisplayExpiredAttachmentHref(remotePath, fallback) {
433
438
  return EXPIRED_ATTACHMENT_URL_ORIGIN + "/" + encodePathSegments(getExpiredAttachmentVisiblePath(remotePath, fallback));
434
439
  }
435
- function sanitizeAttachmentLinksForHistory(content, serviceId) {
436
- if (!content || content.indexOf("Attached files:") === -1) return content;
440
+ function isServiceDbAttachmentHref(href, serviceId) {
441
+ if (!serviceId) return false;
442
+ try {
443
+ var parsed = new URL(href);
444
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
445
+ var segs = normalizeAttachmentPathCandidate(parsed.pathname || "").split("/").filter(Boolean);
446
+ return segs.length > 0 && segs[0] === serviceId;
447
+ } catch (e) {
448
+ return false;
449
+ }
450
+ }
451
+ function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
452
+ if (!content) return content;
453
+ if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
437
454
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
455
+ if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
438
456
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
439
457
  var labelPath = normalizeAttachmentPathCandidate(label);
440
458
  var fullPath = remotePath || labelPath;
441
- if (!fullPath) return "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
459
+ if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
442
460
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
443
461
  });
444
462
  }
@@ -489,7 +507,7 @@ ${options.inlineContentPlaceholder}
489
507
  var trimmed = windowed.map(function(m, i2) {
490
508
  if (i2 === latestIndex) return m;
491
509
  var stripped = stripFileBlocksFromHistory(m.content);
492
- var sanitized = m.role === "user" ? sanitizeAttachmentLinksForHistory(stripped, options.serviceId) : stripped;
510
+ var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.serviceId, m.role !== "user");
493
511
  return Object.assign({}, m, { content: sanitized });
494
512
  });
495
513
  var bounded = [], used = 0;
@@ -633,7 +651,8 @@ ${options.inlineContentPlaceholder}
633
651
  maxTokens = 1e3,
634
652
  system,
635
653
  mcpServer,
636
- extractContent
654
+ extractContent,
655
+ fileUrls
637
656
  }) {
638
657
  const mcpServerDefinition = {
639
658
  type: "url",
@@ -661,6 +680,7 @@ ${options.inlineContentPlaceholder}
661
680
  model,
662
681
  max_tokens: maxTokens,
663
682
  ...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
683
+ ...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
664
684
  ...system ? {
665
685
  system: [
666
686
  {
@@ -698,7 +718,7 @@ ${options.inlineContentPlaceholder}
698
718
  }
699
719
  });
700
720
  }
701
- async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, onResponse, onError) {
721
+ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
702
722
  return callClaudeWithMcp({
703
723
  prompt,
704
724
  messages,
@@ -709,13 +729,14 @@ ${options.inlineContentPlaceholder}
709
729
  maxTokens: MAX_TOKENS,
710
730
  system,
711
731
  extractContent,
732
+ fileUrls,
712
733
  mcpServer: {
713
734
  name: MCP_NAME,
714
735
  url: mcpUrl(),
715
736
  authorizationToken: "$ACCESS_TOKEN"
716
737
  }});
717
738
  }
718
- async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, onResponse, onError) {
739
+ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
719
740
  const resolvedModel = model || DEFAULT_OPENAI_MODEL;
720
741
  const imageDetail = getOpenAIImageDetail(resolvedModel);
721
742
  const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
@@ -752,6 +773,7 @@ ${options.inlineContentPlaceholder}
752
773
  model: resolvedModel,
753
774
  max_output_tokens: MAX_TOKENS,
754
775
  ...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
776
+ ...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
755
777
  input: responseInput,
756
778
  tools: [
757
779
  {
@@ -1026,7 +1048,7 @@ ${options.inlineContentPlaceholder}
1026
1048
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1027
1049
  mapped.push(em);
1028
1050
  } else if (assistantText) {
1029
- var okm = { role: "assistant", content: assistantText };
1051
+ var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1030
1052
  if (item._isBgTask) okm.isBackgroundTask = true;
1031
1053
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1032
1054
  mapped.push(okm);
@@ -1092,16 +1114,16 @@ ${options.inlineContentPlaceholder}
1092
1114
  startKeyHistory: this.state.historyStartKeyHistory.slice()
1093
1115
  };
1094
1116
  }
1095
- _callProviderFor(platform, prompt, messages, system, model, userId, extractContent) {
1117
+ _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls) {
1096
1118
  var id = this.host.getIdentity();
1097
- return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent);
1119
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls);
1098
1120
  }
1099
1121
  dispatchAgentRequest(params) {
1100
1122
  var self = this;
1101
1123
  var dispatchItemId;
1102
1124
  var sendAndPoll = function() {
1103
1125
  return Promise.resolve(
1104
- self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent)
1126
+ self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
1105
1127
  ).then(function(initial) {
1106
1128
  if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
1107
1129
  if (initial.id) {
@@ -1162,7 +1184,7 @@ ${options.inlineContentPlaceholder}
1162
1184
  // composed = clean display text; composedForLlm carries office-extraction
1163
1185
  // placeholders for the provider only. useBgQueue routes a post-attachment turn
1164
1186
  // onto the "-bg" queue so it runs after indexing.
1165
- dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent) {
1187
+ dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
1166
1188
  var self = this;
1167
1189
  if (!composed) return;
1168
1190
  var id = this.host.getIdentity();
@@ -1194,7 +1216,7 @@ ${options.inlineContentPlaceholder}
1194
1216
  this.updateHistoryCache();
1195
1217
  this.host.scrollToBottom(true);
1196
1218
  var capturedComposed = composed, capturedPlatform = aiPlatform;
1197
- Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent)).then(function(result) {
1219
+ Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
1198
1220
  var sendingIdx = self.state.messages.findIndex(function(m) {
1199
1221
  return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1200
1222
  });
@@ -1254,7 +1276,8 @@ ${options.inlineContentPlaceholder}
1254
1276
  text: composed,
1255
1277
  boundedMessages: bounded.messages,
1256
1278
  userId: chatQueue,
1257
- extractContent
1279
+ extractContent,
1280
+ fileUrls
1258
1281
  });
1259
1282
  Promise.resolve(run).catch(function() {
1260
1283
  }).then(function() {
@@ -2016,6 +2039,7 @@ ${options.inlineContentPlaceholder}
2016
2039
  chain = chain.then(function() {
2017
2040
  var hadExists = false;
2018
2041
  var skipped = false;
2042
+ var existedBefore = false;
2019
2043
  var onProg = function(p) {
2020
2044
  if (p && p.total) {
2021
2045
  att.progress = Math.floor((idx + p.loaded / p.total) / total * 100);
@@ -2039,12 +2063,16 @@ ${options.inlineContentPlaceholder}
2039
2063
  var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
2040
2064
  if (!isExists) throw err;
2041
2065
  return self.host.promptOverwrite(member.file.name).then(function(choice) {
2042
- if (choice === "overwrite") return doMemberUpload(false);
2066
+ if (choice === "overwrite") {
2067
+ existedBefore = true;
2068
+ return doMemberUpload(false);
2069
+ }
2043
2070
  if (choice === "skip") {
2044
2071
  skipped = true;
2045
2072
  return;
2046
2073
  }
2047
2074
  hadExists = true;
2075
+ existedBefore = true;
2048
2076
  });
2049
2077
  }).then(function() {
2050
2078
  if (skipped) return;
@@ -2057,9 +2085,11 @@ ${options.inlineContentPlaceholder}
2057
2085
  att.storagePath = member.storagePath;
2058
2086
  }
2059
2087
  var mime = member.file.type || self.host.getMimeType(member.file.name);
2060
- return Promise.resolve(
2061
- parseAttachmentContent(member.file, member.file.name, mime || void 0)
2062
- ).then(function(parsedContent) {
2088
+ var preIndex = existedBefore && typeof self.host.deleteExistingFileRecord === "function" ? Promise.resolve(self.host.deleteExistingFileRecord(member.storagePath)).catch(function() {
2089
+ }) : Promise.resolve();
2090
+ return preIndex.then(function() {
2091
+ return parseAttachmentContent(member.file, member.file.name, mime || void 0);
2092
+ }).then(function(parsedContent) {
2063
2093
  return notifyAgentSaveAttachment({
2064
2094
  platform: id.platform,
2065
2095
  model: id.model,
@@ -2183,7 +2213,7 @@ ${options.inlineContentPlaceholder}
2183
2213
  (function() {
2184
2214
  var MCP_PROD = "https://mcp.broadwayinc.computer";
2185
2215
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
2186
- var BQ_VERSION = "1.5.1" ;
2216
+ var BQ_VERSION = "1.5.3" ;
2187
2217
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
2188
2218
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
2189
2219
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -3808,6 +3838,9 @@ ${options.inlineContentPlaceholder}
3808
3838
  getTemporaryUrl: function(path) {
3809
3839
  return getTemporaryUrlDb(path, ATTACHMENT_URL_EXPIRES_SECONDS);
3810
3840
  },
3841
+ deleteExistingFileRecord: function(path) {
3842
+ return deleteFileIndexRecordDb(path);
3843
+ },
3811
3844
  storagePathFor: function(relPath) {
3812
3845
  return attachmentStoragePath(relPath);
3813
3846
  },
@@ -3957,7 +3990,7 @@ ${options.inlineContentPlaceholder}
3957
3990
  clearSuccessfulAttachments();
3958
3991
  if (text) {
3959
3992
  var c = composeUserMessage(text, attachmentUrls);
3960
- session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent);
3993
+ session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent, c.fileUrls);
3961
3994
  }
3962
3995
  if (failureGroups.length) showUploadErrorReport(failureGroups);
3963
3996
  }).catch(function(err) {
@@ -4168,7 +4201,7 @@ ${options.inlineContentPlaceholder}
4168
4201
  return (reindex ? "Reindexing: " : "Indexing: ") + nameLabel + (extras.length ? " \xB7 " + extras.join(" \xB7 ") : "");
4169
4202
  }
4170
4203
  function sanitizeStorageSegment(name) {
4171
- var n = String(name == null ? "file" : name).trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/_{2,}/g, "_").replace(/^[_]+/, "");
4204
+ var n = String(name == null ? "file" : name).normalize("NFC").trim().replace(/[^\p{L}\p{N}._ -]+/gu, "_").replace(/ {2,}/g, " ").replace(/_{2,}/g, "_").replace(/^[_ ]+/, "");
4172
4205
  return n || "file";
4173
4206
  }
4174
4207
  function attachmentStoragePath(relPath) {
@@ -4228,6 +4261,11 @@ ${options.inlineContentPlaceholder}
4228
4261
  return xhrUploadForm(signed.url, form, onProgress, setAbort);
4229
4262
  });
4230
4263
  }
4264
+ function deleteFileIndexRecordDb(storagePath) {
4265
+ if (!storagePath || !S.skapi || typeof S.skapi.deleteRecords !== "function") return Promise.resolve();
4266
+ return S.skapi.deleteRecords({ service: S.serviceId, unique_id: "src::" + storagePath }).catch(function() {
4267
+ });
4268
+ }
4231
4269
  function getTemporaryUrlDb(path, expires) {
4232
4270
  return S.skapi.util.request("get-signed-url", {
4233
4271
  service: S.serviceId,
@@ -4608,6 +4646,12 @@ ${options.inlineContentPlaceholder}
4608
4646
  row.appendChild(removeAll);
4609
4647
  }
4610
4648
  }
4649
+ function uploadsFrozenForUser() {
4650
+ var conf = S.service && S.service.conf || {};
4651
+ if (!conf.freeze_database) return false;
4652
+ var ag = S.user && typeof S.user.access_group === "number" ? S.user.access_group : 0;
4653
+ return ag < 99;
4654
+ }
4611
4655
  function updateComposerControls() {
4612
4656
  var uploading = CS.uploadingAttachments;
4613
4657
  if (CS.attachBtnEl) CS.attachBtnEl.disabled = uploading;
@@ -4855,7 +4899,7 @@ ${options.inlineContentPlaceholder}
4855
4899
  if (msg.isSendingToServer || msg._cancelling) cls.push("is-sending-to-server");
4856
4900
  var bubble;
4857
4901
  if (msg.isPending) {
4858
- bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader", text: "Thinking" }));
4902
+ bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }));
4859
4903
  } else {
4860
4904
  bubble = h("div", { class: "bq-bubble" });
4861
4905
  if (msg.role === "user" && msg.isPendingQueued) {
@@ -5003,7 +5047,7 @@ ${options.inlineContentPlaceholder}
5003
5047
  box.addEventListener("touchstart", onMessagesTouchStart, { passive: true });
5004
5048
  box.addEventListener("touchmove", onMessagesTouchMove, { passive: true });
5005
5049
  CS.messagesBox = box;
5006
- var input = h("textarea", { class: "bq-input", rows: "1", placeholder: "Hi! Ask me anything about " + (S.serviceName ? '"' + S.serviceName + '"' : "your project") + "." });
5050
+ var input = h("textarea", { class: "bq-input", rows: "1", placeholder: "Ask anything about: " + (S.serviceName || "your project") });
5007
5051
  CS.inputEl = input;
5008
5052
  var composing = false;
5009
5053
  input.addEventListener("compositionstart", function() {
@@ -5031,15 +5075,19 @@ ${options.inlineContentPlaceholder}
5031
5075
  requestAnimationFrame(function() {
5032
5076
  autoGrowInput(input);
5033
5077
  });
5034
- var attachFileInput = h("input", { class: "bq-attach-input", type: "file", multiple: "multiple" });
5035
- attachFileInput.addEventListener("change", function() {
5036
- onAttachInputChange(attachFileInput);
5037
- });
5038
- var attachBtn = h("button", { class: "bq-attach-btn", type: "button", title: "Attach files", html: ATTACH_ICON_SVG });
5039
- attachBtn.addEventListener("click", function() {
5040
- attachFileInput.click();
5041
- });
5042
- CS.attachBtnEl = attachBtn;
5078
+ var attachDisabled = uploadsFrozenForUser();
5079
+ var attachFileInput = null, attachBtn = null;
5080
+ if (!attachDisabled) {
5081
+ attachFileInput = h("input", { class: "bq-attach-input", type: "file", multiple: "multiple" });
5082
+ attachFileInput.addEventListener("change", function() {
5083
+ onAttachInputChange(attachFileInput);
5084
+ });
5085
+ attachBtn = h("button", { class: "bq-attach-btn", type: "button", title: "Attach files", html: ATTACH_ICON_SVG });
5086
+ attachBtn.addEventListener("click", function() {
5087
+ attachFileInput.click();
5088
+ });
5089
+ CS.attachBtnEl = attachBtn;
5090
+ }
5043
5091
  var attachmentsRow = h("div", { class: "bq-attachments" });
5044
5092
  attachmentsRow.style.display = "none";
5045
5093
  CS.attachmentsRow = attachmentsRow;
@@ -5058,7 +5106,7 @@ ${options.inlineContentPlaceholder}
5058
5106
  chatArea = h("div", { class: "bq-chat" }, box, composer);
5059
5107
  CS.chatEl = chatArea;
5060
5108
  CS.composerEl = composer;
5061
- setupDragAndDrop(chatArea);
5109
+ if (!attachDisabled) setupDragAndDrop(chatArea);
5062
5110
  return h("div", { class: "bq-meta" }, header, chatArea);
5063
5111
  });
5064
5112
  if (S.aiPlatform === "none") return;
package/dist/engine.cjs CHANGED
@@ -172,6 +172,7 @@ ${lines.join("\n")}`;
172
172
  }
173
173
  let composedForLlm = composed;
174
174
  let extractContent;
175
+ let fileUrls;
175
176
  if (attachmentUrls.length > 0) {
176
177
  const extractFiles = attachmentUrls.filter((u) => isServerExtractable(u.name));
177
178
  if (extractFiles.length > 0) {
@@ -192,8 +193,12 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
192
193
 
193
194
  ` + sections.join("\n\n");
194
195
  }
196
+ const urlFiles = attachmentUrls.filter((u) => u.url && !isServerExtractable(u.name));
197
+ if (urlFiles.length > 0) {
198
+ fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
199
+ }
195
200
  }
196
- return { composed, composedForLlm, extractContent };
201
+ return { composed, composedForLlm, extractContent, fileUrls };
197
202
  }
198
203
 
199
204
  // src/engine/attachments.ts
@@ -255,7 +260,7 @@ function buildIndexingSystemPrompt(params) {
255
260
  - Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly and do NOT call web_fetch for those files. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
256
261
  - For any file given to you as a temporary URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
257
262
  - Whatever the file type, use the file's storage path (the "storage path" metadata line) as the "src::" unique_id - never the inline content or a temporary URL.
258
- - TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet. If a sheet has many rows, make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. (You may ALSO save one file-level summary record, but the per-row records are mandatory.)
263
+ - TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet. If a sheet has many rows, make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. ALSO save one file-level summary record (file name, sheet name(s), column headers, total row count, overall summary) - this is the record that carries the file's "src::" unique_id - and link EVERY per-row record to it via reference (set each row record's reference to that src:: file record; the row records themselves do NOT carry a src:: unique_id). The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed.
259
264
  - EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in a dedicated table (e.g. "book_chapters") - never collapse the whole book into a single record. Each chapter record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO save one book-level record (title, author, language, overall summary, chapter list / table of contents, genre/subjects) and link each chapter record to it via reference. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
260
265
  - This is a ONE-SHOT background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions or invite back-and-forth. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). Never store raw or binary bytes (base64, blobs); describe them in human-readable text instead.
261
266
  - Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
@@ -438,13 +443,26 @@ function getExpiredAttachmentVisiblePath(remotePath, fallback) {
438
443
  function buildDisplayExpiredAttachmentHref(remotePath, fallback) {
439
444
  return EXPIRED_ATTACHMENT_URL_ORIGIN + "/" + encodePathSegments(getExpiredAttachmentVisiblePath(remotePath, fallback));
440
445
  }
441
- function sanitizeAttachmentLinksForHistory(content, serviceId) {
442
- if (!content || content.indexOf("Attached files:") === -1) return content;
446
+ function isServiceDbAttachmentHref(href, serviceId) {
447
+ if (!serviceId) return false;
448
+ try {
449
+ var parsed = new URL(href);
450
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
451
+ var segs = normalizeAttachmentPathCandidate(parsed.pathname || "").split("/").filter(Boolean);
452
+ return segs.length > 0 && segs[0] === serviceId;
453
+ } catch (e) {
454
+ return false;
455
+ }
456
+ }
457
+ function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
458
+ if (!content) return content;
459
+ if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
443
460
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
461
+ if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
444
462
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
445
463
  var labelPath = normalizeAttachmentPathCandidate(label);
446
464
  var fullPath = remotePath || labelPath;
447
- if (!fullPath) return "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
465
+ if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
448
466
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
449
467
  });
450
468
  }
@@ -496,7 +514,7 @@ function buildBoundedChatMessages(options) {
496
514
  var trimmed = windowed.map(function(m, i2) {
497
515
  if (i2 === latestIndex) return m;
498
516
  var stripped = stripFileBlocksFromHistory(m.content);
499
- var sanitized = m.role === "user" ? sanitizeAttachmentLinksForHistory(stripped, options.serviceId) : stripped;
517
+ var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.serviceId, m.role !== "user");
500
518
  return Object.assign({}, m, { content: sanitized });
501
519
  });
502
520
  var bounded = [], used = 0;
@@ -642,7 +660,8 @@ async function callClaudeWithMcp({
642
660
  maxTokens = 1e3,
643
661
  system,
644
662
  mcpServer,
645
- extractContent
663
+ extractContent,
664
+ fileUrls
646
665
  }) {
647
666
  const mcpServerDefinition = {
648
667
  type: "url",
@@ -670,6 +689,7 @@ async function callClaudeWithMcp({
670
689
  model,
671
690
  max_tokens: maxTokens,
672
691
  ...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
692
+ ...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
673
693
  ...system ? {
674
694
  system: [
675
695
  {
@@ -707,7 +727,7 @@ async function callClaudeWithMcp({
707
727
  }
708
728
  });
709
729
  }
710
- async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, onResponse, onError) {
730
+ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
711
731
  return callClaudeWithMcp({
712
732
  prompt,
713
733
  messages,
@@ -718,13 +738,14 @@ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system,
718
738
  maxTokens: MAX_TOKENS,
719
739
  system,
720
740
  extractContent,
741
+ fileUrls,
721
742
  mcpServer: {
722
743
  name: MCP_NAME,
723
744
  url: mcpUrl(),
724
745
  authorizationToken: "$ACCESS_TOKEN"
725
746
  }});
726
747
  }
727
- async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, onResponse, onError) {
748
+ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system, model, userId, extractContent, fileUrls, onResponse, onError) {
728
749
  const resolvedModel = model || DEFAULT_OPENAI_MODEL;
729
750
  const imageDetail = getOpenAIImageDetail(resolvedModel);
730
751
  const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
@@ -761,6 +782,7 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
761
782
  model: resolvedModel,
762
783
  max_output_tokens: MAX_TOKENS,
763
784
  ...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
785
+ ...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
764
786
  input: responseInput,
765
787
  tools: [
766
788
  {
@@ -1060,7 +1082,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1060
1082
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1061
1083
  mapped.push(em);
1062
1084
  } else if (assistantText) {
1063
- var okm = { role: "assistant", content: assistantText };
1085
+ var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1064
1086
  if (item._isBgTask) okm.isBackgroundTask = true;
1065
1087
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1066
1088
  mapped.push(okm);
@@ -1126,16 +1148,16 @@ var ChatSession = class {
1126
1148
  startKeyHistory: this.state.historyStartKeyHistory.slice()
1127
1149
  };
1128
1150
  }
1129
- _callProviderFor(platform, prompt, messages, system, model, userId, extractContent) {
1151
+ _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls) {
1130
1152
  var id = this.host.getIdentity();
1131
- return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent);
1153
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls);
1132
1154
  }
1133
1155
  dispatchAgentRequest(params) {
1134
1156
  var self = this;
1135
1157
  var dispatchItemId;
1136
1158
  var sendAndPoll = function() {
1137
1159
  return Promise.resolve(
1138
- self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent)
1160
+ self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
1139
1161
  ).then(function(initial) {
1140
1162
  if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
1141
1163
  if (initial.id) {
@@ -1196,7 +1218,7 @@ var ChatSession = class {
1196
1218
  // composed = clean display text; composedForLlm carries office-extraction
1197
1219
  // placeholders for the provider only. useBgQueue routes a post-attachment turn
1198
1220
  // onto the "-bg" queue so it runs after indexing.
1199
- dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent) {
1221
+ dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
1200
1222
  var self = this;
1201
1223
  if (!composed) return;
1202
1224
  var id = this.host.getIdentity();
@@ -1228,7 +1250,7 @@ var ChatSession = class {
1228
1250
  this.updateHistoryCache();
1229
1251
  this.host.scrollToBottom(true);
1230
1252
  var capturedComposed = composed, capturedPlatform = aiPlatform;
1231
- Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent)).then(function(result) {
1253
+ Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
1232
1254
  var sendingIdx = self.state.messages.findIndex(function(m) {
1233
1255
  return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1234
1256
  });
@@ -1288,7 +1310,8 @@ var ChatSession = class {
1288
1310
  text: composed,
1289
1311
  boundedMessages: bounded.messages,
1290
1312
  userId: chatQueue,
1291
- extractContent
1313
+ extractContent,
1314
+ fileUrls
1292
1315
  });
1293
1316
  Promise.resolve(run).catch(function() {
1294
1317
  }).then(function() {
@@ -2050,6 +2073,7 @@ var ChatSession = class {
2050
2073
  chain = chain.then(function() {
2051
2074
  var hadExists = false;
2052
2075
  var skipped = false;
2076
+ var existedBefore = false;
2053
2077
  var onProg = function(p) {
2054
2078
  if (p && p.total) {
2055
2079
  att.progress = Math.floor((idx + p.loaded / p.total) / total * 100);
@@ -2073,12 +2097,16 @@ var ChatSession = class {
2073
2097
  var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
2074
2098
  if (!isExists) throw err;
2075
2099
  return self.host.promptOverwrite(member.file.name).then(function(choice) {
2076
- if (choice === "overwrite") return doMemberUpload(false);
2100
+ if (choice === "overwrite") {
2101
+ existedBefore = true;
2102
+ return doMemberUpload(false);
2103
+ }
2077
2104
  if (choice === "skip") {
2078
2105
  skipped = true;
2079
2106
  return;
2080
2107
  }
2081
2108
  hadExists = true;
2109
+ existedBefore = true;
2082
2110
  });
2083
2111
  }).then(function() {
2084
2112
  if (skipped) return;
@@ -2091,9 +2119,11 @@ var ChatSession = class {
2091
2119
  att.storagePath = member.storagePath;
2092
2120
  }
2093
2121
  var mime = member.file.type || self.host.getMimeType(member.file.name);
2094
- return Promise.resolve(
2095
- parseAttachmentContent(member.file, member.file.name, mime || void 0)
2096
- ).then(function(parsedContent) {
2122
+ var preIndex = existedBefore && typeof self.host.deleteExistingFileRecord === "function" ? Promise.resolve(self.host.deleteExistingFileRecord(member.storagePath)).catch(function() {
2123
+ }) : Promise.resolve();
2124
+ return preIndex.then(function() {
2125
+ return parseAttachmentContent(member.file, member.file.name, mime || void 0);
2126
+ }).then(function(parsedContent) {
2097
2127
  return notifyAgentSaveAttachment({
2098
2128
  platform: id.platform,
2099
2129
  model: id.model,
@@ -2264,6 +2294,7 @@ exports.isErrorResponseBody = isErrorResponseBody;
2264
2294
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
2265
2295
  exports.isOfficeFile = isOfficeFile;
2266
2296
  exports.isServerExtractable = isServerExtractable;
2297
+ exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
2267
2298
  exports.listClaudeModels = listClaudeModels;
2268
2299
  exports.listOpenAIModels = listOpenAIModels;
2269
2300
  exports.makeExtractPlaceholder = makeExtractPlaceholder;