bunnyquery 1.2.3 → 1.3.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.
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Engine configuration / dependency injection.
3
+ *
4
+ * The engine is framework- and transport-agnostic: it never imports a skapi
5
+ * instance or `import.meta.env`. Each consumer calls `configureChatEngine()`
6
+ * once at startup to inject the skapi transport functions, the MCP base URL,
7
+ * and (optionally) the `poll` value to attach to clientSecretRequest.
8
+ *
9
+ * Why `poll` is configurable: agent.vue uses the npm-bundled skapi-js and OMITS
10
+ * `poll` (its clientSecretRequest auto-resolves with the final body), whereas
11
+ * the BunnyQuery widget uses the deployed skapi-js@latest and must pass
12
+ * `poll: 0` to get the early ack + a manual `.poll()` handle (needed for queued-
13
+ * send cancel). So the request builders include `poll` only when it is set.
14
+ */
15
+ interface ChatEngineConfig {
16
+ /** skapi.clientSecretRequest, bound to the consumer's skapi instance. */
17
+ clientSecretRequest: (opts: any) => Promise<any>;
18
+ /** skapi.clientSecretRequestHistory, bound to the consumer's skapi instance. */
19
+ clientSecretRequestHistory: (params: any, fetchOptions: any) => Promise<any>;
20
+ /** MCP server base URL (prod vs dev resolved by the consumer). */
21
+ mcpBaseUrl: string;
22
+ /**
23
+ * Value to attach as `poll` on every clientSecretRequest. When `undefined`
24
+ * the `poll` key is omitted entirely (agent.vue). BunnyQuery sets `0`.
25
+ */
26
+ poll?: number;
27
+ }
28
+ declare function configureChatEngine(config: ChatEngineConfig): void;
29
+ declare function chatEngineConfig(): ChatEngineConfig;
30
+
31
+ /**
32
+ * Office-file server-side extraction helpers.
33
+ *
34
+ * Office documents (Microsoft .docx/.xlsx/.pptx, Hancom .hwpx, etc.) can't be
35
+ * read by web_fetch (binary/zip). The proxy worker downloads them from db
36
+ * storage, extracts their text server-side, and substitutes that text for a
37
+ * placeholder token in the request body (carried under the reserved
38
+ * `_skapi_extract` key, which the producer strips before the upstream call).
39
+ */
40
+ type ExtractDirective = {
41
+ /** db storage path of the file, e.g. "folder/report.docx" (also the src:: value). */
42
+ path: string;
43
+ /** The exact token in the request body to replace with the extracted text. */
44
+ placeholder: string;
45
+ /** Original filename — informational (server logs only). */
46
+ name?: string;
47
+ /** MIME type — informational (server logs only). */
48
+ mime?: string;
49
+ };
50
+ declare function isOfficeFile(name?: string, mime?: string): boolean;
51
+ declare function makeExtractPlaceholder(seed: string): string;
52
+ interface ComposedUserMessage {
53
+ /** Clean display/history copy (attachment links, NO extraction placeholders). */
54
+ composed: string;
55
+ /** LLM-bound copy — `composed` plus inline office-extraction placeholders. */
56
+ composedForLlm: string;
57
+ /** Office-extraction directives for the proxy worker (undefined if no office files). */
58
+ extractContent?: ExtractDirective[];
59
+ }
60
+ declare function composeUserMessage(text: string, attachmentUrls: Array<{
61
+ name: string;
62
+ url: string;
63
+ storagePath?: string;
64
+ }>): ComposedUserMessage;
65
+
66
+ /**
67
+ * BASE PROMPT — Chat assistant
68
+ * ============================================================================
69
+ * System prompt sent on every chat turn. Rebuilt fresh on every send because
70
+ * the project name/description can change at any time.
71
+ *
72
+ * The `${...}` placeholders are filled from the live project (service):
73
+ * formattedServiceId -> the project ID the assistant is scoped to
74
+ * serviceName -> project display name (only added if a description exists)
75
+ * serviceDescription -> project description (only added if present)
76
+ */
77
+ type ChatSystemPromptParams = {
78
+ /** The project/service ID this assistant is scoped to (formatted form). */
79
+ formattedServiceId: string;
80
+ /** Project display name. Only appended when a description is also present. */
81
+ serviceName?: string;
82
+ /** Project description. When present, name + description are appended. */
83
+ serviceDescription?: string;
84
+ };
85
+ declare function buildChatSystemPrompt(params: ChatSystemPromptParams): string;
86
+
87
+ /**
88
+ * BASE PROMPT — Background file-indexing agent (system prompt)
89
+ * ============================================================================
90
+ * System prompt for the BACKGROUND indexing agent (notifyAgentSaveAttachment).
91
+ * Its only job is to read the freshly uploaded file and persist what it learns
92
+ * into the project's knowledge base via the MCP tools. Pairs with the
93
+ * user-message template in ./indexing_user_message.ts.
94
+ */
95
+ type IndexingSystemPromptParams = {
96
+ /** The project/service ID being indexed into. */
97
+ service: string;
98
+ /** Project display name. Only appended when a description is also present. */
99
+ serviceName?: string;
100
+ /** Project description. When present, name + description are appended. */
101
+ serviceDescription?: string;
102
+ };
103
+ declare function buildIndexingSystemPrompt(params: IndexingSystemPromptParams): string;
104
+
105
+ /**
106
+ * BASE PROMPT — Background file-indexing agent (user message)
107
+ * ============================================================================
108
+ * USER-role message paired with the indexing system prompt. Sent by
109
+ * notifyAgentSaveAttachment() each time a file is uploaded or re-indexed.
110
+ *
111
+ * NOTE: the leading line "A new file has just been uploaded. Index it now." and
112
+ * the "- name: ..." line are also what the chat client parses to build the
113
+ * "Indexing: <name>" history bubble — keep those fields on their own lines.
114
+ */
115
+ type IndexingAttachmentInfo = {
116
+ /** Original file name. */
117
+ name: string;
118
+ /** Storage path within the project's db storage. */
119
+ storagePath: string;
120
+ /** MIME type, if detected. Omitted from the message when unknown. */
121
+ mime?: string;
122
+ /** File size in bytes, if known. Omitted from the message when unknown. */
123
+ size?: number;
124
+ /** Temporary signed URL the agent/MCP fetches to read the file contents. */
125
+ url: string;
126
+ };
127
+ type BuildIndexingUserMessageOptions = {
128
+ /**
129
+ * For office files (.docx/.xlsx/.pptx) the model can't read the binary via
130
+ * web_fetch, so the proxy worker extracts the text server-side and replaces
131
+ * this exact token with it. When provided, the message embeds the token (and
132
+ * drops the temporary-URL line — there is nothing for the model to fetch).
133
+ */
134
+ inlineContentPlaceholder?: string;
135
+ };
136
+ declare function buildIndexingUserMessage(attachment: IndexingAttachmentInfo, options?: BuildIndexingUserMessageOptions): string;
137
+
138
+ /**
139
+ * Error detection + message extraction (pure). Moved verbatim from the
140
+ * agent.vue / bunnyquery chatbox so both consumers share one implementation.
141
+ */
142
+ declare function getErrorMessage(input: any): string;
143
+ declare function isErrorResponseBody(response: any): boolean;
144
+ declare function isAuthExpiredError(input: any): boolean;
145
+
146
+ declare var CONTEXT_WINDOW_DEFAULT: Record<string, number>;
147
+ declare var CONTEXT_WINDOW_BY_MODEL: Record<string, number>;
148
+ declare var OUTPUT_TOKEN_RESERVE: number;
149
+ declare var TOOL_AND_RESPONSE_BUFFER: number;
150
+ declare var MIN_INPUT_TOKEN_BUDGET: number;
151
+ declare var CLAUDE_PER_REQUEST_INPUT_CAP: number;
152
+ declare var MAX_HISTORY_MESSAGES: number;
153
+ declare var HISTORY_TOKEN_BUDGET: number;
154
+ declare function estimateTextTokens(text: string): number;
155
+ declare function estimateMessageTokens(msg: {
156
+ role: string;
157
+ content: string;
158
+ }): number;
159
+ declare function getContextWindow(platform: string, model?: string): number;
160
+ declare function stripFileBlocksFromHistory(content: string): string;
161
+ type BoundedChatOptions = {
162
+ platform: string;
163
+ model?: string;
164
+ systemPrompt: string;
165
+ history: Array<{
166
+ role: string;
167
+ content: string;
168
+ }>;
169
+ /** Used to strip/rewrite expired attachment links in older user turns. */
170
+ serviceId: string;
171
+ };
172
+ declare function buildBoundedChatMessages(options: BoundedChatOptions): {
173
+ messages: {
174
+ role: string;
175
+ content: string;
176
+ }[];
177
+ droppedCount: number;
178
+ estimatedInputTokens: number;
179
+ estimatedBudget: number;
180
+ };
181
+
182
+ /**
183
+ * Pure link/path helpers (no DOM, no marked). Moved verbatim from the chatbox.
184
+ * `serviceId` is passed as a PARAMETER (the original read it from a global) so
185
+ * the engine stays consumer-agnostic. The HTML-emitting helpers
186
+ * (buildLinkPartFromGroups, linkToAnchorHtml, fileToAnchorHtml, parseMsgParts*)
187
+ * stay in each VIEW — only these pure pieces move here.
188
+ */
189
+ declare var EXPIRED_ATTACHMENT_URL_HOST: string;
190
+ declare var EXPIRED_ATTACHMENT_URL_ORIGIN: string;
191
+ declare var LINK_LABEL_MAX_DISPLAY_CHARS: number;
192
+ declare function createInlineLinkRegex(): RegExp;
193
+ declare function safeDecodeURIComponent(v: string): string;
194
+ declare function encodePathSegments(path: string): string;
195
+ declare function normalizeAttachmentPathCandidate(value: string): string;
196
+ declare function extractRemotePathFromAttachmentHref(href: string, serviceId: string): string | null;
197
+ declare function getExpiredAttachmentVisiblePath(remotePath: string, fallback?: string): string;
198
+ declare function buildDisplayExpiredAttachmentHref(remotePath: string, fallback?: string): string;
199
+ declare function sanitizeAttachmentLinksForHistory(content: string, serviceId: string): string;
200
+ declare function truncateLabelForDisplay(label: string): string;
201
+
202
+ declare function filterListByClearHorizon(list: any[], clearedAt: number): any[];
203
+ declare function normalizeTextContent(content: any): string;
204
+ declare function extractLastUserTextFromRequest(requestBody: any): string;
205
+ type MapHistoryOptions = {
206
+ clearedAt: number;
207
+ serviceId: string;
208
+ /** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
209
+ formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string) => string;
210
+ };
211
+ declare function mapHistoryListToMessages(list: any[], platform: 'claude' | 'openai', opts: MapHistoryOptions): {
212
+ messages: any[];
213
+ runningItemIds: string[];
214
+ };
215
+
216
+ declare const MCP_NAME = "BunnyQuery";
217
+ declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
218
+ declare const DEFAULT_OPENAI_MODEL = "gpt-5.4";
219
+ type ClaudeRole = 'user' | 'assistant';
220
+ type ClaudeMessage = {
221
+ role: ClaudeRole;
222
+ content: string;
223
+ };
224
+ type OpenAIMessage = {
225
+ role: ClaudeRole;
226
+ content: string;
227
+ };
228
+ type ClaudeMcpToolConfig = {
229
+ enabled?: boolean;
230
+ defer_loading?: boolean;
231
+ };
232
+ type ClaudeMcpServerRequest = {
233
+ name: string;
234
+ url: string;
235
+ authorizationToken?: string;
236
+ defaultConfig?: ClaudeMcpToolConfig;
237
+ configs?: Record<string, ClaudeMcpToolConfig>;
238
+ };
239
+ declare function transformContentWithImages(content: string): string | Array<Record<string, any>>;
240
+ declare function transformContentWithOpenAIImages(content: string, detail?: string): string | Array<Record<string, any>>;
241
+ type CallClaudeWithMcpParams = {
242
+ prompt: string;
243
+ messages?: ClaudeMessage[];
244
+ service: string;
245
+ owner: string;
246
+ userId?: string;
247
+ model?: string;
248
+ maxTokens?: number;
249
+ system?: string;
250
+ mcpServer: ClaudeMcpServerRequest;
251
+ extractContent?: ExtractDirective[];
252
+ onResponse?: (res: any) => void;
253
+ onError?: (err: any) => void;
254
+ };
255
+ declare const POLL_INTERVAL = 1500;
256
+ declare function callClaudeWithMcp({ prompt, messages, service, owner, userId, model, maxTokens, system, mcpServer, extractContent, }: CallClaudeWithMcpParams): Promise<any>;
257
+ declare function callClaudeWithPublicMcp(prompt: string, service: string, owner: string, messages?: ClaudeMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
258
+ declare function callOpenAIWithPublicMcp(prompt: string, service: string, owner: string, messages?: OpenAIMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
259
+ type AttachmentSaveInfo = {
260
+ platform: 'claude' | 'openai';
261
+ model?: string;
262
+ service: string;
263
+ owner: string;
264
+ userId?: string;
265
+ serviceName?: string;
266
+ serviceDescription?: string;
267
+ attachment: {
268
+ name: string;
269
+ storagePath: string;
270
+ mime?: string;
271
+ size?: number;
272
+ url: string;
273
+ };
274
+ };
275
+ declare function notifyAgentSaveAttachment(info: AttachmentSaveInfo): Promise<any>;
276
+ declare function extractClaudeText(response: any): any;
277
+ declare function extractOpenAIText(response: any): any;
278
+ declare function listClaudeModels(service: string, owner: string): Promise<any>;
279
+ declare function listOpenAIModels(service: string, owner: string): Promise<any>;
280
+ declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
281
+ type BgTaskEntry = {
282
+ serviceId: string;
283
+ platform: 'claude' | 'openai';
284
+ id: string;
285
+ filename: string;
286
+ storagePath?: string;
287
+ isReindex?: boolean;
288
+ mime?: string;
289
+ size?: number;
290
+ status: 'running' | 'pending';
291
+ poll: ((opts: {
292
+ latency: number;
293
+ }) => Promise<any>) | undefined;
294
+ };
295
+ declare function getChatHistory(params: {
296
+ service?: string;
297
+ owner?: string;
298
+ platform: 'claude' | 'openai';
299
+ queue?: string;
300
+ }, fetchOptions: Record<string, any>): Promise<any>;
301
+
302
+ /**
303
+ * ChatSession host adapter + state types.
304
+ *
305
+ * ChatSession is DOM-free and Vue-free; the consumer (bunnyquery widget or the
306
+ * agent.vue chatbox) implements `ChatHost` to bridge identity, rendering, scroll,
307
+ * and the skapi cancel/refresh surface. Everything the session needs that would
308
+ * otherwise touch the DOM or a framework goes through a host hook.
309
+ */
310
+ interface ChatIdentity {
311
+ serviceId: string;
312
+ owner: string;
313
+ /** Per-user queue name (falls back to serviceId). */
314
+ userId: string;
315
+ platform: 'claude' | 'openai' | 'none';
316
+ model?: string;
317
+ serviceName?: string;
318
+ serviceDescription?: string;
319
+ }
320
+ interface ChatMessage {
321
+ role: 'user' | 'assistant';
322
+ content: string;
323
+ isPending?: boolean;
324
+ isPendingInProcess?: boolean;
325
+ isPendingQueued?: boolean;
326
+ isPendingOlder?: boolean;
327
+ isSendingToServer?: boolean;
328
+ isCancelled?: boolean;
329
+ isError?: boolean;
330
+ isBackgroundTask?: boolean;
331
+ _useBgQueue?: boolean;
332
+ _serverItemId?: string;
333
+ _localId?: string;
334
+ _cancelling?: boolean;
335
+ _cancelError?: string;
336
+ }
337
+ interface ChatState {
338
+ messages: ChatMessage[];
339
+ /** Pending/uploaded attachment objects (view-shaped); the engine mutates
340
+ * status/progress during upload, the view renders them. */
341
+ attachments: any[];
342
+ uploadingAttachments: boolean;
343
+ sending: boolean;
344
+ typing: boolean;
345
+ typingAbort: boolean;
346
+ loadingHistory: boolean;
347
+ loadingOlderHistory: boolean;
348
+ historyEndOfList: boolean;
349
+ historyStartKeyHistory: string[];
350
+ historyRequestToken: number;
351
+ gateRefreshToken: number;
352
+ }
353
+ interface ChatHost {
354
+ /** Read live (platform/model/name can change between sends). */
355
+ getIdentity(): ChatIdentity;
356
+ /** The chat system prompt (consumer-built; agent.vue uses a formatted id). */
357
+ buildSystemPrompt(): string;
358
+ /** Re-render the whole message list (coalesced). */
359
+ notify(): void;
360
+ /** Re-render a single message bubble in place (typewriter ticks). */
361
+ refreshMessageBubble(idx: number): void;
362
+ scrollToBottom(smooth?: boolean): Promise<void> | void;
363
+ /** Scroll only if the user is pinned to the bottom (does not force-pin). */
364
+ scrollToBottomIfSticky(smooth?: boolean): Promise<void> | void;
365
+ cancelRequest(opts: {
366
+ url: string;
367
+ method: string;
368
+ id: string;
369
+ queue: string;
370
+ service: string;
371
+ owner: string;
372
+ }): Promise<{
373
+ removed?: boolean;
374
+ message?: string;
375
+ } | any>;
376
+ refreshSession(): Promise<boolean>;
377
+ /** Build the "Indexing:/Reindexing: …" label (view-side display formatting). */
378
+ formatIndexingLabel(name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean): string;
379
+ /** drainBgTaskQueue is a no-op until the chat view is mounted. */
380
+ isViewMounted(): boolean;
381
+ /** Clear-horizon timestamp (localStorage, per service#platform) — view-owned. */
382
+ getClearedAt(): number;
383
+ uploadFile(args: {
384
+ file: File;
385
+ storagePath: string;
386
+ checkExistence: boolean;
387
+ onProgress?: (p: any) => void;
388
+ setAbort?: (abort: () => void) => void;
389
+ }): Promise<any>;
390
+ /** Mint a temporary CDN URL for a stored file. */
391
+ getTemporaryUrl(storagePath: string): Promise<string>;
392
+ /** Map a relative path to the consumer's db storage key (e.g. uid-prefixed). */
393
+ storagePathFor(relPath: string): string;
394
+ getMimeType(name: string): string | null;
395
+ /** Non-dismissible "file exists" prompt → keep+reindex vs overwrite. */
396
+ promptOverwrite(filename: string): Promise<'overwrite' | 'reindex'>;
397
+ /** Clear the "apply to all" overwrite choice at the start of a batch. */
398
+ resetOverwriteBatch(): void;
399
+ /** Re-render the attachment chip row (progress / status). */
400
+ renderAttachmentChips(): void;
401
+ /** Enable/disable composer controls during an upload batch. */
402
+ updateComposerControls(): void;
403
+ }
404
+
405
+ /**
406
+ * ChatSession — framework-agnostic stateful chat orchestration.
407
+ *
408
+ * Ported verbatim from the bunnyquery widget's in-place state machine (which was
409
+ * itself ported from agent.vue), with three mechanical substitutions:
410
+ * CS.<field> -> this.state.<field>
411
+ * renderMessages() -> this.host.notify()
412
+ * S.<x> / skapi -> this.host.getIdentity().<x> / this.host.cancelRequest / refreshSession
413
+ * scroll/refresh -> this.host.scrollToBottom(IfSticky) / refreshMessageBubble
414
+ * Module-level singletons (bgTaskQueue, aiChatHistoryCache, pendingAgentRequests,
415
+ * cancelledServerIds, historyItemPolls) become instance fields. The provider
416
+ * request builders are reached through the engine (which already has the skapi
417
+ * transport + poll injected via configureChatEngine), so cancel/poll behavior is
418
+ * preserved.
419
+ *
420
+ * The view (per consumer) keeps: rendering, markdown PARSE, DOM refs + scroll
421
+ * measurement, attachment chips, and the auth/account shell. It drives the
422
+ * session via the public methods and re-renders in host.notify().
423
+ */
424
+
425
+ declare class ChatSession {
426
+ host: ChatHost;
427
+ state: ChatState;
428
+ bgTaskQueue: BgTaskEntry[];
429
+ cancelledServerIds: Set<string>;
430
+ pendingAgentRequests: Record<string, Promise<any>>;
431
+ aiChatHistoryCache: Record<string, {
432
+ messages: ChatMessage[];
433
+ endOfList: boolean;
434
+ startKeyHistory: string[];
435
+ }>;
436
+ historyItemPolls: Map<string, boolean>;
437
+ private _lidSeq;
438
+ constructor(host: ChatHost);
439
+ private _newLocalId;
440
+ getHistoryCacheKey(): string;
441
+ updateHistoryCache(): void;
442
+ private _callProviderFor;
443
+ dispatchAgentRequest(params: any): Promise<any>;
444
+ dispatchComposedMessage(composed: string, useBgQueue?: boolean, composedForLlm?: string, extractContent?: any): void;
445
+ promoteNextBgQueuedToRunning(): void;
446
+ promoteNextQueuedToRunning(): void;
447
+ resolveQueuedUserBubble(serverId?: string): number | undefined;
448
+ insertAtTarget(msg: ChatMessage, targetIdx: number): void;
449
+ onQueuedSendResponse(_composed: string, response: any, platform: string, serverId?: string): void;
450
+ onQueuedSendError(_composed: string, err: any, serverId?: string): void;
451
+ cancelQueuedMessage(msg: ChatMessage, idx: number): void;
452
+ typewriteIntoIndex(idx: number, fullText: string, localId?: string): Promise<void>;
453
+ private typewriterQueue;
454
+ enqueueTypewrite(idx: number, fullText: string, localId?: string): Promise<any>;
455
+ typewriteLatestReply(key: string): Promise<any>;
456
+ resumePendingRequest(token: number): Promise<void>;
457
+ handleHistoryItemResolution(itemId: string, response: any, platform: string): void;
458
+ applyHistoryItemResolution(itemId: string, response: any, platform: string): void;
459
+ drainBgTaskQueue(): void;
460
+ loadHistory(fetchMore?: boolean, token?: number): Promise<void>;
461
+ uploadSingleAttachment(att: any): Promise<Array<{
462
+ name: string;
463
+ url: string;
464
+ storagePath: string;
465
+ }>>;
466
+ uploadPendingAttachments(): Promise<Array<{
467
+ name: string;
468
+ url: string;
469
+ storagePath?: string;
470
+ }>>;
471
+ stop(): void;
472
+ bumpGate(): void;
473
+ }
474
+
475
+ export { type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, isAuthExpiredError, isErrorResponseBody, isOfficeFile, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };