@diegoaltoworks/chatter 0.61.0 → 0.63.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.
Files changed (125) hide show
  1. package/README.md +55 -17
  2. package/dist/auth/secretStrength.d.ts +1 -1
  3. package/dist/auth/secretStrength.d.ts.map +1 -1
  4. package/dist/bin/create-apikey.js +45 -28
  5. package/dist/bin/wa-pair.js +113 -46
  6. package/dist/channels/backoff.d.ts +10 -0
  7. package/dist/channels/backoff.d.ts.map +1 -0
  8. package/dist/channels/gates.d.ts +15 -8
  9. package/dist/channels/gates.d.ts.map +1 -1
  10. package/dist/channels/index.d.ts +1 -1
  11. package/dist/channels/index.d.ts.map +1 -1
  12. package/dist/channels/index.js +38 -5
  13. package/dist/channels/index.mjs +37 -5
  14. package/dist/channels/matrix/api.d.ts +25 -6
  15. package/dist/channels/matrix/api.d.ts.map +1 -1
  16. package/dist/channels/matrix/channel.d.ts +4 -56
  17. package/dist/channels/matrix/channel.d.ts.map +1 -1
  18. package/dist/channels/matrix/handler.d.ts +16 -9
  19. package/dist/channels/matrix/handler.d.ts.map +1 -1
  20. package/dist/channels/matrix/index.d.ts +2 -2
  21. package/dist/channels/matrix/index.d.ts.map +1 -1
  22. package/dist/channels/matrix/index.js +196 -40
  23. package/dist/channels/matrix/index.mjs +189 -39
  24. package/dist/channels/matrix/sync.d.ts +4 -1
  25. package/dist/channels/matrix/sync.d.ts.map +1 -1
  26. package/dist/channels/matrix/updates.d.ts +39 -10
  27. package/dist/channels/matrix/updates.d.ts.map +1 -1
  28. package/dist/channels/pipeline.d.ts +14 -14
  29. package/dist/channels/pipeline.d.ts.map +1 -1
  30. package/dist/channels/polling.d.ts +67 -0
  31. package/dist/channels/polling.d.ts.map +1 -0
  32. package/dist/channels/telegram/channel.d.ts +2 -56
  33. package/dist/channels/telegram/channel.d.ts.map +1 -1
  34. package/dist/channels/telegram/index.js +53 -23
  35. package/dist/channels/telegram/index.mjs +53 -23
  36. package/dist/channels/telegram/poll.d.ts +5 -1
  37. package/dist/channels/telegram/poll.d.ts.map +1 -1
  38. package/dist/channels/telegram/webhook.d.ts +2 -10
  39. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  40. package/dist/channels/whatsapp/authState.d.ts +40 -9
  41. package/dist/channels/whatsapp/authState.d.ts.map +1 -1
  42. package/dist/channels/whatsapp/channel.d.ts +39 -4
  43. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  44. package/dist/channels/whatsapp/inbound.d.ts +23 -25
  45. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  46. package/dist/channels/whatsapp/index.d.ts +3 -3
  47. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  48. package/dist/channels/whatsapp/index.js +188 -66
  49. package/dist/channels/whatsapp/index.mjs +185 -66
  50. package/dist/channels/whatsapp/pairing.d.ts.map +1 -1
  51. package/dist/client/src/Chat.d.ts.map +1 -1
  52. package/dist/client/src/ChatButton.d.ts.map +1 -1
  53. package/dist/client/src/browser.d.ts.map +1 -1
  54. package/dist/core/answer.d.ts +8 -0
  55. package/dist/core/answer.d.ts.map +1 -1
  56. package/dist/core/buildLock.d.ts +52 -0
  57. package/dist/core/buildLock.d.ts.map +1 -0
  58. package/dist/core/pipeline.d.ts +42 -5
  59. package/dist/core/pipeline.d.ts.map +1 -1
  60. package/dist/core/prompts.d.ts +2 -0
  61. package/dist/core/prompts.d.ts.map +1 -1
  62. package/dist/core/retrieval.d.ts +100 -25
  63. package/dist/core/retrieval.d.ts.map +1 -1
  64. package/dist/core/session.d.ts +8 -0
  65. package/dist/core/session.d.ts.map +1 -1
  66. package/dist/flows/index.d.ts +1 -1
  67. package/dist/flows/index.d.ts.map +1 -1
  68. package/dist/flows/index.js +8 -6
  69. package/dist/flows/index.mjs +7 -6
  70. package/dist/flows/intent.d.ts +1 -1
  71. package/dist/flows/intent.d.ts.map +1 -1
  72. package/dist/flows/loader.d.ts +1 -2
  73. package/dist/flows/loader.d.ts.map +1 -1
  74. package/dist/flows/params.d.ts +1 -1
  75. package/dist/flows/params.d.ts.map +1 -1
  76. package/dist/flows/session.d.ts +4 -2
  77. package/dist/flows/session.d.ts.map +1 -1
  78. package/dist/flows/types.d.ts +2 -3
  79. package/dist/flows/types.d.ts.map +1 -1
  80. package/dist/history/index.d.ts +1 -1
  81. package/dist/history/index.d.ts.map +1 -1
  82. package/dist/history/index.js +6 -3
  83. package/dist/history/index.mjs +5 -3
  84. package/dist/history/tursoStore.d.ts +6 -4
  85. package/dist/history/tursoStore.d.ts.map +1 -1
  86. package/dist/images/index.js +2 -2
  87. package/dist/images/index.mjs +2 -2
  88. package/dist/index.d.ts +7 -5
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +596 -295
  91. package/dist/index.mjs +595 -295
  92. package/dist/mcp-server/index.d.ts +5 -5
  93. package/dist/mcp-server/index.d.ts.map +1 -1
  94. package/dist/mcp-server/logger.d.ts +12 -2
  95. package/dist/mcp-server/logger.d.ts.map +1 -1
  96. package/dist/mcp-server/rate-limiter.d.ts +10 -3
  97. package/dist/mcp-server/rate-limiter.d.ts.map +1 -1
  98. package/dist/mcp-server/types.d.ts +20 -5
  99. package/dist/mcp-server/types.d.ts.map +1 -1
  100. package/dist/mcp-server.js +479 -244
  101. package/dist/mcp-server.mjs +485 -244
  102. package/dist/middleware/apiKey.d.ts +10 -0
  103. package/dist/middleware/apiKey.d.ts.map +1 -0
  104. package/dist/middleware/auth.d.ts.map +1 -1
  105. package/dist/middleware/cors.d.ts.map +1 -1
  106. package/dist/middleware/ratelimit.d.ts.map +1 -1
  107. package/dist/middleware/referrer.d.ts.map +1 -1
  108. package/dist/middleware/session.d.ts +1 -1
  109. package/dist/middleware/session.d.ts.map +1 -1
  110. package/dist/routes/demo.d.ts.map +1 -1
  111. package/dist/routes/openai.d.ts.map +1 -1
  112. package/dist/routes/private.d.ts.map +1 -1
  113. package/dist/routes/public.d.ts.map +1 -1
  114. package/dist/scheduler/index.js +1 -1
  115. package/dist/scheduler/index.mjs +1 -1
  116. package/dist/scheduler/scheduler.d.ts +9 -1
  117. package/dist/scheduler/scheduler.d.ts.map +1 -1
  118. package/dist/server.d.ts.map +1 -1
  119. package/dist/server.js +491 -268
  120. package/dist/server.mjs +497 -268
  121. package/dist/shortener.d.ts +5 -4
  122. package/dist/shortener.d.ts.map +1 -1
  123. package/dist/types.d.ts +129 -75
  124. package/dist/types.d.ts.map +1 -1
  125. package/package.json +6 -3
package/dist/server.js CHANGED
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
15
+ };
8
16
  var __export = (target, all) => {
9
17
  for (var name in all)
10
18
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +35,382 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
35
  ));
28
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
37
 
38
+ // src/core/logger.ts
39
+ function createConsoleLogger(level = "info") {
40
+ const threshold = LEVEL_ORDER[level];
41
+ const write = (at) => (...args) => {
42
+ if (LEVEL_ORDER[at] < threshold) return;
43
+ console.error(`[${at}]`, ...args);
44
+ };
45
+ return {
46
+ debug: write("debug"),
47
+ info: write("info"),
48
+ warn: write("warn"),
49
+ error: write("error")
50
+ };
51
+ }
52
+ function resolveLogger(logger, level) {
53
+ return logger ?? createConsoleLogger(level);
54
+ }
55
+ var LEVEL_ORDER;
56
+ var init_logger = __esm({
57
+ "src/core/logger.ts"() {
58
+ "use strict";
59
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
60
+ }
61
+ });
62
+
63
+ // src/core/buildLock.ts
64
+ function ensureBuildLockSchema(client) {
65
+ let ready = schemaReady.get(client);
66
+ if (!ready) {
67
+ ready = client.execute(
68
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
69
+ lock_key TEXT PRIMARY KEY,
70
+ holder_id TEXT NOT NULL,
71
+ heartbeat_at INTEGER NOT NULL
72
+ )`
73
+ ).catch((error) => {
74
+ schemaReady.delete(client);
75
+ throw error;
76
+ });
77
+ schemaReady.set(client, ready);
78
+ }
79
+ return ready;
80
+ }
81
+ function createTursoBuildLock(client) {
82
+ return {
83
+ async tryAcquire(key, holderId, now, staleMs) {
84
+ await ensureBuildLockSchema(client);
85
+ const result = await client.execute({
86
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
87
+ ON CONFLICT(lock_key) DO UPDATE SET
88
+ holder_id = excluded.holder_id,
89
+ heartbeat_at = excluded.heartbeat_at
90
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
91
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
92
+ RETURNING holder_id`,
93
+ args: [key, holderId, now, staleMs]
94
+ });
95
+ const row = result.rows[0];
96
+ return Boolean(row) && row.holder_id === holderId;
97
+ },
98
+ async release(key, holderId) {
99
+ await ensureBuildLockSchema(client);
100
+ await client.execute({
101
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
102
+ args: [key, holderId]
103
+ });
104
+ }
105
+ };
106
+ }
107
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
108
+ var init_buildLock = __esm({
109
+ "src/core/buildLock.ts"() {
110
+ "use strict";
111
+ BUILD_LOCK_KEY = "knowledge_build";
112
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
113
+ schemaReady = /* @__PURE__ */ new WeakMap();
114
+ }
115
+ });
116
+
117
+ // src/core/loaders.ts
118
+ function walk(dir) {
119
+ const items = [];
120
+ try {
121
+ for (const e of (0, import_node_fs2.readdirSync)(dir)) {
122
+ const p = (0, import_node_path2.join)(dir, e);
123
+ const st = (0, import_node_fs2.statSync)(p);
124
+ if (st.isDirectory()) items.push(...walk(p));
125
+ else if (p.endsWith(".md")) items.push(p);
126
+ }
127
+ } catch {
128
+ }
129
+ return items;
130
+ }
131
+ function loadKnowledge(root = "config/knowledge") {
132
+ const docs = [];
133
+ for (const bucket of ["base", "public", "private"]) {
134
+ const dir = (0, import_node_path2.join)(root, bucket);
135
+ for (const f of walk(dir)) {
136
+ docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
137
+ }
138
+ }
139
+ return docs;
140
+ }
141
+ var import_node_fs2, import_node_path2;
142
+ var init_loaders = __esm({
143
+ "src/core/loaders.ts"() {
144
+ "use strict";
145
+ import_node_fs2 = require("node:fs");
146
+ import_node_path2 = require("node:path");
147
+ }
148
+ });
149
+
150
+ // src/core/retrieval.ts
151
+ var retrieval_exports = {};
152
+ __export(retrieval_exports, {
153
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
154
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
155
+ VectorStore: () => VectorStore,
156
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
157
+ openLibsqlClient: () => openLibsqlClient,
158
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
159
+ });
160
+ function createOpenAIEmbedder(client) {
161
+ return async (input) => {
162
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
163
+ return res.data.map((d) => d.embedding);
164
+ };
165
+ }
166
+ function wrapMissingLibsqlError(cause) {
167
+ return new Error(
168
+ "Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
169
+ { cause }
170
+ );
171
+ }
172
+ async function openLibsqlClient(database) {
173
+ const mod = await import("@libsql/client").catch((error) => {
174
+ throw wrapMissingLibsqlError(error);
175
+ });
176
+ return mod.createClient({ url: database.url, authToken: database.authToken });
177
+ }
178
+ function chunk(text, max = 900) {
179
+ const out = [];
180
+ let buf = "";
181
+ for (const line of text.replace(/\r/g, "").split("\n")) {
182
+ if (`${buf}
183
+ ${line}`.length > max) {
184
+ out.push(buf.trim());
185
+ buf = line;
186
+ } else buf += `
187
+ ${line}`;
188
+ }
189
+ if (buf.trim()) out.push(buf.trim());
190
+ return out;
191
+ }
192
+ async function sha256(input) {
193
+ const data = new TextEncoder().encode(input);
194
+ const hash = await crypto.subtle.digest("SHA-256", data);
195
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
196
+ }
197
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
198
+ var init_retrieval = __esm({
199
+ "src/core/retrieval.ts"() {
200
+ "use strict";
201
+ init_buildLock();
202
+ init_loaders();
203
+ init_logger();
204
+ EMB_MODEL = "text-embedding-3-large";
205
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
206
+ DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
207
+ VectorStore = class _VectorStore {
208
+ constructor(embed, options) {
209
+ this.embed = embed;
210
+ this.db = options.databaseClient;
211
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
212
+ this.logger = options.logger ?? createConsoleLogger();
213
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
214
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
215
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
216
+ }
217
+ embed;
218
+ /**
219
+ * The libsql client backing this store - the same instance passed in as
220
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
221
+ * and the store share one connection.
222
+ */
223
+ db;
224
+ knowledgeDir;
225
+ logger;
226
+ buildLock;
227
+ instanceId;
228
+ buildLockStaleMs;
229
+ /**
230
+ * On boot: ingest new chunks and embed only missing ones.
231
+ *
232
+ * The whole ingest runs under a single-writer lock held in the database
233
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
234
+ * every chunk id the current `knowledgeDir` did not produce. Without the
235
+ * lock, a second instance booting mid-build diffs against a database the
236
+ * first one is still writing and deletes its chunks. An instance that
237
+ * cannot take the lock skips its build entirely and serves what the holder
238
+ * has already written, rather than racing it.
239
+ */
240
+ async build() {
241
+ this.logger.info("Building knowledge base...");
242
+ await this.db.execute(`
243
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
244
+ `);
245
+ await this.db.execute(`
246
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
247
+ `);
248
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
249
+ if (!await this.renewBuildLock()) {
250
+ this.logger.warn(
251
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
252
+ );
253
+ return;
254
+ }
255
+ try {
256
+ await this.buildUnderLock();
257
+ } finally {
258
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
259
+ }
260
+ }
261
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
262
+ renewBuildLock() {
263
+ return this.buildLock.tryAcquire(
264
+ BUILD_LOCK_KEY,
265
+ this.instanceId,
266
+ Date.now(),
267
+ this.buildLockStaleMs
268
+ );
269
+ }
270
+ /** The ingest itself. Only ever runs with the build lock held. */
271
+ async buildUnderLock() {
272
+ const docs = loadKnowledge(this.knowledgeDir);
273
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
274
+ if (docs.length === 0) {
275
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
276
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
277
+ if (existingCount > 0) {
278
+ throw new Error(
279
+ `Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
280
+ );
281
+ }
282
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
283
+ return;
284
+ }
285
+ const rows = [];
286
+ for (const d of docs) {
287
+ for (const part of chunk(d.text)) {
288
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
289
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
290
+ }
291
+ }
292
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
293
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
294
+ const UPSERT_BATCH = 500;
295
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
296
+ const batch = rows.slice(i, i + UPSERT_BATCH);
297
+ await this.db.batch(
298
+ batch.map((r) => ({
299
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
300
+ ON CONFLICT(id) DO NOTHING`,
301
+ args: [r.id, r.bucket, r.source, r.text]
302
+ })),
303
+ "write"
304
+ );
305
+ }
306
+ const ids = rows.map((r) => r.id);
307
+ const missing = [];
308
+ for (let i = 0; i < ids.length; i += 500) {
309
+ const batch = ids.slice(i, i + 500);
310
+ const placeholders = batch.map(() => "?").join(",");
311
+ const res = await this.db.execute({
312
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
313
+ EXCEPT SELECT id FROM embeddings`,
314
+ args: batch
315
+ });
316
+ for (const row of res.rows) missing.push(String(row.id));
317
+ }
318
+ if (missing.length === 0) {
319
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
320
+ return;
321
+ }
322
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
323
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
324
+ const BATCH = 96;
325
+ for (let i = 0; i < missing.length; i += BATCH) {
326
+ const batchIds = missing.slice(i, i + BATCH);
327
+ const inputs = batchIds.map((id) => textById.get(id) || "");
328
+ const vectors = await this.embed(inputs);
329
+ await this.db.batch(
330
+ vectors.map((embedding, idx) => ({
331
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
332
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
333
+ })),
334
+ "write"
335
+ );
336
+ if (!await this.renewBuildLock()) {
337
+ this.logger.warn(
338
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
339
+ );
340
+ }
341
+ }
342
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
343
+ }
344
+ // Remove chunks from database that no longer exist in markdown files
345
+ async cleanupStaleChunks(currentIds) {
346
+ const result = await this.db.execute("SELECT id FROM chunks");
347
+ const dbIds = result.rows.map((row) => String(row.id));
348
+ const currentIdSet = new Set(currentIds);
349
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
350
+ if (staleIds.length === 0) {
351
+ return;
352
+ }
353
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
354
+ const BATCH_SIZE = 500;
355
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
356
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
357
+ const placeholders = batch.map(() => "?").join(",");
358
+ await this.db.batch(
359
+ [
360
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
361
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
362
+ ],
363
+ "write"
364
+ );
365
+ }
366
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
367
+ }
368
+ static cosine(a, b) {
369
+ let dot = 0;
370
+ let na = 0;
371
+ let nb = 0;
372
+ for (let i = 0; i < a.length; i++) {
373
+ dot += a[i] * b[i];
374
+ na += a[i] * a[i];
375
+ nb += b[i] * b[i];
376
+ }
377
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
378
+ }
379
+ /**
380
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
381
+ * & portable).
382
+ *
383
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
384
+ * `chunks` table constrains nothing, so a deployment that ingests its own
385
+ * buckets can query them (`build` only writes and prunes the three the
386
+ * knowledge loader walks). Names are bound as query parameters, never
387
+ * interpolated. An empty list retrieves nothing, and short-circuits before
388
+ * the embedding call.
389
+ */
390
+ async query(q, k = 6, allowed = ["base"]) {
391
+ if (allowed.length === 0) return [];
392
+ const [qv] = await this.embed([q]);
393
+ const placeholders = allowed.map(() => "?").join(",");
394
+ const res = await this.db.execute({
395
+ sql: `SELECT c.id, c.text, e.embedding
396
+ FROM chunks c
397
+ JOIN embeddings e ON e.id = c.id
398
+ WHERE c.bucket IN (${placeholders})`,
399
+ args: allowed
400
+ });
401
+ const scored = [];
402
+ for (const row of res.rows) {
403
+ const emb = JSON.parse(String(row.embedding));
404
+ const s = _VectorStore.cosine(qv, emb);
405
+ scored.push({ s, text: String(row.text) });
406
+ }
407
+ scored.sort((a, b) => b.s - a.s);
408
+ return scored.slice(0, k).map((r) => r.text);
409
+ }
410
+ };
411
+ }
412
+ });
413
+
30
414
  // src/server.ts
31
415
  var server_exports = {};
32
416
  __export(server_exports, {
@@ -34,7 +418,6 @@ __export(server_exports, {
34
418
  });
35
419
  module.exports = __toCommonJS(server_exports);
36
420
  var import_node_path4 = require("node:path");
37
- var import_client2 = require("@libsql/client");
38
421
  var import_hono5 = require("hono");
39
422
  var import_openai = __toESM(require("openai"));
40
423
 
@@ -252,7 +635,8 @@ async function answerOnce({
252
635
  async function applyTransformReply(transformReply, input, logger) {
253
636
  if (!transformReply) return input.text;
254
637
  try {
255
- return await transformReply(input);
638
+ const result = await transformReply(input);
639
+ return typeof result === "string" ? result : null;
256
640
  } catch (error) {
257
641
  logger?.error(
258
642
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -382,6 +766,7 @@ async function prepareChat({
382
766
  sender,
383
767
  rewriteQuery,
384
768
  rerankContext,
769
+ fallbackFn,
385
770
  logger
386
771
  }) {
387
772
  const lastUserMsg = lastUserMessage(messages);
@@ -398,47 +783,47 @@ async function prepareChat({
398
783
  logger?.error("rewriteQuery threw; falling back to the original query", error);
399
784
  }
400
785
  }
401
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
786
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
787
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
788
+ let ctx = retrievedChunks;
402
789
  if (rerankContext) {
403
790
  try {
404
- const reranked = await rerankContext({ query, chunks: ctx });
791
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
405
792
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
406
793
  } catch (error) {
407
794
  logger?.error("rerankContext threw; falling back to the original chunks", error);
408
795
  }
409
796
  }
797
+ let fallback;
798
+ if (ctx.length === 0 && fallbackFn) {
799
+ try {
800
+ const guidance = await fallbackFn({
801
+ query,
802
+ mode,
803
+ buckets: resolvedBuckets,
804
+ sender,
805
+ retrievedChunks
806
+ });
807
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
808
+ } catch (error) {
809
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
810
+ }
811
+ }
410
812
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
411
813
  const hint = channelHint?.trim();
412
814
  const system = [
413
815
  prompts.baseSystemRules,
414
816
  persona,
415
817
  ...hint ? [hint] : [],
818
+ ...fallback ? [fallback] : [],
416
819
  `${label}:
417
820
  ${ctx.join("\n\n")}`
418
821
  ].join("\n\n");
419
822
  return { system, messages, context: ctx };
420
823
  }
421
824
 
422
- // src/core/logger.ts
423
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
424
- function createConsoleLogger(level = "info") {
425
- const threshold = LEVEL_ORDER[level];
426
- const write = (at) => (...args) => {
427
- if (LEVEL_ORDER[at] < threshold) return;
428
- console.error(`[${at}]`, ...args);
429
- };
430
- return {
431
- debug: write("debug"),
432
- info: write("info"),
433
- warn: write("warn"),
434
- error: write("error")
435
- };
436
- }
437
- function resolveLogger(logger, level) {
438
- return logger ?? createConsoleLogger(level);
439
- }
440
-
441
825
  // src/channels/senders.ts
826
+ init_logger();
442
827
  function createSenderRegistry(logger = createConsoleLogger()) {
443
828
  const senders = /* @__PURE__ */ new Map();
444
829
  return {
@@ -501,9 +886,13 @@ function createSenderRegistry(logger = createConsoleLogger()) {
501
886
  };
502
887
  }
503
888
 
889
+ // src/server.ts
890
+ init_logger();
891
+
504
892
  // src/core/prompts.ts
505
893
  var import_node_fs = require("node:fs");
506
894
  var import_node_path = require("node:path");
895
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
507
896
  var PromptLoader = class {
508
897
  constructor(promptsDir, bot) {
509
898
  this.promptsDir = promptsDir;
@@ -534,206 +923,8 @@ var PromptLoader = class {
534
923
  }
535
924
  };
536
925
 
537
- // src/core/retrieval.ts
538
- var import_client = require("@libsql/client");
539
-
540
- // src/core/loaders.ts
541
- var import_node_fs2 = require("node:fs");
542
- var import_node_path2 = require("node:path");
543
- function walk(dir) {
544
- const items = [];
545
- try {
546
- for (const e of (0, import_node_fs2.readdirSync)(dir)) {
547
- const p = (0, import_node_path2.join)(dir, e);
548
- const st = (0, import_node_fs2.statSync)(p);
549
- if (st.isDirectory()) items.push(...walk(p));
550
- else if (p.endsWith(".md")) items.push(p);
551
- }
552
- } catch {
553
- }
554
- return items;
555
- }
556
- function loadKnowledge(root = "config/knowledge") {
557
- const docs = [];
558
- for (const bucket of ["base", "public", "private"]) {
559
- const dir = (0, import_node_path2.join)(root, bucket);
560
- for (const f of walk(dir)) {
561
- docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
562
- }
563
- }
564
- return docs;
565
- }
566
-
567
- // src/core/retrieval.ts
568
- var EMB_MODEL = "text-embedding-3-large";
569
- function chunk(text, max = 900) {
570
- const out = [];
571
- let buf = "";
572
- for (const line of text.replace(/\r/g, "").split("\n")) {
573
- if (`${buf}
574
- ${line}`.length > max) {
575
- out.push(buf.trim());
576
- buf = line;
577
- } else buf += `
578
- ${line}`;
579
- }
580
- if (buf.trim()) out.push(buf.trim());
581
- return out;
582
- }
583
- async function sha256(input) {
584
- const data = new TextEncoder().encode(input);
585
- const hash = await crypto.subtle.digest("SHA-256", data);
586
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
587
- }
588
- var VectorStore = class _VectorStore {
589
- constructor(client, options) {
590
- this.client = client;
591
- this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
592
- url: options.databaseUrl,
593
- authToken: options.databaseAuthToken
594
- });
595
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
596
- this.logger = options.logger ?? createConsoleLogger();
597
- }
598
- client;
599
- /**
600
- * The libsql client backing this store. When a client was injected this is
601
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
602
- * and the store share one connection.
603
- */
604
- db;
605
- knowledgeDir;
606
- logger;
607
- // On boot: ingest new chunks and embed only missing ones.
608
- async build() {
609
- this.logger.info("\u{1F504} Building knowledge base...");
610
- await this.db.execute(`
611
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
612
- `);
613
- await this.db.execute(`
614
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
615
- `);
616
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
617
- const docs = loadKnowledge(this.knowledgeDir);
618
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
619
- const rows = [];
620
- for (const d of docs) {
621
- for (const part of chunk(d.text)) {
622
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
623
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
624
- }
625
- }
626
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
627
- await this.cleanupStaleChunks(rows.map((r) => r.id));
628
- const UPSERT_BATCH = 500;
629
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
630
- const batch = rows.slice(i, i + UPSERT_BATCH);
631
- await this.db.batch(
632
- batch.map((r) => ({
633
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
634
- ON CONFLICT(id) DO NOTHING`,
635
- args: [r.id, r.bucket, r.source, r.text]
636
- })),
637
- "write"
638
- );
639
- }
640
- const ids = rows.map((r) => r.id);
641
- const missing = [];
642
- for (let i = 0; i < ids.length; i += 500) {
643
- const batch = ids.slice(i, i + 500);
644
- const placeholders = batch.map(() => "?").join(",");
645
- const res = await this.db.execute({
646
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
647
- EXCEPT SELECT id FROM embeddings`,
648
- args: batch
649
- });
650
- for (const row of res.rows) missing.push(String(row.id));
651
- }
652
- if (missing.length === 0) {
653
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
654
- return;
655
- }
656
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
657
- const textById = new Map(rows.map((r) => [r.id, r.text]));
658
- const BATCH = 96;
659
- for (let i = 0; i < missing.length; i += BATCH) {
660
- const batchIds = missing.slice(i, i + BATCH);
661
- const inputs = batchIds.map((id) => textById.get(id) || "");
662
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
663
- await this.db.batch(
664
- emb.data.map((d, idx) => ({
665
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
666
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
667
- })),
668
- "write"
669
- );
670
- }
671
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
672
- }
673
- // Remove chunks from database that no longer exist in markdown files
674
- async cleanupStaleChunks(currentIds) {
675
- const result = await this.db.execute("SELECT id FROM chunks");
676
- const dbIds = result.rows.map((row) => String(row.id));
677
- const currentIdSet = new Set(currentIds);
678
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
679
- if (staleIds.length === 0) {
680
- return;
681
- }
682
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
683
- const BATCH_SIZE = 500;
684
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
685
- const batch = staleIds.slice(i, i + BATCH_SIZE);
686
- const placeholders = batch.map(() => "?").join(",");
687
- await this.db.execute({
688
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
689
- args: batch
690
- });
691
- }
692
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
693
- }
694
- static cosine(a, b) {
695
- let dot = 0;
696
- let na = 0;
697
- let nb = 0;
698
- for (let i = 0; i < a.length; i++) {
699
- dot += a[i] * b[i];
700
- na += a[i] * a[i];
701
- nb += b[i] * b[i];
702
- }
703
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
704
- }
705
- /**
706
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
707
- * & portable).
708
- *
709
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
710
- * `chunks` table constrains nothing, so a deployment that ingests its own
711
- * buckets can query them (`build` only writes and prunes the three the
712
- * knowledge loader walks). Names are bound as query parameters, never
713
- * interpolated. An empty list retrieves nothing, and short-circuits before
714
- * the embedding call.
715
- */
716
- async query(q, k = 6, allowed = ["base"]) {
717
- if (allowed.length === 0) return [];
718
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
719
- const placeholders = allowed.map(() => "?").join(",");
720
- const res = await this.db.execute({
721
- sql: `SELECT c.id, c.text, e.embedding
722
- FROM chunks c
723
- JOIN embeddings e ON e.id = c.id
724
- WHERE c.bucket IN (${placeholders})`,
725
- args: allowed
726
- });
727
- const scored = [];
728
- for (const row of res.rows) {
729
- const emb = JSON.parse(String(row.embedding));
730
- const s = _VectorStore.cosine(qv, emb);
731
- scored.push({ s, text: String(row.text) });
732
- }
733
- scored.sort((a, b) => b.s - a.s);
734
- return scored.slice(0, k).map((r) => r.text);
735
- }
736
- };
926
+ // src/server.ts
927
+ init_retrieval();
737
928
 
738
929
  // src/core/serve-static.ts
739
930
  var STATIC_ADAPTERS = {
@@ -792,8 +983,12 @@ function resolveStatic(configStaticDir) {
792
983
  return { staticDir: null };
793
984
  }
794
985
 
986
+ // src/middleware/apiKey.ts
987
+ var API_KEY_HEADER = "x-api-key";
988
+ var API_KEY_PREVIEW_LENGTH = 20;
989
+
795
990
  // src/middleware/cors.ts
796
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization, x-conversation-id";
991
+ var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
797
992
  var ALLOWED_METHODS = "POST, GET, OPTIONS";
798
993
  var EXPOSED_HEADERS = "x-conversation-id";
799
994
  var cors = (allowedOrigins) => async (c, next) => {
@@ -857,22 +1052,27 @@ var import_streaming = require("hono/streaming");
857
1052
 
858
1053
  // src/core/session.ts
859
1054
  var import_node_crypto2 = require("node:crypto");
1055
+ var SESSION_KEY_PREFIX = "session_";
1056
+ var DEFAULT_SESSION_MAX_REQUESTS = 20;
1057
+ var DEFAULT_SESSION_TTL_SECONDS = 3600;
1058
+ var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
860
1059
  var sessions = /* @__PURE__ */ new Map();
861
- var sessionSweeper = setInterval(
862
- () => {
863
- const now = Date.now();
864
- for (const [key, data] of Array.from(sessions.entries())) {
865
- if (now > data.expiresAt) {
866
- sessions.delete(key);
867
- }
1060
+ var sessionSweeper = setInterval(() => {
1061
+ const now = Date.now();
1062
+ for (const [key, data] of Array.from(sessions.entries())) {
1063
+ if (now > data.expiresAt) {
1064
+ sessions.delete(key);
868
1065
  }
869
- },
870
- 5 * 60 * 1e3
871
- );
1066
+ }
1067
+ }, SESSION_SWEEP_INTERVAL_MS);
872
1068
  sessionSweeper.unref?.();
873
1069
  function createSession(options = {}) {
874
- const { maxRequests = 20, ttl = 3600, metadata } = options;
875
- const key = `session_${(0, import_node_crypto2.randomUUID)()}`;
1070
+ const {
1071
+ maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
1072
+ ttl = DEFAULT_SESSION_TTL_SECONDS,
1073
+ metadata
1074
+ } = options;
1075
+ const key = `${SESSION_KEY_PREFIX}${(0, import_node_crypto2.randomUUID)()}`;
876
1076
  const now = Date.now();
877
1077
  const session = {
878
1078
  key,
@@ -1035,6 +1235,8 @@ function limitDemo(trustProxy) {
1035
1235
  await next();
1036
1236
  };
1037
1237
  }
1238
+ var DEMO_SESSION_MAX_REQUESTS = 20;
1239
+ var DEMO_SESSION_TTL_SECONDS = 3600;
1038
1240
  function demoRoutes(deps) {
1039
1241
  const { client, store, config, prompts, logger } = deps;
1040
1242
  const app = new import_hono.Hono();
@@ -1047,18 +1249,17 @@ function demoRoutes(deps) {
1047
1249
  limitSessionCreation(trustProxy),
1048
1250
  (c) => {
1049
1251
  const session = createSession({
1050
- maxRequests: 20,
1051
- ttl: 3600
1052
- // 1 hour
1252
+ maxRequests: DEMO_SESSION_MAX_REQUESTS,
1253
+ ttl: DEMO_SESSION_TTL_SECONDS
1053
1254
  });
1054
1255
  logger.debug(
1055
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
1256
+ `[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
1056
1257
  );
1057
1258
  return c.json({
1058
1259
  key: session.key,
1059
- expiresIn: 3600,
1260
+ expiresIn: DEMO_SESSION_TTL_SECONDS,
1060
1261
  maxRequests: session.maxRequests,
1061
- message: "Use this temporary key in the x-api-key header"
1262
+ message: `Use this temporary key in the ${API_KEY_HEADER} header`
1062
1263
  });
1063
1264
  }
1064
1265
  );
@@ -1092,6 +1293,7 @@ function demoRoutes(deps) {
1092
1293
  buckets,
1093
1294
  rewriteQuery: config.rewriteQuery,
1094
1295
  rerankContext: config.rerankContext,
1296
+ fallbackFn: config.fallbackFn,
1095
1297
  logger
1096
1298
  });
1097
1299
  const model = config.openai.model || DEFAULT_MODEL;
@@ -1194,7 +1396,7 @@ function createRateLimiter(config) {
1194
1396
  // Demo keys get 10x stricter rate limits
1195
1397
  limitPublic() {
1196
1398
  return async (c, next) => {
1197
- const apiKey = c.req.header("x-api-key");
1399
+ const apiKey = c.req.header(API_KEY_HEADER);
1198
1400
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1199
1401
  const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1200
1402
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
@@ -1263,9 +1465,13 @@ function openaiRoutes(deps) {
1263
1465
  return errorJson(c, 401, "API key authentication is not configured on this server");
1264
1466
  }
1265
1467
  const bearer = c.req.header("authorization") ?? "";
1266
- const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header("x-api-key");
1468
+ const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
1267
1469
  if (!key) {
1268
- return errorJson(c, 401, "Missing API key (Authorization: Bearer or x-api-key header)");
1470
+ return errorJson(
1471
+ c,
1472
+ 401,
1473
+ `Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
1474
+ );
1269
1475
  }
1270
1476
  try {
1271
1477
  const result = await apiKeyManager.verify(key);
@@ -1305,6 +1511,7 @@ function openaiRoutes(deps) {
1305
1511
  sender,
1306
1512
  rewriteQuery: config.rewriteQuery,
1307
1513
  rerankContext: config.rerankContext,
1514
+ fallbackFn: config.fallbackFn,
1308
1515
  logger
1309
1516
  }));
1310
1517
  } catch {
@@ -1428,6 +1635,7 @@ function privateRoutes(deps) {
1428
1635
  sender,
1429
1636
  rewriteQuery: config.rewriteQuery,
1430
1637
  rerankContext: config.rerankContext,
1638
+ fallbackFn: config.fallbackFn,
1431
1639
  logger
1432
1640
  }));
1433
1641
  } catch {
@@ -1480,9 +1688,9 @@ var import_streaming4 = require("hono/streaming");
1480
1688
  function createAuthMiddleware(deps) {
1481
1689
  const { apiKeyManager, logger } = deps;
1482
1690
  return async function requirePublicKey(c, next) {
1483
- const hdr = c.req.header("x-api-key");
1484
- if (hdr?.startsWith("session_")) {
1485
- logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1691
+ const hdr = c.req.header(API_KEY_HEADER);
1692
+ if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
1693
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1486
1694
  await next();
1487
1695
  return;
1488
1696
  }
@@ -1492,7 +1700,7 @@ function createAuthMiddleware(deps) {
1492
1700
  }
1493
1701
  if (!hdr) {
1494
1702
  logger.debug("[Auth] No API key provided");
1495
- return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1703
+ return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
1496
1704
  }
1497
1705
  try {
1498
1706
  const result = await apiKeyManager.verify(hdr);
@@ -1506,22 +1714,23 @@ function createAuthMiddleware(deps) {
1506
1714
  } catch (error) {
1507
1715
  logger.warn(`[Auth] JWT verification error: ${error}`);
1508
1716
  }
1509
- logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1717
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
1510
1718
  return c.json({ error: "Unauthorized" }, 401);
1511
1719
  };
1512
1720
  }
1513
1721
 
1514
1722
  // src/middleware/referrer.ts
1723
+ init_logger();
1515
1724
  function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1516
1725
  return async (c, next) => {
1517
- const apiKey = c.req.header("x-api-key");
1726
+ const apiKey = c.req.header(API_KEY_HEADER);
1518
1727
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1519
- const isSessionKey = apiKey?.startsWith("session_");
1728
+ const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
1520
1729
  if (isDemoKey || isSessionKey) {
1521
1730
  const referer = c.req.header("referer");
1522
1731
  const origin = c.req.header("origin");
1523
1732
  logger.debug(
1524
- `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1733
+ `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
1525
1734
  );
1526
1735
  logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1527
1736
  const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
@@ -1541,11 +1750,12 @@ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsol
1541
1750
  }
1542
1751
 
1543
1752
  // src/middleware/session.ts
1753
+ init_logger();
1544
1754
  function validateSessionKey(logger = createConsoleLogger()) {
1545
1755
  return async (c, next) => {
1546
- const apiKey = c.req.header("x-api-key");
1547
- if (apiKey?.startsWith("session_")) {
1548
- logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1756
+ const apiKey = c.req.header(API_KEY_HEADER);
1757
+ if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
1758
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1549
1759
  const info = getSessionInfo(apiKey);
1550
1760
  if (info) {
1551
1761
  logger.debug(
@@ -1609,6 +1819,7 @@ function publicRoutes(deps) {
1609
1819
  buckets,
1610
1820
  rewriteQuery: config.rewriteQuery,
1611
1821
  rerankContext: config.rerankContext,
1822
+ fallbackFn: config.fallbackFn,
1612
1823
  logger
1613
1824
  }));
1614
1825
  } catch {
@@ -1648,8 +1859,7 @@ function publicRoutes(deps) {
1648
1859
  async function createServer(config) {
1649
1860
  const logger = resolveLogger(config.logger, config.logLevel);
1650
1861
  logger.info(`\u{1F680} Starting ${config.bot.name}...`);
1651
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
1652
- const promptsDir = config.promptsDir || "./config/prompts";
1862
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
1653
1863
  const publicDir = config.publicDir || "./public";
1654
1864
  const enablePublic = config.features?.enablePublicChat !== false;
1655
1865
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -1660,16 +1870,26 @@ async function createServer(config) {
1660
1870
  const client = new import_openai.default({
1661
1871
  apiKey: config.openai.apiKey
1662
1872
  });
1663
- const db = (0, import_client2.createClient)({
1664
- url: config.database.url,
1665
- authToken: config.database.authToken
1666
- });
1667
- const store = new VectorStore(client, {
1668
- databaseClient: db,
1669
- knowledgeDir,
1670
- logger
1671
- });
1672
- await store.build();
1873
+ let db;
1874
+ if (config.database) {
1875
+ const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1876
+ db = await openLibsqlClient2(config.database);
1877
+ }
1878
+ let store;
1879
+ if (config.retriever) {
1880
+ store = config.retriever;
1881
+ } else {
1882
+ if (!db) {
1883
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
1884
+ }
1885
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1886
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
1887
+ databaseClient: db,
1888
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1889
+ logger
1890
+ });
1891
+ }
1892
+ await store.build?.();
1673
1893
  logger.info("\u2705 Knowledge base ready");
1674
1894
  const prompts = new PromptLoader(promptsDir, config.bot);
1675
1895
  let apiKeyManager;
@@ -1757,6 +1977,9 @@ async function createServer(config) {
1757
1977
  const deps = {
1758
1978
  client,
1759
1979
  store,
1980
+ // See ServerDependencies.db's doc comment: undefined here (cast to
1981
+ // satisfy the still-required type) exactly when config.database was
1982
+ // never set - i.e. a config.retriever host with nothing else needing db.
1760
1983
  db,
1761
1984
  config,
1762
1985
  prompts,