@diegoaltoworks/chatter 0.47.0 → 0.48.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 (118) hide show
  1. package/README.md +3 -4
  2. package/dist/auth/apikeys.d.ts.map +1 -1
  3. package/dist/auth/secretStrength.d.ts +8 -0
  4. package/dist/auth/secretStrength.d.ts.map +1 -0
  5. package/dist/bin/create-apikey.js +22 -3
  6. package/dist/bin/wa-pair.js +59 -11
  7. package/dist/channels/gates.d.ts +9 -0
  8. package/dist/channels/gates.d.ts.map +1 -1
  9. package/dist/channels/index.d.ts +1 -0
  10. package/dist/channels/index.d.ts.map +1 -1
  11. package/dist/channels/index.js +252 -8
  12. package/dist/channels/index.mjs +250 -8
  13. package/dist/channels/pipeline.d.ts +106 -0
  14. package/dist/channels/pipeline.d.ts.map +1 -0
  15. package/dist/channels/senders.d.ts +2 -1
  16. package/dist/channels/senders.d.ts.map +1 -1
  17. package/dist/channels/whatsapp/channel.d.ts +4 -0
  18. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  19. package/dist/channels/whatsapp/crypto.d.ts +17 -3
  20. package/dist/channels/whatsapp/crypto.d.ts.map +1 -1
  21. package/dist/channels/whatsapp/inbound.d.ts +34 -4
  22. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  23. package/dist/channels/whatsapp/index.d.ts +2 -1
  24. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  25. package/dist/channels/whatsapp/index.js +395 -159
  26. package/dist/channels/whatsapp/index.mjs +394 -160
  27. package/dist/channels/whatsapp/router.d.ts +107 -0
  28. package/dist/channels/whatsapp/router.d.ts.map +1 -0
  29. package/dist/core/answer.d.ts +5 -2
  30. package/dist/core/answer.d.ts.map +1 -1
  31. package/dist/core/logger.d.ts +23 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/messages.d.ts +8 -0
  34. package/dist/core/messages.d.ts.map +1 -1
  35. package/dist/core/pipeline.d.ts +2 -0
  36. package/dist/core/pipeline.d.ts.map +1 -1
  37. package/dist/core/retrieval.d.ts +4 -0
  38. package/dist/core/retrieval.d.ts.map +1 -1
  39. package/dist/flows/engine.d.ts +3 -0
  40. package/dist/flows/engine.d.ts.map +1 -1
  41. package/dist/flows/index.d.ts +1 -0
  42. package/dist/flows/index.d.ts.map +1 -1
  43. package/dist/flows/index.js +91 -25
  44. package/dist/flows/index.mjs +91 -26
  45. package/dist/flows/intent.d.ts +2 -1
  46. package/dist/flows/intent.d.ts.map +1 -1
  47. package/dist/flows/loader.d.ts +5 -3
  48. package/dist/flows/loader.d.ts.map +1 -1
  49. package/dist/flows/types.d.ts +9 -0
  50. package/dist/flows/types.d.ts.map +1 -1
  51. package/dist/history/index.d.ts +17 -0
  52. package/dist/history/index.d.ts.map +1 -0
  53. package/dist/history/index.js +100 -0
  54. package/dist/history/index.mjs +73 -0
  55. package/dist/history/tursoStore.d.ts +26 -0
  56. package/dist/history/tursoStore.d.ts.map +1 -0
  57. package/dist/history/types.d.ts +18 -0
  58. package/dist/history/types.d.ts.map +1 -0
  59. package/dist/index.d.ts +5 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1146 -32444
  62. package/dist/index.mjs +1082 -32410
  63. package/dist/mcp-server/cost-tracker.d.ts +11 -6
  64. package/dist/mcp-server/cost-tracker.d.ts.map +1 -1
  65. package/dist/mcp-server/logger.d.ts +5 -2
  66. package/dist/mcp-server/logger.d.ts.map +1 -1
  67. package/dist/mcp-server/sdk.d.ts +23 -0
  68. package/dist/mcp-server/sdk.d.ts.map +1 -0
  69. package/dist/mcp-server/tool-input.d.ts +12 -0
  70. package/dist/mcp-server/tool-input.d.ts.map +1 -0
  71. package/dist/mcp-server/types.d.ts +10 -3
  72. package/dist/mcp-server/types.d.ts.map +1 -1
  73. package/dist/mcp-server.d.ts +1 -2
  74. package/dist/mcp-server.d.ts.map +1 -1
  75. package/dist/mcp-server.js +192 -111
  76. package/dist/mcp-server.mjs +179 -98
  77. package/dist/middleware/auth.d.ts.map +1 -1
  78. package/dist/middleware/bodyLimit.d.ts +11 -0
  79. package/dist/middleware/bodyLimit.d.ts.map +1 -0
  80. package/dist/middleware/clientKey.d.ts +15 -0
  81. package/dist/middleware/clientKey.d.ts.map +1 -0
  82. package/dist/middleware/cors.d.ts.map +1 -1
  83. package/dist/middleware/jwt.d.ts +0 -3
  84. package/dist/middleware/jwt.d.ts.map +1 -1
  85. package/dist/middleware/originMatch.d.ts +27 -0
  86. package/dist/middleware/originMatch.d.ts.map +1 -0
  87. package/dist/middleware/ratelimit.d.ts +0 -6
  88. package/dist/middleware/ratelimit.d.ts.map +1 -1
  89. package/dist/middleware/referrer.d.ts +5 -1
  90. package/dist/middleware/referrer.d.ts.map +1 -1
  91. package/dist/middleware/securityHeaders.d.ts +20 -0
  92. package/dist/middleware/securityHeaders.d.ts.map +1 -0
  93. package/dist/middleware/session.d.ts +2 -1
  94. package/dist/middleware/session.d.ts.map +1 -1
  95. package/dist/middleware/windowBucket.d.ts +19 -0
  96. package/dist/middleware/windowBucket.d.ts.map +1 -0
  97. package/dist/personas/index.js +20 -1
  98. package/dist/personas/index.mjs +20 -1
  99. package/dist/personas/resolver.d.ts +3 -0
  100. package/dist/personas/resolver.d.ts.map +1 -1
  101. package/dist/routes/demo.d.ts.map +1 -1
  102. package/dist/routes/openai.d.ts.map +1 -1
  103. package/dist/routes/private.d.ts.map +1 -1
  104. package/dist/routes/public.d.ts.map +1 -1
  105. package/dist/scheduler/compose.d.ts +2 -1
  106. package/dist/scheduler/compose.d.ts.map +1 -1
  107. package/dist/scheduler/index.js +29 -9
  108. package/dist/scheduler/index.mjs +29 -9
  109. package/dist/scheduler/scheduler.d.ts +3 -0
  110. package/dist/scheduler/scheduler.d.ts.map +1 -1
  111. package/dist/scheduler/tick.d.ts +2 -0
  112. package/dist/scheduler/tick.d.ts.map +1 -1
  113. package/dist/server.d.ts.map +1 -1
  114. package/dist/server.js +678 -528
  115. package/dist/server.mjs +678 -528
  116. package/dist/types.d.ts +87 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/package.json +22 -7
@@ -1,7 +1,5 @@
1
1
  // src/mcp-server.ts
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import OpenAI from "openai";
4
- import { z } from "zod";
5
3
 
6
4
  // src/core/guardrails.ts
7
5
  function detectLeakage(text) {
@@ -47,8 +45,8 @@ async function completeOnce({
47
45
 
48
46
  // src/core/answer.ts
49
47
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
50
- async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
51
- const result = await answerFn({ system, messages, mode, sender });
48
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
49
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
52
50
  const raw = typeof result === "string" ? result : result?.content;
53
51
  const usage = typeof result === "string" ? void 0 : result?.usage;
54
52
  return {
@@ -63,11 +61,12 @@ async function answerOnce({
63
61
  messages,
64
62
  mode,
65
63
  sender,
64
+ conversationId,
66
65
  temperature,
67
66
  model
68
67
  }) {
69
68
  if (answerFn) {
70
- return runAnswerFn(answerFn, { system, messages, mode, sender });
69
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
71
70
  }
72
71
  return completeOnce({ client, system, messages, temperature, model });
73
72
  }
@@ -103,6 +102,62 @@ async function resolveBuckets({
103
102
  return cleaned.filter((bucket) => ceiling.has(bucket));
104
103
  }
105
104
 
105
+ // src/core/logger.ts
106
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
107
+ function createConsoleLogger(level = "info") {
108
+ const threshold = LEVEL_ORDER[level];
109
+ const write = (at) => (...args) => {
110
+ if (LEVEL_ORDER[at] < threshold) return;
111
+ console.error(`[${at}]`, ...args);
112
+ };
113
+ return {
114
+ debug: write("debug"),
115
+ info: write("info"),
116
+ warn: write("warn"),
117
+ error: write("error")
118
+ };
119
+ }
120
+ function resolveLogger(logger, level) {
121
+ return logger ?? createConsoleLogger(level);
122
+ }
123
+
124
+ // src/core/messages.ts
125
+ function lastUserMessage(messages) {
126
+ return [...messages].reverse().find((m) => m.role === "user");
127
+ }
128
+
129
+ // src/core/pipeline.ts
130
+ var MODE_SETTINGS = {
131
+ public: { topK: 6, label: "Context" },
132
+ private: { topK: 8, label: "Internal Context" }
133
+ };
134
+ async function prepareChat({
135
+ store,
136
+ prompts,
137
+ mode,
138
+ messages,
139
+ personaLayer,
140
+ channelHint,
141
+ buckets
142
+ }) {
143
+ const lastUserMsg = lastUserMessage(messages);
144
+ if (!lastUserMsg) {
145
+ throw new Error("no user message found in conversation");
146
+ }
147
+ const { topK, label } = MODE_SETTINGS[mode];
148
+ const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
149
+ const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
150
+ const hint = channelHint?.trim();
151
+ const system = [
152
+ prompts.baseSystemRules,
153
+ persona,
154
+ ...hint ? [hint] : [],
155
+ `${label}:
156
+ ${ctx.join("\n\n")}`
157
+ ].join("\n\n");
158
+ return { system, messages, context: ctx };
159
+ }
160
+
106
161
  // src/core/prompts.ts
107
162
  import { readFileSync } from "node:fs";
108
163
  import { join } from "node:path";
@@ -195,6 +250,7 @@ var VectorStore = class _VectorStore {
195
250
  authToken: options.databaseAuthToken
196
251
  });
197
252
  this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
253
+ this.logger = options.logger ?? createConsoleLogger();
198
254
  }
199
255
  client;
200
256
  /**
@@ -204,9 +260,10 @@ var VectorStore = class _VectorStore {
204
260
  */
205
261
  db;
206
262
  knowledgeDir;
263
+ logger;
207
264
  // On boot: ingest new chunks and embed only missing ones.
208
265
  async build() {
209
- console.log("\u{1F504} Building knowledge base...");
266
+ this.logger.info("\u{1F504} Building knowledge base...");
210
267
  await this.db.execute(`
211
268
  CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
212
269
  `);
@@ -215,7 +272,7 @@ var VectorStore = class _VectorStore {
215
272
  `);
216
273
  await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
217
274
  const docs = loadKnowledge(this.knowledgeDir);
218
- console.log(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
275
+ this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
219
276
  const rows = [];
220
277
  for (const d of docs) {
221
278
  for (const part of chunk(d.text)) {
@@ -223,21 +280,19 @@ var VectorStore = class _VectorStore {
223
280
  rows.push({ id, bucket: d.bucket, source: d.source, text: part });
224
281
  }
225
282
  }
226
- console.log(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
283
+ this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
227
284
  await this.cleanupStaleChunks(rows.map((r) => r.id));
228
- const tx = await this.db.transaction("write");
229
- try {
230
- for (const r of rows) {
231
- await tx.execute({
285
+ const UPSERT_BATCH = 500;
286
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
287
+ const batch = rows.slice(i, i + UPSERT_BATCH);
288
+ await this.db.batch(
289
+ batch.map((r) => ({
232
290
  sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
233
291
  ON CONFLICT(id) DO NOTHING`,
234
292
  args: [r.id, r.bucket, r.source, r.text]
235
- });
236
- }
237
- await tx.commit();
238
- } catch (e) {
239
- await tx.rollback();
240
- throw e;
293
+ })),
294
+ "write"
295
+ );
241
296
  }
242
297
  const ids = rows.map((r) => r.id);
243
298
  const missing = [];
@@ -252,32 +307,25 @@ var VectorStore = class _VectorStore {
252
307
  for (const row of res.rows) missing.push(String(row.id));
253
308
  }
254
309
  if (missing.length === 0) {
255
- console.log("\u2705 No new chunks to embed - knowledge base is up to date");
310
+ this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
256
311
  return;
257
312
  }
258
- console.log(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
313
+ this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
259
314
  const textById = new Map(rows.map((r) => [r.id, r.text]));
260
315
  const BATCH = 96;
261
316
  for (let i = 0; i < missing.length; i += BATCH) {
262
317
  const batchIds = missing.slice(i, i + BATCH);
263
318
  const inputs = batchIds.map((id) => textById.get(id) || "");
264
319
  const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
265
- const tx2 = await this.db.transaction("write");
266
- try {
267
- emb.data.forEach((d, idx) => {
268
- const id = batchIds[idx];
269
- tx2.execute({
270
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
271
- args: [id, EMB_MODEL, JSON.stringify(d.embedding)]
272
- });
273
- });
274
- await tx2.commit();
275
- } catch (e) {
276
- await tx2.rollback();
277
- throw e;
278
- }
320
+ await this.db.batch(
321
+ emb.data.map((d, idx) => ({
322
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
323
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
324
+ })),
325
+ "write"
326
+ );
279
327
  }
280
- console.log(`\u2705 Successfully embedded ${missing.length} new chunks`);
328
+ this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
281
329
  }
282
330
  // Remove chunks from database that no longer exist in markdown files
283
331
  async cleanupStaleChunks(currentIds) {
@@ -288,7 +336,7 @@ var VectorStore = class _VectorStore {
288
336
  if (staleIds.length === 0) {
289
337
  return;
290
338
  }
291
- console.log(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
339
+ this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
292
340
  const BATCH_SIZE = 500;
293
341
  for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
294
342
  const batch = staleIds.slice(i, i + BATCH_SIZE);
@@ -298,7 +346,7 @@ var VectorStore = class _VectorStore {
298
346
  args: batch
299
347
  });
300
348
  }
301
- console.log(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
349
+ this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
302
350
  }
303
351
  static cosine(a, b) {
304
352
  let dot = 0;
@@ -355,13 +403,11 @@ function getOrGenerateConversationId(providedId) {
355
403
  }
356
404
 
357
405
  // src/mcp-server/cost-tracker.ts
358
- function calculateCost(usage) {
406
+ function calculateCost(usage, rates) {
359
407
  const promptTokens = usage.prompt_tokens || 0;
360
408
  const completionTokens = usage.completion_tokens || 0;
361
409
  const totalTokens = promptTokens + completionTokens;
362
- const promptCost = promptTokens / 1e6 * 2.5;
363
- const completionCost = completionTokens / 1e6 * 10;
364
- const estimatedCost = promptCost + completionCost;
410
+ const estimatedCost = rates ? promptTokens / 1e6 * rates.promptPer1M + completionTokens / 1e6 * rates.completionPer1M : null;
365
411
  return {
366
412
  promptTokens,
367
413
  completionTokens,
@@ -374,15 +420,17 @@ function calculateCost(usage) {
374
420
  var MCPLogger = class {
375
421
  enableConsole;
376
422
  callback;
377
- constructor(enableConsole = true, callback) {
423
+ logger;
424
+ constructor(enableConsole = true, callback, logger) {
378
425
  this.enableConsole = enableConsole;
379
426
  this.callback = callback;
427
+ this.logger = logger ?? createConsoleLogger();
380
428
  }
381
429
  /**
382
430
  * Log a chat interaction
383
431
  */
384
432
  async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
385
- const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
433
+ const lastUserMsg = lastUserMessage(conversationMessages);
386
434
  const logEvent = {
387
435
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
388
436
  toolName,
@@ -395,7 +443,7 @@ var MCPLogger = class {
395
443
  cost
396
444
  };
397
445
  if (this.enableConsole) {
398
- console.log(
446
+ this.logger.info(
399
447
  JSON.stringify({
400
448
  event: "mcp_chat",
401
449
  ...logEvent
@@ -406,13 +454,13 @@ var MCPLogger = class {
406
454
  try {
407
455
  await this.callback(logEvent);
408
456
  } catch (err) {
409
- console.error("Logging callback error:", err);
457
+ this.logger.error("Logging callback error:", err);
410
458
  }
411
459
  }
412
460
  }
413
461
  };
414
- function createLogger(enableConsole = true, callback) {
415
- return new MCPLogger(enableConsole, callback);
462
+ function createLogger(enableConsole = true, callback, logger) {
463
+ return new MCPLogger(enableConsole, callback, logger);
416
464
  }
417
465
 
418
466
  // src/mcp-server/rate-limiter.ts
@@ -494,14 +542,57 @@ function createRateLimiter(limit, windowMs = 6e4) {
494
542
  return limit ? new RateLimiter(limit, windowMs) : null;
495
543
  }
496
544
 
545
+ // src/mcp-server/sdk.ts
546
+ function wrapMissingMcpPeerError(packageName, cause) {
547
+ return new Error(
548
+ `The MCP server ('@diegoaltoworks/chatter/mcp') requires the optional peer dependency '${packageName}', which is not installed. Install it with \`bun add ${packageName}\` (or npm/pnpm/yarn) to use this feature.`,
549
+ { cause }
550
+ );
551
+ }
552
+ var cachedSdk;
553
+ function loadMcpSdk() {
554
+ if (!cachedSdk) {
555
+ cachedSdk = import("@modelcontextprotocol/sdk/server/mcp.js").then((mod) => {
556
+ if (!mod.McpServer) throw new Error("module has no McpServer export");
557
+ return mod.McpServer;
558
+ }).catch((error) => {
559
+ cachedSdk = void 0;
560
+ throw wrapMissingMcpPeerError("@modelcontextprotocol/sdk", error);
561
+ });
562
+ }
563
+ return cachedSdk;
564
+ }
565
+ var cachedZod;
566
+ function loadZod() {
567
+ if (!cachedZod) {
568
+ cachedZod = import("zod").then((mod) => {
569
+ if (!mod.z) throw new Error("module has no z export");
570
+ return mod.z;
571
+ }).catch((error) => {
572
+ cachedZod = void 0;
573
+ throw wrapMissingMcpPeerError("zod", error);
574
+ });
575
+ }
576
+ return cachedZod;
577
+ }
578
+
579
+ // src/mcp-server/tool-input.ts
580
+ function resolveConversationMessages(message, messages) {
581
+ if (messages) return messages;
582
+ if (message) return [{ role: "user", content: message }];
583
+ throw new Error("Either 'message' or 'messages' is required");
584
+ }
585
+
497
586
  // src/mcp-server.ts
498
587
  async function createMCPServer(config) {
499
- console.log(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
588
+ const log = resolveLogger(config.logger, config.logLevel);
589
+ log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
590
+ const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
500
591
  const knowledgeDir = config.knowledgeDir || "./config/knowledge";
501
592
  const promptsDir = config.promptsDir || "./config/prompts";
502
593
  const transportMode = config.transport || "stdio";
503
594
  const rateLimiter = createRateLimiter(config.toolRateLimit);
504
- const logger = createLogger(config.logging?.console !== false, config.logging?.onChat);
595
+ const logger = createLogger(config.logging?.console !== false, config.logging?.onChat, log);
505
596
  const publicToolConfig = {
506
597
  enabled: config.tools?.public?.enabled !== false,
507
598
  name: config.tools?.public?.name || "chat_public",
@@ -515,19 +606,21 @@ async function createMCPServer(config) {
515
606
  const client = new OpenAI({
516
607
  apiKey: config.openai.apiKey
517
608
  });
609
+ const model = config.openai.model || DEFAULT_MODEL;
518
610
  const store = new VectorStore(client, {
519
611
  databaseUrl: config.database.url,
520
612
  databaseAuthToken: config.database.authToken,
521
- knowledgeDir
613
+ knowledgeDir,
614
+ logger: log
522
615
  });
523
616
  await store.build();
524
- console.log("\u2705 Knowledge base ready");
617
+ log.info("\u2705 Knowledge base ready");
525
618
  const prompts = new PromptLoader(promptsDir, config.bot);
526
619
  const server = new McpServer({
527
620
  name: config.bot.name,
528
621
  version: "1.0.0"
529
622
  });
530
- console.log("\u2705 MCP server initialized");
623
+ log.info("\u2705 MCP server initialized");
531
624
  if (publicToolConfig.enabled) {
532
625
  const publicShape = {
533
626
  message: z.string().optional().describe("A single message to send (for simple queries)"),
@@ -556,34 +649,28 @@ async function createMCPServer(config) {
556
649
  `Rate limit exceeded for ${publicToolConfig.name}. Maximum ${config.toolRateLimit} requests per minute.`
557
650
  );
558
651
  }
559
- let conversationMessages;
560
- if (messages && Array.isArray(messages)) {
561
- conversationMessages = messages;
562
- } else if (message) {
563
- conversationMessages = [{ role: "user", content: String(message) }];
564
- } else {
565
- throw new Error("Either 'message' or 'messages' is required");
566
- }
567
- const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
568
- if (!lastUserMsg) {
652
+ const conversationMessages = resolveConversationMessages(message, messages);
653
+ if (!lastUserMessage(conversationMessages)) {
569
654
  throw new Error("No user message found in conversation");
570
655
  }
571
- const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor }) ?? defaultBuckets("public");
572
- const ctx = await store.query(lastUserMsg.content, 6, buckets);
573
- const system = [
574
- prompts.baseSystemRules,
575
- prompts.publicPersona,
576
- `Context:
577
- ${ctx.join("\n\n")}`
578
- ].join("\n\n");
656
+ const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor });
657
+ const { system, context: ctx } = await prepareChat({
658
+ store,
659
+ prompts,
660
+ mode: "public",
661
+ messages: conversationMessages,
662
+ buckets
663
+ });
579
664
  const result = await answerOnce({
580
665
  answerFn: config.answerFn,
581
666
  client,
582
667
  system,
583
668
  messages: conversationMessages,
584
- mode: "public"
669
+ mode: "public",
670
+ conversationId: convId,
671
+ model
585
672
  });
586
- const cost = calculateCost(result.usage);
673
+ const cost = calculateCost(result.usage, config.openai.pricing);
587
674
  const duration = Date.now() - startTime;
588
675
  await logger.logChatInteraction(
589
676
  publicToolConfig.name,
@@ -593,7 +680,7 @@ ${ctx.join("\n\n")}`
593
680
  result.content,
594
681
  duration,
595
682
  cost
596
- ).catch((err) => console.error("Logging error:", err));
683
+ ).catch((err) => log.error("Logging error:", err));
597
684
  return {
598
685
  content: [
599
686
  {
@@ -613,7 +700,7 @@ ${ctx.join("\n\n")}`
613
700
  };
614
701
  }
615
702
  );
616
- console.log(`\u2705 Registered tool: ${publicToolConfig.name}`);
703
+ log.info(`\u2705 Registered tool: ${publicToolConfig.name}`);
617
704
  }
618
705
  if (privateToolConfig.enabled) {
619
706
  const privateShape = {
@@ -643,34 +730,28 @@ ${ctx.join("\n\n")}`
643
730
  `Rate limit exceeded for ${privateToolConfig.name}. Maximum ${config.toolRateLimit} requests per minute.`
644
731
  );
645
732
  }
646
- let conversationMessages;
647
- if (messages && Array.isArray(messages)) {
648
- conversationMessages = messages;
649
- } else if (message) {
650
- conversationMessages = [{ role: "user", content: String(message) }];
651
- } else {
652
- throw new Error("Either 'message' or 'messages' is required");
653
- }
654
- const lastUserMsg = [...conversationMessages].reverse().find((m) => m.role === "user");
655
- if (!lastUserMsg) {
733
+ const conversationMessages = resolveConversationMessages(message, messages);
734
+ if (!lastUserMessage(conversationMessages)) {
656
735
  throw new Error("No user message found in conversation");
657
736
  }
658
- const buckets = await resolveBuckets({ mode: "private", bucketsFor: config.bucketsFor }) ?? defaultBuckets("private");
659
- const ctx = await store.query(lastUserMsg.content, 8, buckets);
660
- const system = [
661
- prompts.baseSystemRules,
662
- prompts.privatePersona,
663
- `Internal Context:
664
- ${ctx.join("\n\n")}`
665
- ].join("\n\n");
737
+ const buckets = await resolveBuckets({ mode: "private", bucketsFor: config.bucketsFor });
738
+ const { system, context: ctx } = await prepareChat({
739
+ store,
740
+ prompts,
741
+ mode: "private",
742
+ messages: conversationMessages,
743
+ buckets
744
+ });
666
745
  const result = await answerOnce({
667
746
  answerFn: config.answerFn,
668
747
  client,
669
748
  system,
670
749
  messages: conversationMessages,
671
- mode: "private"
750
+ mode: "private",
751
+ conversationId: convId,
752
+ model
672
753
  });
673
- const cost = calculateCost(result.usage);
754
+ const cost = calculateCost(result.usage, config.openai.pricing);
674
755
  const duration = Date.now() - startTime;
675
756
  await logger.logChatInteraction(
676
757
  privateToolConfig.name,
@@ -680,7 +761,7 @@ ${ctx.join("\n\n")}`
680
761
  result.content,
681
762
  duration,
682
763
  cost
683
- ).catch((err) => console.error("Logging error:", err));
764
+ ).catch((err) => log.error("Logging error:", err));
684
765
  return {
685
766
  content: [
686
767
  {
@@ -700,16 +781,16 @@ ${ctx.join("\n\n")}`
700
781
  };
701
782
  }
702
783
  );
703
- console.log(`\u2705 Registered tool: ${privateToolConfig.name}`);
784
+ log.info(`\u2705 Registered tool: ${privateToolConfig.name}`);
704
785
  }
705
786
  const enabledTools = [
706
787
  publicToolConfig.enabled ? publicToolConfig.name : null,
707
788
  privateToolConfig.enabled ? privateToolConfig.name : null
708
789
  ].filter(Boolean);
709
790
  if (enabledTools.length === 0) {
710
- console.warn("\u26A0\uFE0F Warning: No MCP tools enabled");
791
+ log.warn("\u26A0\uFE0F Warning: No MCP tools enabled");
711
792
  }
712
- console.log(
793
+ log.info(
713
794
  `\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
714
795
  );
715
796
  return server;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,OAGjB,OAAO,QAAQ,IAAI;;8BAuC9D"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,OAGjB,OAAO,QAAQ,IAAI;;8BA0C9D"}
@@ -0,0 +1,11 @@
1
+ /** Generous for chat text, small enough to bound worst-case parse/memory cost. */
2
+ export declare const DEFAULT_MAX_REQUEST_BYTES = 262144;
3
+ /**
4
+ * Rejects oversized chat request bodies before they reach JSON parsing.
5
+ * Wraps Hono's `bodyLimit` so the 413 body matches the caller's error shape
6
+ * instead of the plain-text 413 Hono sends by default. `toError` defaults to
7
+ * this API's usual `{ error }` shape; the OpenAI-compatible routes pass
8
+ * their own `{ error: { message, type } }` formatter.
9
+ */
10
+ export declare function chatBodyLimit(maxSize?: number, toError?: (message: string) => unknown): import("hono").MiddlewareHandler;
11
+ //# sourceMappingURL=bodyLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bodyLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/bodyLimit.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,GAAE,MAAkC,EAC3C,OAAO,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAA2C,oCAM1E"}
@@ -0,0 +1,15 @@
1
+ import type { Context } from "hono";
2
+ /**
3
+ * Resolves the caller identity IP-keyed rate limiters bucket on.
4
+ *
5
+ * `X-Forwarded-For` is set by the client unless something in front of
6
+ * Chatter (nginx, Cloudflare, a cloud load balancer) overwrites it with the
7
+ * real socket address. Trusting it unconditionally lets an attacker rotate a
8
+ * fake value on every request and bypass per-IP limits entirely. Pass
9
+ * `trustProxy: true` only when such a proxy is guaranteed to be in front —
10
+ * otherwise every caller collapses onto one shared bucket, which is coarse
11
+ * but not bypassable, and pair it with a fronting proxy that does the real
12
+ * per-client limiting.
13
+ */
14
+ export declare function clientRateLimitKey(c: Context, trustProxy: boolean): string;
15
+ //# sourceMappingURL=clientKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientKey.d.ts","sourceRoot":"","sources":["../../src/middleware/clientKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,CAK1E"}
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/middleware/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK1C;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,oBAAqB,MAAM,EAAE,SAAe,OAAO,QAAQ,IAAI,kCAyB/E,CAAC"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/middleware/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM1C;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,oBAAqB,MAAM,EAAE,SAAe,OAAO,QAAQ,IAAI,kCA2B/E,CAAC"}
@@ -17,7 +17,4 @@ export declare function jwtSubject(c: Context): string | undefined;
17
17
  export declare function createJWTMiddleware(config: ChatterConfig): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
18
18
  error: string;
19
19
  }, 401, "json">) | undefined>;
20
- export declare const requirePrivateJWT: (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
21
- error: string;
22
- }, 401, "json">) | undefined>;
23
20
  //# sourceMappingURL=jwt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,OAwBZ,OAAO,QAAQ,IAAI;;8BAa/D;AAGD,eAAO,MAAM,iBAAiB,MAhBe,OAAO,QAAQ,IAAI;;6BAgBS,CAAC"}
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,OAwBZ,OAAO,QAAQ,IAAI;;8BAa/D"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared origin/referer matching for anything that gates on
3
+ * `allowedOrigins` — demo access, referrer checks. Centralized so every
4
+ * caller gets the same boundary-aware comparison instead of each
5
+ * reimplementing (and re-breaking) it.
6
+ */
7
+ /**
8
+ * True when `origin` exactly equals one of `allowedOrigins`, or `referer`
9
+ * equals one or is prefixed by one followed by `/` or `?`. A naive
10
+ * `referer.startsWith(allowed)` would let `https://example.com.evil.com`
11
+ * match an allowed `https://example.com` — the boundary check rejects that.
12
+ */
13
+ export declare function matchesAllowedOrigin(origin: string | undefined, referer: string | undefined, allowedOrigins: string[]): boolean;
14
+ /**
15
+ * Same as {@link matchesAllowedOrigin}, plus any-port localhost/127.0.0.1 —
16
+ * for local-dev convenience on surfaces that would otherwise require an
17
+ * explicit `allowedOrigins` entry per dev port.
18
+ *
19
+ * `Origin`/`Referer` are only browser-enforced for browser-driven requests —
20
+ * a direct (non-browser) client can set either to anything, same as it could
21
+ * `Host`. This function is therefore not a substitute for real access
22
+ * control; callers should only reach for it where an always-on localhost
23
+ * allowance is an accepted tradeoff (typically gated behind an explicit
24
+ * opt-in), never as a blanket replacement for `matchesAllowedOrigin`.
25
+ */
26
+ export declare function matchesAllowedOriginOrLocalhost(origin: string | undefined, referer: string | undefined, allowedOrigins: string[]): boolean;
27
+ //# sourceMappingURL=originMatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"originMatch.d.ts","sourceRoot":"","sources":["../../src/middleware/originMatch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,cAAc,EAAE,MAAM,EAAE,GACvB,OAAO,CAUT;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,cAAc,EAAE,MAAM,EAAE,GACvB,OAAO,CAGT"}
@@ -12,10 +12,4 @@ export declare function createRateLimiter(config: ChatterConfig): {
12
12
  error: string;
13
13
  }, 429, "json">) | undefined>;
14
14
  };
15
- export declare const limitPublic: () => (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
16
- error: string;
17
- }, 429, "json">) | undefined>;
18
- export declare const limitPrivate: () => (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
19
- error: string;
20
- }, 429, "json">) | undefined>;
21
15
  //# sourceMappingURL=ratelimit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ratelimit.d.ts","sourceRoot":"","sources":["../../src/middleware/ratelimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAkB9C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;IAsBnD,WAAW,QACQ,OAAO,QAAQ,IAAI;;;IAwBtC,YAAY,QACO,OAAO,QAAQ,IAAI;;;EAQzC;AAID,eAAO,MAAM,WAAW,YArCD,OAAO,QAAQ,IAAI;;6BAqCW,CAAC;AACtD,eAAO,MAAM,YAAY,YAbF,OAAO,QAAQ,IAAI;;6BAaa,CAAC"}
1
+ {"version":3,"file":"ratelimit.d.ts","sourceRoot":"","sources":["../../src/middleware/ratelimit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAQ9C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;IAWnD,WAAW,QACQ,OAAO,QAAQ,IAAI;;;IAwBtC,YAAY,QACO,OAAO,QAAQ,IAAI;;;EAQzC"}
@@ -3,11 +3,15 @@
3
3
  * Validates that requests with demo API keys come from allowed origins
4
4
  */
5
5
  import type { Context, Next } from "hono";
6
+ import { type Logger } from "../core/logger";
6
7
  /**
7
8
  * Middleware to check if the request comes from an allowed origin
8
9
  * Only applies to demo API keys and session keys - production keys bypass this check
10
+ *
11
+ * @param demoApiKeys - API key values treated as demo keys (see
12
+ * `ChatterConfig.rateLimit.demoApiKeys`). Unset: no key is a demo key.
9
13
  */
10
- export declare function requireReferrer(allowedOrigins: string[]): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
14
+ export declare function requireReferrer(allowedOrigins: string[], demoApiKeys?: string[], logger?: Logger): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
11
15
  error: string;
12
16
  }, 403, "json">) | undefined>;
13
17
  export declare const createReferrerCheck: typeof requireReferrer;
@@ -1 +1 @@
1
- {"version":3,"file":"referrer.d.ts","sourceRoot":"","sources":["../../src/middleware/referrer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,OACrC,OAAO,QAAQ,IAAI;;8BA8CrC;AAGD,eAAO,MAAM,mBAAmB,wBAAkB,CAAC"}
1
+ {"version":3,"file":"referrer.d.ts","sourceRoot":"","sources":["../../src/middleware/referrer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGlE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,EAAE,EACxB,WAAW,GAAE,MAAM,EAAO,EAC1B,MAAM,GAAE,MAA8B,OAErB,OAAO,QAAQ,IAAI;;8BAkCrC;AAGD,eAAO,MAAM,mBAAmB,wBAAkB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Context, Next } from "hono";
2
+ import type { ChatterConfig } from "../types";
3
+ /**
4
+ * Sets baseline security headers on every response: a Content-Security-Policy
5
+ * (governs pages this server itself renders — see `server.contentSecurityPolicy`),
6
+ * `X-Content-Type-Options` to stop MIME sniffing, and HSTS (see
7
+ * `server.strictTransportSecurity`). HSTS is only sent when the request
8
+ * actually arrived over HTTPS (directly, or via a trusted proxy's
9
+ * `X-Forwarded-Proto` — see `rateLimit.trustProxy`) or `NODE_ENV=production`.
10
+ * A browser ignores HSTS delivered over plain HTTP (RFC 6797 §8.1), so this
11
+ * gate is about not asserting a guarantee the deployment doesn't back, not
12
+ * about avoiding a localhost lockout.
13
+ *
14
+ * Headers are set after `next()` so they still land on a handler that
15
+ * returns a raw `Response` (e.g. from `config.customRoutes`) rather than
16
+ * `c.json`/`c.text` — Hono only merges headers set before `next()` into
17
+ * responses it builds itself.
18
+ */
19
+ export declare function securityHeaders(config: ChatterConfig): (c: Context, next: Next) => Promise<void>;
20
+ //# sourceMappingURL=securityHeaders.d.ts.map