@diegoaltoworks/chatter 0.60.0 → 0.62.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 +56 -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 +152 -0
  15. package/dist/channels/matrix/api.d.ts.map +1 -0
  16. package/dist/channels/matrix/channel.d.ts +46 -0
  17. package/dist/channels/matrix/channel.d.ts.map +1 -0
  18. package/dist/channels/matrix/handler.d.ts +58 -0
  19. package/dist/channels/matrix/handler.d.ts.map +1 -0
  20. package/dist/channels/matrix/index.d.ts +37 -0
  21. package/dist/channels/matrix/index.d.ts.map +1 -0
  22. package/dist/channels/matrix/index.js +1128 -0
  23. package/dist/channels/matrix/index.mjs +1079 -0
  24. package/dist/channels/matrix/sync.d.ts +53 -0
  25. package/dist/channels/matrix/sync.d.ts.map +1 -0
  26. package/dist/channels/matrix/updates.d.ts +104 -0
  27. package/dist/channels/matrix/updates.d.ts.map +1 -0
  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 +25 -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 +123 -41
  49. package/dist/channels/whatsapp/index.mjs +120 -41
  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 +11 -2
package/dist/server.mjs CHANGED
@@ -1,6 +1,395 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/core/logger.ts
17
+ function createConsoleLogger(level = "info") {
18
+ const threshold = LEVEL_ORDER[level];
19
+ const write = (at) => (...args) => {
20
+ if (LEVEL_ORDER[at] < threshold) return;
21
+ console.error(`[${at}]`, ...args);
22
+ };
23
+ return {
24
+ debug: write("debug"),
25
+ info: write("info"),
26
+ warn: write("warn"),
27
+ error: write("error")
28
+ };
29
+ }
30
+ function resolveLogger(logger, level) {
31
+ return logger ?? createConsoleLogger(level);
32
+ }
33
+ var LEVEL_ORDER;
34
+ var init_logger = __esm({
35
+ "src/core/logger.ts"() {
36
+ "use strict";
37
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
38
+ }
39
+ });
40
+
41
+ // src/core/buildLock.ts
42
+ function ensureBuildLockSchema(client) {
43
+ let ready = schemaReady.get(client);
44
+ if (!ready) {
45
+ ready = client.execute(
46
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
47
+ lock_key TEXT PRIMARY KEY,
48
+ holder_id TEXT NOT NULL,
49
+ heartbeat_at INTEGER NOT NULL
50
+ )`
51
+ ).catch((error) => {
52
+ schemaReady.delete(client);
53
+ throw error;
54
+ });
55
+ schemaReady.set(client, ready);
56
+ }
57
+ return ready;
58
+ }
59
+ function createTursoBuildLock(client) {
60
+ return {
61
+ async tryAcquire(key, holderId, now, staleMs) {
62
+ await ensureBuildLockSchema(client);
63
+ const result = await client.execute({
64
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
65
+ ON CONFLICT(lock_key) DO UPDATE SET
66
+ holder_id = excluded.holder_id,
67
+ heartbeat_at = excluded.heartbeat_at
68
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
69
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
70
+ RETURNING holder_id`,
71
+ args: [key, holderId, now, staleMs]
72
+ });
73
+ const row = result.rows[0];
74
+ return Boolean(row) && row.holder_id === holderId;
75
+ },
76
+ async release(key, holderId) {
77
+ await ensureBuildLockSchema(client);
78
+ await client.execute({
79
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
80
+ args: [key, holderId]
81
+ });
82
+ }
83
+ };
84
+ }
85
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
86
+ var init_buildLock = __esm({
87
+ "src/core/buildLock.ts"() {
88
+ "use strict";
89
+ BUILD_LOCK_KEY = "knowledge_build";
90
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
91
+ schemaReady = /* @__PURE__ */ new WeakMap();
92
+ }
93
+ });
94
+
95
+ // src/core/loaders.ts
96
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
97
+ import { join as join2 } from "node:path";
98
+ function walk(dir) {
99
+ const items = [];
100
+ try {
101
+ for (const e of readdirSync(dir)) {
102
+ const p = join2(dir, e);
103
+ const st = statSync(p);
104
+ if (st.isDirectory()) items.push(...walk(p));
105
+ else if (p.endsWith(".md")) items.push(p);
106
+ }
107
+ } catch {
108
+ }
109
+ return items;
110
+ }
111
+ function loadKnowledge(root = "config/knowledge") {
112
+ const docs = [];
113
+ for (const bucket of ["base", "public", "private"]) {
114
+ const dir = join2(root, bucket);
115
+ for (const f of walk(dir)) {
116
+ docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
117
+ }
118
+ }
119
+ return docs;
120
+ }
121
+ var init_loaders = __esm({
122
+ "src/core/loaders.ts"() {
123
+ "use strict";
124
+ }
125
+ });
126
+
127
+ // src/core/retrieval.ts
128
+ var retrieval_exports = {};
129
+ __export(retrieval_exports, {
130
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
131
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
132
+ VectorStore: () => VectorStore,
133
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
134
+ openLibsqlClient: () => openLibsqlClient,
135
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
136
+ });
137
+ function createOpenAIEmbedder(client) {
138
+ return async (input) => {
139
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
140
+ return res.data.map((d) => d.embedding);
141
+ };
142
+ }
143
+ function wrapMissingLibsqlError(cause) {
144
+ return new Error(
145
+ "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.",
146
+ { cause }
147
+ );
148
+ }
149
+ async function openLibsqlClient(database) {
150
+ const mod = await import("@libsql/client").catch((error) => {
151
+ throw wrapMissingLibsqlError(error);
152
+ });
153
+ return mod.createClient({ url: database.url, authToken: database.authToken });
154
+ }
155
+ function chunk(text, max = 900) {
156
+ const out = [];
157
+ let buf = "";
158
+ for (const line of text.replace(/\r/g, "").split("\n")) {
159
+ if (`${buf}
160
+ ${line}`.length > max) {
161
+ out.push(buf.trim());
162
+ buf = line;
163
+ } else buf += `
164
+ ${line}`;
165
+ }
166
+ if (buf.trim()) out.push(buf.trim());
167
+ return out;
168
+ }
169
+ async function sha256(input) {
170
+ const data = new TextEncoder().encode(input);
171
+ const hash = await crypto.subtle.digest("SHA-256", data);
172
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
173
+ }
174
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
175
+ var init_retrieval = __esm({
176
+ "src/core/retrieval.ts"() {
177
+ "use strict";
178
+ init_buildLock();
179
+ init_loaders();
180
+ init_logger();
181
+ EMB_MODEL = "text-embedding-3-large";
182
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
183
+ 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.";
184
+ VectorStore = class _VectorStore {
185
+ constructor(embed, options) {
186
+ this.embed = embed;
187
+ this.db = options.databaseClient;
188
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
189
+ this.logger = options.logger ?? createConsoleLogger();
190
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
191
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
192
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
193
+ }
194
+ embed;
195
+ /**
196
+ * The libsql client backing this store - the same instance passed in as
197
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
198
+ * and the store share one connection.
199
+ */
200
+ db;
201
+ knowledgeDir;
202
+ logger;
203
+ buildLock;
204
+ instanceId;
205
+ buildLockStaleMs;
206
+ /**
207
+ * On boot: ingest new chunks and embed only missing ones.
208
+ *
209
+ * The whole ingest runs under a single-writer lock held in the database
210
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
211
+ * every chunk id the current `knowledgeDir` did not produce. Without the
212
+ * lock, a second instance booting mid-build diffs against a database the
213
+ * first one is still writing and deletes its chunks. An instance that
214
+ * cannot take the lock skips its build entirely and serves what the holder
215
+ * has already written, rather than racing it.
216
+ */
217
+ async build() {
218
+ this.logger.info("Building knowledge base...");
219
+ await this.db.execute(`
220
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
221
+ `);
222
+ await this.db.execute(`
223
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
224
+ `);
225
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
226
+ if (!await this.renewBuildLock()) {
227
+ this.logger.warn(
228
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
229
+ );
230
+ return;
231
+ }
232
+ try {
233
+ await this.buildUnderLock();
234
+ } finally {
235
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
236
+ }
237
+ }
238
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
239
+ renewBuildLock() {
240
+ return this.buildLock.tryAcquire(
241
+ BUILD_LOCK_KEY,
242
+ this.instanceId,
243
+ Date.now(),
244
+ this.buildLockStaleMs
245
+ );
246
+ }
247
+ /** The ingest itself. Only ever runs with the build lock held. */
248
+ async buildUnderLock() {
249
+ const docs = loadKnowledge(this.knowledgeDir);
250
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
251
+ if (docs.length === 0) {
252
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
253
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
254
+ if (existingCount > 0) {
255
+ throw new Error(
256
+ `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.`
257
+ );
258
+ }
259
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
260
+ return;
261
+ }
262
+ const rows = [];
263
+ for (const d of docs) {
264
+ for (const part of chunk(d.text)) {
265
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
266
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
267
+ }
268
+ }
269
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
270
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
271
+ const UPSERT_BATCH = 500;
272
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
273
+ const batch = rows.slice(i, i + UPSERT_BATCH);
274
+ await this.db.batch(
275
+ batch.map((r) => ({
276
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
277
+ ON CONFLICT(id) DO NOTHING`,
278
+ args: [r.id, r.bucket, r.source, r.text]
279
+ })),
280
+ "write"
281
+ );
282
+ }
283
+ const ids = rows.map((r) => r.id);
284
+ const missing = [];
285
+ for (let i = 0; i < ids.length; i += 500) {
286
+ const batch = ids.slice(i, i + 500);
287
+ const placeholders = batch.map(() => "?").join(",");
288
+ const res = await this.db.execute({
289
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
290
+ EXCEPT SELECT id FROM embeddings`,
291
+ args: batch
292
+ });
293
+ for (const row of res.rows) missing.push(String(row.id));
294
+ }
295
+ if (missing.length === 0) {
296
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
297
+ return;
298
+ }
299
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
300
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
301
+ const BATCH = 96;
302
+ for (let i = 0; i < missing.length; i += BATCH) {
303
+ const batchIds = missing.slice(i, i + BATCH);
304
+ const inputs = batchIds.map((id) => textById.get(id) || "");
305
+ const vectors = await this.embed(inputs);
306
+ await this.db.batch(
307
+ vectors.map((embedding, idx) => ({
308
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
309
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
310
+ })),
311
+ "write"
312
+ );
313
+ if (!await this.renewBuildLock()) {
314
+ this.logger.warn(
315
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
316
+ );
317
+ }
318
+ }
319
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
320
+ }
321
+ // Remove chunks from database that no longer exist in markdown files
322
+ async cleanupStaleChunks(currentIds) {
323
+ const result = await this.db.execute("SELECT id FROM chunks");
324
+ const dbIds = result.rows.map((row) => String(row.id));
325
+ const currentIdSet = new Set(currentIds);
326
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
327
+ if (staleIds.length === 0) {
328
+ return;
329
+ }
330
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
331
+ const BATCH_SIZE = 500;
332
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
333
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
334
+ const placeholders = batch.map(() => "?").join(",");
335
+ await this.db.batch(
336
+ [
337
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
338
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
339
+ ],
340
+ "write"
341
+ );
342
+ }
343
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
344
+ }
345
+ static cosine(a, b) {
346
+ let dot = 0;
347
+ let na = 0;
348
+ let nb = 0;
349
+ for (let i = 0; i < a.length; i++) {
350
+ dot += a[i] * b[i];
351
+ na += a[i] * a[i];
352
+ nb += b[i] * b[i];
353
+ }
354
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
355
+ }
356
+ /**
357
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
358
+ * & portable).
359
+ *
360
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
361
+ * `chunks` table constrains nothing, so a deployment that ingests its own
362
+ * buckets can query them (`build` only writes and prunes the three the
363
+ * knowledge loader walks). Names are bound as query parameters, never
364
+ * interpolated. An empty list retrieves nothing, and short-circuits before
365
+ * the embedding call.
366
+ */
367
+ async query(q, k = 6, allowed = ["base"]) {
368
+ if (allowed.length === 0) return [];
369
+ const [qv] = await this.embed([q]);
370
+ const placeholders = allowed.map(() => "?").join(",");
371
+ const res = await this.db.execute({
372
+ sql: `SELECT c.id, c.text, e.embedding
373
+ FROM chunks c
374
+ JOIN embeddings e ON e.id = c.id
375
+ WHERE c.bucket IN (${placeholders})`,
376
+ args: allowed
377
+ });
378
+ const scored = [];
379
+ for (const row of res.rows) {
380
+ const emb = JSON.parse(String(row.embedding));
381
+ const s = _VectorStore.cosine(qv, emb);
382
+ scored.push({ s, text: String(row.text) });
383
+ }
384
+ scored.sort((a, b) => b.s - a.s);
385
+ return scored.slice(0, k).map((r) => r.text);
386
+ }
387
+ };
388
+ }
389
+ });
390
+
1
391
  // src/server.ts
2
392
  import { relative } from "node:path";
3
- import { createClient as createClient2 } from "@libsql/client";
4
393
  import { Hono as Hono5 } from "hono";
5
394
  import OpenAI from "openai";
6
395
 
@@ -218,7 +607,8 @@ async function answerOnce({
218
607
  async function applyTransformReply(transformReply, input, logger) {
219
608
  if (!transformReply) return input.text;
220
609
  try {
221
- return await transformReply(input);
610
+ const result = await transformReply(input);
611
+ return typeof result === "string" ? result : null;
222
612
  } catch (error) {
223
613
  logger?.error(
224
614
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -348,6 +738,7 @@ async function prepareChat({
348
738
  sender,
349
739
  rewriteQuery,
350
740
  rerankContext,
741
+ fallbackFn,
351
742
  logger
352
743
  }) {
353
744
  const lastUserMsg = lastUserMessage(messages);
@@ -364,47 +755,47 @@ async function prepareChat({
364
755
  logger?.error("rewriteQuery threw; falling back to the original query", error);
365
756
  }
366
757
  }
367
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
758
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
759
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
760
+ let ctx = retrievedChunks;
368
761
  if (rerankContext) {
369
762
  try {
370
- const reranked = await rerankContext({ query, chunks: ctx });
763
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
371
764
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
372
765
  } catch (error) {
373
766
  logger?.error("rerankContext threw; falling back to the original chunks", error);
374
767
  }
375
768
  }
769
+ let fallback;
770
+ if (ctx.length === 0 && fallbackFn) {
771
+ try {
772
+ const guidance = await fallbackFn({
773
+ query,
774
+ mode,
775
+ buckets: resolvedBuckets,
776
+ sender,
777
+ retrievedChunks
778
+ });
779
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
780
+ } catch (error) {
781
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
782
+ }
783
+ }
376
784
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
377
785
  const hint = channelHint?.trim();
378
786
  const system = [
379
787
  prompts.baseSystemRules,
380
788
  persona,
381
789
  ...hint ? [hint] : [],
790
+ ...fallback ? [fallback] : [],
382
791
  `${label}:
383
792
  ${ctx.join("\n\n")}`
384
793
  ].join("\n\n");
385
794
  return { system, messages, context: ctx };
386
795
  }
387
796
 
388
- // src/core/logger.ts
389
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
390
- function createConsoleLogger(level = "info") {
391
- const threshold = LEVEL_ORDER[level];
392
- const write = (at) => (...args) => {
393
- if (LEVEL_ORDER[at] < threshold) return;
394
- console.error(`[${at}]`, ...args);
395
- };
396
- return {
397
- debug: write("debug"),
398
- info: write("info"),
399
- warn: write("warn"),
400
- error: write("error")
401
- };
402
- }
403
- function resolveLogger(logger, level) {
404
- return logger ?? createConsoleLogger(level);
405
- }
406
-
407
797
  // src/channels/senders.ts
798
+ init_logger();
408
799
  function createSenderRegistry(logger = createConsoleLogger()) {
409
800
  const senders = /* @__PURE__ */ new Map();
410
801
  return {
@@ -467,9 +858,13 @@ function createSenderRegistry(logger = createConsoleLogger()) {
467
858
  };
468
859
  }
469
860
 
861
+ // src/server.ts
862
+ init_logger();
863
+
470
864
  // src/core/prompts.ts
471
865
  import { readFileSync } from "node:fs";
472
866
  import { join } from "node:path";
867
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
473
868
  var PromptLoader = class {
474
869
  constructor(promptsDir, bot) {
475
870
  this.promptsDir = promptsDir;
@@ -500,206 +895,8 @@ var PromptLoader = class {
500
895
  }
501
896
  };
502
897
 
503
- // src/core/retrieval.ts
504
- import { createClient } from "@libsql/client";
505
-
506
- // src/core/loaders.ts
507
- import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
508
- import { join as join2 } from "node:path";
509
- function walk(dir) {
510
- const items = [];
511
- try {
512
- for (const e of readdirSync(dir)) {
513
- const p = join2(dir, e);
514
- const st = statSync(p);
515
- if (st.isDirectory()) items.push(...walk(p));
516
- else if (p.endsWith(".md")) items.push(p);
517
- }
518
- } catch {
519
- }
520
- return items;
521
- }
522
- function loadKnowledge(root = "config/knowledge") {
523
- const docs = [];
524
- for (const bucket of ["base", "public", "private"]) {
525
- const dir = join2(root, bucket);
526
- for (const f of walk(dir)) {
527
- docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
528
- }
529
- }
530
- return docs;
531
- }
532
-
533
- // src/core/retrieval.ts
534
- var EMB_MODEL = "text-embedding-3-large";
535
- function chunk(text, max = 900) {
536
- const out = [];
537
- let buf = "";
538
- for (const line of text.replace(/\r/g, "").split("\n")) {
539
- if (`${buf}
540
- ${line}`.length > max) {
541
- out.push(buf.trim());
542
- buf = line;
543
- } else buf += `
544
- ${line}`;
545
- }
546
- if (buf.trim()) out.push(buf.trim());
547
- return out;
548
- }
549
- async function sha256(input) {
550
- const data = new TextEncoder().encode(input);
551
- const hash = await crypto.subtle.digest("SHA-256", data);
552
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
553
- }
554
- var VectorStore = class _VectorStore {
555
- constructor(client, options) {
556
- this.client = client;
557
- this.db = options.databaseClient ? options.databaseClient : createClient({
558
- url: options.databaseUrl,
559
- authToken: options.databaseAuthToken
560
- });
561
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
562
- this.logger = options.logger ?? createConsoleLogger();
563
- }
564
- client;
565
- /**
566
- * The libsql client backing this store. When a client was injected this is
567
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
568
- * and the store share one connection.
569
- */
570
- db;
571
- knowledgeDir;
572
- logger;
573
- // On boot: ingest new chunks and embed only missing ones.
574
- async build() {
575
- this.logger.info("\u{1F504} Building knowledge base...");
576
- await this.db.execute(`
577
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
578
- `);
579
- await this.db.execute(`
580
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
581
- `);
582
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
583
- const docs = loadKnowledge(this.knowledgeDir);
584
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
585
- const rows = [];
586
- for (const d of docs) {
587
- for (const part of chunk(d.text)) {
588
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
589
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
590
- }
591
- }
592
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
593
- await this.cleanupStaleChunks(rows.map((r) => r.id));
594
- const UPSERT_BATCH = 500;
595
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
596
- const batch = rows.slice(i, i + UPSERT_BATCH);
597
- await this.db.batch(
598
- batch.map((r) => ({
599
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
600
- ON CONFLICT(id) DO NOTHING`,
601
- args: [r.id, r.bucket, r.source, r.text]
602
- })),
603
- "write"
604
- );
605
- }
606
- const ids = rows.map((r) => r.id);
607
- const missing = [];
608
- for (let i = 0; i < ids.length; i += 500) {
609
- const batch = ids.slice(i, i + 500);
610
- const placeholders = batch.map(() => "?").join(",");
611
- const res = await this.db.execute({
612
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
613
- EXCEPT SELECT id FROM embeddings`,
614
- args: batch
615
- });
616
- for (const row of res.rows) missing.push(String(row.id));
617
- }
618
- if (missing.length === 0) {
619
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
620
- return;
621
- }
622
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
623
- const textById = new Map(rows.map((r) => [r.id, r.text]));
624
- const BATCH = 96;
625
- for (let i = 0; i < missing.length; i += BATCH) {
626
- const batchIds = missing.slice(i, i + BATCH);
627
- const inputs = batchIds.map((id) => textById.get(id) || "");
628
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
629
- await this.db.batch(
630
- emb.data.map((d, idx) => ({
631
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
632
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
633
- })),
634
- "write"
635
- );
636
- }
637
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
638
- }
639
- // Remove chunks from database that no longer exist in markdown files
640
- async cleanupStaleChunks(currentIds) {
641
- const result = await this.db.execute("SELECT id FROM chunks");
642
- const dbIds = result.rows.map((row) => String(row.id));
643
- const currentIdSet = new Set(currentIds);
644
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
645
- if (staleIds.length === 0) {
646
- return;
647
- }
648
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
649
- const BATCH_SIZE = 500;
650
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
651
- const batch = staleIds.slice(i, i + BATCH_SIZE);
652
- const placeholders = batch.map(() => "?").join(",");
653
- await this.db.execute({
654
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
655
- args: batch
656
- });
657
- }
658
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
659
- }
660
- static cosine(a, b) {
661
- let dot = 0;
662
- let na = 0;
663
- let nb = 0;
664
- for (let i = 0; i < a.length; i++) {
665
- dot += a[i] * b[i];
666
- na += a[i] * a[i];
667
- nb += b[i] * b[i];
668
- }
669
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
670
- }
671
- /**
672
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
673
- * & portable).
674
- *
675
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
676
- * `chunks` table constrains nothing, so a deployment that ingests its own
677
- * buckets can query them (`build` only writes and prunes the three the
678
- * knowledge loader walks). Names are bound as query parameters, never
679
- * interpolated. An empty list retrieves nothing, and short-circuits before
680
- * the embedding call.
681
- */
682
- async query(q, k = 6, allowed = ["base"]) {
683
- if (allowed.length === 0) return [];
684
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
685
- const placeholders = allowed.map(() => "?").join(",");
686
- const res = await this.db.execute({
687
- sql: `SELECT c.id, c.text, e.embedding
688
- FROM chunks c
689
- JOIN embeddings e ON e.id = c.id
690
- WHERE c.bucket IN (${placeholders})`,
691
- args: allowed
692
- });
693
- const scored = [];
694
- for (const row of res.rows) {
695
- const emb = JSON.parse(String(row.embedding));
696
- const s = _VectorStore.cosine(qv, emb);
697
- scored.push({ s, text: String(row.text) });
698
- }
699
- scored.sort((a, b) => b.s - a.s);
700
- return scored.slice(0, k).map((r) => r.text);
701
- }
702
- };
898
+ // src/server.ts
899
+ init_retrieval();
703
900
 
704
901
  // src/core/serve-static.ts
705
902
  var STATIC_ADAPTERS = {
@@ -757,8 +954,12 @@ function resolveStatic(configStaticDir) {
757
954
  return { staticDir: null };
758
955
  }
759
956
 
957
+ // src/middleware/apiKey.ts
958
+ var API_KEY_HEADER = "x-api-key";
959
+ var API_KEY_PREVIEW_LENGTH = 20;
960
+
760
961
  // src/middleware/cors.ts
761
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization, x-conversation-id";
962
+ var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
762
963
  var ALLOWED_METHODS = "POST, GET, OPTIONS";
763
964
  var EXPOSED_HEADERS = "x-conversation-id";
764
965
  var cors = (allowedOrigins) => async (c, next) => {
@@ -822,22 +1023,27 @@ import { stream } from "hono/streaming";
822
1023
 
823
1024
  // src/core/session.ts
824
1025
  import { randomUUID as randomUUID2 } from "node:crypto";
1026
+ var SESSION_KEY_PREFIX = "session_";
1027
+ var DEFAULT_SESSION_MAX_REQUESTS = 20;
1028
+ var DEFAULT_SESSION_TTL_SECONDS = 3600;
1029
+ var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
825
1030
  var sessions = /* @__PURE__ */ new Map();
826
- var sessionSweeper = setInterval(
827
- () => {
828
- const now = Date.now();
829
- for (const [key, data] of Array.from(sessions.entries())) {
830
- if (now > data.expiresAt) {
831
- sessions.delete(key);
832
- }
1031
+ var sessionSweeper = setInterval(() => {
1032
+ const now = Date.now();
1033
+ for (const [key, data] of Array.from(sessions.entries())) {
1034
+ if (now > data.expiresAt) {
1035
+ sessions.delete(key);
833
1036
  }
834
- },
835
- 5 * 60 * 1e3
836
- );
1037
+ }
1038
+ }, SESSION_SWEEP_INTERVAL_MS);
837
1039
  sessionSweeper.unref?.();
838
1040
  function createSession(options = {}) {
839
- const { maxRequests = 20, ttl = 3600, metadata } = options;
840
- const key = `session_${randomUUID2()}`;
1041
+ const {
1042
+ maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
1043
+ ttl = DEFAULT_SESSION_TTL_SECONDS,
1044
+ metadata
1045
+ } = options;
1046
+ const key = `${SESSION_KEY_PREFIX}${randomUUID2()}`;
841
1047
  const now = Date.now();
842
1048
  const session = {
843
1049
  key,
@@ -1000,6 +1206,8 @@ function limitDemo(trustProxy) {
1000
1206
  await next();
1001
1207
  };
1002
1208
  }
1209
+ var DEMO_SESSION_MAX_REQUESTS = 20;
1210
+ var DEMO_SESSION_TTL_SECONDS = 3600;
1003
1211
  function demoRoutes(deps) {
1004
1212
  const { client, store, config, prompts, logger } = deps;
1005
1213
  const app = new Hono();
@@ -1012,18 +1220,17 @@ function demoRoutes(deps) {
1012
1220
  limitSessionCreation(trustProxy),
1013
1221
  (c) => {
1014
1222
  const session = createSession({
1015
- maxRequests: 20,
1016
- ttl: 3600
1017
- // 1 hour
1223
+ maxRequests: DEMO_SESSION_MAX_REQUESTS,
1224
+ ttl: DEMO_SESSION_TTL_SECONDS
1018
1225
  });
1019
1226
  logger.debug(
1020
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
1227
+ `[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
1021
1228
  );
1022
1229
  return c.json({
1023
1230
  key: session.key,
1024
- expiresIn: 3600,
1231
+ expiresIn: DEMO_SESSION_TTL_SECONDS,
1025
1232
  maxRequests: session.maxRequests,
1026
- message: "Use this temporary key in the x-api-key header"
1233
+ message: `Use this temporary key in the ${API_KEY_HEADER} header`
1027
1234
  });
1028
1235
  }
1029
1236
  );
@@ -1057,6 +1264,7 @@ function demoRoutes(deps) {
1057
1264
  buckets,
1058
1265
  rewriteQuery: config.rewriteQuery,
1059
1266
  rerankContext: config.rerankContext,
1267
+ fallbackFn: config.fallbackFn,
1060
1268
  logger
1061
1269
  });
1062
1270
  const model = config.openai.model || DEFAULT_MODEL;
@@ -1159,7 +1367,7 @@ function createRateLimiter(config) {
1159
1367
  // Demo keys get 10x stricter rate limits
1160
1368
  limitPublic() {
1161
1369
  return async (c, next) => {
1162
- const apiKey = c.req.header("x-api-key");
1370
+ const apiKey = c.req.header(API_KEY_HEADER);
1163
1371
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1164
1372
  const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1165
1373
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
@@ -1228,9 +1436,13 @@ function openaiRoutes(deps) {
1228
1436
  return errorJson(c, 401, "API key authentication is not configured on this server");
1229
1437
  }
1230
1438
  const bearer = c.req.header("authorization") ?? "";
1231
- const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header("x-api-key");
1439
+ const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
1232
1440
  if (!key) {
1233
- return errorJson(c, 401, "Missing API key (Authorization: Bearer or x-api-key header)");
1441
+ return errorJson(
1442
+ c,
1443
+ 401,
1444
+ `Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
1445
+ );
1234
1446
  }
1235
1447
  try {
1236
1448
  const result = await apiKeyManager.verify(key);
@@ -1270,6 +1482,7 @@ function openaiRoutes(deps) {
1270
1482
  sender,
1271
1483
  rewriteQuery: config.rewriteQuery,
1272
1484
  rerankContext: config.rerankContext,
1485
+ fallbackFn: config.fallbackFn,
1273
1486
  logger
1274
1487
  }));
1275
1488
  } catch {
@@ -1393,6 +1606,7 @@ function privateRoutes(deps) {
1393
1606
  sender,
1394
1607
  rewriteQuery: config.rewriteQuery,
1395
1608
  rerankContext: config.rerankContext,
1609
+ fallbackFn: config.fallbackFn,
1396
1610
  logger
1397
1611
  }));
1398
1612
  } catch {
@@ -1445,9 +1659,9 @@ import { stream as stream4 } from "hono/streaming";
1445
1659
  function createAuthMiddleware(deps) {
1446
1660
  const { apiKeyManager, logger } = deps;
1447
1661
  return async function requirePublicKey(c, next) {
1448
- const hdr = c.req.header("x-api-key");
1449
- if (hdr?.startsWith("session_")) {
1450
- logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1662
+ const hdr = c.req.header(API_KEY_HEADER);
1663
+ if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
1664
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1451
1665
  await next();
1452
1666
  return;
1453
1667
  }
@@ -1457,7 +1671,7 @@ function createAuthMiddleware(deps) {
1457
1671
  }
1458
1672
  if (!hdr) {
1459
1673
  logger.debug("[Auth] No API key provided");
1460
- return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1674
+ return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
1461
1675
  }
1462
1676
  try {
1463
1677
  const result = await apiKeyManager.verify(hdr);
@@ -1471,22 +1685,23 @@ function createAuthMiddleware(deps) {
1471
1685
  } catch (error) {
1472
1686
  logger.warn(`[Auth] JWT verification error: ${error}`);
1473
1687
  }
1474
- logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1688
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
1475
1689
  return c.json({ error: "Unauthorized" }, 401);
1476
1690
  };
1477
1691
  }
1478
1692
 
1479
1693
  // src/middleware/referrer.ts
1694
+ init_logger();
1480
1695
  function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1481
1696
  return async (c, next) => {
1482
- const apiKey = c.req.header("x-api-key");
1697
+ const apiKey = c.req.header(API_KEY_HEADER);
1483
1698
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1484
- const isSessionKey = apiKey?.startsWith("session_");
1699
+ const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
1485
1700
  if (isDemoKey || isSessionKey) {
1486
1701
  const referer = c.req.header("referer");
1487
1702
  const origin = c.req.header("origin");
1488
1703
  logger.debug(
1489
- `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1704
+ `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
1490
1705
  );
1491
1706
  logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1492
1707
  const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
@@ -1506,11 +1721,12 @@ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsol
1506
1721
  }
1507
1722
 
1508
1723
  // src/middleware/session.ts
1724
+ init_logger();
1509
1725
  function validateSessionKey(logger = createConsoleLogger()) {
1510
1726
  return async (c, next) => {
1511
- const apiKey = c.req.header("x-api-key");
1512
- if (apiKey?.startsWith("session_")) {
1513
- logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1727
+ const apiKey = c.req.header(API_KEY_HEADER);
1728
+ if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
1729
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1514
1730
  const info = getSessionInfo(apiKey);
1515
1731
  if (info) {
1516
1732
  logger.debug(
@@ -1574,6 +1790,7 @@ function publicRoutes(deps) {
1574
1790
  buckets,
1575
1791
  rewriteQuery: config.rewriteQuery,
1576
1792
  rerankContext: config.rerankContext,
1793
+ fallbackFn: config.fallbackFn,
1577
1794
  logger
1578
1795
  }));
1579
1796
  } catch {
@@ -1613,8 +1830,7 @@ function publicRoutes(deps) {
1613
1830
  async function createServer(config) {
1614
1831
  const logger = resolveLogger(config.logger, config.logLevel);
1615
1832
  logger.info(`\u{1F680} Starting ${config.bot.name}...`);
1616
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
1617
- const promptsDir = config.promptsDir || "./config/prompts";
1833
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
1618
1834
  const publicDir = config.publicDir || "./public";
1619
1835
  const enablePublic = config.features?.enablePublicChat !== false;
1620
1836
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -1625,16 +1841,26 @@ async function createServer(config) {
1625
1841
  const client = new OpenAI({
1626
1842
  apiKey: config.openai.apiKey
1627
1843
  });
1628
- const db = createClient2({
1629
- url: config.database.url,
1630
- authToken: config.database.authToken
1631
- });
1632
- const store = new VectorStore(client, {
1633
- databaseClient: db,
1634
- knowledgeDir,
1635
- logger
1636
- });
1637
- await store.build();
1844
+ let db;
1845
+ if (config.database) {
1846
+ const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1847
+ db = await openLibsqlClient2(config.database);
1848
+ }
1849
+ let store;
1850
+ if (config.retriever) {
1851
+ store = config.retriever;
1852
+ } else {
1853
+ if (!db) {
1854
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
1855
+ }
1856
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1857
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
1858
+ databaseClient: db,
1859
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1860
+ logger
1861
+ });
1862
+ }
1863
+ await store.build?.();
1638
1864
  logger.info("\u2705 Knowledge base ready");
1639
1865
  const prompts = new PromptLoader(promptsDir, config.bot);
1640
1866
  let apiKeyManager;
@@ -1722,6 +1948,9 @@ async function createServer(config) {
1722
1948
  const deps = {
1723
1949
  client,
1724
1950
  store,
1951
+ // See ServerDependencies.db's doc comment: undefined here (cast to
1952
+ // satisfy the still-required type) exactly when config.database was
1953
+ // never set - i.e. a config.retriever host with nothing else needing db.
1725
1954
  db,
1726
1955
  config,
1727
1956
  prompts,