bunnyquery 1.4.2 → 1.4.4

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/README.md CHANGED
@@ -24,6 +24,9 @@ you can build your own chat UI on top of it. See
24
24
  first load and on scroll-up.
25
25
  - **Attachments** — drag-and-drop files and folders, per-file upload status
26
26
  (uploading / failed / indexed), and overflow collapsing for large batches.
27
+ Images are read with vision/OCR, Office/text/code files are extracted
28
+ server-side, and PDFs are fetched by the model — see
29
+ [Supported file types](#supported-file-types).
27
30
  - **Attachment parser plugins** — register a client-side parser so the widget
28
31
  extracts text in the browser from formats the model can't otherwise read, and
29
32
  indexes it directly. See [Attachment parser plugins](#attachment-parser-plugins).
@@ -137,12 +140,79 @@ BunnyQuery.logout();
137
140
  > instance rather than re-mounting. On a successful mount it logs its version, e.g.
138
141
  > `[bunnyquery] v1.3.5`.
139
142
 
143
+ ## Supported file types
144
+
145
+ When a user attaches a file, BunnyQuery makes its contents available to the AI
146
+ automatically — detected by extension (with a MIME-type fallback), nothing to
147
+ configure. There are three paths, plus a couple of caveats.
148
+
149
+ ### 1. Images — read directly by the model (vision + OCR)
150
+
151
+ `.jpg` · `.jpeg` · `.png` · `.gif` · `.webp`
152
+
153
+ The image is attached to the request inline, so the model both **describes the
154
+ picture** and **reads any text in it (OCR)**. Works on both Claude and OpenAI.
155
+ Only images referenced in the **most recent** message are inlined (older links
156
+ may have expired).
157
+
158
+ ### 2. Documents, data & code — extracted on the server (inlined as text)
159
+
160
+ The skapi proxy downloads the file, extracts its text **server-side**, and
161
+ inlines that text into the request — the model reads it directly, with no
162
+ fetching. This keeps indexing consistent across model providers.
163
+
164
+ **Office & e-book** (binary/zip, parsed):
165
+ `.docx` · `.xlsx` · `.pptx` · `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub`
166
+
167
+ **Text, data, markup & source code** (decoded as text; `.html`/`.htm` have their
168
+ tags stripped):
169
+
170
+ ```
171
+ .csv .tsv .tab .txt .text .log .md .markdown .rst .json .ndjson .jsonl .geojson
172
+ .xml .yaml .yml .toml .ini .conf .cfg .properties .env .rtf .html .htm
173
+ .js .mjs .cjs .ts .tsx .jsx .py .rb .go .rs .java .kt .c .h .cpp .cc .hpp .cs
174
+ .php .swift .sh .bash .zsh .sql .css .scss .less .vue .svelte .tex .srt .vtt
175
+ ```
176
+
177
+ Plus a **MIME fallback**: any file whose content type is text-like (`text/*`,
178
+ `application/json`, `application/xml`, `*+json`, `*+xml`, `*+yaml`, …) is decoded
179
+ even when its extension isn't in the list above.
180
+
181
+ Encoding is auto-detected — UTF-8 (BOM-aware) → CP949/EUC-KR (Korean) → Latin-1.
182
+ Extracted text is capped at **200,000 characters**; longer files are truncated
183
+ with a `...[truncated for length; original N characters]` marker.
184
+
185
+ ### 3. PDFs & other links — fetched by the model
186
+
187
+ `.pdf` (and any file that is neither an image nor server-extractable) is handed
188
+ to the model as a temporary link, which it opens with its built-in web tool:
189
+ **Claude** via `web_fetch`, **OpenAI** via `web_search` (external web access is
190
+ enabled). Both can open and read PDFs, so PDFs work on either provider.
191
+
192
+ > A provider's web tool opens document/page-style URLs such as PDFs, but not
193
+ > necessarily a bare *data-file* download (e.g. a raw `.csv`/`.tsv` link). That's
194
+ > why those data formats are extracted **server-side** (path 2 above) instead of
195
+ > being left to the model to fetch.
196
+
197
+ ### Caveats
198
+
199
+ - **Legacy / macro Office** — `.doc` `.xls` `.ppt` (legacy binary) and `.docm`
200
+ `.xlsm` `.pptm` (macro-enabled) have no reliable server-side reader. They
201
+ upload fine but are indexed from **metadata only**; re-save as
202
+ `.docx` / `.xlsx` / `.pptx` (or PDF) to capture their contents.
203
+ - **Anything else** — a format covered by none of the above is indexed from its
204
+ metadata. To support it, register your own
205
+ [Attachment parser plugin](#attachment-parser-plugins) — it runs in the browser
206
+ and feeds parsed text straight into indexing.
207
+
140
208
  ## Attachment parser plugins
141
209
 
142
- By default the chat agent reads most uploads via the model's `web_fetch` tool
143
- (text, CSV, PDF, …), and Office/OpenDocument/EPUB files are extracted on the
144
- server. For any format that can be read by **neither** (e.g. a proprietary
145
- binary format), register a **parser plugin**: it runs in the browser, turns the
210
+ By default the chat agent reads images with vision/OCR, extracts
211
+ Office/OpenDocument/EPUB and text/data/code files on the server, and lets the
212
+ model fetch PDFs with its built-in web tool (`web_fetch` on Claude, `web_search`
213
+ on OpenAI) see [Supported file types](#supported-file-types). For any format
214
+ read by **none** of these (e.g. a proprietary binary format), register a
215
+ **parser plugin**: it runs in the browser, turns the
146
216
  uploaded file into text (or an HTML string), and the widget sends that content
147
217
  **inline** for indexing — no `web_fetch`, no server extraction for that file.
148
218
 
package/bunnyquery.js CHANGED
@@ -1116,13 +1116,38 @@ ${options.inlineContentPlaceholder}
1116
1116
  }).catch(function(err) {
1117
1117
  return { content: getErrorMessage(err), isError: true };
1118
1118
  }).then(function(result) {
1119
- var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
1120
- self.aiChatHistoryCache[params.key] = {
1121
- messages: existing.messages.concat([{ role: "assistant", content: result.content, isError: result.isError }]),
1122
- endOfList: existing.endOfList,
1123
- startKeyHistory: existing.startKeyHistory
1124
- };
1125
1119
  delete self.pendingAgentRequests[params.key];
1120
+ var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
1121
+ var reply = { role: "assistant", content: result.content, isError: result.isError };
1122
+ var onThisVisibleChat = self.host.isViewMounted() && self.getHistoryCacheKey() === params.key;
1123
+ if (onThisVisibleChat) {
1124
+ self.aiChatHistoryCache[params.key] = {
1125
+ messages: existing.messages.concat([reply]),
1126
+ endOfList: existing.endOfList,
1127
+ startKeyHistory: existing.startKeyHistory
1128
+ };
1129
+ } else {
1130
+ var msgs = existing.messages.slice();
1131
+ var idx = -1;
1132
+ for (var i = msgs.length - 1; i >= 0; i--) {
1133
+ var m = msgs[i];
1134
+ if (m && m.isPending && m.role === "assistant" && !m.isBackgroundTask) {
1135
+ idx = i;
1136
+ break;
1137
+ }
1138
+ }
1139
+ if (idx !== -1) {
1140
+ reply._serverItemId = msgs[idx]._serverItemId;
1141
+ msgs[idx] = reply;
1142
+ } else {
1143
+ msgs.push(reply);
1144
+ }
1145
+ self.aiChatHistoryCache[params.key] = {
1146
+ messages: msgs,
1147
+ endOfList: existing.endOfList,
1148
+ startKeyHistory: existing.startKeyHistory
1149
+ };
1150
+ }
1126
1151
  return result;
1127
1152
  });
1128
1153
  this.pendingAgentRequests[params.key] = run;
@@ -1212,7 +1237,6 @@ ${options.inlineContentPlaceholder}
1212
1237
  serviceId: id.serviceId,
1213
1238
  history: historyForLlm
1214
1239
  });
1215
- var requestToken = this.state.gateRefreshToken;
1216
1240
  var run = this.dispatchAgentRequest({
1217
1241
  key,
1218
1242
  serviceId: id.serviceId,
@@ -1227,7 +1251,7 @@ ${options.inlineContentPlaceholder}
1227
1251
  });
1228
1252
  Promise.resolve(run).catch(function() {
1229
1253
  }).then(function() {
1230
- if (requestToken !== self.state.gateRefreshToken || self.getHistoryCacheKey() !== key) return;
1254
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1231
1255
  self.state.sending = false;
1232
1256
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1233
1257
  self.host.scrollToBottom(true);
@@ -1841,7 +1865,7 @@ ${options.inlineContentPlaceholder}
1841
1865
  if (item.status !== "running" && item.status !== "pending") return;
1842
1866
  if (!item.poll || !item.id) return;
1843
1867
  if (self.historyItemPolls.has(item.id)) return;
1844
- if (item.status === "running" && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
1868
+ if ((item.status === "running" || item.status === "pending") && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
1845
1869
  self.historyItemPolls.set(item.id, true);
1846
1870
  var capturedId = item.id;
1847
1871
  var pp = item.poll({
@@ -2084,7 +2108,7 @@ ${options.inlineContentPlaceholder}
2084
2108
  (function() {
2085
2109
  var MCP_PROD = "https://mcp.broadwayinc.computer";
2086
2110
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
2087
- var BQ_VERSION = "1.4.2" ;
2111
+ var BQ_VERSION = "1.4.4" ;
2088
2112
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
2089
2113
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
2090
2114
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -2492,6 +2516,43 @@ ${options.inlineContentPlaceholder}
2492
2516
  var mismatched = !!tok && !!currentSub && !!tokenSub && tokenSub !== currentSub;
2493
2517
  return expired || mismatched;
2494
2518
  }
2519
+ function refreshMcpToken() {
2520
+ var client = getStoredMcpClient();
2521
+ var current = getStoredMcpToken();
2522
+ if (!client || !current || !current.refresh_token) return Promise.resolve(null);
2523
+ var body = new URLSearchParams({
2524
+ grant_type: "refresh_token",
2525
+ refresh_token: current.refresh_token,
2526
+ client_id: client.client_id
2527
+ });
2528
+ var headers = { "Content-Type": "application/x-www-form-urlencoded" };
2529
+ if (client.client_secret) {
2530
+ headers.Authorization = basicAuthHeader(client.client_id, client.client_secret);
2531
+ }
2532
+ return fetch(mcpBaseUrl() + "/oauth/token", {
2533
+ method: "POST",
2534
+ headers,
2535
+ body: body.toString()
2536
+ }).then(function(res) {
2537
+ return res.ok ? res.json() : null;
2538
+ }).then(function(json) {
2539
+ if (!json || !json.access_token) return null;
2540
+ var token = Object.assign({}, json, {
2541
+ refresh_token: json.refresh_token || current.refresh_token,
2542
+ expires_at: typeof json.expires_in === "number" ? Date.now() + json.expires_in * 1e3 : void 0
2543
+ });
2544
+ lsSet(skey(SK.mcpToken), JSON.stringify(token));
2545
+ return token;
2546
+ }).catch(function() {
2547
+ return null;
2548
+ });
2549
+ }
2550
+ function ensureMcpGrantFresh() {
2551
+ if (!S.user || !mcpGrantNeedsRefresh(S.user)) return Promise.resolve(true);
2552
+ return refreshMcpToken().then(function(tok) {
2553
+ return !!(tok && !mcpGrantNeedsRefresh(S.user));
2554
+ });
2555
+ }
2495
2556
  function googleEnabled() {
2496
2557
  return !!S.opts.googleClientId;
2497
2558
  }
@@ -3759,6 +3820,8 @@ ${options.inlineContentPlaceholder}
3759
3820
  }
3760
3821
  function refreshSkapiSession() {
3761
3822
  return S.skapi.getProfile({ refreshToken: true }).then(function() {
3823
+ return ensureMcpGrantFresh();
3824
+ }).then(function() {
3762
3825
  return true;
3763
3826
  }).catch(function() {
3764
3827
  return false;
@@ -5099,9 +5162,12 @@ ${options.inlineContentPlaceholder}
5099
5162
  return;
5100
5163
  }
5101
5164
  if (mcpGrantNeedsRefresh(user)) {
5102
- return beginMcpOAuthOnLogin("chat").catch(function(err) {
5103
- console.error("[bunnyquery] MCP refresh failed", err);
5104
- return enterAfterLogin();
5165
+ return refreshMcpToken().then(function(tok) {
5166
+ if (tok && !mcpGrantNeedsRefresh(user)) return enterAfterLogin();
5167
+ return beginMcpOAuthOnLogin("chat").catch(function(err) {
5168
+ console.error("[bunnyquery] MCP refresh failed", err);
5169
+ return enterAfterLogin();
5170
+ });
5105
5171
  });
5106
5172
  }
5107
5173
  return enterAfterLogin();
@@ -5158,6 +5224,12 @@ ${options.inlineContentPlaceholder}
5158
5224
  scheduleAttachmentOverflowRecompute();
5159
5225
  });
5160
5226
  }
5227
+ if (!S._visBound && typeof document !== "undefined" && document.addEventListener) {
5228
+ S._visBound = true;
5229
+ document.addEventListener("visibilitychange", function() {
5230
+ if (document.visibilityState === "visible" && S.user) ensureMcpGrantFresh();
5231
+ });
5232
+ }
5161
5233
  boot();
5162
5234
  return PUBLIC;
5163
5235
  }
package/dist/engine.cjs CHANGED
@@ -1150,13 +1150,38 @@ var ChatSession = class {
1150
1150
  }).catch(function(err) {
1151
1151
  return { content: getErrorMessage(err), isError: true };
1152
1152
  }).then(function(result) {
1153
- var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
1154
- self.aiChatHistoryCache[params.key] = {
1155
- messages: existing.messages.concat([{ role: "assistant", content: result.content, isError: result.isError }]),
1156
- endOfList: existing.endOfList,
1157
- startKeyHistory: existing.startKeyHistory
1158
- };
1159
1153
  delete self.pendingAgentRequests[params.key];
1154
+ var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
1155
+ var reply = { role: "assistant", content: result.content, isError: result.isError };
1156
+ var onThisVisibleChat = self.host.isViewMounted() && self.getHistoryCacheKey() === params.key;
1157
+ if (onThisVisibleChat) {
1158
+ self.aiChatHistoryCache[params.key] = {
1159
+ messages: existing.messages.concat([reply]),
1160
+ endOfList: existing.endOfList,
1161
+ startKeyHistory: existing.startKeyHistory
1162
+ };
1163
+ } else {
1164
+ var msgs = existing.messages.slice();
1165
+ var idx = -1;
1166
+ for (var i = msgs.length - 1; i >= 0; i--) {
1167
+ var m = msgs[i];
1168
+ if (m && m.isPending && m.role === "assistant" && !m.isBackgroundTask) {
1169
+ idx = i;
1170
+ break;
1171
+ }
1172
+ }
1173
+ if (idx !== -1) {
1174
+ reply._serverItemId = msgs[idx]._serverItemId;
1175
+ msgs[idx] = reply;
1176
+ } else {
1177
+ msgs.push(reply);
1178
+ }
1179
+ self.aiChatHistoryCache[params.key] = {
1180
+ messages: msgs,
1181
+ endOfList: existing.endOfList,
1182
+ startKeyHistory: existing.startKeyHistory
1183
+ };
1184
+ }
1160
1185
  return result;
1161
1186
  });
1162
1187
  this.pendingAgentRequests[params.key] = run;
@@ -1246,7 +1271,6 @@ var ChatSession = class {
1246
1271
  serviceId: id.serviceId,
1247
1272
  history: historyForLlm
1248
1273
  });
1249
- var requestToken = this.state.gateRefreshToken;
1250
1274
  var run = this.dispatchAgentRequest({
1251
1275
  key,
1252
1276
  serviceId: id.serviceId,
@@ -1261,7 +1285,7 @@ var ChatSession = class {
1261
1285
  });
1262
1286
  Promise.resolve(run).catch(function() {
1263
1287
  }).then(function() {
1264
- if (requestToken !== self.state.gateRefreshToken || self.getHistoryCacheKey() !== key) return;
1288
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1265
1289
  self.state.sending = false;
1266
1290
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1267
1291
  self.host.scrollToBottom(true);
@@ -1875,7 +1899,7 @@ var ChatSession = class {
1875
1899
  if (item.status !== "running" && item.status !== "pending") return;
1876
1900
  if (!item.poll || !item.id) return;
1877
1901
  if (self.historyItemPolls.has(item.id)) return;
1878
- if (item.status === "running" && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
1902
+ if ((item.status === "running" || item.status === "pending") && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
1879
1903
  self.historyItemPolls.set(item.id, true);
1880
1904
  var capturedId = item.id;
1881
1905
  var pp = item.poll({