@diegoaltoworks/chatter 0.61.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 +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 +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 +4 -1
package/dist/index.mjs CHANGED
@@ -1,3 +1,398 @@
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/buildLock.ts
17
+ function canAcquireBuildLock(existing, holderId, now, staleMs = BUILD_LOCK_STALE_MS) {
18
+ if (!existing) return true;
19
+ if (existing.holderId === holderId) return true;
20
+ return now - existing.heartbeatAt >= staleMs;
21
+ }
22
+ function ensureBuildLockSchema(client) {
23
+ let ready = schemaReady.get(client);
24
+ if (!ready) {
25
+ ready = client.execute(
26
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
27
+ lock_key TEXT PRIMARY KEY,
28
+ holder_id TEXT NOT NULL,
29
+ heartbeat_at INTEGER NOT NULL
30
+ )`
31
+ ).catch((error) => {
32
+ schemaReady.delete(client);
33
+ throw error;
34
+ });
35
+ schemaReady.set(client, ready);
36
+ }
37
+ return ready;
38
+ }
39
+ function createTursoBuildLock(client) {
40
+ return {
41
+ async tryAcquire(key, holderId, now, staleMs) {
42
+ await ensureBuildLockSchema(client);
43
+ const result = await client.execute({
44
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
45
+ ON CONFLICT(lock_key) DO UPDATE SET
46
+ holder_id = excluded.holder_id,
47
+ heartbeat_at = excluded.heartbeat_at
48
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
49
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
50
+ RETURNING holder_id`,
51
+ args: [key, holderId, now, staleMs]
52
+ });
53
+ const row = result.rows[0];
54
+ return Boolean(row) && row.holder_id === holderId;
55
+ },
56
+ async release(key, holderId) {
57
+ await ensureBuildLockSchema(client);
58
+ await client.execute({
59
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
60
+ args: [key, holderId]
61
+ });
62
+ }
63
+ };
64
+ }
65
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
66
+ var init_buildLock = __esm({
67
+ "src/core/buildLock.ts"() {
68
+ "use strict";
69
+ BUILD_LOCK_KEY = "knowledge_build";
70
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
71
+ schemaReady = /* @__PURE__ */ new WeakMap();
72
+ }
73
+ });
74
+
75
+ // src/core/loaders.ts
76
+ import { readdirSync, readFileSync, statSync } from "node:fs";
77
+ import { join } from "node:path";
78
+ function walk(dir) {
79
+ const items = [];
80
+ try {
81
+ for (const e of readdirSync(dir)) {
82
+ const p = join(dir, e);
83
+ const st = statSync(p);
84
+ if (st.isDirectory()) items.push(...walk(p));
85
+ else if (p.endsWith(".md")) items.push(p);
86
+ }
87
+ } catch {
88
+ }
89
+ return items;
90
+ }
91
+ function loadKnowledge(root = "config/knowledge") {
92
+ const docs = [];
93
+ for (const bucket of ["base", "public", "private"]) {
94
+ const dir = join(root, bucket);
95
+ for (const f of walk(dir)) {
96
+ docs.push({ id: f, text: readFileSync(f, "utf8"), source: f, bucket });
97
+ }
98
+ }
99
+ return docs;
100
+ }
101
+ var init_loaders = __esm({
102
+ "src/core/loaders.ts"() {
103
+ "use strict";
104
+ }
105
+ });
106
+
107
+ // src/core/logger.ts
108
+ function createConsoleLogger(level = "info") {
109
+ const threshold = LEVEL_ORDER[level];
110
+ const write = (at) => (...args) => {
111
+ if (LEVEL_ORDER[at] < threshold) return;
112
+ console.error(`[${at}]`, ...args);
113
+ };
114
+ return {
115
+ debug: write("debug"),
116
+ info: write("info"),
117
+ warn: write("warn"),
118
+ error: write("error")
119
+ };
120
+ }
121
+ function resolveLogger(logger, level) {
122
+ return logger ?? createConsoleLogger(level);
123
+ }
124
+ var LEVEL_ORDER;
125
+ var init_logger = __esm({
126
+ "src/core/logger.ts"() {
127
+ "use strict";
128
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
129
+ }
130
+ });
131
+
132
+ // src/core/retrieval.ts
133
+ var retrieval_exports = {};
134
+ __export(retrieval_exports, {
135
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
136
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
137
+ VectorStore: () => VectorStore,
138
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
139
+ openLibsqlClient: () => openLibsqlClient,
140
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
141
+ });
142
+ function createOpenAIEmbedder(client) {
143
+ return async (input) => {
144
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
145
+ return res.data.map((d) => d.embedding);
146
+ };
147
+ }
148
+ function wrapMissingLibsqlError(cause) {
149
+ return new Error(
150
+ "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.",
151
+ { cause }
152
+ );
153
+ }
154
+ async function openLibsqlClient(database) {
155
+ const mod = await import("@libsql/client").catch((error) => {
156
+ throw wrapMissingLibsqlError(error);
157
+ });
158
+ return mod.createClient({ url: database.url, authToken: database.authToken });
159
+ }
160
+ function chunk(text, max = 900) {
161
+ const out = [];
162
+ let buf = "";
163
+ for (const line of text.replace(/\r/g, "").split("\n")) {
164
+ if (`${buf}
165
+ ${line}`.length > max) {
166
+ out.push(buf.trim());
167
+ buf = line;
168
+ } else buf += `
169
+ ${line}`;
170
+ }
171
+ if (buf.trim()) out.push(buf.trim());
172
+ return out;
173
+ }
174
+ async function sha256(input) {
175
+ const data = new TextEncoder().encode(input);
176
+ const hash = await crypto.subtle.digest("SHA-256", data);
177
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
178
+ }
179
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
180
+ var init_retrieval = __esm({
181
+ "src/core/retrieval.ts"() {
182
+ "use strict";
183
+ init_buildLock();
184
+ init_loaders();
185
+ init_logger();
186
+ EMB_MODEL = "text-embedding-3-large";
187
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
188
+ 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.";
189
+ VectorStore = class _VectorStore {
190
+ constructor(embed, options) {
191
+ this.embed = embed;
192
+ this.db = options.databaseClient;
193
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
194
+ this.logger = options.logger ?? createConsoleLogger();
195
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
196
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
197
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
198
+ }
199
+ embed;
200
+ /**
201
+ * The libsql client backing this store - the same instance passed in as
202
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
203
+ * and the store share one connection.
204
+ */
205
+ db;
206
+ knowledgeDir;
207
+ logger;
208
+ buildLock;
209
+ instanceId;
210
+ buildLockStaleMs;
211
+ /**
212
+ * On boot: ingest new chunks and embed only missing ones.
213
+ *
214
+ * The whole ingest runs under a single-writer lock held in the database
215
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
216
+ * every chunk id the current `knowledgeDir` did not produce. Without the
217
+ * lock, a second instance booting mid-build diffs against a database the
218
+ * first one is still writing and deletes its chunks. An instance that
219
+ * cannot take the lock skips its build entirely and serves what the holder
220
+ * has already written, rather than racing it.
221
+ */
222
+ async build() {
223
+ this.logger.info("Building knowledge base...");
224
+ await this.db.execute(`
225
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
226
+ `);
227
+ await this.db.execute(`
228
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
229
+ `);
230
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
231
+ if (!await this.renewBuildLock()) {
232
+ this.logger.warn(
233
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
234
+ );
235
+ return;
236
+ }
237
+ try {
238
+ await this.buildUnderLock();
239
+ } finally {
240
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
241
+ }
242
+ }
243
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
244
+ renewBuildLock() {
245
+ return this.buildLock.tryAcquire(
246
+ BUILD_LOCK_KEY,
247
+ this.instanceId,
248
+ Date.now(),
249
+ this.buildLockStaleMs
250
+ );
251
+ }
252
+ /** The ingest itself. Only ever runs with the build lock held. */
253
+ async buildUnderLock() {
254
+ const docs = loadKnowledge(this.knowledgeDir);
255
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
256
+ if (docs.length === 0) {
257
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
258
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
259
+ if (existingCount > 0) {
260
+ throw new Error(
261
+ `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.`
262
+ );
263
+ }
264
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
265
+ return;
266
+ }
267
+ const rows = [];
268
+ for (const d of docs) {
269
+ for (const part of chunk(d.text)) {
270
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
271
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
272
+ }
273
+ }
274
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
275
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
276
+ const UPSERT_BATCH = 500;
277
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
278
+ const batch = rows.slice(i, i + UPSERT_BATCH);
279
+ await this.db.batch(
280
+ batch.map((r) => ({
281
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
282
+ ON CONFLICT(id) DO NOTHING`,
283
+ args: [r.id, r.bucket, r.source, r.text]
284
+ })),
285
+ "write"
286
+ );
287
+ }
288
+ const ids = rows.map((r) => r.id);
289
+ const missing = [];
290
+ for (let i = 0; i < ids.length; i += 500) {
291
+ const batch = ids.slice(i, i + 500);
292
+ const placeholders = batch.map(() => "?").join(",");
293
+ const res = await this.db.execute({
294
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
295
+ EXCEPT SELECT id FROM embeddings`,
296
+ args: batch
297
+ });
298
+ for (const row of res.rows) missing.push(String(row.id));
299
+ }
300
+ if (missing.length === 0) {
301
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
302
+ return;
303
+ }
304
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
305
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
306
+ const BATCH = 96;
307
+ for (let i = 0; i < missing.length; i += BATCH) {
308
+ const batchIds = missing.slice(i, i + BATCH);
309
+ const inputs = batchIds.map((id) => textById.get(id) || "");
310
+ const vectors = await this.embed(inputs);
311
+ await this.db.batch(
312
+ vectors.map((embedding, idx) => ({
313
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
314
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
315
+ })),
316
+ "write"
317
+ );
318
+ if (!await this.renewBuildLock()) {
319
+ this.logger.warn(
320
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
321
+ );
322
+ }
323
+ }
324
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
325
+ }
326
+ // Remove chunks from database that no longer exist in markdown files
327
+ async cleanupStaleChunks(currentIds) {
328
+ const result = await this.db.execute("SELECT id FROM chunks");
329
+ const dbIds = result.rows.map((row) => String(row.id));
330
+ const currentIdSet = new Set(currentIds);
331
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
332
+ if (staleIds.length === 0) {
333
+ return;
334
+ }
335
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
336
+ const BATCH_SIZE = 500;
337
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
338
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
339
+ const placeholders = batch.map(() => "?").join(",");
340
+ await this.db.batch(
341
+ [
342
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
343
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
344
+ ],
345
+ "write"
346
+ );
347
+ }
348
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
349
+ }
350
+ static cosine(a, b) {
351
+ let dot = 0;
352
+ let na = 0;
353
+ let nb = 0;
354
+ for (let i = 0; i < a.length; i++) {
355
+ dot += a[i] * b[i];
356
+ na += a[i] * a[i];
357
+ nb += b[i] * b[i];
358
+ }
359
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
360
+ }
361
+ /**
362
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
363
+ * & portable).
364
+ *
365
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
366
+ * `chunks` table constrains nothing, so a deployment that ingests its own
367
+ * buckets can query them (`build` only writes and prunes the three the
368
+ * knowledge loader walks). Names are bound as query parameters, never
369
+ * interpolated. An empty list retrieves nothing, and short-circuits before
370
+ * the embedding call.
371
+ */
372
+ async query(q, k = 6, allowed = ["base"]) {
373
+ if (allowed.length === 0) return [];
374
+ const [qv] = await this.embed([q]);
375
+ const placeholders = allowed.map(() => "?").join(",");
376
+ const res = await this.db.execute({
377
+ sql: `SELECT c.id, c.text, e.embedding
378
+ FROM chunks c
379
+ JOIN embeddings e ON e.id = c.id
380
+ WHERE c.bucket IN (${placeholders})`,
381
+ args: allowed
382
+ });
383
+ const scored = [];
384
+ for (const row of res.rows) {
385
+ const emb = JSON.parse(String(row.embedding));
386
+ const s = _VectorStore.cosine(qv, emb);
387
+ scored.push({ s, text: String(row.text) });
388
+ }
389
+ scored.sort((a, b) => b.s - a.s);
390
+ return scored.slice(0, k).map((r) => r.text);
391
+ }
392
+ };
393
+ }
394
+ });
395
+
1
396
  // src/auth/apikeys.ts
2
397
  import { randomUUID } from "node:crypto";
3
398
  import { jwtVerify, SignJWT } from "jose";
@@ -212,7 +607,8 @@ async function answerOnce({
212
607
  async function applyTransformReply(transformReply, input, logger) {
213
608
  if (!transformReply) return input.text;
214
609
  try {
215
- return await transformReply(input);
610
+ const result = await transformReply(input);
611
+ return typeof result === "string" ? result : null;
216
612
  } catch (error) {
217
613
  logger?.error(
218
614
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -277,51 +673,10 @@ async function resolveBuckets({
277
673
  return cleaned.filter((bucket) => ceiling.has(bucket));
278
674
  }
279
675
 
280
- // src/core/loaders.ts
281
- import { readdirSync, readFileSync, statSync } from "node:fs";
282
- import { join } from "node:path";
283
- function walk(dir) {
284
- const items = [];
285
- try {
286
- for (const e of readdirSync(dir)) {
287
- const p = join(dir, e);
288
- const st = statSync(p);
289
- if (st.isDirectory()) items.push(...walk(p));
290
- else if (p.endsWith(".md")) items.push(p);
291
- }
292
- } catch {
293
- }
294
- return items;
295
- }
296
- function loadKnowledge(root = "config/knowledge") {
297
- const docs = [];
298
- for (const bucket of ["base", "public", "private"]) {
299
- const dir = join(root, bucket);
300
- for (const f of walk(dir)) {
301
- docs.push({ id: f, text: readFileSync(f, "utf8"), source: f, bucket });
302
- }
303
- }
304
- return docs;
305
- }
306
-
307
- // src/core/logger.ts
308
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
309
- function createConsoleLogger(level = "info") {
310
- const threshold = LEVEL_ORDER[level];
311
- const write = (at) => (...args) => {
312
- if (LEVEL_ORDER[at] < threshold) return;
313
- console.error(`[${at}]`, ...args);
314
- };
315
- return {
316
- debug: write("debug"),
317
- info: write("info"),
318
- warn: write("warn"),
319
- error: write("error")
320
- };
321
- }
322
- function resolveLogger(logger, level) {
323
- return logger ?? createConsoleLogger(level);
324
- }
676
+ // src/index.ts
677
+ init_buildLock();
678
+ init_loaders();
679
+ init_logger();
325
680
 
326
681
  // src/core/messages.ts
327
682
  function isClientRole(role) {
@@ -388,6 +743,7 @@ async function prepareChat({
388
743
  sender,
389
744
  rewriteQuery,
390
745
  rerankContext,
746
+ fallbackFn,
391
747
  logger
392
748
  }) {
393
749
  const lastUserMsg = lastUserMessage(messages);
@@ -404,21 +760,39 @@ async function prepareChat({
404
760
  logger?.error("rewriteQuery threw; falling back to the original query", error);
405
761
  }
406
762
  }
407
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
763
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
764
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
765
+ let ctx = retrievedChunks;
408
766
  if (rerankContext) {
409
767
  try {
410
- const reranked = await rerankContext({ query, chunks: ctx });
768
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
411
769
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
412
770
  } catch (error) {
413
771
  logger?.error("rerankContext threw; falling back to the original chunks", error);
414
772
  }
415
773
  }
774
+ let fallback;
775
+ if (ctx.length === 0 && fallbackFn) {
776
+ try {
777
+ const guidance = await fallbackFn({
778
+ query,
779
+ mode,
780
+ buckets: resolvedBuckets,
781
+ sender,
782
+ retrievedChunks
783
+ });
784
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
785
+ } catch (error) {
786
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
787
+ }
788
+ }
416
789
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
417
790
  const hint = channelHint?.trim();
418
791
  const system = [
419
792
  prompts.baseSystemRules,
420
793
  persona,
421
794
  ...hint ? [hint] : [],
795
+ ...fallback ? [fallback] : [],
422
796
  `${label}:
423
797
  ${ctx.join("\n\n")}`
424
798
  ].join("\n\n");
@@ -428,6 +802,7 @@ ${ctx.join("\n\n")}`
428
802
  // src/core/prompts.ts
429
803
  import { readFileSync as readFileSync2 } from "node:fs";
430
804
  import { join as join2 } from "node:path";
805
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
431
806
  var PromptLoader = class {
432
807
  constructor(promptsDir, bot) {
433
808
  this.promptsDir = promptsDir;
@@ -458,196 +833,32 @@ var PromptLoader = class {
458
833
  }
459
834
  };
460
835
 
461
- // src/core/retrieval.ts
462
- import { createClient } from "@libsql/client";
463
- var EMB_MODEL = "text-embedding-3-large";
464
- function chunk(text, max = 900) {
465
- const out = [];
466
- let buf = "";
467
- for (const line of text.replace(/\r/g, "").split("\n")) {
468
- if (`${buf}
469
- ${line}`.length > max) {
470
- out.push(buf.trim());
471
- buf = line;
472
- } else buf += `
473
- ${line}`;
474
- }
475
- if (buf.trim()) out.push(buf.trim());
476
- return out;
477
- }
478
- async function sha256(input) {
479
- const data = new TextEncoder().encode(input);
480
- const hash = await crypto.subtle.digest("SHA-256", data);
481
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
482
- }
483
- var VectorStore = class _VectorStore {
484
- constructor(client, options) {
485
- this.client = client;
486
- this.db = options.databaseClient ? options.databaseClient : createClient({
487
- url: options.databaseUrl,
488
- authToken: options.databaseAuthToken
489
- });
490
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
491
- this.logger = options.logger ?? createConsoleLogger();
492
- }
493
- client;
494
- /**
495
- * The libsql client backing this store. When a client was injected this is
496
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
497
- * and the store share one connection.
498
- */
499
- db;
500
- knowledgeDir;
501
- logger;
502
- // On boot: ingest new chunks and embed only missing ones.
503
- async build() {
504
- this.logger.info("\u{1F504} Building knowledge base...");
505
- await this.db.execute(`
506
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
507
- `);
508
- await this.db.execute(`
509
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
510
- `);
511
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
512
- const docs = loadKnowledge(this.knowledgeDir);
513
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
514
- const rows = [];
515
- for (const d of docs) {
516
- for (const part of chunk(d.text)) {
517
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
518
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
519
- }
520
- }
521
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
522
- await this.cleanupStaleChunks(rows.map((r) => r.id));
523
- const UPSERT_BATCH = 500;
524
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
525
- const batch = rows.slice(i, i + UPSERT_BATCH);
526
- await this.db.batch(
527
- batch.map((r) => ({
528
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
529
- ON CONFLICT(id) DO NOTHING`,
530
- args: [r.id, r.bucket, r.source, r.text]
531
- })),
532
- "write"
533
- );
534
- }
535
- const ids = rows.map((r) => r.id);
536
- const missing = [];
537
- for (let i = 0; i < ids.length; i += 500) {
538
- const batch = ids.slice(i, i + 500);
539
- const placeholders = batch.map(() => "?").join(",");
540
- const res = await this.db.execute({
541
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
542
- EXCEPT SELECT id FROM embeddings`,
543
- args: batch
544
- });
545
- for (const row of res.rows) missing.push(String(row.id));
546
- }
547
- if (missing.length === 0) {
548
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
549
- return;
550
- }
551
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
552
- const textById = new Map(rows.map((r) => [r.id, r.text]));
553
- const BATCH = 96;
554
- for (let i = 0; i < missing.length; i += BATCH) {
555
- const batchIds = missing.slice(i, i + BATCH);
556
- const inputs = batchIds.map((id) => textById.get(id) || "");
557
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
558
- await this.db.batch(
559
- emb.data.map((d, idx) => ({
560
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
561
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
562
- })),
563
- "write"
564
- );
565
- }
566
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
567
- }
568
- // Remove chunks from database that no longer exist in markdown files
569
- async cleanupStaleChunks(currentIds) {
570
- const result = await this.db.execute("SELECT id FROM chunks");
571
- const dbIds = result.rows.map((row) => String(row.id));
572
- const currentIdSet = new Set(currentIds);
573
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
574
- if (staleIds.length === 0) {
575
- return;
576
- }
577
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
578
- const BATCH_SIZE = 500;
579
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
580
- const batch = staleIds.slice(i, i + BATCH_SIZE);
581
- const placeholders = batch.map(() => "?").join(",");
582
- await this.db.execute({
583
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
584
- args: batch
585
- });
586
- }
587
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
588
- }
589
- static cosine(a, b) {
590
- let dot = 0;
591
- let na = 0;
592
- let nb = 0;
593
- for (let i = 0; i < a.length; i++) {
594
- dot += a[i] * b[i];
595
- na += a[i] * a[i];
596
- nb += b[i] * b[i];
597
- }
598
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
599
- }
600
- /**
601
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
602
- * & portable).
603
- *
604
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
605
- * `chunks` table constrains nothing, so a deployment that ingests its own
606
- * buckets can query them (`build` only writes and prunes the three the
607
- * knowledge loader walks). Names are bound as query parameters, never
608
- * interpolated. An empty list retrieves nothing, and short-circuits before
609
- * the embedding call.
610
- */
611
- async query(q, k = 6, allowed = ["base"]) {
612
- if (allowed.length === 0) return [];
613
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
614
- const placeholders = allowed.map(() => "?").join(",");
615
- const res = await this.db.execute({
616
- sql: `SELECT c.id, c.text, e.embedding
617
- FROM chunks c
618
- JOIN embeddings e ON e.id = c.id
619
- WHERE c.bucket IN (${placeholders})`,
620
- args: allowed
621
- });
622
- const scored = [];
623
- for (const row of res.rows) {
624
- const emb = JSON.parse(String(row.embedding));
625
- const s = _VectorStore.cosine(qv, emb);
626
- scored.push({ s, text: String(row.text) });
627
- }
628
- scored.sort((a, b) => b.s - a.s);
629
- return scored.slice(0, k).map((r) => r.text);
630
- }
631
- };
836
+ // src/index.ts
837
+ init_retrieval();
632
838
 
633
839
  // src/core/session.ts
634
840
  import { randomUUID as randomUUID2 } from "node:crypto";
841
+ var SESSION_KEY_PREFIX = "session_";
842
+ var DEFAULT_SESSION_MAX_REQUESTS = 20;
843
+ var DEFAULT_SESSION_TTL_SECONDS = 3600;
844
+ var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
635
845
  var sessions = /* @__PURE__ */ new Map();
636
- var sessionSweeper = setInterval(
637
- () => {
638
- const now = Date.now();
639
- for (const [key, data] of Array.from(sessions.entries())) {
640
- if (now > data.expiresAt) {
641
- sessions.delete(key);
642
- }
846
+ var sessionSweeper = setInterval(() => {
847
+ const now = Date.now();
848
+ for (const [key, data] of Array.from(sessions.entries())) {
849
+ if (now > data.expiresAt) {
850
+ sessions.delete(key);
643
851
  }
644
- },
645
- 5 * 60 * 1e3
646
- );
852
+ }
853
+ }, SESSION_SWEEP_INTERVAL_MS);
647
854
  sessionSweeper.unref?.();
648
855
  function createSession(options = {}) {
649
- const { maxRequests = 20, ttl = 3600, metadata } = options;
650
- const key = `session_${randomUUID2()}`;
856
+ const {
857
+ maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
858
+ ttl = DEFAULT_SESSION_TTL_SECONDS,
859
+ metadata
860
+ } = options;
861
+ const key = `${SESSION_KEY_PREFIX}${randomUUID2()}`;
651
862
  const now = Date.now();
652
863
  const session = {
653
864
  key,
@@ -683,6 +894,8 @@ function getActiveSessions() {
683
894
 
684
895
  // src/mcp-server.ts
685
896
  import OpenAI from "openai";
897
+ init_logger();
898
+ init_retrieval();
686
899
 
687
900
  // src/mcp-server/conversation-id.ts
688
901
  function generateConversationId() {
@@ -709,28 +922,38 @@ function calculateCost(usage, rates) {
709
922
  }
710
923
 
711
924
  // src/mcp-server/logger.ts
925
+ init_logger();
712
926
  var MCPLogger = class {
713
927
  enableConsole;
714
928
  callback;
715
929
  logger;
716
- constructor(enableConsole = true, callback, logger) {
930
+ logContent;
931
+ constructor(enableConsole = true, callback, logger, logContent = false) {
717
932
  this.enableConsole = enableConsole;
718
933
  this.callback = callback;
719
934
  this.logger = logger ?? createConsoleLogger();
935
+ this.logContent = logContent;
720
936
  }
721
937
  /**
722
938
  * Log a chat interaction
723
939
  */
724
940
  async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
725
941
  const lastUserMsg = lastUserMessage(conversationMessages);
942
+ const content = {
943
+ userMessage: scrubOutput(lastUserMsg?.content || ""),
944
+ conversationHistory: conversationMessages.map((msg) => ({
945
+ ...msg,
946
+ content: scrubOutput(msg.content)
947
+ })),
948
+ ragContext: ragContext.map(scrubOutput),
949
+ response: scrubOutput(response)
950
+ };
951
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
726
952
  const logEvent = {
727
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
953
+ timestamp,
728
954
  toolName,
729
955
  conversationId,
730
- userMessage: lastUserMsg?.content || "",
731
- conversationHistory: conversationMessages,
732
- ragContext,
733
- response,
956
+ ...content,
734
957
  duration,
735
958
  cost
736
959
  };
@@ -738,9 +961,27 @@ var MCPLogger = class {
738
961
  this.logger.info(
739
962
  JSON.stringify({
740
963
  event: "mcp_chat",
741
- ...logEvent
964
+ timestamp,
965
+ toolName,
966
+ conversationId,
967
+ messageCount: conversationMessages.length,
968
+ ragContextCount: ragContext.length,
969
+ responseChars: response.length,
970
+ duration,
971
+ cost
742
972
  })
743
973
  );
974
+ if (this.logContent) {
975
+ this.logger.debug(
976
+ JSON.stringify({
977
+ event: "mcp_chat_content",
978
+ timestamp,
979
+ toolName,
980
+ conversationId,
981
+ ...content
982
+ })
983
+ );
984
+ }
744
985
  }
745
986
  if (this.callback) {
746
987
  try {
@@ -751,11 +992,12 @@ var MCPLogger = class {
751
992
  }
752
993
  }
753
994
  };
754
- function createLogger(enableConsole = true, callback, logger) {
755
- return new MCPLogger(enableConsole, callback, logger);
995
+ function createLogger(enableConsole = true, callback, logger, logContent = false) {
996
+ return new MCPLogger(enableConsole, callback, logger, logContent);
756
997
  }
757
998
 
758
999
  // src/mcp-server/rate-limiter.ts
1000
+ var DEFAULT_WINDOW_MS = 6e4;
759
1001
  var RateLimiter = class {
760
1002
  requestMap;
761
1003
  limit;
@@ -764,9 +1006,9 @@ var RateLimiter = class {
764
1006
  * Create a new rate limiter
765
1007
  *
766
1008
  * @param limit - Maximum requests allowed in the time window
767
- * @param windowMs - Time window in milliseconds (default: 60000 = 1 minute)
1009
+ * @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
768
1010
  */
769
- constructor(limit, windowMs = 6e4) {
1011
+ constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
770
1012
  this.requestMap = /* @__PURE__ */ new Map();
771
1013
  this.limit = limit;
772
1014
  this.windowMs = windowMs;
@@ -830,7 +1072,7 @@ var RateLimiter = class {
830
1072
  return Math.max(0, timeUntilReset);
831
1073
  }
832
1074
  };
833
- function createRateLimiter(limit, windowMs = 6e4) {
1075
+ function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
834
1076
  return limit ? new RateLimiter(limit, windowMs) : null;
835
1077
  }
836
1078
 
@@ -880,11 +1122,15 @@ async function createMCPServer(config) {
880
1122
  const log = resolveLogger(config.logger, config.logLevel);
881
1123
  log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
882
1124
  const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
883
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
884
- const promptsDir = config.promptsDir || "./config/prompts";
1125
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
885
1126
  const transportMode = config.transport || "stdio";
886
- const rateLimiter = createRateLimiter(config.toolRateLimit);
887
- const logger = createLogger(config.logging?.console !== false, config.logging?.onChat, log);
1127
+ const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
1128
+ const logger = createLogger(
1129
+ config.logging?.console !== false,
1130
+ config.logging?.onChat,
1131
+ log,
1132
+ config.logging?.content === true
1133
+ );
888
1134
  const publicToolConfig = {
889
1135
  enabled: config.tools?.public?.enabled !== false,
890
1136
  name: config.tools?.public?.name || "chat_public",
@@ -899,13 +1145,22 @@ async function createMCPServer(config) {
899
1145
  apiKey: config.openai.apiKey
900
1146
  });
901
1147
  const model = config.openai.model || DEFAULT_MODEL;
902
- const store = new VectorStore(client, {
903
- databaseUrl: config.database.url,
904
- databaseAuthToken: config.database.authToken,
905
- knowledgeDir,
906
- logger: log
907
- });
908
- await store.build();
1148
+ let store;
1149
+ if (config.retriever) {
1150
+ store = config.retriever;
1151
+ } else {
1152
+ if (!config.database) {
1153
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
1154
+ }
1155
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1156
+ const db = await openLibsqlClient2(config.database);
1157
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
1158
+ databaseClient: db,
1159
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1160
+ logger: log
1161
+ });
1162
+ }
1163
+ await store.build?.();
909
1164
  log.info("\u2705 Knowledge base ready");
910
1165
  const prompts = new PromptLoader(promptsDir, config.bot);
911
1166
  const server = new McpServer({
@@ -954,6 +1209,7 @@ async function createMCPServer(config) {
954
1209
  buckets,
955
1210
  rewriteQuery: config.rewriteQuery,
956
1211
  rerankContext: config.rerankContext,
1212
+ fallbackFn: config.fallbackFn,
957
1213
  logger: log
958
1214
  });
959
1215
  const result = await answerOnce({
@@ -1043,6 +1299,7 @@ async function createMCPServer(config) {
1043
1299
  buckets,
1044
1300
  rewriteQuery: config.rewriteQuery,
1045
1301
  rerankContext: config.rerankContext,
1302
+ fallbackFn: config.fallbackFn,
1046
1303
  logger: log
1047
1304
  });
1048
1305
  const result = await answerOnce({
@@ -1104,13 +1361,17 @@ async function createMCPServer(config) {
1104
1361
  return server;
1105
1362
  }
1106
1363
 
1364
+ // src/middleware/apiKey.ts
1365
+ var API_KEY_HEADER = "x-api-key";
1366
+ var API_KEY_PREVIEW_LENGTH = 20;
1367
+
1107
1368
  // src/middleware/auth.ts
1108
1369
  function createAuthMiddleware(deps) {
1109
1370
  const { apiKeyManager, logger } = deps;
1110
1371
  return async function requirePublicKey(c, next) {
1111
- const hdr = c.req.header("x-api-key");
1112
- if (hdr?.startsWith("session_")) {
1113
- logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1372
+ const hdr = c.req.header(API_KEY_HEADER);
1373
+ if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
1374
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1114
1375
  await next();
1115
1376
  return;
1116
1377
  }
@@ -1120,7 +1381,7 @@ function createAuthMiddleware(deps) {
1120
1381
  }
1121
1382
  if (!hdr) {
1122
1383
  logger.debug("[Auth] No API key provided");
1123
- return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1384
+ return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
1124
1385
  }
1125
1386
  try {
1126
1387
  const result = await apiKeyManager.verify(hdr);
@@ -1134,7 +1395,7 @@ function createAuthMiddleware(deps) {
1134
1395
  } catch (error) {
1135
1396
  logger.warn(`[Auth] JWT verification error: ${error}`);
1136
1397
  }
1137
- logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1398
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
1138
1399
  return c.json({ error: "Unauthorized" }, 401);
1139
1400
  };
1140
1401
  }
@@ -1150,7 +1411,7 @@ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message)
1150
1411
  }
1151
1412
 
1152
1413
  // src/middleware/cors.ts
1153
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization, x-conversation-id";
1414
+ var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
1154
1415
  var ALLOWED_METHODS = "POST, GET, OPTIONS";
1155
1416
  var EXPOSED_HEADERS = "x-conversation-id";
1156
1417
  var cors = (allowedOrigins) => async (c, next) => {
@@ -1266,7 +1527,7 @@ function createWindowBucketLimiter(windowMs, now = Date.now) {
1266
1527
  }
1267
1528
 
1268
1529
  // src/middleware/ratelimit.ts
1269
- function createRateLimiter2(config) {
1530
+ function createRateLimiter(config) {
1270
1531
  const limiter = createWindowBucketLimiter(6e4);
1271
1532
  const publicLimit = config.rateLimit?.public || 60;
1272
1533
  const privateLimit = config.rateLimit?.private || 120;
@@ -1277,7 +1538,7 @@ function createRateLimiter2(config) {
1277
1538
  // Demo keys get 10x stricter rate limits
1278
1539
  limitPublic() {
1279
1540
  return async (c, next) => {
1280
- const apiKey = c.req.header("x-api-key");
1541
+ const apiKey = c.req.header(API_KEY_HEADER);
1281
1542
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1282
1543
  const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1283
1544
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
@@ -1304,6 +1565,9 @@ function createRateLimiter2(config) {
1304
1565
  };
1305
1566
  }
1306
1567
 
1568
+ // src/middleware/referrer.ts
1569
+ init_logger();
1570
+
1307
1571
  // src/middleware/originMatch.ts
1308
1572
  function matchesAllowedOrigin(origin, referer, allowedOrigins) {
1309
1573
  return allowedOrigins.some((allowed) => {
@@ -1334,14 +1598,14 @@ function matchesAllowedOriginOrLocalhost(origin, referer, allowedOrigins) {
1334
1598
  // src/middleware/referrer.ts
1335
1599
  function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1336
1600
  return async (c, next) => {
1337
- const apiKey = c.req.header("x-api-key");
1601
+ const apiKey = c.req.header(API_KEY_HEADER);
1338
1602
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1339
- const isSessionKey = apiKey?.startsWith("session_");
1603
+ const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
1340
1604
  if (isDemoKey || isSessionKey) {
1341
1605
  const referer = c.req.header("referer");
1342
1606
  const origin = c.req.header("origin");
1343
1607
  logger.debug(
1344
- `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1608
+ `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
1345
1609
  );
1346
1610
  logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1347
1611
  const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
@@ -1384,11 +1648,12 @@ function securityHeaders(config) {
1384
1648
  }
1385
1649
 
1386
1650
  // src/middleware/session.ts
1651
+ init_logger();
1387
1652
  function validateSessionKey(logger = createConsoleLogger()) {
1388
1653
  return async (c, next) => {
1389
- const apiKey = c.req.header("x-api-key");
1390
- if (apiKey?.startsWith("session_")) {
1391
- logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1654
+ const apiKey = c.req.header(API_KEY_HEADER);
1655
+ if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
1656
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1392
1657
  const info = getSessionInfo(apiKey);
1393
1658
  if (info) {
1394
1659
  logger.debug(
@@ -1469,6 +1734,8 @@ function limitDemo(trustProxy) {
1469
1734
  await next();
1470
1735
  };
1471
1736
  }
1737
+ var DEMO_SESSION_MAX_REQUESTS = 20;
1738
+ var DEMO_SESSION_TTL_SECONDS = 3600;
1472
1739
  function demoRoutes(deps) {
1473
1740
  const { client, store, config, prompts, logger } = deps;
1474
1741
  const app = new Hono();
@@ -1481,18 +1748,17 @@ function demoRoutes(deps) {
1481
1748
  limitSessionCreation(trustProxy),
1482
1749
  (c) => {
1483
1750
  const session = createSession({
1484
- maxRequests: 20,
1485
- ttl: 3600
1486
- // 1 hour
1751
+ maxRequests: DEMO_SESSION_MAX_REQUESTS,
1752
+ ttl: DEMO_SESSION_TTL_SECONDS
1487
1753
  });
1488
1754
  logger.debug(
1489
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
1755
+ `[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
1490
1756
  );
1491
1757
  return c.json({
1492
1758
  key: session.key,
1493
- expiresIn: 3600,
1759
+ expiresIn: DEMO_SESSION_TTL_SECONDS,
1494
1760
  maxRequests: session.maxRequests,
1495
- message: "Use this temporary key in the x-api-key header"
1761
+ message: `Use this temporary key in the ${API_KEY_HEADER} header`
1496
1762
  });
1497
1763
  }
1498
1764
  );
@@ -1526,6 +1792,7 @@ function demoRoutes(deps) {
1526
1792
  buckets,
1527
1793
  rewriteQuery: config.rewriteQuery,
1528
1794
  rerankContext: config.rerankContext,
1795
+ fallbackFn: config.fallbackFn,
1529
1796
  logger
1530
1797
  });
1531
1798
  const model = config.openai.model || DEFAULT_MODEL;
@@ -1606,7 +1873,7 @@ function oversizedBodyError(message) {
1606
1873
  function openaiRoutes(deps) {
1607
1874
  const { client, store, config, prompts, apiKeyManager, logger } = deps;
1608
1875
  const app = new Hono2();
1609
- const { limitPublic, limitPrivate } = createRateLimiter2(config);
1876
+ const { limitPublic, limitPrivate } = createRateLimiter(config);
1610
1877
  const requirePrivateJWT = createJWTMiddleware(config);
1611
1878
  const enablePublic = config.features?.enablePublicChat !== false;
1612
1879
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -1616,9 +1883,13 @@ function openaiRoutes(deps) {
1616
1883
  return errorJson(c, 401, "API key authentication is not configured on this server");
1617
1884
  }
1618
1885
  const bearer = c.req.header("authorization") ?? "";
1619
- const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header("x-api-key");
1886
+ const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
1620
1887
  if (!key) {
1621
- return errorJson(c, 401, "Missing API key (Authorization: Bearer or x-api-key header)");
1888
+ return errorJson(
1889
+ c,
1890
+ 401,
1891
+ `Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
1892
+ );
1622
1893
  }
1623
1894
  try {
1624
1895
  const result = await apiKeyManager.verify(key);
@@ -1658,6 +1929,7 @@ function openaiRoutes(deps) {
1658
1929
  sender,
1659
1930
  rewriteQuery: config.rewriteQuery,
1660
1931
  rerankContext: config.rerankContext,
1932
+ fallbackFn: config.fallbackFn,
1661
1933
  logger
1662
1934
  }));
1663
1935
  } catch {
@@ -1753,7 +2025,7 @@ function privateRoutes(deps) {
1753
2025
  const { client, store, config, prompts, logger } = deps;
1754
2026
  const app = new Hono3();
1755
2027
  const requirePrivateJWT = createJWTMiddleware(config);
1756
- const { limitPrivate } = createRateLimiter2(config);
2028
+ const { limitPrivate } = createRateLimiter(config);
1757
2029
  app.use("/api/private/*", chatBodyLimit(config.server?.maxRequestBytes));
1758
2030
  app.use("/api/private/*", requirePrivateJWT);
1759
2031
  app.use("/api/private/*", limitPrivate());
@@ -1781,6 +2053,7 @@ function privateRoutes(deps) {
1781
2053
  sender,
1782
2054
  rewriteQuery: config.rewriteQuery,
1783
2055
  rerankContext: config.rerankContext,
2056
+ fallbackFn: config.fallbackFn,
1784
2057
  logger
1785
2058
  }));
1786
2059
  } catch {
@@ -1838,7 +2111,7 @@ function publicRoutes(deps) {
1838
2111
  const { client, store, config, prompts, logger } = deps;
1839
2112
  const app = new Hono4();
1840
2113
  const requirePublicKey = createAuthMiddleware(deps);
1841
- const { limitPublic } = createRateLimiter2(config);
2114
+ const { limitPublic } = createRateLimiter(config);
1842
2115
  const referrerOrigins = config.server?.allowedOrigins?.length ? config.server.allowedOrigins.filter((o) => o !== "*") : [config.bot.publicUrl, "http://localhost:8181", "http://127.0.0.1:8181"];
1843
2116
  app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
1844
2117
  app.use("/api/public/*", validateSessionKey(logger));
@@ -1866,6 +2139,7 @@ function publicRoutes(deps) {
1866
2139
  buckets,
1867
2140
  rewriteQuery: config.rewriteQuery,
1868
2141
  rerankContext: config.rerankContext,
2142
+ fallbackFn: config.fallbackFn,
1869
2143
  logger
1870
2144
  }));
1871
2145
  } catch {
@@ -1903,11 +2177,11 @@ function publicRoutes(deps) {
1903
2177
 
1904
2178
  // src/server.ts
1905
2179
  import { relative } from "node:path";
1906
- import { createClient as createClient2 } from "@libsql/client";
1907
2180
  import { Hono as Hono5 } from "hono";
1908
2181
  import OpenAI2 from "openai";
1909
2182
 
1910
2183
  // src/channels/senders.ts
2184
+ init_logger();
1911
2185
  function createSenderRegistry(logger = createConsoleLogger()) {
1912
2186
  const senders = /* @__PURE__ */ new Map();
1913
2187
  return {
@@ -1970,6 +2244,10 @@ function createSenderRegistry(logger = createConsoleLogger()) {
1970
2244
  };
1971
2245
  }
1972
2246
 
2247
+ // src/server.ts
2248
+ init_logger();
2249
+ init_retrieval();
2250
+
1973
2251
  // src/core/serve-static.ts
1974
2252
  var STATIC_ADAPTERS = {
1975
2253
  bun: "hono/bun",
@@ -2030,8 +2308,7 @@ function resolveStatic(configStaticDir) {
2030
2308
  async function createServer(config) {
2031
2309
  const logger = resolveLogger(config.logger, config.logLevel);
2032
2310
  logger.info(`\u{1F680} Starting ${config.bot.name}...`);
2033
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
2034
- const promptsDir = config.promptsDir || "./config/prompts";
2311
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
2035
2312
  const publicDir = config.publicDir || "./public";
2036
2313
  const enablePublic = config.features?.enablePublicChat !== false;
2037
2314
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -2042,16 +2319,26 @@ async function createServer(config) {
2042
2319
  const client = new OpenAI2({
2043
2320
  apiKey: config.openai.apiKey
2044
2321
  });
2045
- const db = createClient2({
2046
- url: config.database.url,
2047
- authToken: config.database.authToken
2048
- });
2049
- const store = new VectorStore(client, {
2050
- databaseClient: db,
2051
- knowledgeDir,
2052
- logger
2053
- });
2054
- await store.build();
2322
+ let db;
2323
+ if (config.database) {
2324
+ const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
2325
+ db = await openLibsqlClient2(config.database);
2326
+ }
2327
+ let store;
2328
+ if (config.retriever) {
2329
+ store = config.retriever;
2330
+ } else {
2331
+ if (!db) {
2332
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
2333
+ }
2334
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
2335
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
2336
+ databaseClient: db,
2337
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
2338
+ logger
2339
+ });
2340
+ }
2341
+ await store.build?.();
2055
2342
  logger.info("\u2705 Knowledge base ready");
2056
2343
  const prompts = new PromptLoader(promptsDir, config.bot);
2057
2344
  let apiKeyManager;
@@ -2139,6 +2426,9 @@ async function createServer(config) {
2139
2426
  const deps = {
2140
2427
  client,
2141
2428
  store,
2429
+ // See ServerDependencies.db's doc comment: undefined here (cast to
2430
+ // satisfy the still-required type) exactly when config.database was
2431
+ // never set - i.e. a config.retriever host with nothing else needing db.
2142
2432
  db,
2143
2433
  config,
2144
2434
  prompts,
@@ -2194,13 +2484,16 @@ async function createServer(config) {
2194
2484
  }
2195
2485
 
2196
2486
  // src/shortener.ts
2197
- var DEFAULT_TIMEOUT_MS = 5e3;
2487
+ var SHORTENER_TIMEOUT_MS = 5e3;
2198
2488
  function createShortener(config, fetchImpl = fetch) {
2199
2489
  const apiUrl = config.apiUrl.replace(/\/+$/, "");
2200
2490
  return {
2201
2491
  async shorten(url) {
2202
2492
  const controller = new AbortController();
2203
- const timeout = setTimeout(() => controller.abort(), config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2493
+ const timeout = setTimeout(
2494
+ () => controller.abort(),
2495
+ config.timeoutMs ?? SHORTENER_TIMEOUT_MS
2496
+ );
2204
2497
  try {
2205
2498
  const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
2206
2499
  method: "POST",
@@ -2228,12 +2521,17 @@ function createShortener(config, fetchImpl = fetch) {
2228
2521
  }
2229
2522
  export {
2230
2523
  ApiKeyManager,
2524
+ BUILD_LOCK_KEY,
2525
+ BUILD_LOCK_STALE_MS,
2526
+ DEFAULT_KNOWLEDGE_DIR,
2231
2527
  DEFAULT_MODEL,
2528
+ DEFAULT_PROMPTS_DIR,
2232
2529
  PromptLoader,
2233
2530
  VectorStore,
2234
2531
  answerOnce,
2235
2532
  answerStream,
2236
2533
  applyTransformReply,
2534
+ canAcquireBuildLock,
2237
2535
  chatBodyLimit,
2238
2536
  completeOnce,
2239
2537
  completeStream,
@@ -2242,12 +2540,14 @@ export {
2242
2540
  createConsoleLogger,
2243
2541
  createJWTMiddleware,
2244
2542
  createMCPServer,
2245
- createRateLimiter2 as createRateLimiter,
2543
+ createOpenAIEmbedder,
2544
+ createRateLimiter,
2246
2545
  createReferrerCheck,
2247
2546
  createServer,
2248
2547
  createSession,
2249
2548
  createSessionMiddleware,
2250
2549
  createShortener,
2550
+ createTursoBuildLock,
2251
2551
  defaultBuckets,
2252
2552
  demoRoutes,
2253
2553
  detectLeakage,