@diegoaltoworks/chatter 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +55 -17
  2. package/dist/auth/secretStrength.d.ts +1 -1
  3. package/dist/auth/secretStrength.d.ts.map +1 -1
  4. package/dist/bin/create-apikey.js +45 -28
  5. package/dist/bin/wa-pair.js +113 -46
  6. package/dist/channels/backoff.d.ts +10 -0
  7. package/dist/channels/backoff.d.ts.map +1 -0
  8. package/dist/channels/gates.d.ts +15 -8
  9. package/dist/channels/gates.d.ts.map +1 -1
  10. package/dist/channels/index.d.ts +1 -1
  11. package/dist/channels/index.d.ts.map +1 -1
  12. package/dist/channels/index.js +38 -5
  13. package/dist/channels/index.mjs +37 -5
  14. package/dist/channels/matrix/api.d.ts +25 -6
  15. package/dist/channels/matrix/api.d.ts.map +1 -1
  16. package/dist/channels/matrix/channel.d.ts +4 -56
  17. package/dist/channels/matrix/channel.d.ts.map +1 -1
  18. package/dist/channels/matrix/handler.d.ts +16 -9
  19. package/dist/channels/matrix/handler.d.ts.map +1 -1
  20. package/dist/channels/matrix/index.d.ts +2 -2
  21. package/dist/channels/matrix/index.d.ts.map +1 -1
  22. package/dist/channels/matrix/index.js +196 -40
  23. package/dist/channels/matrix/index.mjs +189 -39
  24. package/dist/channels/matrix/sync.d.ts +4 -1
  25. package/dist/channels/matrix/sync.d.ts.map +1 -1
  26. package/dist/channels/matrix/updates.d.ts +39 -10
  27. package/dist/channels/matrix/updates.d.ts.map +1 -1
  28. package/dist/channels/pipeline.d.ts +14 -14
  29. package/dist/channels/pipeline.d.ts.map +1 -1
  30. package/dist/channels/polling.d.ts +67 -0
  31. package/dist/channels/polling.d.ts.map +1 -0
  32. package/dist/channels/telegram/channel.d.ts +2 -56
  33. package/dist/channels/telegram/channel.d.ts.map +1 -1
  34. package/dist/channels/telegram/index.js +53 -23
  35. package/dist/channels/telegram/index.mjs +53 -23
  36. package/dist/channels/telegram/poll.d.ts +5 -1
  37. package/dist/channels/telegram/poll.d.ts.map +1 -1
  38. package/dist/channels/telegram/webhook.d.ts +2 -10
  39. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  40. package/dist/channels/whatsapp/authState.d.ts +40 -9
  41. package/dist/channels/whatsapp/authState.d.ts.map +1 -1
  42. package/dist/channels/whatsapp/channel.d.ts +39 -4
  43. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  44. package/dist/channels/whatsapp/inbound.d.ts +23 -25
  45. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  46. package/dist/channels/whatsapp/index.d.ts +3 -3
  47. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  48. package/dist/channels/whatsapp/index.js +188 -66
  49. package/dist/channels/whatsapp/index.mjs +185 -66
  50. package/dist/channels/whatsapp/pairing.d.ts.map +1 -1
  51. package/dist/client/src/Chat.d.ts.map +1 -1
  52. package/dist/client/src/ChatButton.d.ts.map +1 -1
  53. package/dist/client/src/browser.d.ts.map +1 -1
  54. package/dist/core/answer.d.ts +8 -0
  55. package/dist/core/answer.d.ts.map +1 -1
  56. package/dist/core/buildLock.d.ts +52 -0
  57. package/dist/core/buildLock.d.ts.map +1 -0
  58. package/dist/core/pipeline.d.ts +42 -5
  59. package/dist/core/pipeline.d.ts.map +1 -1
  60. package/dist/core/prompts.d.ts +2 -0
  61. package/dist/core/prompts.d.ts.map +1 -1
  62. package/dist/core/retrieval.d.ts +100 -25
  63. package/dist/core/retrieval.d.ts.map +1 -1
  64. package/dist/core/session.d.ts +8 -0
  65. package/dist/core/session.d.ts.map +1 -1
  66. package/dist/flows/index.d.ts +1 -1
  67. package/dist/flows/index.d.ts.map +1 -1
  68. package/dist/flows/index.js +8 -6
  69. package/dist/flows/index.mjs +7 -6
  70. package/dist/flows/intent.d.ts +1 -1
  71. package/dist/flows/intent.d.ts.map +1 -1
  72. package/dist/flows/loader.d.ts +1 -2
  73. package/dist/flows/loader.d.ts.map +1 -1
  74. package/dist/flows/params.d.ts +1 -1
  75. package/dist/flows/params.d.ts.map +1 -1
  76. package/dist/flows/session.d.ts +4 -2
  77. package/dist/flows/session.d.ts.map +1 -1
  78. package/dist/flows/types.d.ts +2 -3
  79. package/dist/flows/types.d.ts.map +1 -1
  80. package/dist/history/index.d.ts +1 -1
  81. package/dist/history/index.d.ts.map +1 -1
  82. package/dist/history/index.js +6 -3
  83. package/dist/history/index.mjs +5 -3
  84. package/dist/history/tursoStore.d.ts +6 -4
  85. package/dist/history/tursoStore.d.ts.map +1 -1
  86. package/dist/images/index.js +2 -2
  87. package/dist/images/index.mjs +2 -2
  88. package/dist/index.d.ts +7 -5
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +596 -295
  91. package/dist/index.mjs +595 -295
  92. package/dist/mcp-server/index.d.ts +5 -5
  93. package/dist/mcp-server/index.d.ts.map +1 -1
  94. package/dist/mcp-server/logger.d.ts +12 -2
  95. package/dist/mcp-server/logger.d.ts.map +1 -1
  96. package/dist/mcp-server/rate-limiter.d.ts +10 -3
  97. package/dist/mcp-server/rate-limiter.d.ts.map +1 -1
  98. package/dist/mcp-server/types.d.ts +20 -5
  99. package/dist/mcp-server/types.d.ts.map +1 -1
  100. package/dist/mcp-server.js +479 -244
  101. package/dist/mcp-server.mjs +485 -244
  102. package/dist/middleware/apiKey.d.ts +10 -0
  103. package/dist/middleware/apiKey.d.ts.map +1 -0
  104. package/dist/middleware/auth.d.ts.map +1 -1
  105. package/dist/middleware/cors.d.ts.map +1 -1
  106. package/dist/middleware/ratelimit.d.ts.map +1 -1
  107. package/dist/middleware/referrer.d.ts.map +1 -1
  108. package/dist/middleware/session.d.ts +1 -1
  109. package/dist/middleware/session.d.ts.map +1 -1
  110. package/dist/routes/demo.d.ts.map +1 -1
  111. package/dist/routes/openai.d.ts.map +1 -1
  112. package/dist/routes/private.d.ts.map +1 -1
  113. package/dist/routes/public.d.ts.map +1 -1
  114. package/dist/scheduler/index.js +1 -1
  115. package/dist/scheduler/index.mjs +1 -1
  116. package/dist/scheduler/scheduler.d.ts +9 -1
  117. package/dist/scheduler/scheduler.d.ts.map +1 -1
  118. package/dist/server.d.ts.map +1 -1
  119. package/dist/server.js +491 -268
  120. package/dist/server.mjs +497 -268
  121. package/dist/shortener.d.ts +5 -4
  122. package/dist/shortener.d.ts.map +1 -1
  123. package/dist/types.d.ts +129 -75
  124. package/dist/types.d.ts.map +1 -1
  125. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
15
+ };
8
16
  var __export = (target, all) => {
9
17
  for (var name in all)
10
18
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,16 +35,402 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
35
  ));
28
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
37
 
38
+ // src/core/buildLock.ts
39
+ function canAcquireBuildLock(existing, holderId, now, staleMs = BUILD_LOCK_STALE_MS) {
40
+ if (!existing) return true;
41
+ if (existing.holderId === holderId) return true;
42
+ return now - existing.heartbeatAt >= staleMs;
43
+ }
44
+ function ensureBuildLockSchema(client) {
45
+ let ready = schemaReady.get(client);
46
+ if (!ready) {
47
+ ready = client.execute(
48
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
49
+ lock_key TEXT PRIMARY KEY,
50
+ holder_id TEXT NOT NULL,
51
+ heartbeat_at INTEGER NOT NULL
52
+ )`
53
+ ).catch((error) => {
54
+ schemaReady.delete(client);
55
+ throw error;
56
+ });
57
+ schemaReady.set(client, ready);
58
+ }
59
+ return ready;
60
+ }
61
+ function createTursoBuildLock(client) {
62
+ return {
63
+ async tryAcquire(key, holderId, now, staleMs) {
64
+ await ensureBuildLockSchema(client);
65
+ const result = await client.execute({
66
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
67
+ ON CONFLICT(lock_key) DO UPDATE SET
68
+ holder_id = excluded.holder_id,
69
+ heartbeat_at = excluded.heartbeat_at
70
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
71
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
72
+ RETURNING holder_id`,
73
+ args: [key, holderId, now, staleMs]
74
+ });
75
+ const row = result.rows[0];
76
+ return Boolean(row) && row.holder_id === holderId;
77
+ },
78
+ async release(key, holderId) {
79
+ await ensureBuildLockSchema(client);
80
+ await client.execute({
81
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
82
+ args: [key, holderId]
83
+ });
84
+ }
85
+ };
86
+ }
87
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
88
+ var init_buildLock = __esm({
89
+ "src/core/buildLock.ts"() {
90
+ "use strict";
91
+ BUILD_LOCK_KEY = "knowledge_build";
92
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
93
+ schemaReady = /* @__PURE__ */ new WeakMap();
94
+ }
95
+ });
96
+
97
+ // src/core/loaders.ts
98
+ function walk(dir) {
99
+ const items = [];
100
+ try {
101
+ for (const e of (0, import_node_fs.readdirSync)(dir)) {
102
+ const p = (0, import_node_path.join)(dir, e);
103
+ const st = (0, import_node_fs.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 = (0, import_node_path.join)(root, bucket);
115
+ for (const f of walk(dir)) {
116
+ docs.push({ id: f, text: (0, import_node_fs.readFileSync)(f, "utf8"), source: f, bucket });
117
+ }
118
+ }
119
+ return docs;
120
+ }
121
+ var import_node_fs, import_node_path;
122
+ var init_loaders = __esm({
123
+ "src/core/loaders.ts"() {
124
+ "use strict";
125
+ import_node_fs = require("node:fs");
126
+ import_node_path = require("node:path");
127
+ }
128
+ });
129
+
130
+ // src/core/logger.ts
131
+ function createConsoleLogger(level = "info") {
132
+ const threshold = LEVEL_ORDER[level];
133
+ const write = (at) => (...args) => {
134
+ if (LEVEL_ORDER[at] < threshold) return;
135
+ console.error(`[${at}]`, ...args);
136
+ };
137
+ return {
138
+ debug: write("debug"),
139
+ info: write("info"),
140
+ warn: write("warn"),
141
+ error: write("error")
142
+ };
143
+ }
144
+ function resolveLogger(logger, level) {
145
+ return logger ?? createConsoleLogger(level);
146
+ }
147
+ var LEVEL_ORDER;
148
+ var init_logger = __esm({
149
+ "src/core/logger.ts"() {
150
+ "use strict";
151
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
152
+ }
153
+ });
154
+
155
+ // src/core/retrieval.ts
156
+ var retrieval_exports = {};
157
+ __export(retrieval_exports, {
158
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
159
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
160
+ VectorStore: () => VectorStore,
161
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
162
+ openLibsqlClient: () => openLibsqlClient,
163
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
164
+ });
165
+ function createOpenAIEmbedder(client) {
166
+ return async (input) => {
167
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
168
+ return res.data.map((d) => d.embedding);
169
+ };
170
+ }
171
+ function wrapMissingLibsqlError(cause) {
172
+ return new Error(
173
+ "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.",
174
+ { cause }
175
+ );
176
+ }
177
+ async function openLibsqlClient(database) {
178
+ const mod = await import("@libsql/client").catch((error) => {
179
+ throw wrapMissingLibsqlError(error);
180
+ });
181
+ return mod.createClient({ url: database.url, authToken: database.authToken });
182
+ }
183
+ function chunk(text, max = 900) {
184
+ const out = [];
185
+ let buf = "";
186
+ for (const line of text.replace(/\r/g, "").split("\n")) {
187
+ if (`${buf}
188
+ ${line}`.length > max) {
189
+ out.push(buf.trim());
190
+ buf = line;
191
+ } else buf += `
192
+ ${line}`;
193
+ }
194
+ if (buf.trim()) out.push(buf.trim());
195
+ return out;
196
+ }
197
+ async function sha256(input) {
198
+ const data = new TextEncoder().encode(input);
199
+ const hash = await crypto.subtle.digest("SHA-256", data);
200
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
201
+ }
202
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
203
+ var init_retrieval = __esm({
204
+ "src/core/retrieval.ts"() {
205
+ "use strict";
206
+ init_buildLock();
207
+ init_loaders();
208
+ init_logger();
209
+ EMB_MODEL = "text-embedding-3-large";
210
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
211
+ 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.";
212
+ VectorStore = class _VectorStore {
213
+ constructor(embed, options) {
214
+ this.embed = embed;
215
+ this.db = options.databaseClient;
216
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
217
+ this.logger = options.logger ?? createConsoleLogger();
218
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
219
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
220
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
221
+ }
222
+ embed;
223
+ /**
224
+ * The libsql client backing this store - the same instance passed in as
225
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
226
+ * and the store share one connection.
227
+ */
228
+ db;
229
+ knowledgeDir;
230
+ logger;
231
+ buildLock;
232
+ instanceId;
233
+ buildLockStaleMs;
234
+ /**
235
+ * On boot: ingest new chunks and embed only missing ones.
236
+ *
237
+ * The whole ingest runs under a single-writer lock held in the database
238
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
239
+ * every chunk id the current `knowledgeDir` did not produce. Without the
240
+ * lock, a second instance booting mid-build diffs against a database the
241
+ * first one is still writing and deletes its chunks. An instance that
242
+ * cannot take the lock skips its build entirely and serves what the holder
243
+ * has already written, rather than racing it.
244
+ */
245
+ async build() {
246
+ this.logger.info("Building knowledge base...");
247
+ await this.db.execute(`
248
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
249
+ `);
250
+ await this.db.execute(`
251
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
252
+ `);
253
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
254
+ if (!await this.renewBuildLock()) {
255
+ this.logger.warn(
256
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
257
+ );
258
+ return;
259
+ }
260
+ try {
261
+ await this.buildUnderLock();
262
+ } finally {
263
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
264
+ }
265
+ }
266
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
267
+ renewBuildLock() {
268
+ return this.buildLock.tryAcquire(
269
+ BUILD_LOCK_KEY,
270
+ this.instanceId,
271
+ Date.now(),
272
+ this.buildLockStaleMs
273
+ );
274
+ }
275
+ /** The ingest itself. Only ever runs with the build lock held. */
276
+ async buildUnderLock() {
277
+ const docs = loadKnowledge(this.knowledgeDir);
278
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
279
+ if (docs.length === 0) {
280
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
281
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
282
+ if (existingCount > 0) {
283
+ throw new Error(
284
+ `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.`
285
+ );
286
+ }
287
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
288
+ return;
289
+ }
290
+ const rows = [];
291
+ for (const d of docs) {
292
+ for (const part of chunk(d.text)) {
293
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
294
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
295
+ }
296
+ }
297
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
298
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
299
+ const UPSERT_BATCH = 500;
300
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
301
+ const batch = rows.slice(i, i + UPSERT_BATCH);
302
+ await this.db.batch(
303
+ batch.map((r) => ({
304
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
305
+ ON CONFLICT(id) DO NOTHING`,
306
+ args: [r.id, r.bucket, r.source, r.text]
307
+ })),
308
+ "write"
309
+ );
310
+ }
311
+ const ids = rows.map((r) => r.id);
312
+ const missing = [];
313
+ for (let i = 0; i < ids.length; i += 500) {
314
+ const batch = ids.slice(i, i + 500);
315
+ const placeholders = batch.map(() => "?").join(",");
316
+ const res = await this.db.execute({
317
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
318
+ EXCEPT SELECT id FROM embeddings`,
319
+ args: batch
320
+ });
321
+ for (const row of res.rows) missing.push(String(row.id));
322
+ }
323
+ if (missing.length === 0) {
324
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
325
+ return;
326
+ }
327
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
328
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
329
+ const BATCH = 96;
330
+ for (let i = 0; i < missing.length; i += BATCH) {
331
+ const batchIds = missing.slice(i, i + BATCH);
332
+ const inputs = batchIds.map((id) => textById.get(id) || "");
333
+ const vectors = await this.embed(inputs);
334
+ await this.db.batch(
335
+ vectors.map((embedding, idx) => ({
336
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
337
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
338
+ })),
339
+ "write"
340
+ );
341
+ if (!await this.renewBuildLock()) {
342
+ this.logger.warn(
343
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
344
+ );
345
+ }
346
+ }
347
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
348
+ }
349
+ // Remove chunks from database that no longer exist in markdown files
350
+ async cleanupStaleChunks(currentIds) {
351
+ const result = await this.db.execute("SELECT id FROM chunks");
352
+ const dbIds = result.rows.map((row) => String(row.id));
353
+ const currentIdSet = new Set(currentIds);
354
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
355
+ if (staleIds.length === 0) {
356
+ return;
357
+ }
358
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
359
+ const BATCH_SIZE = 500;
360
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
361
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
362
+ const placeholders = batch.map(() => "?").join(",");
363
+ await this.db.batch(
364
+ [
365
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
366
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
367
+ ],
368
+ "write"
369
+ );
370
+ }
371
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
372
+ }
373
+ static cosine(a, b) {
374
+ let dot = 0;
375
+ let na = 0;
376
+ let nb = 0;
377
+ for (let i = 0; i < a.length; i++) {
378
+ dot += a[i] * b[i];
379
+ na += a[i] * a[i];
380
+ nb += b[i] * b[i];
381
+ }
382
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
383
+ }
384
+ /**
385
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
386
+ * & portable).
387
+ *
388
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
389
+ * `chunks` table constrains nothing, so a deployment that ingests its own
390
+ * buckets can query them (`build` only writes and prunes the three the
391
+ * knowledge loader walks). Names are bound as query parameters, never
392
+ * interpolated. An empty list retrieves nothing, and short-circuits before
393
+ * the embedding call.
394
+ */
395
+ async query(q, k = 6, allowed = ["base"]) {
396
+ if (allowed.length === 0) return [];
397
+ const [qv] = await this.embed([q]);
398
+ const placeholders = allowed.map(() => "?").join(",");
399
+ const res = await this.db.execute({
400
+ sql: `SELECT c.id, c.text, e.embedding
401
+ FROM chunks c
402
+ JOIN embeddings e ON e.id = c.id
403
+ WHERE c.bucket IN (${placeholders})`,
404
+ args: allowed
405
+ });
406
+ const scored = [];
407
+ for (const row of res.rows) {
408
+ const emb = JSON.parse(String(row.embedding));
409
+ const s = _VectorStore.cosine(qv, emb);
410
+ scored.push({ s, text: String(row.text) });
411
+ }
412
+ scored.sort((a, b) => b.s - a.s);
413
+ return scored.slice(0, k).map((r) => r.text);
414
+ }
415
+ };
416
+ }
417
+ });
418
+
30
419
  // src/index.ts
31
420
  var index_exports = {};
32
421
  __export(index_exports, {
33
422
  ApiKeyManager: () => ApiKeyManager,
423
+ BUILD_LOCK_KEY: () => BUILD_LOCK_KEY,
424
+ BUILD_LOCK_STALE_MS: () => BUILD_LOCK_STALE_MS,
425
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
34
426
  DEFAULT_MODEL: () => DEFAULT_MODEL,
427
+ DEFAULT_PROMPTS_DIR: () => DEFAULT_PROMPTS_DIR,
35
428
  PromptLoader: () => PromptLoader,
36
429
  VectorStore: () => VectorStore,
37
430
  answerOnce: () => answerOnce,
38
431
  answerStream: () => answerStream,
39
432
  applyTransformReply: () => applyTransformReply,
433
+ canAcquireBuildLock: () => canAcquireBuildLock,
40
434
  chatBodyLimit: () => chatBodyLimit,
41
435
  completeOnce: () => completeOnce,
42
436
  completeStream: () => completeStream,
@@ -45,12 +439,14 @@ __export(index_exports, {
45
439
  createConsoleLogger: () => createConsoleLogger,
46
440
  createJWTMiddleware: () => createJWTMiddleware,
47
441
  createMCPServer: () => createMCPServer,
48
- createRateLimiter: () => createRateLimiter2,
442
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
443
+ createRateLimiter: () => createRateLimiter,
49
444
  createReferrerCheck: () => createReferrerCheck,
50
445
  createServer: () => createServer,
51
446
  createSession: () => createSession,
52
447
  createSessionMiddleware: () => createSessionMiddleware,
53
448
  createShortener: () => createShortener,
449
+ createTursoBuildLock: () => createTursoBuildLock,
54
450
  defaultBuckets: () => defaultBuckets,
55
451
  demoRoutes: () => demoRoutes,
56
452
  detectLeakage: () => detectLeakage,
@@ -286,7 +682,8 @@ async function answerOnce({
286
682
  async function applyTransformReply(transformReply, input, logger) {
287
683
  if (!transformReply) return input.text;
288
684
  try {
289
- return await transformReply(input);
685
+ const result = await transformReply(input);
686
+ return typeof result === "string" ? result : null;
290
687
  } catch (error) {
291
688
  logger?.error(
292
689
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -351,51 +748,10 @@ async function resolveBuckets({
351
748
  return cleaned.filter((bucket) => ceiling.has(bucket));
352
749
  }
353
750
 
354
- // src/core/loaders.ts
355
- var import_node_fs = require("node:fs");
356
- var import_node_path = require("node:path");
357
- function walk(dir) {
358
- const items = [];
359
- try {
360
- for (const e of (0, import_node_fs.readdirSync)(dir)) {
361
- const p = (0, import_node_path.join)(dir, e);
362
- const st = (0, import_node_fs.statSync)(p);
363
- if (st.isDirectory()) items.push(...walk(p));
364
- else if (p.endsWith(".md")) items.push(p);
365
- }
366
- } catch {
367
- }
368
- return items;
369
- }
370
- function loadKnowledge(root = "config/knowledge") {
371
- const docs = [];
372
- for (const bucket of ["base", "public", "private"]) {
373
- const dir = (0, import_node_path.join)(root, bucket);
374
- for (const f of walk(dir)) {
375
- docs.push({ id: f, text: (0, import_node_fs.readFileSync)(f, "utf8"), source: f, bucket });
376
- }
377
- }
378
- return docs;
379
- }
380
-
381
- // src/core/logger.ts
382
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
383
- function createConsoleLogger(level = "info") {
384
- const threshold = LEVEL_ORDER[level];
385
- const write = (at) => (...args) => {
386
- if (LEVEL_ORDER[at] < threshold) return;
387
- console.error(`[${at}]`, ...args);
388
- };
389
- return {
390
- debug: write("debug"),
391
- info: write("info"),
392
- warn: write("warn"),
393
- error: write("error")
394
- };
395
- }
396
- function resolveLogger(logger, level) {
397
- return logger ?? createConsoleLogger(level);
398
- }
751
+ // src/index.ts
752
+ init_buildLock();
753
+ init_loaders();
754
+ init_logger();
399
755
 
400
756
  // src/core/messages.ts
401
757
  function isClientRole(role) {
@@ -462,6 +818,7 @@ async function prepareChat({
462
818
  sender,
463
819
  rewriteQuery,
464
820
  rerankContext,
821
+ fallbackFn,
465
822
  logger
466
823
  }) {
467
824
  const lastUserMsg = lastUserMessage(messages);
@@ -478,21 +835,39 @@ async function prepareChat({
478
835
  logger?.error("rewriteQuery threw; falling back to the original query", error);
479
836
  }
480
837
  }
481
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
838
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
839
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
840
+ let ctx = retrievedChunks;
482
841
  if (rerankContext) {
483
842
  try {
484
- const reranked = await rerankContext({ query, chunks: ctx });
843
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
485
844
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
486
845
  } catch (error) {
487
846
  logger?.error("rerankContext threw; falling back to the original chunks", error);
488
847
  }
489
848
  }
849
+ let fallback;
850
+ if (ctx.length === 0 && fallbackFn) {
851
+ try {
852
+ const guidance = await fallbackFn({
853
+ query,
854
+ mode,
855
+ buckets: resolvedBuckets,
856
+ sender,
857
+ retrievedChunks
858
+ });
859
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
860
+ } catch (error) {
861
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
862
+ }
863
+ }
490
864
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
491
865
  const hint = channelHint?.trim();
492
866
  const system = [
493
867
  prompts.baseSystemRules,
494
868
  persona,
495
869
  ...hint ? [hint] : [],
870
+ ...fallback ? [fallback] : [],
496
871
  `${label}:
497
872
  ${ctx.join("\n\n")}`
498
873
  ].join("\n\n");
@@ -502,6 +877,7 @@ ${ctx.join("\n\n")}`
502
877
  // src/core/prompts.ts
503
878
  var import_node_fs2 = require("node:fs");
504
879
  var import_node_path2 = require("node:path");
880
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
505
881
  var PromptLoader = class {
506
882
  constructor(promptsDir, bot) {
507
883
  this.promptsDir = promptsDir;
@@ -532,196 +908,32 @@ var PromptLoader = class {
532
908
  }
533
909
  };
534
910
 
535
- // src/core/retrieval.ts
536
- var import_client = require("@libsql/client");
537
- var EMB_MODEL = "text-embedding-3-large";
538
- function chunk(text, max = 900) {
539
- const out = [];
540
- let buf = "";
541
- for (const line of text.replace(/\r/g, "").split("\n")) {
542
- if (`${buf}
543
- ${line}`.length > max) {
544
- out.push(buf.trim());
545
- buf = line;
546
- } else buf += `
547
- ${line}`;
548
- }
549
- if (buf.trim()) out.push(buf.trim());
550
- return out;
551
- }
552
- async function sha256(input) {
553
- const data = new TextEncoder().encode(input);
554
- const hash = await crypto.subtle.digest("SHA-256", data);
555
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
556
- }
557
- var VectorStore = class _VectorStore {
558
- constructor(client, options) {
559
- this.client = client;
560
- this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
561
- url: options.databaseUrl,
562
- authToken: options.databaseAuthToken
563
- });
564
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
565
- this.logger = options.logger ?? createConsoleLogger();
566
- }
567
- client;
568
- /**
569
- * The libsql client backing this store. When a client was injected this is
570
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
571
- * and the store share one connection.
572
- */
573
- db;
574
- knowledgeDir;
575
- logger;
576
- // On boot: ingest new chunks and embed only missing ones.
577
- async build() {
578
- this.logger.info("\u{1F504} Building knowledge base...");
579
- await this.db.execute(`
580
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
581
- `);
582
- await this.db.execute(`
583
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
584
- `);
585
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
586
- const docs = loadKnowledge(this.knowledgeDir);
587
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
588
- const rows = [];
589
- for (const d of docs) {
590
- for (const part of chunk(d.text)) {
591
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
592
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
593
- }
594
- }
595
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
596
- await this.cleanupStaleChunks(rows.map((r) => r.id));
597
- const UPSERT_BATCH = 500;
598
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
599
- const batch = rows.slice(i, i + UPSERT_BATCH);
600
- await this.db.batch(
601
- batch.map((r) => ({
602
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
603
- ON CONFLICT(id) DO NOTHING`,
604
- args: [r.id, r.bucket, r.source, r.text]
605
- })),
606
- "write"
607
- );
608
- }
609
- const ids = rows.map((r) => r.id);
610
- const missing = [];
611
- for (let i = 0; i < ids.length; i += 500) {
612
- const batch = ids.slice(i, i + 500);
613
- const placeholders = batch.map(() => "?").join(",");
614
- const res = await this.db.execute({
615
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
616
- EXCEPT SELECT id FROM embeddings`,
617
- args: batch
618
- });
619
- for (const row of res.rows) missing.push(String(row.id));
620
- }
621
- if (missing.length === 0) {
622
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
623
- return;
624
- }
625
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
626
- const textById = new Map(rows.map((r) => [r.id, r.text]));
627
- const BATCH = 96;
628
- for (let i = 0; i < missing.length; i += BATCH) {
629
- const batchIds = missing.slice(i, i + BATCH);
630
- const inputs = batchIds.map((id) => textById.get(id) || "");
631
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
632
- await this.db.batch(
633
- emb.data.map((d, idx) => ({
634
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
635
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
636
- })),
637
- "write"
638
- );
639
- }
640
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
641
- }
642
- // Remove chunks from database that no longer exist in markdown files
643
- async cleanupStaleChunks(currentIds) {
644
- const result = await this.db.execute("SELECT id FROM chunks");
645
- const dbIds = result.rows.map((row) => String(row.id));
646
- const currentIdSet = new Set(currentIds);
647
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
648
- if (staleIds.length === 0) {
649
- return;
650
- }
651
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
652
- const BATCH_SIZE = 500;
653
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
654
- const batch = staleIds.slice(i, i + BATCH_SIZE);
655
- const placeholders = batch.map(() => "?").join(",");
656
- await this.db.execute({
657
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
658
- args: batch
659
- });
660
- }
661
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
662
- }
663
- static cosine(a, b) {
664
- let dot = 0;
665
- let na = 0;
666
- let nb = 0;
667
- for (let i = 0; i < a.length; i++) {
668
- dot += a[i] * b[i];
669
- na += a[i] * a[i];
670
- nb += b[i] * b[i];
671
- }
672
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
673
- }
674
- /**
675
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
676
- * & portable).
677
- *
678
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
679
- * `chunks` table constrains nothing, so a deployment that ingests its own
680
- * buckets can query them (`build` only writes and prunes the three the
681
- * knowledge loader walks). Names are bound as query parameters, never
682
- * interpolated. An empty list retrieves nothing, and short-circuits before
683
- * the embedding call.
684
- */
685
- async query(q, k = 6, allowed = ["base"]) {
686
- if (allowed.length === 0) return [];
687
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
688
- const placeholders = allowed.map(() => "?").join(",");
689
- const res = await this.db.execute({
690
- sql: `SELECT c.id, c.text, e.embedding
691
- FROM chunks c
692
- JOIN embeddings e ON e.id = c.id
693
- WHERE c.bucket IN (${placeholders})`,
694
- args: allowed
695
- });
696
- const scored = [];
697
- for (const row of res.rows) {
698
- const emb = JSON.parse(String(row.embedding));
699
- const s = _VectorStore.cosine(qv, emb);
700
- scored.push({ s, text: String(row.text) });
701
- }
702
- scored.sort((a, b) => b.s - a.s);
703
- return scored.slice(0, k).map((r) => r.text);
704
- }
705
- };
911
+ // src/index.ts
912
+ init_retrieval();
706
913
 
707
914
  // src/core/session.ts
708
915
  var import_node_crypto2 = require("node:crypto");
916
+ var SESSION_KEY_PREFIX = "session_";
917
+ var DEFAULT_SESSION_MAX_REQUESTS = 20;
918
+ var DEFAULT_SESSION_TTL_SECONDS = 3600;
919
+ var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
709
920
  var sessions = /* @__PURE__ */ new Map();
710
- var sessionSweeper = setInterval(
711
- () => {
712
- const now = Date.now();
713
- for (const [key, data] of Array.from(sessions.entries())) {
714
- if (now > data.expiresAt) {
715
- sessions.delete(key);
716
- }
921
+ var sessionSweeper = setInterval(() => {
922
+ const now = Date.now();
923
+ for (const [key, data] of Array.from(sessions.entries())) {
924
+ if (now > data.expiresAt) {
925
+ sessions.delete(key);
717
926
  }
718
- },
719
- 5 * 60 * 1e3
720
- );
927
+ }
928
+ }, SESSION_SWEEP_INTERVAL_MS);
721
929
  sessionSweeper.unref?.();
722
930
  function createSession(options = {}) {
723
- const { maxRequests = 20, ttl = 3600, metadata } = options;
724
- const key = `session_${(0, import_node_crypto2.randomUUID)()}`;
931
+ const {
932
+ maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
933
+ ttl = DEFAULT_SESSION_TTL_SECONDS,
934
+ metadata
935
+ } = options;
936
+ const key = `${SESSION_KEY_PREFIX}${(0, import_node_crypto2.randomUUID)()}`;
725
937
  const now = Date.now();
726
938
  const session = {
727
939
  key,
@@ -757,6 +969,8 @@ function getActiveSessions() {
757
969
 
758
970
  // src/mcp-server.ts
759
971
  var import_openai = __toESM(require("openai"));
972
+ init_logger();
973
+ init_retrieval();
760
974
 
761
975
  // src/mcp-server/conversation-id.ts
762
976
  function generateConversationId() {
@@ -783,28 +997,38 @@ function calculateCost(usage, rates) {
783
997
  }
784
998
 
785
999
  // src/mcp-server/logger.ts
1000
+ init_logger();
786
1001
  var MCPLogger = class {
787
1002
  enableConsole;
788
1003
  callback;
789
1004
  logger;
790
- constructor(enableConsole = true, callback, logger) {
1005
+ logContent;
1006
+ constructor(enableConsole = true, callback, logger, logContent = false) {
791
1007
  this.enableConsole = enableConsole;
792
1008
  this.callback = callback;
793
1009
  this.logger = logger ?? createConsoleLogger();
1010
+ this.logContent = logContent;
794
1011
  }
795
1012
  /**
796
1013
  * Log a chat interaction
797
1014
  */
798
1015
  async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
799
1016
  const lastUserMsg = lastUserMessage(conversationMessages);
1017
+ const content = {
1018
+ userMessage: scrubOutput(lastUserMsg?.content || ""),
1019
+ conversationHistory: conversationMessages.map((msg) => ({
1020
+ ...msg,
1021
+ content: scrubOutput(msg.content)
1022
+ })),
1023
+ ragContext: ragContext.map(scrubOutput),
1024
+ response: scrubOutput(response)
1025
+ };
1026
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
800
1027
  const logEvent = {
801
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1028
+ timestamp,
802
1029
  toolName,
803
1030
  conversationId,
804
- userMessage: lastUserMsg?.content || "",
805
- conversationHistory: conversationMessages,
806
- ragContext,
807
- response,
1031
+ ...content,
808
1032
  duration,
809
1033
  cost
810
1034
  };
@@ -812,9 +1036,27 @@ var MCPLogger = class {
812
1036
  this.logger.info(
813
1037
  JSON.stringify({
814
1038
  event: "mcp_chat",
815
- ...logEvent
1039
+ timestamp,
1040
+ toolName,
1041
+ conversationId,
1042
+ messageCount: conversationMessages.length,
1043
+ ragContextCount: ragContext.length,
1044
+ responseChars: response.length,
1045
+ duration,
1046
+ cost
816
1047
  })
817
1048
  );
1049
+ if (this.logContent) {
1050
+ this.logger.debug(
1051
+ JSON.stringify({
1052
+ event: "mcp_chat_content",
1053
+ timestamp,
1054
+ toolName,
1055
+ conversationId,
1056
+ ...content
1057
+ })
1058
+ );
1059
+ }
818
1060
  }
819
1061
  if (this.callback) {
820
1062
  try {
@@ -825,11 +1067,12 @@ var MCPLogger = class {
825
1067
  }
826
1068
  }
827
1069
  };
828
- function createLogger(enableConsole = true, callback, logger) {
829
- return new MCPLogger(enableConsole, callback, logger);
1070
+ function createLogger(enableConsole = true, callback, logger, logContent = false) {
1071
+ return new MCPLogger(enableConsole, callback, logger, logContent);
830
1072
  }
831
1073
 
832
1074
  // src/mcp-server/rate-limiter.ts
1075
+ var DEFAULT_WINDOW_MS = 6e4;
833
1076
  var RateLimiter = class {
834
1077
  requestMap;
835
1078
  limit;
@@ -838,9 +1081,9 @@ var RateLimiter = class {
838
1081
  * Create a new rate limiter
839
1082
  *
840
1083
  * @param limit - Maximum requests allowed in the time window
841
- * @param windowMs - Time window in milliseconds (default: 60000 = 1 minute)
1084
+ * @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
842
1085
  */
843
- constructor(limit, windowMs = 6e4) {
1086
+ constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
844
1087
  this.requestMap = /* @__PURE__ */ new Map();
845
1088
  this.limit = limit;
846
1089
  this.windowMs = windowMs;
@@ -904,7 +1147,7 @@ var RateLimiter = class {
904
1147
  return Math.max(0, timeUntilReset);
905
1148
  }
906
1149
  };
907
- function createRateLimiter(limit, windowMs = 6e4) {
1150
+ function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
908
1151
  return limit ? new RateLimiter(limit, windowMs) : null;
909
1152
  }
910
1153
 
@@ -954,11 +1197,15 @@ async function createMCPServer(config) {
954
1197
  const log = resolveLogger(config.logger, config.logLevel);
955
1198
  log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
956
1199
  const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
957
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
958
- const promptsDir = config.promptsDir || "./config/prompts";
1200
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
959
1201
  const transportMode = config.transport || "stdio";
960
- const rateLimiter = createRateLimiter(config.toolRateLimit);
961
- const logger = createLogger(config.logging?.console !== false, config.logging?.onChat, log);
1202
+ const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
1203
+ const logger = createLogger(
1204
+ config.logging?.console !== false,
1205
+ config.logging?.onChat,
1206
+ log,
1207
+ config.logging?.content === true
1208
+ );
962
1209
  const publicToolConfig = {
963
1210
  enabled: config.tools?.public?.enabled !== false,
964
1211
  name: config.tools?.public?.name || "chat_public",
@@ -973,13 +1220,22 @@ async function createMCPServer(config) {
973
1220
  apiKey: config.openai.apiKey
974
1221
  });
975
1222
  const model = config.openai.model || DEFAULT_MODEL;
976
- const store = new VectorStore(client, {
977
- databaseUrl: config.database.url,
978
- databaseAuthToken: config.database.authToken,
979
- knowledgeDir,
980
- logger: log
981
- });
982
- await store.build();
1223
+ let store;
1224
+ if (config.retriever) {
1225
+ store = config.retriever;
1226
+ } else {
1227
+ if (!config.database) {
1228
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
1229
+ }
1230
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1231
+ const db = await openLibsqlClient2(config.database);
1232
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
1233
+ databaseClient: db,
1234
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1235
+ logger: log
1236
+ });
1237
+ }
1238
+ await store.build?.();
983
1239
  log.info("\u2705 Knowledge base ready");
984
1240
  const prompts = new PromptLoader(promptsDir, config.bot);
985
1241
  const server = new McpServer({
@@ -1028,6 +1284,7 @@ async function createMCPServer(config) {
1028
1284
  buckets,
1029
1285
  rewriteQuery: config.rewriteQuery,
1030
1286
  rerankContext: config.rerankContext,
1287
+ fallbackFn: config.fallbackFn,
1031
1288
  logger: log
1032
1289
  });
1033
1290
  const result = await answerOnce({
@@ -1117,6 +1374,7 @@ async function createMCPServer(config) {
1117
1374
  buckets,
1118
1375
  rewriteQuery: config.rewriteQuery,
1119
1376
  rerankContext: config.rerankContext,
1377
+ fallbackFn: config.fallbackFn,
1120
1378
  logger: log
1121
1379
  });
1122
1380
  const result = await answerOnce({
@@ -1178,13 +1436,17 @@ async function createMCPServer(config) {
1178
1436
  return server;
1179
1437
  }
1180
1438
 
1439
+ // src/middleware/apiKey.ts
1440
+ var API_KEY_HEADER = "x-api-key";
1441
+ var API_KEY_PREVIEW_LENGTH = 20;
1442
+
1181
1443
  // src/middleware/auth.ts
1182
1444
  function createAuthMiddleware(deps) {
1183
1445
  const { apiKeyManager, logger } = deps;
1184
1446
  return async function requirePublicKey(c, next) {
1185
- const hdr = c.req.header("x-api-key");
1186
- if (hdr?.startsWith("session_")) {
1187
- logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1447
+ const hdr = c.req.header(API_KEY_HEADER);
1448
+ if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
1449
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1188
1450
  await next();
1189
1451
  return;
1190
1452
  }
@@ -1194,7 +1456,7 @@ function createAuthMiddleware(deps) {
1194
1456
  }
1195
1457
  if (!hdr) {
1196
1458
  logger.debug("[Auth] No API key provided");
1197
- return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1459
+ return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
1198
1460
  }
1199
1461
  try {
1200
1462
  const result = await apiKeyManager.verify(hdr);
@@ -1208,7 +1470,7 @@ function createAuthMiddleware(deps) {
1208
1470
  } catch (error) {
1209
1471
  logger.warn(`[Auth] JWT verification error: ${error}`);
1210
1472
  }
1211
- logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1473
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
1212
1474
  return c.json({ error: "Unauthorized" }, 401);
1213
1475
  };
1214
1476
  }
@@ -1224,7 +1486,7 @@ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message)
1224
1486
  }
1225
1487
 
1226
1488
  // src/middleware/cors.ts
1227
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization, x-conversation-id";
1489
+ var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
1228
1490
  var ALLOWED_METHODS = "POST, GET, OPTIONS";
1229
1491
  var EXPOSED_HEADERS = "x-conversation-id";
1230
1492
  var cors = (allowedOrigins) => async (c, next) => {
@@ -1340,7 +1602,7 @@ function createWindowBucketLimiter(windowMs, now = Date.now) {
1340
1602
  }
1341
1603
 
1342
1604
  // src/middleware/ratelimit.ts
1343
- function createRateLimiter2(config) {
1605
+ function createRateLimiter(config) {
1344
1606
  const limiter = createWindowBucketLimiter(6e4);
1345
1607
  const publicLimit = config.rateLimit?.public || 60;
1346
1608
  const privateLimit = config.rateLimit?.private || 120;
@@ -1351,7 +1613,7 @@ function createRateLimiter2(config) {
1351
1613
  // Demo keys get 10x stricter rate limits
1352
1614
  limitPublic() {
1353
1615
  return async (c, next) => {
1354
- const apiKey = c.req.header("x-api-key");
1616
+ const apiKey = c.req.header(API_KEY_HEADER);
1355
1617
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1356
1618
  const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1357
1619
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
@@ -1378,6 +1640,9 @@ function createRateLimiter2(config) {
1378
1640
  };
1379
1641
  }
1380
1642
 
1643
+ // src/middleware/referrer.ts
1644
+ init_logger();
1645
+
1381
1646
  // src/middleware/originMatch.ts
1382
1647
  function matchesAllowedOrigin(origin, referer, allowedOrigins) {
1383
1648
  return allowedOrigins.some((allowed) => {
@@ -1408,14 +1673,14 @@ function matchesAllowedOriginOrLocalhost(origin, referer, allowedOrigins) {
1408
1673
  // src/middleware/referrer.ts
1409
1674
  function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1410
1675
  return async (c, next) => {
1411
- const apiKey = c.req.header("x-api-key");
1676
+ const apiKey = c.req.header(API_KEY_HEADER);
1412
1677
  const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1413
- const isSessionKey = apiKey?.startsWith("session_");
1678
+ const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
1414
1679
  if (isDemoKey || isSessionKey) {
1415
1680
  const referer = c.req.header("referer");
1416
1681
  const origin = c.req.header("origin");
1417
1682
  logger.debug(
1418
- `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1683
+ `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
1419
1684
  );
1420
1685
  logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1421
1686
  const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
@@ -1458,11 +1723,12 @@ function securityHeaders(config) {
1458
1723
  }
1459
1724
 
1460
1725
  // src/middleware/session.ts
1726
+ init_logger();
1461
1727
  function validateSessionKey(logger = createConsoleLogger()) {
1462
1728
  return async (c, next) => {
1463
- const apiKey = c.req.header("x-api-key");
1464
- if (apiKey?.startsWith("session_")) {
1465
- logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1729
+ const apiKey = c.req.header(API_KEY_HEADER);
1730
+ if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
1731
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
1466
1732
  const info = getSessionInfo(apiKey);
1467
1733
  if (info) {
1468
1734
  logger.debug(
@@ -1543,6 +1809,8 @@ function limitDemo(trustProxy) {
1543
1809
  await next();
1544
1810
  };
1545
1811
  }
1812
+ var DEMO_SESSION_MAX_REQUESTS = 20;
1813
+ var DEMO_SESSION_TTL_SECONDS = 3600;
1546
1814
  function demoRoutes(deps) {
1547
1815
  const { client, store, config, prompts, logger } = deps;
1548
1816
  const app = new import_hono.Hono();
@@ -1555,18 +1823,17 @@ function demoRoutes(deps) {
1555
1823
  limitSessionCreation(trustProxy),
1556
1824
  (c) => {
1557
1825
  const session = createSession({
1558
- maxRequests: 20,
1559
- ttl: 3600
1560
- // 1 hour
1826
+ maxRequests: DEMO_SESSION_MAX_REQUESTS,
1827
+ ttl: DEMO_SESSION_TTL_SECONDS
1561
1828
  });
1562
1829
  logger.debug(
1563
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
1830
+ `[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
1564
1831
  );
1565
1832
  return c.json({
1566
1833
  key: session.key,
1567
- expiresIn: 3600,
1834
+ expiresIn: DEMO_SESSION_TTL_SECONDS,
1568
1835
  maxRequests: session.maxRequests,
1569
- message: "Use this temporary key in the x-api-key header"
1836
+ message: `Use this temporary key in the ${API_KEY_HEADER} header`
1570
1837
  });
1571
1838
  }
1572
1839
  );
@@ -1600,6 +1867,7 @@ function demoRoutes(deps) {
1600
1867
  buckets,
1601
1868
  rewriteQuery: config.rewriteQuery,
1602
1869
  rerankContext: config.rerankContext,
1870
+ fallbackFn: config.fallbackFn,
1603
1871
  logger
1604
1872
  });
1605
1873
  const model = config.openai.model || DEFAULT_MODEL;
@@ -1680,7 +1948,7 @@ function oversizedBodyError(message) {
1680
1948
  function openaiRoutes(deps) {
1681
1949
  const { client, store, config, prompts, apiKeyManager, logger } = deps;
1682
1950
  const app = new import_hono2.Hono();
1683
- const { limitPublic, limitPrivate } = createRateLimiter2(config);
1951
+ const { limitPublic, limitPrivate } = createRateLimiter(config);
1684
1952
  const requirePrivateJWT = createJWTMiddleware(config);
1685
1953
  const enablePublic = config.features?.enablePublicChat !== false;
1686
1954
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -1690,9 +1958,13 @@ function openaiRoutes(deps) {
1690
1958
  return errorJson(c, 401, "API key authentication is not configured on this server");
1691
1959
  }
1692
1960
  const bearer = c.req.header("authorization") ?? "";
1693
- const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header("x-api-key");
1961
+ const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
1694
1962
  if (!key) {
1695
- return errorJson(c, 401, "Missing API key (Authorization: Bearer or x-api-key header)");
1963
+ return errorJson(
1964
+ c,
1965
+ 401,
1966
+ `Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
1967
+ );
1696
1968
  }
1697
1969
  try {
1698
1970
  const result = await apiKeyManager.verify(key);
@@ -1732,6 +2004,7 @@ function openaiRoutes(deps) {
1732
2004
  sender,
1733
2005
  rewriteQuery: config.rewriteQuery,
1734
2006
  rerankContext: config.rerankContext,
2007
+ fallbackFn: config.fallbackFn,
1735
2008
  logger
1736
2009
  }));
1737
2010
  } catch {
@@ -1827,7 +2100,7 @@ function privateRoutes(deps) {
1827
2100
  const { client, store, config, prompts, logger } = deps;
1828
2101
  const app = new import_hono3.Hono();
1829
2102
  const requirePrivateJWT = createJWTMiddleware(config);
1830
- const { limitPrivate } = createRateLimiter2(config);
2103
+ const { limitPrivate } = createRateLimiter(config);
1831
2104
  app.use("/api/private/*", chatBodyLimit(config.server?.maxRequestBytes));
1832
2105
  app.use("/api/private/*", requirePrivateJWT);
1833
2106
  app.use("/api/private/*", limitPrivate());
@@ -1855,6 +2128,7 @@ function privateRoutes(deps) {
1855
2128
  sender,
1856
2129
  rewriteQuery: config.rewriteQuery,
1857
2130
  rerankContext: config.rerankContext,
2131
+ fallbackFn: config.fallbackFn,
1858
2132
  logger
1859
2133
  }));
1860
2134
  } catch {
@@ -1912,7 +2186,7 @@ function publicRoutes(deps) {
1912
2186
  const { client, store, config, prompts, logger } = deps;
1913
2187
  const app = new import_hono4.Hono();
1914
2188
  const requirePublicKey = createAuthMiddleware(deps);
1915
- const { limitPublic } = createRateLimiter2(config);
2189
+ const { limitPublic } = createRateLimiter(config);
1916
2190
  const referrerOrigins = config.server?.allowedOrigins?.length ? config.server.allowedOrigins.filter((o) => o !== "*") : [config.bot.publicUrl, "http://localhost:8181", "http://127.0.0.1:8181"];
1917
2191
  app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
1918
2192
  app.use("/api/public/*", validateSessionKey(logger));
@@ -1940,6 +2214,7 @@ function publicRoutes(deps) {
1940
2214
  buckets,
1941
2215
  rewriteQuery: config.rewriteQuery,
1942
2216
  rerankContext: config.rerankContext,
2217
+ fallbackFn: config.fallbackFn,
1943
2218
  logger
1944
2219
  }));
1945
2220
  } catch {
@@ -1977,11 +2252,11 @@ function publicRoutes(deps) {
1977
2252
 
1978
2253
  // src/server.ts
1979
2254
  var import_node_path4 = require("node:path");
1980
- var import_client2 = require("@libsql/client");
1981
2255
  var import_hono5 = require("hono");
1982
2256
  var import_openai2 = __toESM(require("openai"));
1983
2257
 
1984
2258
  // src/channels/senders.ts
2259
+ init_logger();
1985
2260
  function createSenderRegistry(logger = createConsoleLogger()) {
1986
2261
  const senders = /* @__PURE__ */ new Map();
1987
2262
  return {
@@ -2044,6 +2319,10 @@ function createSenderRegistry(logger = createConsoleLogger()) {
2044
2319
  };
2045
2320
  }
2046
2321
 
2322
+ // src/server.ts
2323
+ init_logger();
2324
+ init_retrieval();
2325
+
2047
2326
  // src/core/serve-static.ts
2048
2327
  var STATIC_ADAPTERS = {
2049
2328
  bun: "hono/bun",
@@ -2105,8 +2384,7 @@ function resolveStatic(configStaticDir) {
2105
2384
  async function createServer(config) {
2106
2385
  const logger = resolveLogger(config.logger, config.logLevel);
2107
2386
  logger.info(`\u{1F680} Starting ${config.bot.name}...`);
2108
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
2109
- const promptsDir = config.promptsDir || "./config/prompts";
2387
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
2110
2388
  const publicDir = config.publicDir || "./public";
2111
2389
  const enablePublic = config.features?.enablePublicChat !== false;
2112
2390
  const enablePrivate = config.features?.enablePrivateChat !== false;
@@ -2117,16 +2395,26 @@ async function createServer(config) {
2117
2395
  const client = new import_openai2.default({
2118
2396
  apiKey: config.openai.apiKey
2119
2397
  });
2120
- const db = (0, import_client2.createClient)({
2121
- url: config.database.url,
2122
- authToken: config.database.authToken
2123
- });
2124
- const store = new VectorStore(client, {
2125
- databaseClient: db,
2126
- knowledgeDir,
2127
- logger
2128
- });
2129
- await store.build();
2398
+ let db;
2399
+ if (config.database) {
2400
+ const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
2401
+ db = await openLibsqlClient2(config.database);
2402
+ }
2403
+ let store;
2404
+ if (config.retriever) {
2405
+ store = config.retriever;
2406
+ } else {
2407
+ if (!db) {
2408
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
2409
+ }
2410
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
2411
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
2412
+ databaseClient: db,
2413
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
2414
+ logger
2415
+ });
2416
+ }
2417
+ await store.build?.();
2130
2418
  logger.info("\u2705 Knowledge base ready");
2131
2419
  const prompts = new PromptLoader(promptsDir, config.bot);
2132
2420
  let apiKeyManager;
@@ -2214,6 +2502,9 @@ async function createServer(config) {
2214
2502
  const deps = {
2215
2503
  client,
2216
2504
  store,
2505
+ // See ServerDependencies.db's doc comment: undefined here (cast to
2506
+ // satisfy the still-required type) exactly when config.database was
2507
+ // never set - i.e. a config.retriever host with nothing else needing db.
2217
2508
  db,
2218
2509
  config,
2219
2510
  prompts,
@@ -2269,13 +2560,16 @@ async function createServer(config) {
2269
2560
  }
2270
2561
 
2271
2562
  // src/shortener.ts
2272
- var DEFAULT_TIMEOUT_MS = 5e3;
2563
+ var SHORTENER_TIMEOUT_MS = 5e3;
2273
2564
  function createShortener(config, fetchImpl = fetch) {
2274
2565
  const apiUrl = config.apiUrl.replace(/\/+$/, "");
2275
2566
  return {
2276
2567
  async shorten(url) {
2277
2568
  const controller = new AbortController();
2278
- const timeout = setTimeout(() => controller.abort(), config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
2569
+ const timeout = setTimeout(
2570
+ () => controller.abort(),
2571
+ config.timeoutMs ?? SHORTENER_TIMEOUT_MS
2572
+ );
2279
2573
  try {
2280
2574
  const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
2281
2575
  method: "POST",
@@ -2304,12 +2598,17 @@ function createShortener(config, fetchImpl = fetch) {
2304
2598
  // Annotate the CommonJS export names for ESM import in node:
2305
2599
  0 && (module.exports = {
2306
2600
  ApiKeyManager,
2601
+ BUILD_LOCK_KEY,
2602
+ BUILD_LOCK_STALE_MS,
2603
+ DEFAULT_KNOWLEDGE_DIR,
2307
2604
  DEFAULT_MODEL,
2605
+ DEFAULT_PROMPTS_DIR,
2308
2606
  PromptLoader,
2309
2607
  VectorStore,
2310
2608
  answerOnce,
2311
2609
  answerStream,
2312
2610
  applyTransformReply,
2611
+ canAcquireBuildLock,
2313
2612
  chatBodyLimit,
2314
2613
  completeOnce,
2315
2614
  completeStream,
@@ -2318,12 +2617,14 @@ function createShortener(config, fetchImpl = fetch) {
2318
2617
  createConsoleLogger,
2319
2618
  createJWTMiddleware,
2320
2619
  createMCPServer,
2620
+ createOpenAIEmbedder,
2321
2621
  createRateLimiter,
2322
2622
  createReferrerCheck,
2323
2623
  createServer,
2324
2624
  createSession,
2325
2625
  createSessionMiddleware,
2326
2626
  createShortener,
2627
+ createTursoBuildLock,
2327
2628
  defaultBuckets,
2328
2629
  demoRoutes,
2329
2630
  detectLeakage,