@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
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
15
+ };
8
16
  var __export = (target, all) => {
9
17
  for (var name in all)
10
18
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +35,382 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
35
  ));
28
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
37
 
38
+ // src/core/logger.ts
39
+ function createConsoleLogger(level = "info") {
40
+ const threshold = LEVEL_ORDER[level];
41
+ const write = (at) => (...args) => {
42
+ if (LEVEL_ORDER[at] < threshold) return;
43
+ console.error(`[${at}]`, ...args);
44
+ };
45
+ return {
46
+ debug: write("debug"),
47
+ info: write("info"),
48
+ warn: write("warn"),
49
+ error: write("error")
50
+ };
51
+ }
52
+ function resolveLogger(logger, level) {
53
+ return logger ?? createConsoleLogger(level);
54
+ }
55
+ var LEVEL_ORDER;
56
+ var init_logger = __esm({
57
+ "src/core/logger.ts"() {
58
+ "use strict";
59
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
60
+ }
61
+ });
62
+
63
+ // src/core/buildLock.ts
64
+ function ensureBuildLockSchema(client) {
65
+ let ready = schemaReady.get(client);
66
+ if (!ready) {
67
+ ready = client.execute(
68
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
69
+ lock_key TEXT PRIMARY KEY,
70
+ holder_id TEXT NOT NULL,
71
+ heartbeat_at INTEGER NOT NULL
72
+ )`
73
+ ).catch((error) => {
74
+ schemaReady.delete(client);
75
+ throw error;
76
+ });
77
+ schemaReady.set(client, ready);
78
+ }
79
+ return ready;
80
+ }
81
+ function createTursoBuildLock(client) {
82
+ return {
83
+ async tryAcquire(key, holderId, now, staleMs) {
84
+ await ensureBuildLockSchema(client);
85
+ const result = await client.execute({
86
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
87
+ ON CONFLICT(lock_key) DO UPDATE SET
88
+ holder_id = excluded.holder_id,
89
+ heartbeat_at = excluded.heartbeat_at
90
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
91
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
92
+ RETURNING holder_id`,
93
+ args: [key, holderId, now, staleMs]
94
+ });
95
+ const row = result.rows[0];
96
+ return Boolean(row) && row.holder_id === holderId;
97
+ },
98
+ async release(key, holderId) {
99
+ await ensureBuildLockSchema(client);
100
+ await client.execute({
101
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
102
+ args: [key, holderId]
103
+ });
104
+ }
105
+ };
106
+ }
107
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
108
+ var init_buildLock = __esm({
109
+ "src/core/buildLock.ts"() {
110
+ "use strict";
111
+ BUILD_LOCK_KEY = "knowledge_build";
112
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
113
+ schemaReady = /* @__PURE__ */ new WeakMap();
114
+ }
115
+ });
116
+
117
+ // src/core/loaders.ts
118
+ function walk(dir) {
119
+ const items = [];
120
+ try {
121
+ for (const e of (0, import_node_fs2.readdirSync)(dir)) {
122
+ const p = (0, import_node_path2.join)(dir, e);
123
+ const st = (0, import_node_fs2.statSync)(p);
124
+ if (st.isDirectory()) items.push(...walk(p));
125
+ else if (p.endsWith(".md")) items.push(p);
126
+ }
127
+ } catch {
128
+ }
129
+ return items;
130
+ }
131
+ function loadKnowledge(root = "config/knowledge") {
132
+ const docs = [];
133
+ for (const bucket of ["base", "public", "private"]) {
134
+ const dir = (0, import_node_path2.join)(root, bucket);
135
+ for (const f of walk(dir)) {
136
+ docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
137
+ }
138
+ }
139
+ return docs;
140
+ }
141
+ var import_node_fs2, import_node_path2;
142
+ var init_loaders = __esm({
143
+ "src/core/loaders.ts"() {
144
+ "use strict";
145
+ import_node_fs2 = require("node:fs");
146
+ import_node_path2 = require("node:path");
147
+ }
148
+ });
149
+
150
+ // src/core/retrieval.ts
151
+ var retrieval_exports = {};
152
+ __export(retrieval_exports, {
153
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
154
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
155
+ VectorStore: () => VectorStore,
156
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
157
+ openLibsqlClient: () => openLibsqlClient,
158
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
159
+ });
160
+ function createOpenAIEmbedder(client) {
161
+ return async (input) => {
162
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
163
+ return res.data.map((d) => d.embedding);
164
+ };
165
+ }
166
+ function wrapMissingLibsqlError(cause) {
167
+ return new Error(
168
+ "Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
169
+ { cause }
170
+ );
171
+ }
172
+ async function openLibsqlClient(database) {
173
+ const mod = await import("@libsql/client").catch((error) => {
174
+ throw wrapMissingLibsqlError(error);
175
+ });
176
+ return mod.createClient({ url: database.url, authToken: database.authToken });
177
+ }
178
+ function chunk(text, max = 900) {
179
+ const out = [];
180
+ let buf = "";
181
+ for (const line of text.replace(/\r/g, "").split("\n")) {
182
+ if (`${buf}
183
+ ${line}`.length > max) {
184
+ out.push(buf.trim());
185
+ buf = line;
186
+ } else buf += `
187
+ ${line}`;
188
+ }
189
+ if (buf.trim()) out.push(buf.trim());
190
+ return out;
191
+ }
192
+ async function sha256(input) {
193
+ const data = new TextEncoder().encode(input);
194
+ const hash = await crypto.subtle.digest("SHA-256", data);
195
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
196
+ }
197
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
198
+ var init_retrieval = __esm({
199
+ "src/core/retrieval.ts"() {
200
+ "use strict";
201
+ init_buildLock();
202
+ init_loaders();
203
+ init_logger();
204
+ EMB_MODEL = "text-embedding-3-large";
205
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
206
+ DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
207
+ VectorStore = class _VectorStore {
208
+ constructor(embed, options) {
209
+ this.embed = embed;
210
+ this.db = options.databaseClient;
211
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
212
+ this.logger = options.logger ?? createConsoleLogger();
213
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
214
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
215
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
216
+ }
217
+ embed;
218
+ /**
219
+ * The libsql client backing this store - the same instance passed in as
220
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
221
+ * and the store share one connection.
222
+ */
223
+ db;
224
+ knowledgeDir;
225
+ logger;
226
+ buildLock;
227
+ instanceId;
228
+ buildLockStaleMs;
229
+ /**
230
+ * On boot: ingest new chunks and embed only missing ones.
231
+ *
232
+ * The whole ingest runs under a single-writer lock held in the database
233
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
234
+ * every chunk id the current `knowledgeDir` did not produce. Without the
235
+ * lock, a second instance booting mid-build diffs against a database the
236
+ * first one is still writing and deletes its chunks. An instance that
237
+ * cannot take the lock skips its build entirely and serves what the holder
238
+ * has already written, rather than racing it.
239
+ */
240
+ async build() {
241
+ this.logger.info("Building knowledge base...");
242
+ await this.db.execute(`
243
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
244
+ `);
245
+ await this.db.execute(`
246
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
247
+ `);
248
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
249
+ if (!await this.renewBuildLock()) {
250
+ this.logger.warn(
251
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
252
+ );
253
+ return;
254
+ }
255
+ try {
256
+ await this.buildUnderLock();
257
+ } finally {
258
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
259
+ }
260
+ }
261
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
262
+ renewBuildLock() {
263
+ return this.buildLock.tryAcquire(
264
+ BUILD_LOCK_KEY,
265
+ this.instanceId,
266
+ Date.now(),
267
+ this.buildLockStaleMs
268
+ );
269
+ }
270
+ /** The ingest itself. Only ever runs with the build lock held. */
271
+ async buildUnderLock() {
272
+ const docs = loadKnowledge(this.knowledgeDir);
273
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
274
+ if (docs.length === 0) {
275
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
276
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
277
+ if (existingCount > 0) {
278
+ throw new Error(
279
+ `Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
280
+ );
281
+ }
282
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
283
+ return;
284
+ }
285
+ const rows = [];
286
+ for (const d of docs) {
287
+ for (const part of chunk(d.text)) {
288
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
289
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
290
+ }
291
+ }
292
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
293
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
294
+ const UPSERT_BATCH = 500;
295
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
296
+ const batch = rows.slice(i, i + UPSERT_BATCH);
297
+ await this.db.batch(
298
+ batch.map((r) => ({
299
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
300
+ ON CONFLICT(id) DO NOTHING`,
301
+ args: [r.id, r.bucket, r.source, r.text]
302
+ })),
303
+ "write"
304
+ );
305
+ }
306
+ const ids = rows.map((r) => r.id);
307
+ const missing = [];
308
+ for (let i = 0; i < ids.length; i += 500) {
309
+ const batch = ids.slice(i, i + 500);
310
+ const placeholders = batch.map(() => "?").join(",");
311
+ const res = await this.db.execute({
312
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
313
+ EXCEPT SELECT id FROM embeddings`,
314
+ args: batch
315
+ });
316
+ for (const row of res.rows) missing.push(String(row.id));
317
+ }
318
+ if (missing.length === 0) {
319
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
320
+ return;
321
+ }
322
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
323
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
324
+ const BATCH = 96;
325
+ for (let i = 0; i < missing.length; i += BATCH) {
326
+ const batchIds = missing.slice(i, i + BATCH);
327
+ const inputs = batchIds.map((id) => textById.get(id) || "");
328
+ const vectors = await this.embed(inputs);
329
+ await this.db.batch(
330
+ vectors.map((embedding, idx) => ({
331
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
332
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
333
+ })),
334
+ "write"
335
+ );
336
+ if (!await this.renewBuildLock()) {
337
+ this.logger.warn(
338
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
339
+ );
340
+ }
341
+ }
342
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
343
+ }
344
+ // Remove chunks from database that no longer exist in markdown files
345
+ async cleanupStaleChunks(currentIds) {
346
+ const result = await this.db.execute("SELECT id FROM chunks");
347
+ const dbIds = result.rows.map((row) => String(row.id));
348
+ const currentIdSet = new Set(currentIds);
349
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
350
+ if (staleIds.length === 0) {
351
+ return;
352
+ }
353
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
354
+ const BATCH_SIZE = 500;
355
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
356
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
357
+ const placeholders = batch.map(() => "?").join(",");
358
+ await this.db.batch(
359
+ [
360
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
361
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
362
+ ],
363
+ "write"
364
+ );
365
+ }
366
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
367
+ }
368
+ static cosine(a, b) {
369
+ let dot = 0;
370
+ let na = 0;
371
+ let nb = 0;
372
+ for (let i = 0; i < a.length; i++) {
373
+ dot += a[i] * b[i];
374
+ na += a[i] * a[i];
375
+ nb += b[i] * b[i];
376
+ }
377
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
378
+ }
379
+ /**
380
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
381
+ * & portable).
382
+ *
383
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
384
+ * `chunks` table constrains nothing, so a deployment that ingests its own
385
+ * buckets can query them (`build` only writes and prunes the three the
386
+ * knowledge loader walks). Names are bound as query parameters, never
387
+ * interpolated. An empty list retrieves nothing, and short-circuits before
388
+ * the embedding call.
389
+ */
390
+ async query(q, k = 6, allowed = ["base"]) {
391
+ if (allowed.length === 0) return [];
392
+ const [qv] = await this.embed([q]);
393
+ const placeholders = allowed.map(() => "?").join(",");
394
+ const res = await this.db.execute({
395
+ sql: `SELECT c.id, c.text, e.embedding
396
+ FROM chunks c
397
+ JOIN embeddings e ON e.id = c.id
398
+ WHERE c.bucket IN (${placeholders})`,
399
+ args: allowed
400
+ });
401
+ const scored = [];
402
+ for (const row of res.rows) {
403
+ const emb = JSON.parse(String(row.embedding));
404
+ const s = _VectorStore.cosine(qv, emb);
405
+ scored.push({ s, text: String(row.text) });
406
+ }
407
+ scored.sort((a, b) => b.s - a.s);
408
+ return scored.slice(0, k).map((r) => r.text);
409
+ }
410
+ };
411
+ }
412
+ });
413
+
30
414
  // src/mcp-server.ts
31
415
  var mcp_server_exports = {};
32
416
  __export(mcp_server_exports, {
@@ -107,7 +491,8 @@ async function answerOnce({
107
491
  async function applyTransformReply(transformReply, input, logger) {
108
492
  if (!transformReply) return input.text;
109
493
  try {
110
- return await transformReply(input);
494
+ const result = await transformReply(input);
495
+ return typeof result === "string" ? result : null;
111
496
  } catch (error) {
112
497
  logger?.error(
113
498
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -148,24 +533,8 @@ async function resolveBuckets({
148
533
  return cleaned.filter((bucket) => ceiling.has(bucket));
149
534
  }
150
535
 
151
- // src/core/logger.ts
152
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
153
- function createConsoleLogger(level = "info") {
154
- const threshold = LEVEL_ORDER[level];
155
- const write = (at) => (...args) => {
156
- if (LEVEL_ORDER[at] < threshold) return;
157
- console.error(`[${at}]`, ...args);
158
- };
159
- return {
160
- debug: write("debug"),
161
- info: write("info"),
162
- warn: write("warn"),
163
- error: write("error")
164
- };
165
- }
166
- function resolveLogger(logger, level) {
167
- return logger ?? createConsoleLogger(level);
168
- }
536
+ // src/mcp-server.ts
537
+ init_logger();
169
538
 
170
539
  // src/core/messages.ts
171
540
  function lastUserMessage(messages) {
@@ -188,6 +557,7 @@ async function prepareChat({
188
557
  sender,
189
558
  rewriteQuery,
190
559
  rerankContext,
560
+ fallbackFn,
191
561
  logger
192
562
  }) {
193
563
  const lastUserMsg = lastUserMessage(messages);
@@ -204,21 +574,39 @@ async function prepareChat({
204
574
  logger?.error("rewriteQuery threw; falling back to the original query", error);
205
575
  }
206
576
  }
207
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
577
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
578
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
579
+ let ctx = retrievedChunks;
208
580
  if (rerankContext) {
209
581
  try {
210
- const reranked = await rerankContext({ query, chunks: ctx });
582
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
211
583
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
212
584
  } catch (error) {
213
585
  logger?.error("rerankContext threw; falling back to the original chunks", error);
214
586
  }
215
587
  }
588
+ let fallback;
589
+ if (ctx.length === 0 && fallbackFn) {
590
+ try {
591
+ const guidance = await fallbackFn({
592
+ query,
593
+ mode,
594
+ buckets: resolvedBuckets,
595
+ sender,
596
+ retrievedChunks
597
+ });
598
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
599
+ } catch (error) {
600
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
601
+ }
602
+ }
216
603
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
217
604
  const hint = channelHint?.trim();
218
605
  const system = [
219
606
  prompts.baseSystemRules,
220
607
  persona,
221
608
  ...hint ? [hint] : [],
609
+ ...fallback ? [fallback] : [],
222
610
  `${label}:
223
611
  ${ctx.join("\n\n")}`
224
612
  ].join("\n\n");
@@ -228,6 +616,7 @@ ${ctx.join("\n\n")}`
228
616
  // src/core/prompts.ts
229
617
  var import_node_fs = require("node:fs");
230
618
  var import_node_path = require("node:path");
619
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
231
620
  var PromptLoader = class {
232
621
  constructor(promptsDir, bot) {
233
622
  this.promptsDir = promptsDir;
@@ -258,206 +647,8 @@ var PromptLoader = class {
258
647
  }
259
648
  };
260
649
 
261
- // src/core/retrieval.ts
262
- var import_client = require("@libsql/client");
263
-
264
- // src/core/loaders.ts
265
- var import_node_fs2 = require("node:fs");
266
- var import_node_path2 = require("node:path");
267
- function walk(dir) {
268
- const items = [];
269
- try {
270
- for (const e of (0, import_node_fs2.readdirSync)(dir)) {
271
- const p = (0, import_node_path2.join)(dir, e);
272
- const st = (0, import_node_fs2.statSync)(p);
273
- if (st.isDirectory()) items.push(...walk(p));
274
- else if (p.endsWith(".md")) items.push(p);
275
- }
276
- } catch {
277
- }
278
- return items;
279
- }
280
- function loadKnowledge(root = "config/knowledge") {
281
- const docs = [];
282
- for (const bucket of ["base", "public", "private"]) {
283
- const dir = (0, import_node_path2.join)(root, bucket);
284
- for (const f of walk(dir)) {
285
- docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
286
- }
287
- }
288
- return docs;
289
- }
290
-
291
- // src/core/retrieval.ts
292
- var EMB_MODEL = "text-embedding-3-large";
293
- function chunk(text, max = 900) {
294
- const out = [];
295
- let buf = "";
296
- for (const line of text.replace(/\r/g, "").split("\n")) {
297
- if (`${buf}
298
- ${line}`.length > max) {
299
- out.push(buf.trim());
300
- buf = line;
301
- } else buf += `
302
- ${line}`;
303
- }
304
- if (buf.trim()) out.push(buf.trim());
305
- return out;
306
- }
307
- async function sha256(input) {
308
- const data = new TextEncoder().encode(input);
309
- const hash = await crypto.subtle.digest("SHA-256", data);
310
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
311
- }
312
- var VectorStore = class _VectorStore {
313
- constructor(client, options) {
314
- this.client = client;
315
- this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
316
- url: options.databaseUrl,
317
- authToken: options.databaseAuthToken
318
- });
319
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
320
- this.logger = options.logger ?? createConsoleLogger();
321
- }
322
- client;
323
- /**
324
- * The libsql client backing this store. When a client was injected this is
325
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
326
- * and the store share one connection.
327
- */
328
- db;
329
- knowledgeDir;
330
- logger;
331
- // On boot: ingest new chunks and embed only missing ones.
332
- async build() {
333
- this.logger.info("\u{1F504} Building knowledge base...");
334
- await this.db.execute(`
335
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
336
- `);
337
- await this.db.execute(`
338
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
339
- `);
340
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
341
- const docs = loadKnowledge(this.knowledgeDir);
342
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
343
- const rows = [];
344
- for (const d of docs) {
345
- for (const part of chunk(d.text)) {
346
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
347
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
348
- }
349
- }
350
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
351
- await this.cleanupStaleChunks(rows.map((r) => r.id));
352
- const UPSERT_BATCH = 500;
353
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
354
- const batch = rows.slice(i, i + UPSERT_BATCH);
355
- await this.db.batch(
356
- batch.map((r) => ({
357
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
358
- ON CONFLICT(id) DO NOTHING`,
359
- args: [r.id, r.bucket, r.source, r.text]
360
- })),
361
- "write"
362
- );
363
- }
364
- const ids = rows.map((r) => r.id);
365
- const missing = [];
366
- for (let i = 0; i < ids.length; i += 500) {
367
- const batch = ids.slice(i, i + 500);
368
- const placeholders = batch.map(() => "?").join(",");
369
- const res = await this.db.execute({
370
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
371
- EXCEPT SELECT id FROM embeddings`,
372
- args: batch
373
- });
374
- for (const row of res.rows) missing.push(String(row.id));
375
- }
376
- if (missing.length === 0) {
377
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
378
- return;
379
- }
380
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
381
- const textById = new Map(rows.map((r) => [r.id, r.text]));
382
- const BATCH = 96;
383
- for (let i = 0; i < missing.length; i += BATCH) {
384
- const batchIds = missing.slice(i, i + BATCH);
385
- const inputs = batchIds.map((id) => textById.get(id) || "");
386
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
387
- await this.db.batch(
388
- emb.data.map((d, idx) => ({
389
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
390
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
391
- })),
392
- "write"
393
- );
394
- }
395
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
396
- }
397
- // Remove chunks from database that no longer exist in markdown files
398
- async cleanupStaleChunks(currentIds) {
399
- const result = await this.db.execute("SELECT id FROM chunks");
400
- const dbIds = result.rows.map((row) => String(row.id));
401
- const currentIdSet = new Set(currentIds);
402
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
403
- if (staleIds.length === 0) {
404
- return;
405
- }
406
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
407
- const BATCH_SIZE = 500;
408
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
409
- const batch = staleIds.slice(i, i + BATCH_SIZE);
410
- const placeholders = batch.map(() => "?").join(",");
411
- await this.db.execute({
412
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
413
- args: batch
414
- });
415
- }
416
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
417
- }
418
- static cosine(a, b) {
419
- let dot = 0;
420
- let na = 0;
421
- let nb = 0;
422
- for (let i = 0; i < a.length; i++) {
423
- dot += a[i] * b[i];
424
- na += a[i] * a[i];
425
- nb += b[i] * b[i];
426
- }
427
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
428
- }
429
- /**
430
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
431
- * & portable).
432
- *
433
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
434
- * `chunks` table constrains nothing, so a deployment that ingests its own
435
- * buckets can query them (`build` only writes and prunes the three the
436
- * knowledge loader walks). Names are bound as query parameters, never
437
- * interpolated. An empty list retrieves nothing, and short-circuits before
438
- * the embedding call.
439
- */
440
- async query(q, k = 6, allowed = ["base"]) {
441
- if (allowed.length === 0) return [];
442
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
443
- const placeholders = allowed.map(() => "?").join(",");
444
- const res = await this.db.execute({
445
- sql: `SELECT c.id, c.text, e.embedding
446
- FROM chunks c
447
- JOIN embeddings e ON e.id = c.id
448
- WHERE c.bucket IN (${placeholders})`,
449
- args: allowed
450
- });
451
- const scored = [];
452
- for (const row of res.rows) {
453
- const emb = JSON.parse(String(row.embedding));
454
- const s = _VectorStore.cosine(qv, emb);
455
- scored.push({ s, text: String(row.text) });
456
- }
457
- scored.sort((a, b) => b.s - a.s);
458
- return scored.slice(0, k).map((r) => r.text);
459
- }
460
- };
650
+ // src/mcp-server.ts
651
+ init_retrieval();
461
652
 
462
653
  // src/mcp-server/conversation-id.ts
463
654
  function generateConversationId() {
@@ -484,28 +675,38 @@ function calculateCost(usage, rates) {
484
675
  }
485
676
 
486
677
  // src/mcp-server/logger.ts
678
+ init_logger();
487
679
  var MCPLogger = class {
488
680
  enableConsole;
489
681
  callback;
490
682
  logger;
491
- constructor(enableConsole = true, callback, logger) {
683
+ logContent;
684
+ constructor(enableConsole = true, callback, logger, logContent = false) {
492
685
  this.enableConsole = enableConsole;
493
686
  this.callback = callback;
494
687
  this.logger = logger ?? createConsoleLogger();
688
+ this.logContent = logContent;
495
689
  }
496
690
  /**
497
691
  * Log a chat interaction
498
692
  */
499
693
  async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
500
694
  const lastUserMsg = lastUserMessage(conversationMessages);
695
+ const content = {
696
+ userMessage: scrubOutput(lastUserMsg?.content || ""),
697
+ conversationHistory: conversationMessages.map((msg) => ({
698
+ ...msg,
699
+ content: scrubOutput(msg.content)
700
+ })),
701
+ ragContext: ragContext.map(scrubOutput),
702
+ response: scrubOutput(response)
703
+ };
704
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
501
705
  const logEvent = {
502
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
706
+ timestamp,
503
707
  toolName,
504
708
  conversationId,
505
- userMessage: lastUserMsg?.content || "",
506
- conversationHistory: conversationMessages,
507
- ragContext,
508
- response,
709
+ ...content,
509
710
  duration,
510
711
  cost
511
712
  };
@@ -513,9 +714,27 @@ var MCPLogger = class {
513
714
  this.logger.info(
514
715
  JSON.stringify({
515
716
  event: "mcp_chat",
516
- ...logEvent
717
+ timestamp,
718
+ toolName,
719
+ conversationId,
720
+ messageCount: conversationMessages.length,
721
+ ragContextCount: ragContext.length,
722
+ responseChars: response.length,
723
+ duration,
724
+ cost
517
725
  })
518
726
  );
727
+ if (this.logContent) {
728
+ this.logger.debug(
729
+ JSON.stringify({
730
+ event: "mcp_chat_content",
731
+ timestamp,
732
+ toolName,
733
+ conversationId,
734
+ ...content
735
+ })
736
+ );
737
+ }
519
738
  }
520
739
  if (this.callback) {
521
740
  try {
@@ -526,11 +745,12 @@ var MCPLogger = class {
526
745
  }
527
746
  }
528
747
  };
529
- function createLogger(enableConsole = true, callback, logger) {
530
- return new MCPLogger(enableConsole, callback, logger);
748
+ function createLogger(enableConsole = true, callback, logger, logContent = false) {
749
+ return new MCPLogger(enableConsole, callback, logger, logContent);
531
750
  }
532
751
 
533
752
  // src/mcp-server/rate-limiter.ts
753
+ var DEFAULT_WINDOW_MS = 6e4;
534
754
  var RateLimiter = class {
535
755
  requestMap;
536
756
  limit;
@@ -539,9 +759,9 @@ var RateLimiter = class {
539
759
  * Create a new rate limiter
540
760
  *
541
761
  * @param limit - Maximum requests allowed in the time window
542
- * @param windowMs - Time window in milliseconds (default: 60000 = 1 minute)
762
+ * @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
543
763
  */
544
- constructor(limit, windowMs = 6e4) {
764
+ constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
545
765
  this.requestMap = /* @__PURE__ */ new Map();
546
766
  this.limit = limit;
547
767
  this.windowMs = windowMs;
@@ -605,7 +825,7 @@ var RateLimiter = class {
605
825
  return Math.max(0, timeUntilReset);
606
826
  }
607
827
  };
608
- function createRateLimiter(limit, windowMs = 6e4) {
828
+ function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
609
829
  return limit ? new RateLimiter(limit, windowMs) : null;
610
830
  }
611
831
 
@@ -655,11 +875,15 @@ async function createMCPServer(config) {
655
875
  const log = resolveLogger(config.logger, config.logLevel);
656
876
  log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
657
877
  const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
658
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
659
- const promptsDir = config.promptsDir || "./config/prompts";
878
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
660
879
  const transportMode = config.transport || "stdio";
661
- const rateLimiter = createRateLimiter(config.toolRateLimit);
662
- const logger = createLogger(config.logging?.console !== false, config.logging?.onChat, log);
880
+ const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
881
+ const logger = createLogger(
882
+ config.logging?.console !== false,
883
+ config.logging?.onChat,
884
+ log,
885
+ config.logging?.content === true
886
+ );
663
887
  const publicToolConfig = {
664
888
  enabled: config.tools?.public?.enabled !== false,
665
889
  name: config.tools?.public?.name || "chat_public",
@@ -674,13 +898,22 @@ async function createMCPServer(config) {
674
898
  apiKey: config.openai.apiKey
675
899
  });
676
900
  const model = config.openai.model || DEFAULT_MODEL;
677
- const store = new VectorStore(client, {
678
- databaseUrl: config.database.url,
679
- databaseAuthToken: config.database.authToken,
680
- knowledgeDir,
681
- logger: log
682
- });
683
- await store.build();
901
+ let store;
902
+ if (config.retriever) {
903
+ store = config.retriever;
904
+ } else {
905
+ if (!config.database) {
906
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
907
+ }
908
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
909
+ const db = await openLibsqlClient2(config.database);
910
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
911
+ databaseClient: db,
912
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
913
+ logger: log
914
+ });
915
+ }
916
+ await store.build?.();
684
917
  log.info("\u2705 Knowledge base ready");
685
918
  const prompts = new PromptLoader(promptsDir, config.bot);
686
919
  const server = new McpServer({
@@ -729,6 +962,7 @@ async function createMCPServer(config) {
729
962
  buckets,
730
963
  rewriteQuery: config.rewriteQuery,
731
964
  rerankContext: config.rerankContext,
965
+ fallbackFn: config.fallbackFn,
732
966
  logger: log
733
967
  });
734
968
  const result = await answerOnce({
@@ -818,6 +1052,7 @@ async function createMCPServer(config) {
818
1052
  buckets,
819
1053
  rewriteQuery: config.rewriteQuery,
820
1054
  rerankContext: config.rerankContext,
1055
+ fallbackFn: config.fallbackFn,
821
1056
  logger: log
822
1057
  });
823
1058
  const result = await answerOnce({