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 +74 -4
- package/bunnyquery.js +85 -13
- package/dist/engine.cjs +33 -9
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.mjs +33 -9
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/session.ts +58 -15
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
5103
|
-
|
|
5104
|
-
return
|
|
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 (
|
|
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({
|