bunnyquery 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }
@@ -473,7 +491,7 @@ ${options.inlineContentPlaceholder}
473
491
  }
474
492
  function stripFileBlocksFromHistory(content) {
475
493
  if (!content) return content;
476
- return content.replace(/```([\w.-]+\.[a-zA-Z0-9]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
494
+ return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
477
495
  }
478
496
  function buildBoundedChatMessages(options) {
479
497
  var contextWindow = getContextWindow(options.platform, options.model);
@@ -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() {
@@ -1530,7 +1553,7 @@ ${options.inlineContentPlaceholder}
1530
1553
  var MIN_STEP = 1;
1531
1554
  var MAX_FRAME_MS = 1e3;
1532
1555
  var regions = [], m;
1533
- var fenceRegex = /```[\w.-]+\.[a-zA-Z0-9]+\n[\s\S]*?```/g;
1556
+ var fenceRegex = /```[^\n`]+?\.[^\s.`]+\n[\s\S]*?```/g;
1534
1557
  while ((m = fenceRegex.exec(fullText)) !== null) regions.push({ start: m.index, end: m.index + m[0].length });
1535
1558
  var linkRegex = createInlineLinkRegex();
1536
1559
  while ((m = linkRegex.exec(fullText)) !== null) regions.push({ start: m.index, end: m.index + m[0].length });
@@ -2015,6 +2038,8 @@ ${options.inlineContentPlaceholder}
2015
2038
  members.forEach(function(member, idx) {
2016
2039
  chain = chain.then(function() {
2017
2040
  var hadExists = false;
2041
+ var skipped = false;
2042
+ var existedBefore = false;
2018
2043
  var onProg = function(p) {
2019
2044
  if (p && p.total) {
2020
2045
  att.progress = Math.floor((idx + p.loaded / p.total) / total * 100);
@@ -2038,21 +2063,33 @@ ${options.inlineContentPlaceholder}
2038
2063
  var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
2039
2064
  if (!isExists) throw err;
2040
2065
  return self.host.promptOverwrite(member.file.name).then(function(choice) {
2041
- if (choice === "overwrite") return doMemberUpload(false);
2066
+ if (choice === "overwrite") {
2067
+ existedBefore = true;
2068
+ return doMemberUpload(false);
2069
+ }
2070
+ if (choice === "skip") {
2071
+ skipped = true;
2072
+ return;
2073
+ }
2042
2074
  hadExists = true;
2075
+ existedBefore = true;
2043
2076
  });
2044
2077
  }).then(function() {
2078
+ if (skipped) return;
2045
2079
  return self.host.getTemporaryUrl(member.storagePath);
2046
2080
  }).then(function(url) {
2081
+ if (skipped) return;
2047
2082
  urls.push({ name: member.relPath, url, storagePath: member.storagePath });
2048
2083
  if (att.kind !== "folder") {
2049
2084
  att.uploadedUrl = url;
2050
2085
  att.storagePath = member.storagePath;
2051
2086
  }
2052
2087
  var mime = member.file.type || self.host.getMimeType(member.file.name);
2053
- return Promise.resolve(
2054
- parseAttachmentContent(member.file, member.file.name, mime || void 0)
2055
- ).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) {
2056
2093
  return notifyAgentSaveAttachment({
2057
2094
  platform: id.platform,
2058
2095
  model: id.model,
@@ -2176,7 +2213,7 @@ ${options.inlineContentPlaceholder}
2176
2213
  (function() {
2177
2214
  var MCP_PROD = "https://mcp.broadwayinc.computer";
2178
2215
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
2179
- var BQ_VERSION = "1.5.0" ;
2216
+ var BQ_VERSION = "1.5.1" ;
2180
2217
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
2181
2218
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
2182
2219
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -3801,6 +3838,9 @@ ${options.inlineContentPlaceholder}
3801
3838
  getTemporaryUrl: function(path) {
3802
3839
  return getTemporaryUrlDb(path, ATTACHMENT_URL_EXPIRES_SECONDS);
3803
3840
  },
3841
+ deleteExistingFileRecord: function(path) {
3842
+ return deleteFileIndexRecordDb(path);
3843
+ },
3804
3844
  storagePathFor: function(relPath) {
3805
3845
  return attachmentStoragePath(relPath);
3806
3846
  },
@@ -3950,7 +3990,7 @@ ${options.inlineContentPlaceholder}
3950
3990
  clearSuccessfulAttachments();
3951
3991
  if (text) {
3952
3992
  var c = composeUserMessage(text, attachmentUrls);
3953
- session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent);
3993
+ session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent, c.fileUrls);
3954
3994
  }
3955
3995
  if (failureGroups.length) showUploadErrorReport(failureGroups);
3956
3996
  }).catch(function(err) {
@@ -4018,13 +4058,18 @@ ${options.inlineContentPlaceholder}
4018
4058
  var existing = fileBlobCache.get(key);
4019
4059
  if (existing) return existing;
4020
4060
  var contentType = mimeGetType(filename) || "text/plain";
4021
- var href = URL.createObjectURL(new Blob([body], { type: contentType }));
4061
+ var ext = (String(filename || "").split(".").pop() || "").toLowerCase();
4062
+ var isText = /^text\//i.test(contentType) || /application\/(json|xml|csv|yaml|x-yaml|javascript)/i.test(contentType);
4063
+ var needsBom = ext === "csv" || ext === "tsv" || ext === "tab";
4064
+ var type = isText ? contentType + "; charset=utf-8" : contentType;
4065
+ var data = needsBom ? "\uFEFF" + body : body;
4066
+ var href = URL.createObjectURL(new Blob([data], { type }));
4022
4067
  fileBlobCache.set(key, href);
4023
4068
  return href;
4024
4069
  }
4025
4070
  function fileToAnchorHtml(filename, href) {
4026
4071
  var text = "\u2197 " + filename;
4027
- return '<a class="bq-file-download" href="' + escapeHtml(href) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + "</a>";
4072
+ return '<a class="bq-file-download" href="' + escapeHtml(href) + '" download="' + escapeHtml(filename) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + "</a>";
4028
4073
  }
4029
4074
  function linkToAnchorHtml(link) {
4030
4075
  var refreshing = !!refreshingLinkMap[link.expiredHref || link.href];
@@ -4095,13 +4140,13 @@ ${options.inlineContentPlaceholder}
4095
4140
  return PH(idx);
4096
4141
  };
4097
4142
  var working = String(content == null ? "" : content).replace(
4098
- /```([\w.-]+\.[a-zA-Z0-9]+)\n([\s\S]*?)```/g,
4143
+ /```([^\n`]+?\.[^\s.`]+)\n([\s\S]*?)```/g,
4099
4144
  function(_full, filename, body) {
4100
4145
  return pushPlaceholder(fileToAnchorHtml(filename, getOrCreateFileHref(filename, body)));
4101
4146
  }
4102
4147
  );
4103
4148
  if (CS.typing) {
4104
- var openFence = working.match(/```([\w.-]+\.[a-zA-Z0-9]+)\n?/);
4149
+ var openFence = working.match(/```([^\n`]+?\.[^\s.`]+)\n?/);
4105
4150
  if (openFence && typeof openFence.index === "number") {
4106
4151
  working = working.slice(0, openFence.index) + "\n[generating " + openFence[1] + "\u2026]";
4107
4152
  }
@@ -4216,6 +4261,11 @@ ${options.inlineContentPlaceholder}
4216
4261
  return xhrUploadForm(signed.url, form, onProgress, setAbort);
4217
4262
  });
4218
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
+ }
4219
4269
  function getTemporaryUrlDb(path, expires) {
4220
4270
  return S.skapi.util.request("get-signed-url", {
4221
4271
  service: S.serviceId,
@@ -4843,7 +4893,7 @@ ${options.inlineContentPlaceholder}
4843
4893
  if (msg.isSendingToServer || msg._cancelling) cls.push("is-sending-to-server");
4844
4894
  var bubble;
4845
4895
  if (msg.isPending) {
4846
- bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader", text: "Thinking" }));
4896
+ bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }));
4847
4897
  } else {
4848
4898
  bubble = h("div", { class: "bq-bubble" });
4849
4899
  if (msg.role === "user" && msg.isPendingQueued) {
@@ -4991,7 +5041,7 @@ ${options.inlineContentPlaceholder}
4991
5041
  box.addEventListener("touchstart", onMessagesTouchStart, { passive: true });
4992
5042
  box.addEventListener("touchmove", onMessagesTouchMove, { passive: true });
4993
5043
  CS.messagesBox = box;
4994
- var input = h("textarea", { class: "bq-input", rows: "1", placeholder: "Hi! Ask me anything about " + (S.serviceName ? '"' + S.serviceName + '"' : "your project") + "." });
5044
+ var input = h("textarea", { class: "bq-input", rows: "1", placeholder: "Ask anything about: " + (S.serviceName || "your project") });
4995
5045
  CS.inputEl = input;
4996
5046
  var composing = false;
4997
5047
  input.addEventListener("compositionstart", function() {
@@ -5107,12 +5157,15 @@ ${options.inlineContentPlaceholder}
5107
5157
  h(
5108
5158
  "p",
5109
5159
  { class: "bq-modal-desc" },
5110
- "A file named \u201C" + filename + "\u201D already exists. Keep the existing file and just reindex it, or overwrite it completely?"
5160
+ "A file named \u201C" + filename + "\u201D already exists. Skip it, keep the existing file and just reindex it, or overwrite it completely?"
5111
5161
  ),
5112
5162
  applyLabel,
5113
5163
  h(
5114
5164
  "div",
5115
5165
  { class: "bq-modal-btns" },
5166
+ h("button", { class: "btn btn--outline", type: "button", onclick: function() {
5167
+ chooseOverwrite("skip");
5168
+ } }, "Skip"),
5116
5169
  h("button", { class: "btn btn--outline", type: "button", onclick: function() {
5117
5170
  chooseOverwrite("reindex");
5118
5171
  } }, "Reindex only"),
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
  }
@@ -480,7 +498,7 @@ function getContextWindow(platform, model) {
480
498
  }
481
499
  function stripFileBlocksFromHistory(content) {
482
500
  if (!content) return content;
483
- return content.replace(/```([\w.-]+\.[a-zA-Z0-9]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
501
+ return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
484
502
  }
485
503
  function buildBoundedChatMessages(options) {
486
504
  var contextWindow = getContextWindow(options.platform, options.model);
@@ -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() {
@@ -1564,7 +1587,7 @@ var ChatSession = class {
1564
1587
  var MIN_STEP = 1;
1565
1588
  var MAX_FRAME_MS = 1e3;
1566
1589
  var regions = [], m;
1567
- var fenceRegex = /```[\w.-]+\.[a-zA-Z0-9]+\n[\s\S]*?```/g;
1590
+ var fenceRegex = /```[^\n`]+?\.[^\s.`]+\n[\s\S]*?```/g;
1568
1591
  while ((m = fenceRegex.exec(fullText)) !== null) regions.push({ start: m.index, end: m.index + m[0].length });
1569
1592
  var linkRegex = createInlineLinkRegex();
1570
1593
  while ((m = linkRegex.exec(fullText)) !== null) regions.push({ start: m.index, end: m.index + m[0].length });
@@ -2049,6 +2072,8 @@ var ChatSession = class {
2049
2072
  members.forEach(function(member, idx) {
2050
2073
  chain = chain.then(function() {
2051
2074
  var hadExists = false;
2075
+ var skipped = false;
2076
+ var existedBefore = false;
2052
2077
  var onProg = function(p) {
2053
2078
  if (p && p.total) {
2054
2079
  att.progress = Math.floor((idx + p.loaded / p.total) / total * 100);
@@ -2072,21 +2097,33 @@ var ChatSession = class {
2072
2097
  var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
2073
2098
  if (!isExists) throw err;
2074
2099
  return self.host.promptOverwrite(member.file.name).then(function(choice) {
2075
- if (choice === "overwrite") return doMemberUpload(false);
2100
+ if (choice === "overwrite") {
2101
+ existedBefore = true;
2102
+ return doMemberUpload(false);
2103
+ }
2104
+ if (choice === "skip") {
2105
+ skipped = true;
2106
+ return;
2107
+ }
2076
2108
  hadExists = true;
2109
+ existedBefore = true;
2077
2110
  });
2078
2111
  }).then(function() {
2112
+ if (skipped) return;
2079
2113
  return self.host.getTemporaryUrl(member.storagePath);
2080
2114
  }).then(function(url) {
2115
+ if (skipped) return;
2081
2116
  urls.push({ name: member.relPath, url, storagePath: member.storagePath });
2082
2117
  if (att.kind !== "folder") {
2083
2118
  att.uploadedUrl = url;
2084
2119
  att.storagePath = member.storagePath;
2085
2120
  }
2086
2121
  var mime = member.file.type || self.host.getMimeType(member.file.name);
2087
- return Promise.resolve(
2088
- parseAttachmentContent(member.file, member.file.name, mime || void 0)
2089
- ).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) {
2090
2127
  return notifyAgentSaveAttachment({
2091
2128
  platform: id.platform,
2092
2129
  model: id.model,
@@ -2257,6 +2294,7 @@ exports.isErrorResponseBody = isErrorResponseBody;
2257
2294
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
2258
2295
  exports.isOfficeFile = isOfficeFile;
2259
2296
  exports.isServerExtractable = isServerExtractable;
2297
+ exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
2260
2298
  exports.listClaudeModels = listClaudeModels;
2261
2299
  exports.listOpenAIModels = listOpenAIModels;
2262
2300
  exports.makeExtractPlaceholder = makeExtractPlaceholder;