bunnyquery 1.8.11 → 1.8.13

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.css CHANGED
@@ -1018,6 +1018,15 @@
1018
1018
  .bq-input-row { flex-direction: column; padding: 0.65rem 0.75rem 0.75rem; }
1019
1019
  .bq-input-wrap { width: 100%; flex: 0 0 auto; }
1020
1020
  .bq-input-row .btn { width: 100%; }
1021
+ /* `flex-basis: 100%` on these two means FULL WIDTH only while the row is a
1022
+ row. The line above flips it to a column, where the main axis is vertical
1023
+ and the same declaration claims the container's full HEIGHT: the chips
1024
+ then fill the column, and since the row still wraps, the input and Send
1025
+ wrap into a SECOND COLUMN, off to the right of the screen. That is the
1026
+ attachment bug, and it is why it never appeared on the dashboard, whose
1027
+ own mobile block already carries these two lines (agent.vue). */
1028
+ .bq-attachments { flex-basis: auto; width: 100%; }
1029
+ .bq-attachment-warning { flex-basis: auto; width: 100%; }
1021
1030
  .bq-bubble { max-width: 100%; }
1022
1031
  .bq-page { padding: 2rem 1.25rem; }
1023
1032
  .bq-section-title { padding: 0.75rem; }
@@ -1088,7 +1097,11 @@
1088
1097
  cancelled bubbles, indexing passes (the collapsed rows' members), and the
1089
1098
  empty-chat greeting. Inlined data URI so both consumers get it without
1090
1099
  markup or asset-hosting changes. */
1091
- .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass):not(.bq-empty-greeting) .bq-bubble::before {
1100
+ /* The greeting is NOT excluded here any more. It used to be
1101
+ (:not(.bq-empty-greeting)), because it was a one-off notice shown only to an
1102
+ empty chat. It is now a permanent first message in every conversation, so it
1103
+ is an assistant bubble like any other and wears the same face. */
1104
+ .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble::before {
1092
1105
  content: "";
1093
1106
  /* block, not float: the bunny takes the first line by itself and the
1094
1107
  message starts on the next line below it */
@@ -1103,12 +1116,12 @@
1103
1116
  /* While the reply is still "Thinking..." the bubble holds only the dot-trail
1104
1117
  loader: put the bunny INLINE with the dots instead of stacking above them.
1105
1118
  Settled replies keep the block bunny with the message on the next line. */
1106
- .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass):not(.bq-empty-greeting) .bq-bubble:has(> .bq-loader)::before {
1119
+ .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble:has(> .bq-loader)::before {
1107
1120
  display: inline-block;
1108
1121
  vertical-align: middle;
1109
1122
  margin-right: 0.4rem;
1110
1123
  }
1111
- .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass):not(.bq-empty-greeting) .bq-bubble:has(> .bq-loader) > .bq-loader {
1124
+ .bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble:has(> .bq-loader) > .bq-loader {
1112
1125
  vertical-align: middle;
1113
1126
  }
1114
1127
 
package/bunnyquery.js CHANGED
@@ -276,14 +276,14 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
276
276
 
277
277
  // src/engine/prompts/chat_system_prompt.ts
278
278
  function buildChatSystemPrompt(params) {
279
- const { projectId, serviceName, serviceDescription } = params;
279
+ const { projectId, serviceName, serviceDescription, greeting, canUpload} = params;
280
280
  let systemPrompt = `
281
281
  You are a dedicated assistant for the project ID: "${projectId}".
282
- 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.
282
+ 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.
283
283
  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.
284
284
  Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized" (where the indexer writes; pass access_group explicitly, including 0, to search another group), while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
285
285
  Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
286
- Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
286
+ Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
287
287
  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.
288
288
  - 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.
289
289
  - Other 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) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
@@ -309,6 +309,16 @@ Mushrooms,41,$73.80
309
309
  Zucchini,29,$43.50
310
310
  \`\`\`
311
311
  The same pattern applies to any format - name the block after the file you intend: \`\`\`my-data.json, \`\`\`index.html, \`\`\`sample.txt, and so on.`;
312
+ systemPrompt += `
313
+ About BunnyQuery (this app - questions about it are in scope):
314
+ You are the assistant inside BunnyQuery, an AI assistant for the user's own business data. Instead of digging through folders, dashboards and files, the user uploads their documents, spreadsheets, images, notes and records, BunnyQuery indexes them into this project's database, and you answer questions, write reports and summarize from THAT data rather than from the open internet. Each project has its own data, its own AI platform (ChatGPT or Claude, powered by the project owner's own API key) and its own base prompt. BunnyQuery is built on Skapi (www.skapi.com), so the same project database is also reachable over MCP from any MCP-compatible AI client (mcp.broadwayinc.computer), and this chat can be embedded in a website as a widget with one script tag. Answer product questions from the facts in this section. If you are asked something about BunnyQuery that is NOT stated here - pricing, plan limits, a roadmap, a feature you cannot see - say you are not certain and point the user at the project owner or the BunnyQuery site, rather than inventing it.
315
+ How data gets in: ${canUpload === false ? `this user CANNOT upload in this session (they are not signed in, or the project's database is frozen for non-admins), and the attach affordances are hidden from them. Never instruct them to attach, drag in or upload a file, and never blame a missing answer on them not having uploaded it. Answer from what is already indexed, and when something genuinely is not in the project, say so and suggest asking the project's owner to add it.` : `the user attaches files to a chat message with the paperclip button in the composer, or drags and drops them onto the chat (whole folders work; up to 20 files per message). Uploaded files land in this project's file storage and are indexed automatically: read end to end and turned into database records. "Indexed" means exactly that, and it is why you can only answer from a file once its indexing has finished. While a file indexes, the chat shows a status row for it: yellow while it is working, green when it is indexed, red if it failed. A large file is indexed in windows over several passes, which takes longer; indexing runs on the server, so it keeps going if the user closes the page and the row is still there when they come back. The user can also paste plain text straight into the chat and ask you to save it - store it with the postRecords tool. BunnyQuery reads over 50 formats: office documents (.docx, .xlsx, .pptx, .hwp, .hwpx, .odt, .ods, .odp, .epub), PDFs, images, .csv/.tsv, .json, .xml, .html, .txt/.md and source code. Images and scanned PDFs are read with vision at index time.`}
316
+ Getting answers out: the user asks in plain language, in any language, and you answer from this project's data. You can also produce reports and downloadable files (CSV and the rest) as described in the File generation rules above, and any stored file can be handed back as a link, with images rendering inline in the chat.${""}${`
317
+ This chat is the BunnyQuery widget embedded in a website, so the user may have no access to the project console: keep any instructions to what can be done here in the chat.` }`;
318
+ if (greeting) {
319
+ systemPrompt += `
320
+ Your opening message: this chat always opens with a fixed line from you, reading """${greeting}""". It is rendered by the client and is NOT part of the message history you receive, so the user can reply to it ("which files?", "what do you mean by indexed?", "what can you do?") with nothing in the transcript to refer back to. Treat that line as something you said, and answer the follow-up from this section.`;
321
+ }
312
322
  if (serviceDescription) {
313
323
  systemPrompt += `
314
324
  Project name: "${serviceName ?? ""}"
@@ -455,6 +465,14 @@ Records for the earlier windows/pages of this file are ALREADY saved (they refer
455
465
  Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. A \xABPHOTO <cell>\xBB marker in a window marks an embedded picture whose extracted file already has a record in table "__MEDIA__": find it with getRecords reference "src::" + the storage path above and match the cell against data.anchor or tags (a repeated picture is stored under its first anchor only), then enrich it with updateRecords. Never create a photo record of your own and never construct a path for one. Do NOT re-save windows that are already saved. Set every record's reference to exactly "src::" + the storage path above (no sheet, window or summary suffix added). That file record already exists, so do NOT post it; enrich it with updateRecords. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
456
466
  }
457
467
 
468
+ // src/engine/greeting.ts
469
+ function buildChatGreeting(params) {
470
+ const name = params.projectName ? '"' + params.projectName + '"' : "";
471
+ const lead = params.canUpload === false ? "Hi! Ask me anything about the data in your project" : "Hi! Start by attaching the files related to your project";
472
+ const tail = params.canUpload === false ? "." : ", or pasting plain text into the chat. Once they are indexed, ask me anything about that data.";
473
+ return { lead, name, tail, text: lead + (name ? " " + name : "") + tail };
474
+ }
475
+
458
476
  // src/engine/errors.ts
459
477
  var STATUS_MESSAGE = {
460
478
  "408": "The AI provider timed out before it started.",
@@ -5980,7 +5998,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5980
5998
  (function() {
5981
5999
  var MCP_PROD = "https://mcp.broadwayinc.computer";
5982
6000
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
5983
- var BQ_VERSION = "1.8.11" ;
6001
+ var BQ_VERSION = "1.8.12" ;
5984
6002
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
5985
6003
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
5986
6004
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -7816,8 +7834,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7816
7834
  return buildChatSystemPrompt({
7817
7835
  projectId: promptProjectId,
7818
7836
  serviceName: S.serviceName,
7819
- serviceDescription: S.serviceDescription
7820
- });
7837
+ serviceDescription: S.serviceDescription,
7838
+ // The opening bubble never enters the history (it is DOM the client
7839
+ // paints), so the model is told what it opened with. Same call as
7840
+ // buildGreetingEl, so the two can never disagree.
7841
+ greeting: greetingParts().text,
7842
+ canUpload: !uploadsFrozenForUser()});
7821
7843
  }
7822
7844
  function refreshSkapiSession() {
7823
7845
  return S.skapi.getProfile({ refreshToken: true }).then(function() {
@@ -9218,7 +9240,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9218
9240
  var stubRecheckTimer = null;
9219
9241
  var stubRecheckSig = "";
9220
9242
  var stubRecheckRounds = 0;
9221
- var markerSweepSettled = false;
9222
9243
  function armStubRecheck() {
9223
9244
  if (stubRecheckTimer !== null) return;
9224
9245
  stubRecheckTimer = setTimeout(function() {
@@ -9262,14 +9283,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9262
9283
  function refreshIndexMarkers(invalidate) {
9263
9284
  if (invalidate) invalidateIndexMarkerSweep();
9264
9285
  return sweepIndexMarkersDb().then(function(res) {
9265
- markerSweepSettled = true;
9266
9286
  if (res) {
9267
9287
  maybeArmStubRecheck();
9268
9288
  renderMessages();
9269
9289
  }
9270
9290
  return res;
9271
9291
  }).catch(function() {
9272
- markerSweepSettled = true;
9273
9292
  renderMessages();
9274
9293
  return null;
9275
9294
  });
@@ -9554,6 +9573,18 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9554
9573
  }
9555
9574
  return el;
9556
9575
  }
9576
+ function greetingParts() {
9577
+ return buildChatGreeting({ projectName: S.serviceName, canUpload: !uploadsFrozenForUser() });
9578
+ }
9579
+ function buildGreetingEl() {
9580
+ var parts = greetingParts();
9581
+ var name = parts.name ? [document.createTextNode(" "), h("strong", { translate: "no", text: parts.name })] : null;
9582
+ var bubble = h("div", { class: "bq-bubble" });
9583
+ append(bubble, parts.lead);
9584
+ append(bubble, name);
9585
+ append(bubble, parts.tail);
9586
+ return h("div", { class: "bq-message is-assistant bq-empty-greeting" }, bubble);
9587
+ }
9557
9588
  function historyLoadingEl(initial) {
9558
9589
  if (initial) {
9559
9590
  return h(
@@ -9648,6 +9679,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9648
9679
  h("span", { class: "bq-loader" })
9649
9680
  ));
9650
9681
  }
9682
+ CS.messagesBox.appendChild(buildGreetingEl());
9651
9683
  if (!CS.messages.length) {
9652
9684
  if (CS.loadingHistory && !CS.loadingOlderHistory) {
9653
9685
  CS.messagesBox.appendChild(historyLoadingEl(true));
@@ -9664,17 +9696,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9664
9696
  }
9665
9697
  } catch (e) {
9666
9698
  }
9667
- if (!emptyStubEls.length && !session.state.bgHistoryLoading && markerSweepSettled) {
9668
- CS.messagesBox.appendChild(h(
9669
- "div",
9670
- { class: "bq-message is-assistant bq-empty-greeting" },
9671
- h(
9672
- "div",
9673
- { class: "bq-bubble" },
9674
- document.createTextNode("Hi! Ask me anything about " + (S.serviceName ? '"' + S.serviceName + '"' : "your project") + ".")
9675
- )
9676
- ));
9677
- }
9678
9699
  for (var gse = 0; gse < emptyStubEls.length; gse++) CS.messagesBox.appendChild(emptyStubEls[gse]);
9679
9700
  syncDraftingIndicator();
9680
9701
  return;
package/dist/engine.cjs CHANGED
@@ -283,14 +283,14 @@ function groupAttachmentFailures(attachments) {
283
283
 
284
284
  // src/engine/prompts/chat_system_prompt.ts
285
285
  function buildChatSystemPrompt(params) {
286
- const { projectId, serviceName, serviceDescription } = params;
286
+ const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
287
287
  let systemPrompt = `
288
288
  You are a dedicated assistant for the project ID: "${projectId}".
289
- 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.
289
+ 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.
290
290
  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.
291
291
  Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized" (where the indexer writes; pass access_group explicitly, including 0, to search another group), while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
292
292
  Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
293
- Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
293
+ Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
294
294
  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.
295
295
  - 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.
296
296
  - Other 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) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
@@ -316,6 +316,17 @@ Mushrooms,41,$73.80
316
316
  Zucchini,29,$43.50
317
317
  \`\`\`
318
318
  The same pattern applies to any format - name the block after the file you intend: \`\`\`my-data.json, \`\`\`index.html, \`\`\`sample.txt, and so on.`;
319
+ systemPrompt += `
320
+ About BunnyQuery (this app - questions about it are in scope):
321
+ You are the assistant inside BunnyQuery, an AI assistant for the user's own business data. Instead of digging through folders, dashboards and files, the user uploads their documents, spreadsheets, images, notes and records, BunnyQuery indexes them into this project's database, and you answer questions, write reports and summarize from THAT data rather than from the open internet. Each project has its own data, its own AI platform (ChatGPT or Claude, powered by the project owner's own API key) and its own base prompt. BunnyQuery is built on Skapi (www.skapi.com), so the same project database is also reachable over MCP from any MCP-compatible AI client (mcp.broadwayinc.computer), and this chat can be embedded in a website as a widget with one script tag. Answer product questions from the facts in this section. If you are asked something about BunnyQuery that is NOT stated here - pricing, plan limits, a roadmap, a feature you cannot see - say you are not certain and point the user at the project owner or the BunnyQuery site, rather than inventing it.
322
+ How data gets in: ${canUpload === false ? `this user CANNOT upload in this session (they are not signed in, or the project's database is frozen for non-admins), and the attach affordances are hidden from them. Never instruct them to attach, drag in or upload a file, and never blame a missing answer on them not having uploaded it. Answer from what is already indexed, and when something genuinely is not in the project, say so and suggest asking the project's owner to add it.` : `the user attaches files to a chat message with the paperclip button in the composer, or drags and drops them onto the chat (whole folders work; up to 20 files per message). Uploaded files land in this project's file storage and are indexed automatically: read end to end and turned into database records. "Indexed" means exactly that, and it is why you can only answer from a file once its indexing has finished. While a file indexes, the chat shows a status row for it: yellow while it is working, green when it is indexed, red if it failed. A large file is indexed in windows over several passes, which takes longer; indexing runs on the server, so it keeps going if the user closes the page and the row is still there when they come back. The user can also paste plain text straight into the chat and ask you to save it - store it with the postRecords tool. BunnyQuery reads over 50 formats: office documents (.docx, .xlsx, .pptx, .hwp, .hwpx, .odt, .ods, .odp, .epub), PDFs, images, .csv/.tsv, .json, .xml, .html, .txt/.md and source code. Images and scanned PDFs are read with vision at index time.`}
323
+ Getting answers out: the user asks in plain language, in any language, and you answer from this project's data. You can also produce reports and downloadable files (CSV and the rest) as described in the File generation rules above, and any stored file can be handed back as a link, with images rendering inline in the chat.${client === "console" ? `
324
+ Where things are in the BunnyQuery console (this user is in it, at bunnyquery.com): the left nav has "Query" (this chat), "Files" (browse this project's stored files, upload more, and see which are indexed), "Collaborators" (invite teammates or clients so they can ask questions themselves) and "Settings" (the AI platform, model and API key, the project's description / base prompt, which is added to your instructions, and the Freeze Database switch that blocks writes). Plans and billing live on the project's Subscription page - send the user there rather than quoting prices, which you do not know.` : ""}${client === "widget" ? `
325
+ This chat is the BunnyQuery widget embedded in a website, so the user may have no access to the project console: keep any instructions to what can be done here in the chat.` : ""}`;
326
+ if (greeting) {
327
+ systemPrompt += `
328
+ Your opening message: this chat always opens with a fixed line from you, reading """${greeting}""". It is rendered by the client and is NOT part of the message history you receive, so the user can reply to it ("which files?", "what do you mean by indexed?", "what can you do?") with nothing in the transcript to refer back to. Treat that line as something you said, and answer the follow-up from this section.`;
329
+ }
319
330
  if (serviceDescription) {
320
331
  systemPrompt += `
321
332
  Project name: "${serviceName ?? ""}"
@@ -462,6 +473,14 @@ Records for the earlier windows/pages of this file are ALREADY saved (they refer
462
473
  Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. A \xABPHOTO <cell>\xBB marker in a window marks an embedded picture whose extracted file already has a record in table "__MEDIA__": find it with getRecords reference "src::" + the storage path above and match the cell against data.anchor or tags (a repeated picture is stored under its first anchor only), then enrich it with updateRecords. Never create a photo record of your own and never construct a path for one. Do NOT re-save windows that are already saved. Set every record's reference to exactly "src::" + the storage path above (no sheet, window or summary suffix added). That file record already exists, so do NOT post it; enrich it with updateRecords. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
463
474
  }
464
475
 
476
+ // src/engine/greeting.ts
477
+ function buildChatGreeting(params) {
478
+ const name = params.projectName ? '"' + params.projectName + '"' : "";
479
+ const lead = params.canUpload === false ? "Hi! Ask me anything about the data in your project" : "Hi! Start by attaching the files related to your project";
480
+ const tail = params.canUpload === false ? "." : ", or pasting plain text into the chat. Once they are indexed, ask me anything about that data.";
481
+ return { lead, name, tail, text: lead + (name ? " " + name : "") + tail };
482
+ }
483
+
465
484
  // src/engine/errors.ts
466
485
  var STATUS_MESSAGE = {
467
486
  "408": "The AI provider timed out before it started.",
@@ -6173,6 +6192,7 @@ exports.bgIndexingQueueName = bgIndexingQueueName;
6173
6192
  exports.buildAiAgentValue = buildAiAgentValue;
6174
6193
  exports.buildBoundedChatMessages = buildBoundedChatMessages;
6175
6194
  exports.buildChatDisplayList = buildChatDisplayList;
6195
+ exports.buildChatGreeting = buildChatGreeting;
6176
6196
  exports.buildChatSystemPrompt = buildChatSystemPrompt;
6177
6197
  exports.buildDisplayExpiredAttachmentHref = buildDisplayExpiredAttachmentHref;
6178
6198
  exports.buildHistoryItemFullId = buildHistoryItemFullId;