bunnyquery 1.3.5 → 1.4.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/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,14 +78,83 @@ 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"
34
86
  ]);
35
- function isOfficeFile(name, mime) {
87
+ var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
88
+ "csv",
89
+ "tsv",
90
+ "tab",
91
+ "txt",
92
+ "text",
93
+ "log",
94
+ "md",
95
+ "markdown",
96
+ "rst",
97
+ "json",
98
+ "ndjson",
99
+ "jsonl",
100
+ "geojson",
101
+ "xml",
102
+ "yaml",
103
+ "yml",
104
+ "toml",
105
+ "ini",
106
+ "conf",
107
+ "cfg",
108
+ "properties",
109
+ "env",
110
+ "rtf",
111
+ "html",
112
+ "htm",
113
+ "js",
114
+ "mjs",
115
+ "cjs",
116
+ "ts",
117
+ "tsx",
118
+ "jsx",
119
+ "py",
120
+ "rb",
121
+ "go",
122
+ "rs",
123
+ "java",
124
+ "kt",
125
+ "c",
126
+ "h",
127
+ "cpp",
128
+ "cc",
129
+ "hpp",
130
+ "cs",
131
+ "php",
132
+ "swift",
133
+ "sh",
134
+ "bash",
135
+ "zsh",
136
+ "sql",
137
+ "css",
138
+ "scss",
139
+ "less",
140
+ "vue",
141
+ "svelte",
142
+ "tex",
143
+ "srt",
144
+ "vtt"
145
+ ]);
146
+ function isTextMime(m) {
147
+ return m.startsWith("text/") || m.endsWith("+json") || m.endsWith("+xml") || m.endsWith("+yaml") || m === "application/json" || m === "application/ld+json" || m === "application/xml" || m === "application/yaml" || m === "application/x-yaml" || m === "application/javascript" || m === "application/x-javascript" || m === "application/x-sh" || m === "application/x-ndjson" || m === "application/csv" || m === "application/rtf" || m === "application/sql" || m === "application/toml";
148
+ }
149
+ function isServerExtractable(name, mime) {
36
150
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
37
151
  if (OFFICE_FILE_EXTENSIONS.has(ext)) return true;
152
+ if (TEXT_FILE_EXTENSIONS.has(ext)) return true;
38
153
  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";
154
+ if (isTextMime(m)) return true;
155
+ 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
156
  }
157
+ var isOfficeFile = isServerExtractable;
41
158
  var _extractPlaceholderSeq = 0;
42
159
  function makeExtractPlaceholder(seed) {
43
160
  _extractPlaceholderSeq += 1;
@@ -56,10 +173,10 @@ ${lines.join("\n")}`;
56
173
  let composedForLlm = composed;
57
174
  let extractContent;
58
175
  if (attachmentUrls.length > 0) {
59
- const officeFiles = attachmentUrls.filter((u) => isOfficeFile(u.name));
60
- if (officeFiles.length > 0) {
176
+ const extractFiles = attachmentUrls.filter((u) => isServerExtractable(u.name));
177
+ if (extractFiles.length > 0) {
61
178
  const directives = [];
62
- const sections = officeFiles.map((u) => {
179
+ const sections = extractFiles.map((u) => {
63
180
  const storagePath = u.storagePath || u.name;
64
181
  const placeholder = makeExtractPlaceholder(storagePath);
65
182
  directives.push({ path: storagePath, placeholder, name: u.name });
@@ -110,8 +227,8 @@ Scope: Only answer questions about this project and its data. Do not answer ques
110
227
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
111
228
  File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
112
229
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
113
- - Office documents (Microsoft .docx/.xlsx/.pptx, Hancom .hwpx, etc.) cannot be read by web_fetch (they are binary/zip). When one is attached, the server has ALREADY extracted its text and inlined it in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for that file. A "[skapi: ...]" note in that block means the file could not be extracted.
114
- - For all other file types (text, code, csv, json, pdf, etc.), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
230
+ - Most attached 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 had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
231
+ - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
115
232
  File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](path/to/file) for storage paths, or [filename](https://...) for external URLs. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
116
233
  File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records. Present each result as a markdown link as described above. Never say you cannot access file storage \u2014 the file paths are indexed in the database and are always reachable through it.
117
234
  File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only \u2014 never base64 or any other encoding. Example for CSV:
@@ -135,10 +252,13 @@ function buildIndexingSystemPrompt(params) {
135
252
  const { service, serviceName, serviceDescription } = params;
136
253
  let systemPrompt = `You are a background indexing agent for project ${service}.
137
254
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
138
- - Office documents (Microsoft .docx/.xlsx/.pptx, Hancom .hwpx, etc.) cannot be read by web_fetch (they are binary/zip). For these, the server has ALREADY extracted the text content and included it inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for that file. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
139
- - For all other file types (text, code, csv, json, pdf, etc.), 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.
255
+ - 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
+ - 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.
140
257
  - 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.
141
- - Do NOT reply to the user. Only let user know when the indexing is complete. This is a background indexing task. Always use the MCP tools to save what you learn. Be exhaustive about meaning, terse about bytes.`;
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.)
259
+ - 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
+ - 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
+ - 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>.`;
142
262
  if (serviceDescription) {
143
263
  systemPrompt += `
144
264
  Project name: "${serviceName ?? ""}"
@@ -157,6 +277,14 @@ File metadata:
157
277
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
158
278
  ` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
159
279
  ` : "");
280
+ if (options?.inlineContent) {
281
+ return head + `
282
+ 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.
283
+
284
+ ----- BEGIN FILE CONTENT -----
285
+ ${options.inlineContent}
286
+ ----- END FILE CONTENT -----`;
287
+ }
160
288
  if (options?.inlineContentPlaceholder) {
161
289
  return head + `
162
290
  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 +783,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
655
783
  });
656
784
  }
657
785
  async function notifyAgentSaveAttachment(info) {
658
- const { platform, service, owner, attachment } = info;
659
- const office = isOfficeFile(attachment.name, attachment.mime);
660
- const placeholder = office ? makeExtractPlaceholder(attachment.storagePath) : void 0;
661
- const extractContent = office && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
786
+ const { platform, service, owner, attachment, parsedContent } = info;
787
+ const serverExtract = !parsedContent && isServerExtractable(attachment.name, attachment.mime);
788
+ const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
789
+ const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
662
790
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
663
791
  const userMessage = buildIndexingUserMessage(
664
792
  attachment,
665
- placeholder ? { inlineContentPlaceholder: placeholder } : void 0
793
+ parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : void 0
666
794
  );
667
795
  const systemPrompt = buildIndexingSystemPrompt({
668
796
  service,
@@ -1864,44 +1992,49 @@ var ChatSession = class {
1864
1992
  att.storagePath = member.storagePath;
1865
1993
  }
1866
1994
  var mime = member.file.type || self.host.getMimeType(member.file.name);
1867
- return notifyAgentSaveAttachment({
1868
- platform: id.platform,
1869
- model: id.model,
1870
- service: id.serviceId,
1871
- owner: id.owner,
1872
- userId: id.userId || id.serviceId,
1873
- serviceName: id.serviceName,
1874
- serviceDescription: id.serviceDescription,
1875
- attachment: {
1876
- name: member.file.name,
1877
- storagePath: member.storagePath,
1878
- mime: mime || void 0,
1879
- size: member.file.size,
1880
- url
1881
- }
1882
- }).then(function(ack) {
1883
- if (ack && typeof ack.id === "string") {
1884
- self.bgTaskQueue.push({
1885
- serviceId: id.serviceId,
1886
- platform: id.platform,
1887
- id: ack.id,
1888
- filename: member.file.name,
1995
+ return Promise.resolve(
1996
+ parseAttachmentContent(member.file, member.file.name, mime || void 0)
1997
+ ).then(function(parsedContent) {
1998
+ return notifyAgentSaveAttachment({
1999
+ platform: id.platform,
2000
+ model: id.model,
2001
+ service: id.serviceId,
2002
+ owner: id.owner,
2003
+ userId: id.userId || id.serviceId,
2004
+ serviceName: id.serviceName,
2005
+ serviceDescription: id.serviceDescription,
2006
+ attachment: {
2007
+ name: member.file.name,
1889
2008
  storagePath: member.storagePath,
1890
- isReindex: hadExists,
1891
2009
  mime: mime || void 0,
1892
2010
  size: member.file.size,
1893
- status: ack.status === "running" ? "running" : "pending",
1894
- poll: ack.poll
1895
- });
1896
- self.drainBgTaskQueue();
1897
- }
1898
- }, function(e) {
1899
- console.error("[chat-engine] indexing request failed", e);
1900
- anyIndexFailed = true;
1901
- if (!att.errorCode && !att.errorDetail) {
1902
- att.errorCode = e && (e.code || e.body && e.body.code) || "";
1903
- att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
1904
- }
2011
+ url
2012
+ },
2013
+ parsedContent: parsedContent || void 0
2014
+ }).then(function(ack) {
2015
+ if (ack && typeof ack.id === "string") {
2016
+ self.bgTaskQueue.push({
2017
+ serviceId: id.serviceId,
2018
+ platform: id.platform,
2019
+ id: ack.id,
2020
+ filename: member.file.name,
2021
+ storagePath: member.storagePath,
2022
+ isReindex: hadExists,
2023
+ mime: mime || void 0,
2024
+ size: member.file.size,
2025
+ status: ack.status === "running" ? "running" : "pending",
2026
+ poll: ack.poll
2027
+ });
2028
+ self.drainBgTaskQueue();
2029
+ }
2030
+ }, function(e) {
2031
+ console.error("[chat-engine] indexing request failed", e);
2032
+ anyIndexFailed = true;
2033
+ if (!att.errorCode && !att.errorDetail) {
2034
+ att.errorCode = e && (e.code || e.body && e.body.code) || "";
2035
+ att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
2036
+ }
2037
+ });
1905
2038
  });
1906
2039
  });
1907
2040
  });
@@ -1993,6 +2126,7 @@ exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
1993
2126
  exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
1994
2127
  exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
1995
2128
  exports.MAX_HISTORY_MESSAGES = MAX_HISTORY_MESSAGES;
2129
+ exports.MAX_PARSED_CONTENT_CHARS = MAX_PARSED_CONTENT_CHARS;
1996
2130
  exports.MCP_NAME = MCP_NAME;
1997
2131
  exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
1998
2132
  exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
@@ -2007,6 +2141,7 @@ exports.callClaudeWithMcp = callClaudeWithMcp;
2007
2141
  exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
2008
2142
  exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
2009
2143
  exports.chatEngineConfig = chatEngineConfig;
2144
+ exports.clearAttachmentParsers = clearAttachmentParsers;
2010
2145
  exports.composeUserMessage = composeUserMessage;
2011
2146
  exports.configureChatEngine = configureChatEngine;
2012
2147
  exports.createInlineLinkRegex = createInlineLinkRegex;
@@ -2018,6 +2153,8 @@ exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
2018
2153
  exports.extractOpenAIText = extractOpenAIText;
2019
2154
  exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
2020
2155
  exports.filterListByClearHorizon = filterListByClearHorizon;
2156
+ exports.findAttachmentParser = findAttachmentParser;
2157
+ exports.getAttachmentParsers = getAttachmentParsers;
2021
2158
  exports.getChatHistory = getChatHistory;
2022
2159
  exports.getContextWindow = getContextWindow;
2023
2160
  exports.getErrorMessage = getErrorMessage;
@@ -2027,6 +2164,7 @@ exports.isAuthExpiredError = isAuthExpiredError;
2027
2164
  exports.isErrorResponseBody = isErrorResponseBody;
2028
2165
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
2029
2166
  exports.isOfficeFile = isOfficeFile;
2167
+ exports.isServerExtractable = isServerExtractable;
2030
2168
  exports.listClaudeModels = listClaudeModels;
2031
2169
  exports.listOpenAIModels = listOpenAIModels;
2032
2170
  exports.makeExtractPlaceholder = makeExtractPlaceholder;
@@ -2034,6 +2172,8 @@ exports.mapHistoryListToMessages = mapHistoryListToMessages;
2034
2172
  exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
2035
2173
  exports.normalizeTextContent = normalizeTextContent;
2036
2174
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
2175
+ exports.parseAttachmentContent = parseAttachmentContent;
2176
+ exports.registerAttachmentParser = registerAttachmentParser;
2037
2177
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
2038
2178
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
2039
2179
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;