@diegoaltoworks/chatter 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +55 -17
  2. package/dist/auth/secretStrength.d.ts +1 -1
  3. package/dist/auth/secretStrength.d.ts.map +1 -1
  4. package/dist/bin/create-apikey.js +45 -28
  5. package/dist/bin/wa-pair.js +113 -46
  6. package/dist/channels/backoff.d.ts +10 -0
  7. package/dist/channels/backoff.d.ts.map +1 -0
  8. package/dist/channels/gates.d.ts +15 -8
  9. package/dist/channels/gates.d.ts.map +1 -1
  10. package/dist/channels/index.d.ts +1 -1
  11. package/dist/channels/index.d.ts.map +1 -1
  12. package/dist/channels/index.js +38 -5
  13. package/dist/channels/index.mjs +37 -5
  14. package/dist/channels/matrix/api.d.ts +25 -6
  15. package/dist/channels/matrix/api.d.ts.map +1 -1
  16. package/dist/channels/matrix/channel.d.ts +4 -56
  17. package/dist/channels/matrix/channel.d.ts.map +1 -1
  18. package/dist/channels/matrix/handler.d.ts +16 -9
  19. package/dist/channels/matrix/handler.d.ts.map +1 -1
  20. package/dist/channels/matrix/index.d.ts +2 -2
  21. package/dist/channels/matrix/index.d.ts.map +1 -1
  22. package/dist/channels/matrix/index.js +196 -40
  23. package/dist/channels/matrix/index.mjs +189 -39
  24. package/dist/channels/matrix/sync.d.ts +4 -1
  25. package/dist/channels/matrix/sync.d.ts.map +1 -1
  26. package/dist/channels/matrix/updates.d.ts +39 -10
  27. package/dist/channels/matrix/updates.d.ts.map +1 -1
  28. package/dist/channels/pipeline.d.ts +14 -14
  29. package/dist/channels/pipeline.d.ts.map +1 -1
  30. package/dist/channels/polling.d.ts +67 -0
  31. package/dist/channels/polling.d.ts.map +1 -0
  32. package/dist/channels/telegram/channel.d.ts +2 -56
  33. package/dist/channels/telegram/channel.d.ts.map +1 -1
  34. package/dist/channels/telegram/index.js +53 -23
  35. package/dist/channels/telegram/index.mjs +53 -23
  36. package/dist/channels/telegram/poll.d.ts +5 -1
  37. package/dist/channels/telegram/poll.d.ts.map +1 -1
  38. package/dist/channels/telegram/webhook.d.ts +2 -10
  39. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  40. package/dist/channels/whatsapp/authState.d.ts +40 -9
  41. package/dist/channels/whatsapp/authState.d.ts.map +1 -1
  42. package/dist/channels/whatsapp/channel.d.ts +25 -4
  43. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  44. package/dist/channels/whatsapp/inbound.d.ts +23 -25
  45. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  46. package/dist/channels/whatsapp/index.d.ts +3 -3
  47. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  48. package/dist/channels/whatsapp/index.js +123 -41
  49. package/dist/channels/whatsapp/index.mjs +120 -41
  50. package/dist/channels/whatsapp/pairing.d.ts.map +1 -1
  51. package/dist/client/src/Chat.d.ts.map +1 -1
  52. package/dist/client/src/ChatButton.d.ts.map +1 -1
  53. package/dist/client/src/browser.d.ts.map +1 -1
  54. package/dist/core/answer.d.ts +8 -0
  55. package/dist/core/answer.d.ts.map +1 -1
  56. package/dist/core/buildLock.d.ts +52 -0
  57. package/dist/core/buildLock.d.ts.map +1 -0
  58. package/dist/core/pipeline.d.ts +42 -5
  59. package/dist/core/pipeline.d.ts.map +1 -1
  60. package/dist/core/prompts.d.ts +2 -0
  61. package/dist/core/prompts.d.ts.map +1 -1
  62. package/dist/core/retrieval.d.ts +100 -25
  63. package/dist/core/retrieval.d.ts.map +1 -1
  64. package/dist/core/session.d.ts +8 -0
  65. package/dist/core/session.d.ts.map +1 -1
  66. package/dist/flows/index.d.ts +1 -1
  67. package/dist/flows/index.d.ts.map +1 -1
  68. package/dist/flows/index.js +8 -6
  69. package/dist/flows/index.mjs +7 -6
  70. package/dist/flows/intent.d.ts +1 -1
  71. package/dist/flows/intent.d.ts.map +1 -1
  72. package/dist/flows/loader.d.ts +1 -2
  73. package/dist/flows/loader.d.ts.map +1 -1
  74. package/dist/flows/params.d.ts +1 -1
  75. package/dist/flows/params.d.ts.map +1 -1
  76. package/dist/flows/session.d.ts +4 -2
  77. package/dist/flows/session.d.ts.map +1 -1
  78. package/dist/flows/types.d.ts +2 -3
  79. package/dist/flows/types.d.ts.map +1 -1
  80. package/dist/history/index.d.ts +1 -1
  81. package/dist/history/index.d.ts.map +1 -1
  82. package/dist/history/index.js +6 -3
  83. package/dist/history/index.mjs +5 -3
  84. package/dist/history/tursoStore.d.ts +6 -4
  85. package/dist/history/tursoStore.d.ts.map +1 -1
  86. package/dist/images/index.js +2 -2
  87. package/dist/images/index.mjs +2 -2
  88. package/dist/index.d.ts +7 -5
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +596 -295
  91. package/dist/index.mjs +595 -295
  92. package/dist/mcp-server/index.d.ts +5 -5
  93. package/dist/mcp-server/index.d.ts.map +1 -1
  94. package/dist/mcp-server/logger.d.ts +12 -2
  95. package/dist/mcp-server/logger.d.ts.map +1 -1
  96. package/dist/mcp-server/rate-limiter.d.ts +10 -3
  97. package/dist/mcp-server/rate-limiter.d.ts.map +1 -1
  98. package/dist/mcp-server/types.d.ts +20 -5
  99. package/dist/mcp-server/types.d.ts.map +1 -1
  100. package/dist/mcp-server.js +479 -244
  101. package/dist/mcp-server.mjs +485 -244
  102. package/dist/middleware/apiKey.d.ts +10 -0
  103. package/dist/middleware/apiKey.d.ts.map +1 -0
  104. package/dist/middleware/auth.d.ts.map +1 -1
  105. package/dist/middleware/cors.d.ts.map +1 -1
  106. package/dist/middleware/ratelimit.d.ts.map +1 -1
  107. package/dist/middleware/referrer.d.ts.map +1 -1
  108. package/dist/middleware/session.d.ts +1 -1
  109. package/dist/middleware/session.d.ts.map +1 -1
  110. package/dist/routes/demo.d.ts.map +1 -1
  111. package/dist/routes/openai.d.ts.map +1 -1
  112. package/dist/routes/private.d.ts.map +1 -1
  113. package/dist/routes/public.d.ts.map +1 -1
  114. package/dist/scheduler/index.js +1 -1
  115. package/dist/scheduler/index.mjs +1 -1
  116. package/dist/scheduler/scheduler.d.ts +9 -1
  117. package/dist/scheduler/scheduler.d.ts.map +1 -1
  118. package/dist/server.d.ts.map +1 -1
  119. package/dist/server.js +491 -268
  120. package/dist/server.mjs +497 -268
  121. package/dist/shortener.d.ts +5 -4
  122. package/dist/shortener.d.ts.map +1 -1
  123. package/dist/types.d.ts +129 -75
  124. package/dist/types.d.ts.map +1 -1
  125. package/package.json +4 -1
@@ -1,3 +1,393 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/core/logger.ts
17
+ function createConsoleLogger(level = "info") {
18
+ const threshold = LEVEL_ORDER[level];
19
+ const write = (at) => (...args) => {
20
+ if (LEVEL_ORDER[at] < threshold) return;
21
+ console.error(`[${at}]`, ...args);
22
+ };
23
+ return {
24
+ debug: write("debug"),
25
+ info: write("info"),
26
+ warn: write("warn"),
27
+ error: write("error")
28
+ };
29
+ }
30
+ function resolveLogger(logger, level) {
31
+ return logger ?? createConsoleLogger(level);
32
+ }
33
+ var LEVEL_ORDER;
34
+ var init_logger = __esm({
35
+ "src/core/logger.ts"() {
36
+ "use strict";
37
+ LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
38
+ }
39
+ });
40
+
41
+ // src/core/buildLock.ts
42
+ function ensureBuildLockSchema(client) {
43
+ let ready = schemaReady.get(client);
44
+ if (!ready) {
45
+ ready = client.execute(
46
+ `CREATE TABLE IF NOT EXISTS chatter_build_lock (
47
+ lock_key TEXT PRIMARY KEY,
48
+ holder_id TEXT NOT NULL,
49
+ heartbeat_at INTEGER NOT NULL
50
+ )`
51
+ ).catch((error) => {
52
+ schemaReady.delete(client);
53
+ throw error;
54
+ });
55
+ schemaReady.set(client, ready);
56
+ }
57
+ return ready;
58
+ }
59
+ function createTursoBuildLock(client) {
60
+ return {
61
+ async tryAcquire(key, holderId, now, staleMs) {
62
+ await ensureBuildLockSchema(client);
63
+ const result = await client.execute({
64
+ sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
65
+ ON CONFLICT(lock_key) DO UPDATE SET
66
+ holder_id = excluded.holder_id,
67
+ heartbeat_at = excluded.heartbeat_at
68
+ WHERE chatter_build_lock.holder_id = excluded.holder_id
69
+ OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
70
+ RETURNING holder_id`,
71
+ args: [key, holderId, now, staleMs]
72
+ });
73
+ const row = result.rows[0];
74
+ return Boolean(row) && row.holder_id === holderId;
75
+ },
76
+ async release(key, holderId) {
77
+ await ensureBuildLockSchema(client);
78
+ await client.execute({
79
+ sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
80
+ args: [key, holderId]
81
+ });
82
+ }
83
+ };
84
+ }
85
+ var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
86
+ var init_buildLock = __esm({
87
+ "src/core/buildLock.ts"() {
88
+ "use strict";
89
+ BUILD_LOCK_KEY = "knowledge_build";
90
+ BUILD_LOCK_STALE_MS = 10 * 6e4;
91
+ schemaReady = /* @__PURE__ */ new WeakMap();
92
+ }
93
+ });
94
+
95
+ // src/core/loaders.ts
96
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
97
+ import { join as join2 } from "node:path";
98
+ function walk(dir) {
99
+ const items = [];
100
+ try {
101
+ for (const e of readdirSync(dir)) {
102
+ const p = join2(dir, e);
103
+ const st = statSync(p);
104
+ if (st.isDirectory()) items.push(...walk(p));
105
+ else if (p.endsWith(".md")) items.push(p);
106
+ }
107
+ } catch {
108
+ }
109
+ return items;
110
+ }
111
+ function loadKnowledge(root = "config/knowledge") {
112
+ const docs = [];
113
+ for (const bucket of ["base", "public", "private"]) {
114
+ const dir = join2(root, bucket);
115
+ for (const f of walk(dir)) {
116
+ docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
117
+ }
118
+ }
119
+ return docs;
120
+ }
121
+ var init_loaders = __esm({
122
+ "src/core/loaders.ts"() {
123
+ "use strict";
124
+ }
125
+ });
126
+
127
+ // src/core/retrieval.ts
128
+ var retrieval_exports = {};
129
+ __export(retrieval_exports, {
130
+ DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
131
+ DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
132
+ VectorStore: () => VectorStore,
133
+ createOpenAIEmbedder: () => createOpenAIEmbedder,
134
+ openLibsqlClient: () => openLibsqlClient,
135
+ wrapMissingLibsqlError: () => wrapMissingLibsqlError
136
+ });
137
+ function createOpenAIEmbedder(client) {
138
+ return async (input) => {
139
+ const res = await client.embeddings.create({ model: EMB_MODEL, input });
140
+ return res.data.map((d) => d.embedding);
141
+ };
142
+ }
143
+ function wrapMissingLibsqlError(cause) {
144
+ return new Error(
145
+ "Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
146
+ { cause }
147
+ );
148
+ }
149
+ async function openLibsqlClient(database) {
150
+ const mod = await import("@libsql/client").catch((error) => {
151
+ throw wrapMissingLibsqlError(error);
152
+ });
153
+ return mod.createClient({ url: database.url, authToken: database.authToken });
154
+ }
155
+ function chunk(text, max = 900) {
156
+ const out = [];
157
+ let buf = "";
158
+ for (const line of text.replace(/\r/g, "").split("\n")) {
159
+ if (`${buf}
160
+ ${line}`.length > max) {
161
+ out.push(buf.trim());
162
+ buf = line;
163
+ } else buf += `
164
+ ${line}`;
165
+ }
166
+ if (buf.trim()) out.push(buf.trim());
167
+ return out;
168
+ }
169
+ async function sha256(input) {
170
+ const data = new TextEncoder().encode(input);
171
+ const hash = await crypto.subtle.digest("SHA-256", data);
172
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
173
+ }
174
+ var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
175
+ var init_retrieval = __esm({
176
+ "src/core/retrieval.ts"() {
177
+ "use strict";
178
+ init_buildLock();
179
+ init_loaders();
180
+ init_logger();
181
+ EMB_MODEL = "text-embedding-3-large";
182
+ DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
183
+ DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
184
+ VectorStore = class _VectorStore {
185
+ constructor(embed, options) {
186
+ this.embed = embed;
187
+ this.db = options.databaseClient;
188
+ this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
189
+ this.logger = options.logger ?? createConsoleLogger();
190
+ this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
191
+ this.instanceId = options.instanceId ?? crypto.randomUUID();
192
+ this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
193
+ }
194
+ embed;
195
+ /**
196
+ * The libsql client backing this store - the same instance passed in as
197
+ * `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
198
+ * and the store share one connection.
199
+ */
200
+ db;
201
+ knowledgeDir;
202
+ logger;
203
+ buildLock;
204
+ instanceId;
205
+ buildLockStaleMs;
206
+ /**
207
+ * On boot: ingest new chunks and embed only missing ones.
208
+ *
209
+ * The whole ingest runs under a single-writer lock held in the database
210
+ * (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
211
+ * every chunk id the current `knowledgeDir` did not produce. Without the
212
+ * lock, a second instance booting mid-build diffs against a database the
213
+ * first one is still writing and deletes its chunks. An instance that
214
+ * cannot take the lock skips its build entirely and serves what the holder
215
+ * has already written, rather than racing it.
216
+ */
217
+ async build() {
218
+ this.logger.info("Building knowledge base...");
219
+ await this.db.execute(`
220
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
221
+ `);
222
+ await this.db.execute(`
223
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
224
+ `);
225
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
226
+ if (!await this.renewBuildLock()) {
227
+ this.logger.warn(
228
+ "Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
229
+ );
230
+ return;
231
+ }
232
+ try {
233
+ await this.buildUnderLock();
234
+ } finally {
235
+ await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
236
+ }
237
+ }
238
+ /** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
239
+ renewBuildLock() {
240
+ return this.buildLock.tryAcquire(
241
+ BUILD_LOCK_KEY,
242
+ this.instanceId,
243
+ Date.now(),
244
+ this.buildLockStaleMs
245
+ );
246
+ }
247
+ /** The ingest itself. Only ever runs with the build lock held. */
248
+ async buildUnderLock() {
249
+ const docs = loadKnowledge(this.knowledgeDir);
250
+ this.logger.info(`Loaded ${docs.length} knowledge documents`);
251
+ if (docs.length === 0) {
252
+ const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
253
+ const existingCount = Number(existing.rows[0]?.count ?? 0);
254
+ if (existingCount > 0) {
255
+ throw new Error(
256
+ `Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
257
+ );
258
+ }
259
+ this.logger.info("No knowledge documents and no existing chunks - nothing to build");
260
+ return;
261
+ }
262
+ const rows = [];
263
+ for (const d of docs) {
264
+ for (const part of chunk(d.text)) {
265
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
266
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
267
+ }
268
+ }
269
+ this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
270
+ await this.cleanupStaleChunks(rows.map((r) => r.id));
271
+ const UPSERT_BATCH = 500;
272
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
273
+ const batch = rows.slice(i, i + UPSERT_BATCH);
274
+ await this.db.batch(
275
+ batch.map((r) => ({
276
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
277
+ ON CONFLICT(id) DO NOTHING`,
278
+ args: [r.id, r.bucket, r.source, r.text]
279
+ })),
280
+ "write"
281
+ );
282
+ }
283
+ const ids = rows.map((r) => r.id);
284
+ const missing = [];
285
+ for (let i = 0; i < ids.length; i += 500) {
286
+ const batch = ids.slice(i, i + 500);
287
+ const placeholders = batch.map(() => "?").join(",");
288
+ const res = await this.db.execute({
289
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
290
+ EXCEPT SELECT id FROM embeddings`,
291
+ args: batch
292
+ });
293
+ for (const row of res.rows) missing.push(String(row.id));
294
+ }
295
+ if (missing.length === 0) {
296
+ this.logger.info("No new chunks to embed - knowledge base is up to date");
297
+ return;
298
+ }
299
+ this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
300
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
301
+ const BATCH = 96;
302
+ for (let i = 0; i < missing.length; i += BATCH) {
303
+ const batchIds = missing.slice(i, i + BATCH);
304
+ const inputs = batchIds.map((id) => textById.get(id) || "");
305
+ const vectors = await this.embed(inputs);
306
+ await this.db.batch(
307
+ vectors.map((embedding, idx) => ({
308
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
309
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
310
+ })),
311
+ "write"
312
+ );
313
+ if (!await this.renewBuildLock()) {
314
+ this.logger.warn(
315
+ "Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
316
+ );
317
+ }
318
+ }
319
+ this.logger.info(`Successfully embedded ${missing.length} new chunks`);
320
+ }
321
+ // Remove chunks from database that no longer exist in markdown files
322
+ async cleanupStaleChunks(currentIds) {
323
+ const result = await this.db.execute("SELECT id FROM chunks");
324
+ const dbIds = result.rows.map((row) => String(row.id));
325
+ const currentIdSet = new Set(currentIds);
326
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
327
+ if (staleIds.length === 0) {
328
+ return;
329
+ }
330
+ this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
331
+ const BATCH_SIZE = 500;
332
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
333
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
334
+ const placeholders = batch.map(() => "?").join(",");
335
+ await this.db.batch(
336
+ [
337
+ { sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
338
+ { sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
339
+ ],
340
+ "write"
341
+ );
342
+ }
343
+ this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
344
+ }
345
+ static cosine(a, b) {
346
+ let dot = 0;
347
+ let na = 0;
348
+ let nb = 0;
349
+ for (let i = 0; i < a.length; i++) {
350
+ dot += a[i] * b[i];
351
+ na += a[i] * a[i];
352
+ nb += b[i] * b[i];
353
+ }
354
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
355
+ }
356
+ /**
357
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
358
+ * & portable).
359
+ *
360
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
361
+ * `chunks` table constrains nothing, so a deployment that ingests its own
362
+ * buckets can query them (`build` only writes and prunes the three the
363
+ * knowledge loader walks). Names are bound as query parameters, never
364
+ * interpolated. An empty list retrieves nothing, and short-circuits before
365
+ * the embedding call.
366
+ */
367
+ async query(q, k = 6, allowed = ["base"]) {
368
+ if (allowed.length === 0) return [];
369
+ const [qv] = await this.embed([q]);
370
+ const placeholders = allowed.map(() => "?").join(",");
371
+ const res = await this.db.execute({
372
+ sql: `SELECT c.id, c.text, e.embedding
373
+ FROM chunks c
374
+ JOIN embeddings e ON e.id = c.id
375
+ WHERE c.bucket IN (${placeholders})`,
376
+ args: allowed
377
+ });
378
+ const scored = [];
379
+ for (const row of res.rows) {
380
+ const emb = JSON.parse(String(row.embedding));
381
+ const s = _VectorStore.cosine(qv, emb);
382
+ scored.push({ s, text: String(row.text) });
383
+ }
384
+ scored.sort((a, b) => b.s - a.s);
385
+ return scored.slice(0, k).map((r) => r.text);
386
+ }
387
+ };
388
+ }
389
+ });
390
+
1
391
  // src/mcp-server.ts
2
392
  import OpenAI from "openai";
3
393
 
@@ -73,7 +463,8 @@ async function answerOnce({
73
463
  async function applyTransformReply(transformReply, input, logger) {
74
464
  if (!transformReply) return input.text;
75
465
  try {
76
- return await transformReply(input);
466
+ const result = await transformReply(input);
467
+ return typeof result === "string" ? result : null;
77
468
  } catch (error) {
78
469
  logger?.error(
79
470
  `transformReply threw for channel "${input.channel}"; sending the original reply`,
@@ -114,24 +505,8 @@ async function resolveBuckets({
114
505
  return cleaned.filter((bucket) => ceiling.has(bucket));
115
506
  }
116
507
 
117
- // src/core/logger.ts
118
- var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
119
- function createConsoleLogger(level = "info") {
120
- const threshold = LEVEL_ORDER[level];
121
- const write = (at) => (...args) => {
122
- if (LEVEL_ORDER[at] < threshold) return;
123
- console.error(`[${at}]`, ...args);
124
- };
125
- return {
126
- debug: write("debug"),
127
- info: write("info"),
128
- warn: write("warn"),
129
- error: write("error")
130
- };
131
- }
132
- function resolveLogger(logger, level) {
133
- return logger ?? createConsoleLogger(level);
134
- }
508
+ // src/mcp-server.ts
509
+ init_logger();
135
510
 
136
511
  // src/core/messages.ts
137
512
  function lastUserMessage(messages) {
@@ -154,6 +529,7 @@ async function prepareChat({
154
529
  sender,
155
530
  rewriteQuery,
156
531
  rerankContext,
532
+ fallbackFn,
157
533
  logger
158
534
  }) {
159
535
  const lastUserMsg = lastUserMessage(messages);
@@ -170,21 +546,39 @@ async function prepareChat({
170
546
  logger?.error("rewriteQuery threw; falling back to the original query", error);
171
547
  }
172
548
  }
173
- let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
549
+ const resolvedBuckets = buckets ?? defaultBuckets(mode);
550
+ const retrievedChunks = await store.query(query, topK, resolvedBuckets);
551
+ let ctx = retrievedChunks;
174
552
  if (rerankContext) {
175
553
  try {
176
- const reranked = await rerankContext({ query, chunks: ctx });
554
+ const reranked = await rerankContext({ query, chunks: [...ctx] });
177
555
  if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
178
556
  } catch (error) {
179
557
  logger?.error("rerankContext threw; falling back to the original chunks", error);
180
558
  }
181
559
  }
560
+ let fallback;
561
+ if (ctx.length === 0 && fallbackFn) {
562
+ try {
563
+ const guidance = await fallbackFn({
564
+ query,
565
+ mode,
566
+ buckets: resolvedBuckets,
567
+ sender,
568
+ retrievedChunks
569
+ });
570
+ if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
571
+ } catch (error) {
572
+ logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
573
+ }
574
+ }
182
575
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
183
576
  const hint = channelHint?.trim();
184
577
  const system = [
185
578
  prompts.baseSystemRules,
186
579
  persona,
187
580
  ...hint ? [hint] : [],
581
+ ...fallback ? [fallback] : [],
188
582
  `${label}:
189
583
  ${ctx.join("\n\n")}`
190
584
  ].join("\n\n");
@@ -194,6 +588,7 @@ ${ctx.join("\n\n")}`
194
588
  // src/core/prompts.ts
195
589
  import { readFileSync } from "node:fs";
196
590
  import { join } from "node:path";
591
+ var DEFAULT_PROMPTS_DIR = "./config/prompts";
197
592
  var PromptLoader = class {
198
593
  constructor(promptsDir, bot) {
199
594
  this.promptsDir = promptsDir;
@@ -224,206 +619,8 @@ var PromptLoader = class {
224
619
  }
225
620
  };
226
621
 
227
- // src/core/retrieval.ts
228
- import { createClient } from "@libsql/client";
229
-
230
- // src/core/loaders.ts
231
- import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
232
- import { join as join2 } from "node:path";
233
- function walk(dir) {
234
- const items = [];
235
- try {
236
- for (const e of readdirSync(dir)) {
237
- const p = join2(dir, e);
238
- const st = statSync(p);
239
- if (st.isDirectory()) items.push(...walk(p));
240
- else if (p.endsWith(".md")) items.push(p);
241
- }
242
- } catch {
243
- }
244
- return items;
245
- }
246
- function loadKnowledge(root = "config/knowledge") {
247
- const docs = [];
248
- for (const bucket of ["base", "public", "private"]) {
249
- const dir = join2(root, bucket);
250
- for (const f of walk(dir)) {
251
- docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
252
- }
253
- }
254
- return docs;
255
- }
256
-
257
- // src/core/retrieval.ts
258
- var EMB_MODEL = "text-embedding-3-large";
259
- function chunk(text, max = 900) {
260
- const out = [];
261
- let buf = "";
262
- for (const line of text.replace(/\r/g, "").split("\n")) {
263
- if (`${buf}
264
- ${line}`.length > max) {
265
- out.push(buf.trim());
266
- buf = line;
267
- } else buf += `
268
- ${line}`;
269
- }
270
- if (buf.trim()) out.push(buf.trim());
271
- return out;
272
- }
273
- async function sha256(input) {
274
- const data = new TextEncoder().encode(input);
275
- const hash = await crypto.subtle.digest("SHA-256", data);
276
- return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
277
- }
278
- var VectorStore = class _VectorStore {
279
- constructor(client, options) {
280
- this.client = client;
281
- this.db = options.databaseClient ? options.databaseClient : createClient({
282
- url: options.databaseUrl,
283
- authToken: options.databaseAuthToken
284
- });
285
- this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
286
- this.logger = options.logger ?? createConsoleLogger();
287
- }
288
- client;
289
- /**
290
- * The libsql client backing this store. When a client was injected this is
291
- * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
292
- * and the store share one connection.
293
- */
294
- db;
295
- knowledgeDir;
296
- logger;
297
- // On boot: ingest new chunks and embed only missing ones.
298
- async build() {
299
- this.logger.info("\u{1F504} Building knowledge base...");
300
- await this.db.execute(`
301
- CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
302
- `);
303
- await this.db.execute(`
304
- CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
305
- `);
306
- await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
307
- const docs = loadKnowledge(this.knowledgeDir);
308
- this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
309
- const rows = [];
310
- for (const d of docs) {
311
- for (const part of chunk(d.text)) {
312
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
313
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
314
- }
315
- }
316
- this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
317
- await this.cleanupStaleChunks(rows.map((r) => r.id));
318
- const UPSERT_BATCH = 500;
319
- for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
320
- const batch = rows.slice(i, i + UPSERT_BATCH);
321
- await this.db.batch(
322
- batch.map((r) => ({
323
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
324
- ON CONFLICT(id) DO NOTHING`,
325
- args: [r.id, r.bucket, r.source, r.text]
326
- })),
327
- "write"
328
- );
329
- }
330
- const ids = rows.map((r) => r.id);
331
- const missing = [];
332
- for (let i = 0; i < ids.length; i += 500) {
333
- const batch = ids.slice(i, i + 500);
334
- const placeholders = batch.map(() => "?").join(",");
335
- const res = await this.db.execute({
336
- sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
337
- EXCEPT SELECT id FROM embeddings`,
338
- args: batch
339
- });
340
- for (const row of res.rows) missing.push(String(row.id));
341
- }
342
- if (missing.length === 0) {
343
- this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
344
- return;
345
- }
346
- this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
347
- const textById = new Map(rows.map((r) => [r.id, r.text]));
348
- const BATCH = 96;
349
- for (let i = 0; i < missing.length; i += BATCH) {
350
- const batchIds = missing.slice(i, i + BATCH);
351
- const inputs = batchIds.map((id) => textById.get(id) || "");
352
- const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
353
- await this.db.batch(
354
- emb.data.map((d, idx) => ({
355
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
356
- args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
357
- })),
358
- "write"
359
- );
360
- }
361
- this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
362
- }
363
- // Remove chunks from database that no longer exist in markdown files
364
- async cleanupStaleChunks(currentIds) {
365
- const result = await this.db.execute("SELECT id FROM chunks");
366
- const dbIds = result.rows.map((row) => String(row.id));
367
- const currentIdSet = new Set(currentIds);
368
- const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
369
- if (staleIds.length === 0) {
370
- return;
371
- }
372
- this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
373
- const BATCH_SIZE = 500;
374
- for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
375
- const batch = staleIds.slice(i, i + BATCH_SIZE);
376
- const placeholders = batch.map(() => "?").join(",");
377
- await this.db.execute({
378
- sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
379
- args: batch
380
- });
381
- }
382
- this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
383
- }
384
- static cosine(a, b) {
385
- let dot = 0;
386
- let na = 0;
387
- let nb = 0;
388
- for (let i = 0; i < a.length; i++) {
389
- dot += a[i] * b[i];
390
- na += a[i] * a[i];
391
- nb += b[i] * b[i];
392
- }
393
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
394
- }
395
- /**
396
- * Retrieve top-k across allowed buckets; compute similarity in app (simple
397
- * & portable).
398
- *
399
- * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
400
- * `chunks` table constrains nothing, so a deployment that ingests its own
401
- * buckets can query them (`build` only writes and prunes the three the
402
- * knowledge loader walks). Names are bound as query parameters, never
403
- * interpolated. An empty list retrieves nothing, and short-circuits before
404
- * the embedding call.
405
- */
406
- async query(q, k = 6, allowed = ["base"]) {
407
- if (allowed.length === 0) return [];
408
- const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
409
- const placeholders = allowed.map(() => "?").join(",");
410
- const res = await this.db.execute({
411
- sql: `SELECT c.id, c.text, e.embedding
412
- FROM chunks c
413
- JOIN embeddings e ON e.id = c.id
414
- WHERE c.bucket IN (${placeholders})`,
415
- args: allowed
416
- });
417
- const scored = [];
418
- for (const row of res.rows) {
419
- const emb = JSON.parse(String(row.embedding));
420
- const s = _VectorStore.cosine(qv, emb);
421
- scored.push({ s, text: String(row.text) });
422
- }
423
- scored.sort((a, b) => b.s - a.s);
424
- return scored.slice(0, k).map((r) => r.text);
425
- }
426
- };
622
+ // src/mcp-server.ts
623
+ init_retrieval();
427
624
 
428
625
  // src/mcp-server/conversation-id.ts
429
626
  function generateConversationId() {
@@ -450,28 +647,38 @@ function calculateCost(usage, rates) {
450
647
  }
451
648
 
452
649
  // src/mcp-server/logger.ts
650
+ init_logger();
453
651
  var MCPLogger = class {
454
652
  enableConsole;
455
653
  callback;
456
654
  logger;
457
- constructor(enableConsole = true, callback, logger) {
655
+ logContent;
656
+ constructor(enableConsole = true, callback, logger, logContent = false) {
458
657
  this.enableConsole = enableConsole;
459
658
  this.callback = callback;
460
659
  this.logger = logger ?? createConsoleLogger();
660
+ this.logContent = logContent;
461
661
  }
462
662
  /**
463
663
  * Log a chat interaction
464
664
  */
465
665
  async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
466
666
  const lastUserMsg = lastUserMessage(conversationMessages);
667
+ const content = {
668
+ userMessage: scrubOutput(lastUserMsg?.content || ""),
669
+ conversationHistory: conversationMessages.map((msg) => ({
670
+ ...msg,
671
+ content: scrubOutput(msg.content)
672
+ })),
673
+ ragContext: ragContext.map(scrubOutput),
674
+ response: scrubOutput(response)
675
+ };
676
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
467
677
  const logEvent = {
468
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
678
+ timestamp,
469
679
  toolName,
470
680
  conversationId,
471
- userMessage: lastUserMsg?.content || "",
472
- conversationHistory: conversationMessages,
473
- ragContext,
474
- response,
681
+ ...content,
475
682
  duration,
476
683
  cost
477
684
  };
@@ -479,9 +686,27 @@ var MCPLogger = class {
479
686
  this.logger.info(
480
687
  JSON.stringify({
481
688
  event: "mcp_chat",
482
- ...logEvent
689
+ timestamp,
690
+ toolName,
691
+ conversationId,
692
+ messageCount: conversationMessages.length,
693
+ ragContextCount: ragContext.length,
694
+ responseChars: response.length,
695
+ duration,
696
+ cost
483
697
  })
484
698
  );
699
+ if (this.logContent) {
700
+ this.logger.debug(
701
+ JSON.stringify({
702
+ event: "mcp_chat_content",
703
+ timestamp,
704
+ toolName,
705
+ conversationId,
706
+ ...content
707
+ })
708
+ );
709
+ }
485
710
  }
486
711
  if (this.callback) {
487
712
  try {
@@ -492,11 +717,12 @@ var MCPLogger = class {
492
717
  }
493
718
  }
494
719
  };
495
- function createLogger(enableConsole = true, callback, logger) {
496
- return new MCPLogger(enableConsole, callback, logger);
720
+ function createLogger(enableConsole = true, callback, logger, logContent = false) {
721
+ return new MCPLogger(enableConsole, callback, logger, logContent);
497
722
  }
498
723
 
499
724
  // src/mcp-server/rate-limiter.ts
725
+ var DEFAULT_WINDOW_MS = 6e4;
500
726
  var RateLimiter = class {
501
727
  requestMap;
502
728
  limit;
@@ -505,9 +731,9 @@ var RateLimiter = class {
505
731
  * Create a new rate limiter
506
732
  *
507
733
  * @param limit - Maximum requests allowed in the time window
508
- * @param windowMs - Time window in milliseconds (default: 60000 = 1 minute)
734
+ * @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
509
735
  */
510
- constructor(limit, windowMs = 6e4) {
736
+ constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
511
737
  this.requestMap = /* @__PURE__ */ new Map();
512
738
  this.limit = limit;
513
739
  this.windowMs = windowMs;
@@ -571,7 +797,7 @@ var RateLimiter = class {
571
797
  return Math.max(0, timeUntilReset);
572
798
  }
573
799
  };
574
- function createRateLimiter(limit, windowMs = 6e4) {
800
+ function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
575
801
  return limit ? new RateLimiter(limit, windowMs) : null;
576
802
  }
577
803
 
@@ -621,11 +847,15 @@ async function createMCPServer(config) {
621
847
  const log = resolveLogger(config.logger, config.logLevel);
622
848
  log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
623
849
  const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
624
- const knowledgeDir = config.knowledgeDir || "./config/knowledge";
625
- const promptsDir = config.promptsDir || "./config/prompts";
850
+ const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
626
851
  const transportMode = config.transport || "stdio";
627
- const rateLimiter = createRateLimiter(config.toolRateLimit);
628
- const logger = createLogger(config.logging?.console !== false, config.logging?.onChat, log);
852
+ const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
853
+ const logger = createLogger(
854
+ config.logging?.console !== false,
855
+ config.logging?.onChat,
856
+ log,
857
+ config.logging?.content === true
858
+ );
629
859
  const publicToolConfig = {
630
860
  enabled: config.tools?.public?.enabled !== false,
631
861
  name: config.tools?.public?.name || "chat_public",
@@ -640,13 +870,22 @@ async function createMCPServer(config) {
640
870
  apiKey: config.openai.apiKey
641
871
  });
642
872
  const model = config.openai.model || DEFAULT_MODEL;
643
- const store = new VectorStore(client, {
644
- databaseUrl: config.database.url,
645
- databaseAuthToken: config.database.authToken,
646
- knowledgeDir,
647
- logger: log
648
- });
649
- await store.build();
873
+ let store;
874
+ if (config.retriever) {
875
+ store = config.retriever;
876
+ } else {
877
+ if (!config.database) {
878
+ throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
879
+ }
880
+ const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
881
+ const db = await openLibsqlClient2(config.database);
882
+ store = new VectorStore2(createOpenAIEmbedder2(client), {
883
+ databaseClient: db,
884
+ knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
885
+ logger: log
886
+ });
887
+ }
888
+ await store.build?.();
650
889
  log.info("\u2705 Knowledge base ready");
651
890
  const prompts = new PromptLoader(promptsDir, config.bot);
652
891
  const server = new McpServer({
@@ -695,6 +934,7 @@ async function createMCPServer(config) {
695
934
  buckets,
696
935
  rewriteQuery: config.rewriteQuery,
697
936
  rerankContext: config.rerankContext,
937
+ fallbackFn: config.fallbackFn,
698
938
  logger: log
699
939
  });
700
940
  const result = await answerOnce({
@@ -784,6 +1024,7 @@ async function createMCPServer(config) {
784
1024
  buckets,
785
1025
  rewriteQuery: config.rewriteQuery,
786
1026
  rerankContext: config.rerankContext,
1027
+ fallbackFn: config.fallbackFn,
787
1028
  logger: log
788
1029
  });
789
1030
  const result = await answerOnce({