bunnyquery 1.3.4 → 1.4.0

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/dist/engine.cjs CHANGED
@@ -1,9 +1,57 @@
1
1
  'use strict';
2
2
 
3
+ // src/engine/attachment_parsers.ts
4
+ var MAX_PARSED_CONTENT_CHARS = 2e5;
5
+ var _parsers = [];
6
+ function registerAttachmentParser(parser) {
7
+ if (parser && typeof parser.match === "function" && typeof parser.parse === "function" && _parsers.indexOf(parser) === -1) {
8
+ _parsers.push(parser);
9
+ }
10
+ }
11
+ function clearAttachmentParsers() {
12
+ _parsers.length = 0;
13
+ }
14
+ function getAttachmentParsers() {
15
+ return _parsers.slice();
16
+ }
17
+ function findAttachmentParser(name, mime) {
18
+ for (let i = 0; i < _parsers.length; i++) {
19
+ try {
20
+ if (_parsers[i].match({ name, mime })) return _parsers[i];
21
+ } catch {
22
+ }
23
+ }
24
+ return void 0;
25
+ }
26
+ async function parseAttachmentContent(file, name, mime) {
27
+ const parser = findAttachmentParser(name, mime);
28
+ if (!parser) return null;
29
+ let raw;
30
+ try {
31
+ raw = await parser.parse(file);
32
+ } catch (err) {
33
+ console.error(
34
+ `[chat-engine] attachment parser ${parser.name || "(unnamed)"} failed for ${name}:`,
35
+ err
36
+ );
37
+ return null;
38
+ }
39
+ let text = (raw == null ? "" : String(raw)).trim();
40
+ if (!text) return null;
41
+ if (text.length > MAX_PARSED_CONTENT_CHARS) {
42
+ text = text.slice(0, MAX_PARSED_CONTENT_CHARS) + `
43
+ ...[truncated for length; original ${text.length} characters]`;
44
+ }
45
+ return text;
46
+ }
47
+
3
48
  // src/engine/config.ts
4
49
  var _config = null;
5
50
  function configureChatEngine(config) {
6
51
  _config = config;
52
+ if (config.attachmentParsers) {
53
+ for (const parser of config.attachmentParsers) registerAttachmentParser(parser);
54
+ }
7
55
  }
8
56
  function chatEngineConfig() {
9
57
  if (!_config) {
@@ -30,13 +78,41 @@ var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
30
78
  "pptx",
31
79
  "pptm",
32
80
  "hwp",
33
- "hwpx"
81
+ "hwpx",
82
+ "ods",
83
+ "odt",
84
+ "odp",
85
+ "epub"
86
+ ]);
87
+ var WEB_FETCHABLE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
88
+ "csv",
89
+ "tsv",
90
+ "tab",
91
+ "txt",
92
+ "text",
93
+ "md",
94
+ "markdown",
95
+ "json",
96
+ "ndjson",
97
+ "jsonl",
98
+ "xml",
99
+ "yaml",
100
+ "yml",
101
+ "log",
102
+ // RTF is a TEXT format (not a binary zip), so web_fetch can read it and the
103
+ // model decodes its control words. Pin it here so a `.rtf` reported as
104
+ // `application/msword` isn't misrouted to server-side extraction (which has no
105
+ // .rtf extractor → "unsupported format" note).
106
+ "rtf",
107
+ "htm",
108
+ "html"
34
109
  ]);
35
110
  function isOfficeFile(name, mime) {
36
111
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
37
112
  if (OFFICE_FILE_EXTENSIONS.has(ext)) return true;
113
+ if (WEB_FETCHABLE_TEXT_EXTENSIONS.has(ext)) return false;
38
114
  const m = (mime || "").toLowerCase();
39
- return m.includes("officedocument") || m.includes("hwp") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
115
+ return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
40
116
  }
41
117
  var _extractPlaceholderSeq = 0;
42
118
  function makeExtractPlaceholder(seed) {
@@ -157,6 +233,14 @@ File metadata:
157
233
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
158
234
  ` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
159
235
  ` : "");
236
+ if (options?.inlineContent) {
237
+ return head + `
238
+ The file's content was parsed by the client and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
239
+
240
+ ----- BEGIN FILE CONTENT -----
241
+ ${options.inlineContent}
242
+ ----- END FILE CONTENT -----`;
243
+ }
160
244
  if (options?.inlineContentPlaceholder) {
161
245
  return head + `
162
246
  The file's text content was extracted on the server and is provided inline below. Read it directly \u2014 do NOT fetch any URL for this file. Use the storage path above (not this content) for the "src::" unique_id.
@@ -655,14 +739,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
655
739
  });
656
740
  }
657
741
  async function notifyAgentSaveAttachment(info) {
658
- const { platform, service, owner, attachment } = info;
659
- const office = isOfficeFile(attachment.name, attachment.mime);
742
+ const { platform, service, owner, attachment, parsedContent } = info;
743
+ const office = !parsedContent && isOfficeFile(attachment.name, attachment.mime);
660
744
  const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : void 0;
661
745
  const extractContent = office && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
662
746
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
663
747
  const userMessage = buildIndexingUserMessage(
664
748
  attachment,
665
- placeholder ? { inlineContentPlaceholder: placeholder } : void 0
749
+ parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : void 0
666
750
  );
667
751
  const systemPrompt = buildIndexingSystemPrompt({
668
752
  service,
@@ -1248,6 +1332,7 @@ var ChatSession = class {
1248
1332
  this.enqueueTypewrite(aiIdx, answer, lid);
1249
1333
  }
1250
1334
  }
1335
+ this._removeStrayPendingAssistants();
1251
1336
  this.promoteNextQueuedToRunning();
1252
1337
  this.updateHistoryCache();
1253
1338
  this.host.notify();
@@ -1290,6 +1375,7 @@ var ChatSession = class {
1290
1375
  if (thPos2 !== -1) this.state.messages.splice(thPos2, 1);
1291
1376
  }
1292
1377
  if (serverId) this.cancelledServerIds.delete(serverId);
1378
+ this._removeStrayPendingAssistants();
1293
1379
  this.promoteNextQueuedToRunning();
1294
1380
  this.updateHistoryCache();
1295
1381
  this.host.notify();
@@ -1303,6 +1389,7 @@ var ChatSession = class {
1303
1389
  return;
1304
1390
  }
1305
1391
  this.insertAtTarget({ role: "assistant", content: getErrorMessage(err), isError: true }, targetIdx);
1392
+ this._removeStrayPendingAssistants();
1306
1393
  this.promoteNextQueuedToRunning();
1307
1394
  this.updateHistoryCache();
1308
1395
  this.host.notify();
@@ -1464,16 +1551,48 @@ var ChatSession = class {
1464
1551
  if (pendingIdx === -1) return Promise.resolve();
1465
1552
  if (latest.isError || !latest.content) {
1466
1553
  this.state.messages[pendingIdx] = { role: "assistant", content: latest.content || "", isError: !!latest.isError };
1554
+ this._removeStrayPendingAssistants();
1467
1555
  this.host.notify();
1468
1556
  this.promoteNextQueuedToRunning();
1469
1557
  return Promise.resolve();
1470
1558
  }
1471
1559
  var lid = this._newLocalId();
1472
1560
  this.state.messages[pendingIdx] = { role: "assistant", content: "", isPending: false, _localId: lid };
1561
+ this._removeStrayPendingAssistants();
1473
1562
  this.host.notify();
1474
1563
  this.promoteNextQueuedToRunning();
1475
1564
  return this.enqueueTypewrite(pendingIdx, latest.content, lid);
1476
1565
  }
1566
+ // Remove any leftover non-background pending ("Thinking…") assistant bubbles.
1567
+ // There is normally at most ONE such bubble at a time (promoteNext* refuses to
1568
+ // add a second), so any extra is a duplicate — it appears when a concurrent
1569
+ // history refetch re-maps the still-"running" turn into a pending placeholder
1570
+ // (with a real _serverItemId) while the local pending bubble (no _serverItemId)
1571
+ // is rescued and re-appended (see loadHistory rescue below). Each resolve path
1572
+ // only replaces the FIRST pending bubble, so without this a stray "Thinking…"
1573
+ // survives next to the reply/error. MUST run AFTER the resolved bubble has been
1574
+ // made non-pending and BEFORE promoteNext*() (so a freshly-promoted Thinking,
1575
+ // which is added only once no pending assistant remains, is preserved).
1576
+ _removeStrayPendingAssistants() {
1577
+ for (var k = this.state.messages.length - 1; k >= 0; k--) {
1578
+ var m = this.state.messages[k];
1579
+ if (m.isPending && m.role === "assistant" && !m.isBackgroundTask) this.state.messages.splice(k, 1);
1580
+ }
1581
+ }
1582
+ // Drop the pending flags on the resolved turn's USER bubble (preserving its
1583
+ // content + background-task marker). Needed because a bg "Indexing:" turn's user
1584
+ // bubble carries isPendingInProcess; leaving it set keeps the bubble visually
1585
+ // stuck and keeps its bgTaskQueue entry alive forever.
1586
+ _clearPendingUserBubble(itemId) {
1587
+ var uIdx = this.state.messages.findIndex(function(m) {
1588
+ return m.role === "user" && m._serverItemId === itemId && (m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
1589
+ });
1590
+ if (uIdx === -1) return;
1591
+ var u = this.state.messages[uIdx];
1592
+ var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
1593
+ if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
1594
+ this.state.messages[uIdx] = cleaned;
1595
+ }
1477
1596
  // If an immediate-send request for the current cache key is still in flight
1478
1597
  // (e.g. the view unmounted then remounted mid-request), show the sending
1479
1598
  // state, await it, then render the reply from the cache. Skipped when the
@@ -1512,6 +1631,7 @@ var ChatSession = class {
1512
1631
  return m.isPending && m._serverItemId === itemId;
1513
1632
  });
1514
1633
  if (idx !== -1) {
1634
+ this._clearPendingUserBubble(itemId);
1515
1635
  if (isErr) {
1516
1636
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
1517
1637
  this.host.notify();
@@ -1661,12 +1781,16 @@ var ChatSession = class {
1661
1781
  self.state.messages.forEach(function(m) {
1662
1782
  if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
1663
1783
  });
1784
+ var mappedHasPendingAssistant = mapped.some(function(m) {
1785
+ return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
1786
+ });
1664
1787
  var rescued = [];
1665
1788
  for (var ri = 0; ri < self.state.messages.length; ri++) {
1666
1789
  var mm = self.state.messages[ri];
1667
1790
  if (mm.isBackgroundTask) continue;
1668
1791
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
1669
1792
  if (!mm._serverItemId) {
1793
+ if (mappedHasPendingAssistant) continue;
1670
1794
  if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
1671
1795
  else if (self.state.sending && mm.role === "user") {
1672
1796
  var next = self.state.messages[ri + 1];
@@ -1824,44 +1948,49 @@ var ChatSession = class {
1824
1948
  att.storagePath = member.storagePath;
1825
1949
  }
1826
1950
  var mime = member.file.type || self.host.getMimeType(member.file.name);
1827
- return notifyAgentSaveAttachment({
1828
- platform: id.platform,
1829
- model: id.model,
1830
- service: id.serviceId,
1831
- owner: id.owner,
1832
- userId: id.userId || id.serviceId,
1833
- serviceName: id.serviceName,
1834
- serviceDescription: id.serviceDescription,
1835
- attachment: {
1836
- name: member.file.name,
1837
- storagePath: member.storagePath,
1838
- mime: mime || void 0,
1839
- size: member.file.size,
1840
- url
1841
- }
1842
- }).then(function(ack) {
1843
- if (ack && typeof ack.id === "string") {
1844
- self.bgTaskQueue.push({
1845
- serviceId: id.serviceId,
1846
- platform: id.platform,
1847
- id: ack.id,
1848
- filename: member.file.name,
1951
+ return Promise.resolve(
1952
+ parseAttachmentContent(member.file, member.file.name, mime || void 0)
1953
+ ).then(function(parsedContent) {
1954
+ return notifyAgentSaveAttachment({
1955
+ platform: id.platform,
1956
+ model: id.model,
1957
+ service: id.serviceId,
1958
+ owner: id.owner,
1959
+ userId: id.userId || id.serviceId,
1960
+ serviceName: id.serviceName,
1961
+ serviceDescription: id.serviceDescription,
1962
+ attachment: {
1963
+ name: member.file.name,
1849
1964
  storagePath: member.storagePath,
1850
- isReindex: hadExists,
1851
1965
  mime: mime || void 0,
1852
1966
  size: member.file.size,
1853
- status: ack.status === "running" ? "running" : "pending",
1854
- poll: ack.poll
1855
- });
1856
- self.drainBgTaskQueue();
1857
- }
1858
- }, function(e) {
1859
- console.error("[chat-engine] indexing request failed", e);
1860
- anyIndexFailed = true;
1861
- if (!att.errorCode && !att.errorDetail) {
1862
- att.errorCode = e && (e.code || e.body && e.body.code) || "";
1863
- att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
1864
- }
1967
+ url
1968
+ },
1969
+ parsedContent: parsedContent || void 0
1970
+ }).then(function(ack) {
1971
+ if (ack && typeof ack.id === "string") {
1972
+ self.bgTaskQueue.push({
1973
+ serviceId: id.serviceId,
1974
+ platform: id.platform,
1975
+ id: ack.id,
1976
+ filename: member.file.name,
1977
+ storagePath: member.storagePath,
1978
+ isReindex: hadExists,
1979
+ mime: mime || void 0,
1980
+ size: member.file.size,
1981
+ status: ack.status === "running" ? "running" : "pending",
1982
+ poll: ack.poll
1983
+ });
1984
+ self.drainBgTaskQueue();
1985
+ }
1986
+ }, function(e) {
1987
+ console.error("[chat-engine] indexing request failed", e);
1988
+ anyIndexFailed = true;
1989
+ if (!att.errorCode && !att.errorDetail) {
1990
+ att.errorCode = e && (e.code || e.body && e.body.code) || "";
1991
+ att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
1992
+ }
1993
+ });
1865
1994
  });
1866
1995
  });
1867
1996
  });
@@ -1953,6 +2082,7 @@ exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
1953
2082
  exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
1954
2083
  exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
1955
2084
  exports.MAX_HISTORY_MESSAGES = MAX_HISTORY_MESSAGES;
2085
+ exports.MAX_PARSED_CONTENT_CHARS = MAX_PARSED_CONTENT_CHARS;
1956
2086
  exports.MCP_NAME = MCP_NAME;
1957
2087
  exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
1958
2088
  exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
@@ -1967,6 +2097,7 @@ exports.callClaudeWithMcp = callClaudeWithMcp;
1967
2097
  exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
1968
2098
  exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
1969
2099
  exports.chatEngineConfig = chatEngineConfig;
2100
+ exports.clearAttachmentParsers = clearAttachmentParsers;
1970
2101
  exports.composeUserMessage = composeUserMessage;
1971
2102
  exports.configureChatEngine = configureChatEngine;
1972
2103
  exports.createInlineLinkRegex = createInlineLinkRegex;
@@ -1978,6 +2109,8 @@ exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
1978
2109
  exports.extractOpenAIText = extractOpenAIText;
1979
2110
  exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
1980
2111
  exports.filterListByClearHorizon = filterListByClearHorizon;
2112
+ exports.findAttachmentParser = findAttachmentParser;
2113
+ exports.getAttachmentParsers = getAttachmentParsers;
1981
2114
  exports.getChatHistory = getChatHistory;
1982
2115
  exports.getContextWindow = getContextWindow;
1983
2116
  exports.getErrorMessage = getErrorMessage;
@@ -1994,6 +2127,8 @@ exports.mapHistoryListToMessages = mapHistoryListToMessages;
1994
2127
  exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
1995
2128
  exports.normalizeTextContent = normalizeTextContent;
1996
2129
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
2130
+ exports.parseAttachmentContent = parseAttachmentContent;
2131
+ exports.registerAttachmentParser = registerAttachmentParser;
1997
2132
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
1998
2133
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
1999
2134
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;