bunnyquery 1.12.1 → 1.13.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/bunnyquery.js CHANGED
@@ -352,10 +352,15 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
352
352
  const { projectId, serviceName, serviceDescription, greeting, canUpload} = params;
353
353
  const g = params.indexAccessGroup;
354
354
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
355
+ const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
356
+ WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
357
+ KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
358
+ These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
355
359
  let systemPrompt = `
356
360
  You are a dedicated assistant for the project ID: "${projectId}".
357
361
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
358
362
  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.
363
+ ${groundingRules}
359
364
  NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
360
365
  CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
361
366
  A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
@@ -3111,8 +3116,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3111
3116
  var createdTs = Number(item && item.created);
3112
3117
  var updatedTs = Number(item && item.updated);
3113
3118
  var executedTs = Number(item && item.executed);
3119
+ var respondedTs = Number(item && item.responded);
3114
3120
  var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
3115
- var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3121
+ var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3116
3122
  if (userText) {
3117
3123
  var displayContent;
3118
3124
  var indexFile = void 0;
@@ -3739,6 +3745,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3739
3745
  // the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
3740
3746
  /** Live streams by server item id. One per in-flight streamed turn. */
3741
3747
  this.liveStreams = {};
3748
+ /** When skapi recorded a turn's response as having finished arriving, by server
3749
+ * item id. Read off the terminal envelope of a STREAMED turn (the only settle
3750
+ * that carries it, see _settleLiveStream) and used as the answer bubble's time
3751
+ * in place of this browser's clock. Entries are never removed: a settle reaches
3752
+ * here twice (the poll's onResponse, then the promise), and the value for an id
3753
+ * cannot change. */
3754
+ this._respondedAt = {};
3742
3755
  // ─── compact-stub hydration ─────────────────────────────────────────────
3743
3756
  // Split-fetch bg pages arrive as label stubs (no bodies). When the user
3744
3757
  // expands a row, the real reply text is fetched per item (csr-poll point
@@ -4264,6 +4277,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4264
4277
  });
4265
4278
  return n;
4266
4279
  }
4280
+ _noteRespondedAt(itemId, response) {
4281
+ if (!itemId || !isCsrStatusEnvelope(response)) return;
4282
+ var at = Number(response.responded);
4283
+ if (!isFinite(at) || at <= 0) return;
4284
+ if (!this._respondedAt) this._respondedAt = {};
4285
+ this._respondedAt[itemId] = at;
4286
+ }
4287
+ /** The time a live answer bubble is stamped with: when skapi received the
4288
+ * response, if the settle told us, else now. "Now" is what every bubble got
4289
+ * before, and it is still right for a buffered turn, whose settle hands back the
4290
+ * destination's body and nothing of skapi's; a history load then replaces it
4291
+ * with the server's value. */
4292
+ _replyStampFor(msg) {
4293
+ var id = msg ? msg._serverItemId : void 0;
4294
+ var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
4295
+ return typeof at === "number" ? at : wallClockNow();
4296
+ }
4267
4297
  /**
4268
4298
  * Open (or re-open) the live stream for `itemId`, or null when this poll must
4269
4299
  * not carry one.
@@ -4453,6 +4483,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4453
4483
  _settleLiveStream(st, response) {
4454
4484
  this._closeLiveStream(st, true);
4455
4485
  if (!isCsrStatusEnvelope(response)) return response;
4486
+ this._noteRespondedAt(st.id, response);
4456
4487
  if (response.status !== "resolved") return response;
4457
4488
  if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
4458
4489
  if (st.finalBody == null) return response;
@@ -5894,7 +5925,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5894
5925
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
5895
5926
  }
5896
5927
  insertAtTarget(msg, targetIdx) {
5897
- if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
5928
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
5898
5929
  var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
5899
5930
  var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
5900
5931
  if (replaceable) this.state.messages[targetIdx] = msg;
@@ -6517,7 +6548,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6517
6548
  enqueueTypewrite(idx, fullText, localId, paintedText) {
6518
6549
  var self = this;
6519
6550
  var target = this.state.messages[idx];
6520
- if (target && target._ts === void 0) target._ts = wallClockNow();
6551
+ if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
6521
6552
  if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
6522
6553
  this.typewriterQueue = this.typewriterQueue.then(function() {
6523
6554
  return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
@@ -8597,7 +8628,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8597
8628
  (function() {
8598
8629
  var MCP_PROD = "https://mcp.broadwayinc.computer";
8599
8630
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
8600
- var BQ_VERSION = "1.12.1" ;
8631
+ var BQ_VERSION = "1.13.0" ;
8601
8632
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
8602
8633
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8603
8634
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -12173,6 +12204,42 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12173
12204
  }
12174
12205
  return byFile;
12175
12206
  }
12207
+ var stopIndexOthers = { checked: false, row: null, text: null, box: null };
12208
+ function otherStoppableIndexGroups(target) {
12209
+ if (!target) return [];
12210
+ var list = buildChatDisplayList(CS.messages, displayListOptions());
12211
+ var others = [];
12212
+ for (var i = 0; i < list.length; i++) {
12213
+ var row = list[i];
12214
+ if (row.kind !== "indexing") continue;
12215
+ if (!indexGroupStoppable(row.group) || row.group.key === target.key) continue;
12216
+ others.push(row.group);
12217
+ }
12218
+ return others;
12219
+ }
12220
+ function countIndexFiles(groups) {
12221
+ var seen = {}, n = 0;
12222
+ for (var i = 0; i < groups.length; i++) {
12223
+ if (seen[groups[i].key]) continue;
12224
+ seen[groups[i].key] = true;
12225
+ n++;
12226
+ }
12227
+ return n;
12228
+ }
12229
+ function stopOthersLabel(n) {
12230
+ return n === 1 ? "Also stop the other file being indexed" : "Also stop the " + n + " other files being indexed";
12231
+ }
12232
+ function refreshStopIndexOthers() {
12233
+ if (!stopIndexOthers.row) return;
12234
+ var n = countIndexFiles(otherStoppableIndexGroups(findCancellableIndexGroup(stopIndexState.runKey, stopIndexState.fileKey)));
12235
+ if (!n) {
12236
+ stopIndexOthers.checked = false;
12237
+ if (stopIndexOthers.box) stopIndexOthers.box.checked = false;
12238
+ } else if (stopIndexOthers.text) {
12239
+ stopIndexOthers.text.textContent = stopOthersLabel(n);
12240
+ }
12241
+ stopIndexOthers.row.style.display = n ? "" : "none";
12242
+ }
12176
12243
  function stopIndexModalIsOpen() {
12177
12244
  var hnd = stopIndexState.handle;
12178
12245
  if (hnd && hnd.root && hnd.root.parentNode) return true;
@@ -12191,6 +12258,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12191
12258
  function syncStopIndexModal() {
12192
12259
  if (!stopIndexModalIsOpen()) return;
12193
12260
  if (!findCancellableIndexGroup(stopIndexState.runKey, stopIndexState.fileKey)) closeStopIndexModal();
12261
+ else refreshStopIndexOthers();
12194
12262
  }
12195
12263
  function openStopIndexModal(group) {
12196
12264
  if (!indexGroupStoppable(group)) return;
@@ -12203,10 +12271,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12203
12271
  stopBtn.addEventListener("click", function() {
12204
12272
  var runKey = stopIndexState.runKey;
12205
12273
  var fileKey = stopIndexState.fileKey;
12274
+ var alsoOthers = stopIndexOthers.checked;
12206
12275
  closeStopIndexModal();
12207
12276
  var live = findCancellableIndexGroup(runKey, fileKey);
12277
+ var others = alsoOthers ? otherStoppableIndexGroups(live) : [];
12208
12278
  if (live) session.cancelIndexingGroup(live);
12279
+ for (var oi = 0; oi < others.length; oi++) session.cancelIndexingGroup(others[oi]);
12280
+ });
12281
+ var othersBox = h("input", { type: "checkbox" });
12282
+ othersBox.addEventListener("change", function() {
12283
+ stopIndexOthers.checked = !!othersBox.checked;
12209
12284
  });
12285
+ var othersText = h("span");
12286
+ var othersRow = h("label", { class: "bq-overwrite-applyall" }, othersBox, othersText);
12287
+ stopIndexOthers.checked = false;
12288
+ stopIndexOthers.box = othersBox;
12289
+ stopIndexOthers.text = othersText;
12290
+ stopIndexOthers.row = othersRow;
12291
+ refreshStopIndexOthers();
12210
12292
  return h(
12211
12293
  "div",
12212
12294
  { class: "bq-modal" },
@@ -12226,6 +12308,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12226
12308
  { class: "bq-modal-desc bq-modal-delete-warn" },
12227
12309
  "Whatever has been indexed so far stays searchable, and the pass already running finishes on the server. The remaining passes are dropped, not paused, so the file stays partly indexed until you reindex it."
12228
12310
  ),
12311
+ othersRow,
12229
12312
  h(
12230
12313
  "div",
12231
12314
  { class: "bq-modal-btns" },
package/dist/engine.cjs CHANGED
@@ -359,10 +359,15 @@ function buildChatSystemPrompt(params) {
359
359
  const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
360
360
  const g = params.indexAccessGroup;
361
361
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
362
+ const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
363
+ WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
364
+ KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
365
+ These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
362
366
  let systemPrompt = `
363
367
  You are a dedicated assistant for the project ID: "${projectId}".
364
368
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
365
369
  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.
370
+ ${groundingRules}
366
371
  NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
367
372
  CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
368
373
  A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
@@ -3255,8 +3260,9 @@ function mapHistoryListToMessages(list, platform, opts) {
3255
3260
  var createdTs = Number(item && item.created);
3256
3261
  var updatedTs = Number(item && item.updated);
3257
3262
  var executedTs = Number(item && item.executed);
3263
+ var respondedTs = Number(item && item.responded);
3258
3264
  var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
3259
- var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3265
+ var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3260
3266
  if (userText) {
3261
3267
  var displayContent;
3262
3268
  var indexFile = void 0;
@@ -3883,6 +3889,13 @@ var ChatSession = class {
3883
3889
  // the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
3884
3890
  /** Live streams by server item id. One per in-flight streamed turn. */
3885
3891
  this.liveStreams = {};
3892
+ /** When skapi recorded a turn's response as having finished arriving, by server
3893
+ * item id. Read off the terminal envelope of a STREAMED turn (the only settle
3894
+ * that carries it, see _settleLiveStream) and used as the answer bubble's time
3895
+ * in place of this browser's clock. Entries are never removed: a settle reaches
3896
+ * here twice (the poll's onResponse, then the promise), and the value for an id
3897
+ * cannot change. */
3898
+ this._respondedAt = {};
3886
3899
  // ─── compact-stub hydration ─────────────────────────────────────────────
3887
3900
  // Split-fetch bg pages arrive as label stubs (no bodies). When the user
3888
3901
  // expands a row, the real reply text is fetched per item (csr-poll point
@@ -4408,6 +4421,23 @@ var ChatSession = class {
4408
4421
  });
4409
4422
  return n;
4410
4423
  }
4424
+ _noteRespondedAt(itemId, response) {
4425
+ if (!itemId || !isCsrStatusEnvelope(response)) return;
4426
+ var at = Number(response.responded);
4427
+ if (!isFinite(at) || at <= 0) return;
4428
+ if (!this._respondedAt) this._respondedAt = {};
4429
+ this._respondedAt[itemId] = at;
4430
+ }
4431
+ /** The time a live answer bubble is stamped with: when skapi received the
4432
+ * response, if the settle told us, else now. "Now" is what every bubble got
4433
+ * before, and it is still right for a buffered turn, whose settle hands back the
4434
+ * destination's body and nothing of skapi's; a history load then replaces it
4435
+ * with the server's value. */
4436
+ _replyStampFor(msg) {
4437
+ var id = msg ? msg._serverItemId : void 0;
4438
+ var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
4439
+ return typeof at === "number" ? at : wallClockNow();
4440
+ }
4411
4441
  /**
4412
4442
  * Open (or re-open) the live stream for `itemId`, or null when this poll must
4413
4443
  * not carry one.
@@ -4597,6 +4627,7 @@ var ChatSession = class {
4597
4627
  _settleLiveStream(st, response) {
4598
4628
  this._closeLiveStream(st, true);
4599
4629
  if (!isCsrStatusEnvelope(response)) return response;
4630
+ this._noteRespondedAt(st.id, response);
4600
4631
  if (response.status !== "resolved") return response;
4601
4632
  if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
4602
4633
  if (st.finalBody == null) return response;
@@ -6038,7 +6069,7 @@ var ChatSession = class {
6038
6069
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
6039
6070
  }
6040
6071
  insertAtTarget(msg, targetIdx) {
6041
- if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
6072
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
6042
6073
  var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
6043
6074
  var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
6044
6075
  if (replaceable) this.state.messages[targetIdx] = msg;
@@ -6661,7 +6692,7 @@ var ChatSession = class {
6661
6692
  enqueueTypewrite(idx, fullText, localId, paintedText) {
6662
6693
  var self = this;
6663
6694
  var target = this.state.messages[idx];
6664
- if (target && target._ts === void 0) target._ts = wallClockNow();
6695
+ if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
6665
6696
  if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
6666
6697
  this.typewriterQueue = this.typewriterQueue.then(function() {
6667
6698
  return self.typewriteIntoIndex(idx, fullText, localId, paintedText);