morpheus-cli 0.9.13 → 0.9.20

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 (86) hide show
  1. package/README.md +48 -17
  2. package/dist/channels/discord.js +93 -6
  3. package/dist/channels/telegram.js +109 -9
  4. package/dist/cli/commands/start.js +15 -0
  5. package/dist/config/manager.js +20 -1
  6. package/dist/config/paths.js +4 -0
  7. package/dist/config/schemas.js +15 -0
  8. package/dist/http/api.js +2 -1
  9. package/dist/http/routers/danger.js +4 -5
  10. package/dist/http/routers/link.js +2 -2
  11. package/dist/runtime/__tests__/telephonist-tts.test.js +84 -0
  12. package/dist/runtime/adapters/AuditRepositoryAdapter.js +6 -0
  13. package/dist/runtime/adapters/ChannelNotifierAdapter.js +9 -0
  14. package/dist/runtime/adapters/LangChainProviderAdapter.js +9 -0
  15. package/dist/runtime/adapters/SQLiteChatHistoryAdapter.js +15 -0
  16. package/dist/runtime/adapters/SQLiteTaskEnqueuerAdapter.js +6 -0
  17. package/dist/runtime/adapters/index.js +5 -0
  18. package/dist/runtime/audit/repository.js +6 -2
  19. package/dist/runtime/chronos/repository.js +2 -2
  20. package/dist/runtime/container.js +50 -0
  21. package/dist/runtime/hot-reload.js +6 -9
  22. package/dist/runtime/memory/backfill-embeddings.js +2 -3
  23. package/dist/runtime/memory/sati/repository.js +3 -3
  24. package/dist/runtime/memory/sqlite.js +3 -3
  25. package/dist/runtime/memory/trinity-db.js +2 -2
  26. package/dist/runtime/ports/IAuditEmitter.js +1 -0
  27. package/dist/runtime/ports/IChatHistory.js +1 -0
  28. package/dist/runtime/ports/ILLMProviderFactory.js +1 -0
  29. package/dist/runtime/ports/INotifier.js +1 -0
  30. package/dist/runtime/ports/ITaskEnqueuer.js +1 -0
  31. package/dist/runtime/ports/index.js +1 -0
  32. package/dist/runtime/providers/factory.js +8 -52
  33. package/dist/runtime/providers/strategies.js +66 -0
  34. package/dist/runtime/setup/repository.js +2 -2
  35. package/dist/runtime/subagents/apoc.js +2 -2
  36. package/dist/runtime/subagents/link/link.js +2 -2
  37. package/dist/runtime/subagents/link/repository.js +2 -2
  38. package/dist/runtime/subagents/link/worker.js +3 -3
  39. package/dist/runtime/subagents/neo.js +2 -2
  40. package/dist/runtime/subagents/trinity/trinity.js +2 -2
  41. package/dist/runtime/tasks/repository.js +2 -2
  42. package/dist/runtime/telephonist.js +160 -0
  43. package/dist/runtime/tools/delegation-utils.js +5 -7
  44. package/dist/runtime/tools/morpheus-tools.js +6 -7
  45. package/dist/runtime/tools/smith-tool.js +5 -7
  46. package/dist/runtime/webhooks/repository.js +2 -2
  47. package/dist/types/config.js +6 -0
  48. package/dist/ui/assets/AuditDashboard-Cu33zb_7.js +1 -0
  49. package/dist/ui/assets/{Chat-UVoDlqqM.js → Chat-mt1j5V55.js} +1 -1
  50. package/dist/ui/assets/{Chronos-Dfs_pOsc.js → Chronos-Bq_h41cw.js} +1 -1
  51. package/dist/ui/assets/{ConfirmationModal-BBIjVef7.js → ConfirmationModal-CxLP8iC6.js} +1 -1
  52. package/dist/ui/assets/{Dashboard-BdSQDB14.js → Dashboard-D0LAlHtG.js} +1 -1
  53. package/dist/ui/assets/{DeleteConfirmationModal-Du85q5u2.js → DeleteConfirmationModal-kZ_c3sFk.js} +1 -1
  54. package/dist/ui/assets/{Documents-DguILrI8.js → Documents-nlQNoUcq.js} +1 -1
  55. package/dist/ui/assets/{Logs-BDup2FET.js → Logs-C1tlg574.js} +1 -1
  56. package/dist/ui/assets/{MCPManager-WBdh1rum.js → MCPManager-Do7isizG.js} +1 -1
  57. package/dist/ui/assets/ModelPricing-BeJ7oXBA.js +1 -0
  58. package/dist/ui/assets/{Notifications-BslO2Ect.js → Notifications-Cg5CMlY0.js} +1 -1
  59. package/dist/ui/assets/{SatiMemories-DzaLaZ6M.js → SatiMemories-D9l6s8Pc.js} +1 -1
  60. package/dist/ui/assets/SessionAudit-Da1ySlYg.js +9 -0
  61. package/dist/ui/assets/Settings-DpXwpEhO.js +49 -0
  62. package/dist/ui/assets/{Skills-BnDg1HCb.js → Skills-DaqCY8QH.js} +1 -1
  63. package/dist/ui/assets/Smiths-DA-x4KFT.js +1 -0
  64. package/dist/ui/assets/Switch-CJTE4ZQm.js +1 -0
  65. package/dist/ui/assets/{Tasks-BuoNCvI-.js → Tasks-DU49M9U-.js} +1 -1
  66. package/dist/ui/assets/{TrinityDatabases-DYHJunk7.js → TrinityDatabases-CoKzKTL-.js} +1 -1
  67. package/dist/ui/assets/{UsageStats-BpGXaHgW.js → UsageStats-cds352Pj.js} +1 -1
  68. package/dist/ui/assets/{WebhookManager-D2muhYy9.js → WebhookManager-DdAdHQUk.js} +1 -1
  69. package/dist/ui/assets/{agents-CgqJea9n.js → agents-B1z_dlQC.js} +1 -1
  70. package/dist/ui/assets/{audit-Dc3YW0-4.js → audit-BAhaGrKY.js} +1 -1
  71. package/dist/ui/assets/{chronos-CZvGhZQB.js → chronos-DGD_Md9M.js} +1 -1
  72. package/dist/ui/assets/config-BwTXe5M2.js +1 -0
  73. package/dist/ui/assets/{index-Bta9YXEm.js → index-BcX5O7kY.js} +2 -2
  74. package/dist/ui/assets/{mcp-vIffcwd6.js → mcp-BlkruPaA.js} +1 -1
  75. package/dist/ui/assets/{skills-wANsorUj.js → skills-CtCb-52u.js} +1 -1
  76. package/dist/ui/assets/{stats-xnlA4NwX.js → stats-BiPI2kaw.js} +1 -1
  77. package/dist/ui/assets/useCurrency-BCdG-pHx.js +1 -0
  78. package/dist/ui/index.html +1 -1
  79. package/dist/ui/sw.js +1 -1
  80. package/package.json +1 -1
  81. package/dist/ui/assets/AuditDashboard-BVyKnpVm.js +0 -1
  82. package/dist/ui/assets/ModelPricing-BQPw0r6z.js +0 -1
  83. package/dist/ui/assets/SessionAudit-CBDThjBi.js +0 -9
  84. package/dist/ui/assets/Settings-JPTCA7C7.js +0 -49
  85. package/dist/ui/assets/Smiths-DR6g_o3D.js +0 -1
  86. package/dist/ui/assets/config-pKL8Y4V9.js +0 -1
package/README.md CHANGED
@@ -273,7 +273,7 @@ Task results are delivered proactively with metadata (task id, agent, status) an
273
273
  - `/session switch <id>` — Switch to existing session
274
274
  - `/session rename <name>` — Rename current session
275
275
 
276
- **Voice messages:** Telegram voice messages are automatically transcribed (Gemini / Whisper / OpenRouter) and processed as text through the Oracle.
276
+ **Voice messages:** Telegram voice messages are automatically transcribed (Gemini / Whisper / OpenRouter) and processed as text through the Oracle. If **TTS** is enabled (`audio.tts.enabled: true`), Oracle's response is synthesized back to audio and sent as a voice message. Falls back to text if synthesis fails.
277
277
 
278
278
  ## Discord Experience
279
279
 
@@ -302,7 +302,7 @@ Discord bot responds to **DMs only** from authorized user IDs (`allowedUsers`).
302
302
  | `/chronos_enable id:` | Enable a job |
303
303
  | `/chronos_delete id:` | Delete a job |
304
304
 
305
- **Voice messages:** Discord voice messages and audio file attachments are transcribed and processed identically to Telegram.
305
+ **Voice messages:** Discord voice messages and audio file attachments are transcribed and processed identically to Telegram. TTS audio responses are also supported when enabled.
306
306
 
307
307
  **Setup:**
308
308
  1. Create an application at [discord.com/developers](https://discord.com/developers/applications).
@@ -330,14 +330,15 @@ The dashboard includes:
330
330
  - Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
331
331
  - MCP manager (add/edit/delete/toggle/reload)
332
332
  - Sati memories (search, bulk delete, pagination)
333
- - Usage stats and model pricing
333
+ - Usage stats and model pricing (with optional currency conversion)
334
334
  - Trinity databases (register/test/refresh schema)
335
335
  - Chronos scheduler (create/edit/delete jobs, execution history)
336
336
  - Smiths management (add/edit/delete, real-time status, ping)
337
- - Audit dashboard (session audit, tool call tracking, cost breakdowns)
337
+ - Audit dashboard (session audit, tool call tracking, cost breakdowns with currency conversion)
338
338
  - Webhooks and notification inbox
339
339
  - Logs viewer
340
340
  - Danger Zone (Settings → reset sessions, tasks, jobs, audit, or factory reset)
341
+ - Display currency setting (Settings → Interface): converts USD costs to BRL, EUR, CAD, JPY, GBP, AUD, CHF, ARS, or any custom currency
341
342
 
342
343
  Chat-specific rendering:
343
344
  - AI messages rendered as markdown
@@ -422,6 +423,17 @@ audio:
422
423
  provider: google
423
424
  model: gemini-2.5-flash-lite
424
425
  maxDurationSeconds: 300
426
+ tts:
427
+ enabled: false # set to true to respond with audio
428
+ provider: google # openai | google
429
+ model: gemini-2.5-flash-preview-tts
430
+ voice: Kore # voice name (provider-specific)
431
+ style_prompt: "" # optional tone/style prefix (Gemini only)
432
+
433
+ currency: # display currency for cost dashboards
434
+ code: USD # ISO 4217 code (e.g. BRL, EUR)
435
+ symbol: $ # symbol shown in UI
436
+ rate: 1.0 # conversion rate from USD
425
437
 
426
438
  logging:
427
439
  enabled: true
@@ -490,6 +502,15 @@ Generic Morpheus overrides (selected):
490
502
  | `MORPHEUS_AUDIO_ENABLED` | `audio.enabled` |
491
503
  | `MORPHEUS_AUDIO_API_KEY` | `audio.apiKey` |
492
504
  | `MORPHEUS_AUDIO_MAX_DURATION` | `audio.maxDurationSeconds` |
505
+ | `MORPHEUS_AUDIO_TTS_ENABLED` | `audio.tts.enabled` |
506
+ | `MORPHEUS_AUDIO_TTS_PROVIDER` | `audio.tts.provider` |
507
+ | `MORPHEUS_AUDIO_TTS_MODEL` | `audio.tts.model` |
508
+ | `MORPHEUS_AUDIO_TTS_VOICE` | `audio.tts.voice` |
509
+ | `MORPHEUS_AUDIO_TTS_API_KEY` | `audio.tts.apiKey` |
510
+ | `MORPHEUS_AUDIO_TTS_STYLE_PROMPT` | `audio.tts.style_prompt` |
511
+ | `MORPHEUS_CURRENCY_CODE` | `currency.code` |
512
+ | `MORPHEUS_CURRENCY_SYMBOL` | `currency.symbol` |
513
+ | `MORPHEUS_CURRENCY_RATE` | `currency.rate` |
493
514
  | `MORPHEUS_TELEGRAM_ENABLED` | `channels.telegram.enabled` |
494
515
  | `MORPHEUS_TELEGRAM_TOKEN` | `channels.telegram.token` |
495
516
  | `MORPHEUS_TELEGRAM_ALLOWED_USERS` | `channels.telegram.allowedUsers` |
@@ -780,24 +801,34 @@ src/
780
801
  discord.ts # Discord adapter (slash commands, voice, DM-only)
781
802
  registry.ts # ChannelRegistry — central adapter router
782
803
  cli/ # start/stop/restart/status/doctor
783
- config/ # config loading, precedence, schemas
804
+ config/
805
+ paths.ts # Centralized PATHS constants (all DB/dir paths)
806
+ schemas.ts # Zod validation schemas
807
+ manager.ts # Config loading + env precedence
784
808
  devkit/ # Apoc tool factories
785
809
  http/ # API, auth, webhooks, server
786
810
  runtime/
787
- apoc.ts
788
- neo.ts
789
- oracle.ts
790
- trinity.ts
791
- trinity-connector.ts # PostgreSQL/MySQL/SQLite/MongoDB drivers
792
- trinity-crypto.ts # AES-256-GCM encryption for DB passwords
811
+ container.ts # ServiceContainer — DI composition root
812
+ oracle.ts # Orchestration brain
813
+ ports/ # Port interfaces (INotifier, ITaskEnqueuer, IChatHistory, ILLMProviderFactory, IAuditEmitter)
814
+ adapters/ # Concrete adapter implementations for ports
815
+ providers/
816
+ factory.ts # ProviderFactory strategy-based LLM creation
817
+ strategies.ts # IProviderStrategy + per-provider implementations
818
+ subagents/
819
+ registry.ts # SubagentRegistry — single source of truth
820
+ apoc.ts # DevKit execution subagent
821
+ neo.ts # MCP tool orchestration subagent
822
+ trinity/ # Database specialist subagent
823
+ link/ # Document RAG subagent
793
824
  chronos/
794
- worker.ts # polling timer and job execution
795
- repository.ts # SQLite-backed job and execution store
796
- parser.ts # natural-language schedule parser
825
+ worker.ts # polling timer and job execution
826
+ repository.ts # SQLite-backed job and execution store
827
+ parser.ts # natural-language schedule parser
797
828
  smiths/
798
- registry.ts # SmithRegistry — manages all connections
799
- connection.ts # WebSocket client per Smith instance
800
- delegator.ts # LLM agent with proxy tools for remote execution
829
+ registry.ts # SmithRegistry — manages all connections
830
+ connection.ts # WebSocket client per Smith instance
831
+ delegator.ts # LLM agent with proxy tools for remote execution
801
832
  memory/
802
833
  tasks/
803
834
  tools/
@@ -1,4 +1,4 @@
1
- import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, } from 'discord.js';
1
+ import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, } from 'discord.js';
2
2
  import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
@@ -6,7 +6,8 @@ import os from 'os';
6
6
  import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
7
7
  import { DisplayManager } from '../runtime/display.js';
8
8
  import { ConfigManager } from '../config/manager.js';
9
- import { createTelephonist } from '../runtime/telephonist.js';
9
+ import { createTelephonist, createTtsTelephonist } from '../runtime/telephonist.js';
10
+ import { AuditRepository } from '../runtime/audit/repository.js';
10
11
  import { getUsableApiKey } from '../runtime/trinity-crypto.js';
11
12
  // ─── Slash Command Definitions ────────────────────────────────────────────────
12
13
  const SLASH_COMMANDS = [
@@ -117,6 +118,9 @@ export class DiscordAdapter {
117
118
  telephonist = null;
118
119
  telephonistProvider = null;
119
120
  telephonistModel = null;
121
+ ttsTelephonist = null;
122
+ ttsProvider = null;
123
+ ttsModel = null;
120
124
  RATE_LIMIT_MS = 3000;
121
125
  constructor(oracle) {
122
126
  this.oracle = oracle;
@@ -281,6 +285,13 @@ export class DiscordAdapter {
281
285
  this.telephonistProvider = config.audio.provider;
282
286
  this.telephonistModel = config.audio.model;
283
287
  }
288
+ // Lazy-create TTS telephonist
289
+ const ttsConfig = config.audio.tts;
290
+ if (ttsConfig?.enabled && (!this.ttsTelephonist || this.ttsProvider !== ttsConfig.provider || this.ttsModel !== ttsConfig.model)) {
291
+ this.ttsTelephonist = createTtsTelephonist(ttsConfig);
292
+ this.ttsProvider = ttsConfig.provider;
293
+ this.ttsModel = ttsConfig.model;
294
+ }
284
295
  // Voice messages expose duration (in seconds); regular attachments don't
285
296
  const duration = attachment.duration;
286
297
  if (duration && duration > config.audio.maxDurationSeconds) {
@@ -310,11 +321,87 @@ export class DiscordAdapter {
310
321
  origin_user_id: userId,
311
322
  });
312
323
  if (response) {
313
- const chunks = this.chunkText(response);
314
- for (const chunk of chunks) {
315
- await channel.send(chunk);
324
+ if (ttsConfig?.enabled && this.ttsTelephonist?.synthesize) {
325
+ // TTS path: synthesize and send as voice attachment
326
+ let ttsFilePath = null;
327
+ const ttsStart = Date.now();
328
+ try {
329
+ const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
330
+ getUsableApiKey(config.audio.apiKey) ||
331
+ (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
332
+ ? getUsableApiKey(config.llm.api_key) : undefined);
333
+ const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
334
+ ttsFilePath = ttsResult.filePath;
335
+ const ttsDurationMs = Date.now() - ttsStart;
336
+ const attachment = new AttachmentBuilder(ttsFilePath, { name: 'response.ogg' });
337
+ await channel.send({ files: [attachment] });
338
+ this.display.log(`Responded to ${message.author.tag} (TTS audio)`, { source: 'Discord' });
339
+ // Audit TTS success
340
+ try {
341
+ const auditSessionId = await this.getSessionForUser(userId);
342
+ AuditRepository.getInstance().insert({
343
+ session_id: auditSessionId,
344
+ event_type: 'telephonist',
345
+ agent: 'telephonist',
346
+ provider: ttsConfig.provider,
347
+ model: ttsConfig.model,
348
+ input_tokens: ttsResult.usage.input_tokens || null,
349
+ output_tokens: ttsResult.usage.output_tokens || null,
350
+ duration_ms: ttsDurationMs,
351
+ status: 'success',
352
+ metadata: {
353
+ operation: 'tts',
354
+ characters: response.length,
355
+ voice: ttsConfig.voice,
356
+ user: message.author.tag,
357
+ },
358
+ });
359
+ }
360
+ catch {
361
+ // Audit failure never breaks the main flow
362
+ }
363
+ }
364
+ catch (ttsError) {
365
+ const ttsDetail = ttsError?.message || String(ttsError);
366
+ this.display.log(`TTS synthesis failed for ${message.author.tag}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
367
+ // Audit TTS failure
368
+ try {
369
+ const auditSessionId = await this.getSessionForUser(userId);
370
+ AuditRepository.getInstance().insert({
371
+ session_id: auditSessionId,
372
+ event_type: 'telephonist',
373
+ agent: 'telephonist',
374
+ provider: ttsConfig.provider,
375
+ model: ttsConfig.model,
376
+ duration_ms: Date.now() - ttsStart,
377
+ status: 'error',
378
+ metadata: { operation: 'tts', error: ttsDetail, user: message.author.tag },
379
+ });
380
+ }
381
+ catch {
382
+ // Audit failure never breaks the main flow
383
+ }
384
+ // Fallback to text
385
+ const chunks = this.chunkText(response);
386
+ for (const chunk of chunks) {
387
+ await channel.send(chunk);
388
+ }
389
+ }
390
+ finally {
391
+ // Cleanup TTS temp file
392
+ if (ttsFilePath) {
393
+ await fs.unlink(ttsFilePath).catch(() => { });
394
+ }
395
+ }
396
+ }
397
+ else {
398
+ // Text path (TTS disabled)
399
+ const chunks = this.chunkText(response);
400
+ for (const chunk of chunks) {
401
+ await channel.send(chunk);
402
+ }
403
+ this.display.log(`Responded to ${message.author.tag} (via audio)`, { source: 'Discord' });
316
404
  }
317
- this.display.log(`Responded to ${message.author.tag} (via audio)`, { source: 'Discord' });
318
405
  }
319
406
  processingMsg?.delete().catch(() => { });
320
407
  }
@@ -7,7 +7,7 @@ import os from 'os';
7
7
  import { spawn } from 'child_process';
8
8
  import { ConfigManager } from '../config/manager.js';
9
9
  import { DisplayManager } from '../runtime/display.js';
10
- import { createTelephonist } from '../runtime/telephonist.js';
10
+ import { createTelephonist, createTtsTelephonist } from '../runtime/telephonist.js';
11
11
  import { getUsableApiKey } from '../runtime/trinity-crypto.js';
12
12
  import { readPid, isProcessRunning, checkStalePid } from '../runtime/lifecycle.js';
13
13
  import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
@@ -173,6 +173,9 @@ export class TelegramAdapter {
173
173
  telephonist = null;
174
174
  telephonistProvider = null;
175
175
  telephonistModel = null;
176
+ ttsTelephonist = null;
177
+ ttsProvider = null;
178
+ ttsModel = null;
176
179
  history = new SQLiteChatMessageHistory({ sessionId: '' });
177
180
  /** Per-user session tracking — maps userId to sessionId */
178
181
  userSessions = new Map();
@@ -326,6 +329,13 @@ export class TelegramAdapter {
326
329
  this.telephonistProvider = config.audio.provider;
327
330
  this.telephonistModel = config.audio.model;
328
331
  }
332
+ // Lazy-create TTS telephonist
333
+ const ttsConfig = config.audio.tts;
334
+ if (ttsConfig?.enabled && (!this.ttsTelephonist || this.ttsProvider !== ttsConfig.provider || this.ttsModel !== ttsConfig.model)) {
335
+ this.ttsTelephonist = createTtsTelephonist(ttsConfig);
336
+ this.ttsProvider = ttsConfig.provider;
337
+ this.ttsModel = ttsConfig.model;
338
+ }
329
339
  const duration = ctx.message.voice.duration;
330
340
  if (duration > config.audio.maxDurationSeconds) {
331
341
  await ctx.reply(`Voice message too long. Max duration is ${config.audio.maxDurationSeconds}s.`);
@@ -392,18 +402,108 @@ export class TelegramAdapter {
392
402
  // }
393
403
  // }
394
404
  if (response) {
395
- const rich = await toTelegramRichText(response);
396
- for (const chunk of rich.chunks) {
405
+ const ttsConfig = config.audio.tts;
406
+ if (ttsConfig?.enabled && this.ttsTelephonist?.synthesize) {
407
+ // TTS path: synthesize and send as voice message
408
+ let ttsFilePath = null;
409
+ const ttsStart = Date.now();
397
410
  try {
398
- await ctx.reply(chunk, { parse_mode: rich.parse_mode });
411
+ const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
412
+ getUsableApiKey(config.audio.apiKey) ||
413
+ (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
414
+ ? getUsableApiKey(config.llm.api_key) : undefined);
415
+ const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
416
+ ttsFilePath = ttsResult.filePath;
417
+ const ttsDurationMs = Date.now() - ttsStart;
418
+ // OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
419
+ const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
420
+ if (isOgg) {
421
+ await ctx.replyWithVoice({ source: ttsFilePath });
422
+ }
423
+ else {
424
+ await ctx.replyWithAudio({ source: ttsFilePath });
425
+ }
426
+ this.display.log(`Responded to @${user} (TTS audio)`, { source: 'Telegram' });
427
+ // Audit TTS success
428
+ try {
429
+ const auditSessionId = await this.getSessionForUser(userId);
430
+ AuditRepository.getInstance().insert({
431
+ session_id: auditSessionId,
432
+ event_type: 'telephonist',
433
+ agent: 'telephonist',
434
+ provider: ttsConfig.provider,
435
+ model: ttsConfig.model,
436
+ input_tokens: ttsResult.usage.input_tokens || null,
437
+ output_tokens: ttsResult.usage.output_tokens || null,
438
+ duration_ms: ttsDurationMs,
439
+ status: 'success',
440
+ metadata: {
441
+ operation: 'tts',
442
+ characters: response.length,
443
+ voice: ttsConfig.voice,
444
+ user,
445
+ },
446
+ });
447
+ }
448
+ catch {
449
+ // Audit failure never breaks the main flow
450
+ }
399
451
  }
400
- catch {
401
- const plain = stripHtmlTags(chunk).slice(0, 4096);
402
- if (plain)
403
- await ctx.reply(plain);
452
+ catch (ttsError) {
453
+ const ttsDetail = ttsError?.message || String(ttsError);
454
+ this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
455
+ // Audit TTS failure
456
+ try {
457
+ const auditSessionId = await this.getSessionForUser(userId);
458
+ AuditRepository.getInstance().insert({
459
+ session_id: auditSessionId,
460
+ event_type: 'telephonist',
461
+ agent: 'telephonist',
462
+ provider: ttsConfig.provider,
463
+ model: ttsConfig.model,
464
+ duration_ms: Date.now() - ttsStart,
465
+ status: 'error',
466
+ metadata: { operation: 'tts', error: ttsDetail, user },
467
+ });
468
+ }
469
+ catch {
470
+ // Audit failure never breaks the main flow
471
+ }
472
+ // Fallback to text
473
+ const rich = await toTelegramRichText(response);
474
+ for (const chunk of rich.chunks) {
475
+ try {
476
+ await ctx.reply(chunk, { parse_mode: rich.parse_mode });
477
+ }
478
+ catch {
479
+ const plain = stripHtmlTags(chunk).slice(0, 4096);
480
+ if (plain)
481
+ await ctx.reply(plain);
482
+ }
483
+ }
484
+ }
485
+ finally {
486
+ // Cleanup TTS temp file
487
+ if (ttsFilePath) {
488
+ await fs.promises.unlink(ttsFilePath).catch(() => { });
489
+ }
490
+ }
491
+ }
492
+ else {
493
+ // Text path (TTS disabled)
494
+ const rich = await toTelegramRichText(response);
495
+ for (const chunk of rich.chunks) {
496
+ try {
497
+ await ctx.reply(chunk, { parse_mode: rich.parse_mode });
498
+ }
499
+ catch {
500
+ const plain = stripHtmlTags(chunk).slice(0, 4096);
501
+ if (plain)
502
+ await ctx.reply(plain);
503
+ }
404
504
  }
505
+ this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
405
506
  }
406
- this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
407
507
  }
408
508
  }
409
509
  catch (error) {
@@ -28,6 +28,12 @@ import { MCPToolCache } from '../../runtime/tools/cache.js';
28
28
  import { SmithRegistry } from '../../runtime/smiths/registry.js';
29
29
  import { Link } from '../../runtime/subagents/link/link.js';
30
30
  import { LinkWorker } from '../../runtime/subagents/link/worker.js';
31
+ import { ServiceContainer, SERVICE_KEYS } from '../../runtime/container.js';
32
+ import { ChannelNotifierAdapter } from '../../runtime/adapters/ChannelNotifierAdapter.js';
33
+ import { SQLiteTaskEnqueuerAdapter } from '../../runtime/adapters/SQLiteTaskEnqueuerAdapter.js';
34
+ import { SQLiteChatHistoryAdapter } from '../../runtime/adapters/SQLiteChatHistoryAdapter.js';
35
+ import { LangChainProviderAdapter } from '../../runtime/adapters/LangChainProviderAdapter.js';
36
+ import { AuditRepositoryAdapter } from '../../runtime/adapters/AuditRepositoryAdapter.js';
31
37
  // Load .env file explicitly in start command
32
38
  const envPath = path.join(process.cwd(), '.env');
33
39
  if (fs.existsSync(envPath)) {
@@ -214,6 +220,15 @@ export const startCommand = new Command('start')
214
220
  await clearPid();
215
221
  process.exit(1);
216
222
  }
223
+ // ── Composition Root ─────────────────────────────────────────────────────
224
+ // Register port adapters in the ServiceContainer so consumers can
225
+ // depend on interfaces instead of concrete implementations.
226
+ ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
227
+ ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
228
+ ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
229
+ ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
230
+ ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
231
+ // ─────────────────────────────────────────────────────────────────────────
217
232
  const adapters = [];
218
233
  let httpServer;
219
234
  const taskWorker = new TaskWorker();
@@ -284,13 +284,27 @@ export class ConfigManager {
284
284
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
285
285
  // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
286
286
  const audioProviderForKey = (audioProvider === 'google' ? 'gemini' : audioProvider);
287
+ // TTS config
288
+ const ttsDefaults = DEFAULT_CONFIG.audio.tts;
289
+ const ttsCfg = config.audio.tts;
290
+ const ttsProvider = resolveString('MORPHEUS_AUDIO_TTS_PROVIDER', ttsCfg?.provider, ttsDefaults.provider);
291
+ const ttsProviderForKey = (ttsProvider === 'google' ? 'gemini' : ttsProvider);
292
+ const ttsConfig = {
293
+ enabled: resolveBoolean('MORPHEUS_AUDIO_TTS_ENABLED', ttsCfg?.enabled, ttsDefaults.enabled),
294
+ provider: ttsProvider,
295
+ model: resolveString('MORPHEUS_AUDIO_TTS_MODEL', ttsCfg?.model, ttsDefaults.model),
296
+ voice: resolveString('MORPHEUS_AUDIO_TTS_VOICE', ttsCfg?.voice, ttsDefaults.voice),
297
+ apiKey: resolveApiKey(ttsProviderForKey, 'MORPHEUS_AUDIO_TTS_API_KEY', ttsCfg?.apiKey),
298
+ style_prompt: resolveString('MORPHEUS_AUDIO_TTS_STYLE_PROMPT', ttsCfg?.style_prompt, ''),
299
+ };
287
300
  const audioConfig = {
288
301
  provider: audioProvider,
289
302
  model: resolveString('MORPHEUS_AUDIO_MODEL', config.audio.model, DEFAULT_CONFIG.audio.model),
290
303
  enabled: resolveBoolean('MORPHEUS_AUDIO_ENABLED', config.audio.enabled, DEFAULT_CONFIG.audio.enabled),
291
304
  apiKey: resolveApiKey(audioProviderForKey, 'MORPHEUS_AUDIO_API_KEY', config.audio.apiKey),
292
305
  maxDurationSeconds: resolveNumeric('MORPHEUS_AUDIO_MAX_DURATION', config.audio.maxDurationSeconds, DEFAULT_CONFIG.audio.maxDurationSeconds),
293
- supportedMimeTypes: config.audio.supportedMimeTypes
306
+ supportedMimeTypes: config.audio.supportedMimeTypes,
307
+ tts: ttsConfig,
294
308
  };
295
309
  // Apply precedence to channel configs
296
310
  const channelsConfig = {
@@ -376,6 +390,11 @@ export class ConfigManager {
376
390
  enabled: resolveBoolean('MORPHEUS_SETUP_ENABLED', config.setup?.enabled, true),
377
391
  fields: config.setup?.fields ?? ['name', 'timezone', 'preferred_language'],
378
392
  },
393
+ currency: {
394
+ code: resolveString('MORPHEUS_CURRENCY_CODE', config.currency?.code, 'USD'),
395
+ symbol: resolveString('MORPHEUS_CURRENCY_SYMBOL', config.currency?.symbol, '$'),
396
+ rate: resolveNumeric('MORPHEUS_CURRENCY_RATE', config.currency?.rate, 1.0),
397
+ },
379
398
  };
380
399
  }
381
400
  get() {
@@ -16,4 +16,8 @@ export const PATHS = {
16
16
  mcps: path.join(MORPHEUS_ROOT, 'mcps.json'),
17
17
  skills: path.join(MORPHEUS_ROOT, 'skills'),
18
18
  docs: path.join(MORPHEUS_ROOT, 'docs'),
19
+ shortMemoryDb: path.join(MORPHEUS_ROOT, 'memory', 'short-memory.db'),
20
+ trinityDb: path.join(MORPHEUS_ROOT, 'memory', 'trinity.db'),
21
+ satiDb: path.join(MORPHEUS_ROOT, 'memory', 'sati-memory.db'),
22
+ linkDb: path.join(MORPHEUS_ROOT, 'memory', 'link.db'),
19
23
  };
@@ -1,5 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { DEFAULT_CONFIG } from '../types/config.js';
3
+ export const TtsConfigSchema = z.object({
4
+ enabled: z.boolean().default(false),
5
+ provider: z.enum(['openai', 'google']).default('google'),
6
+ model: z.string().min(1).default('gemini-2.5-flash-preview-tts'),
7
+ voice: z.string().min(1).default('Kore'),
8
+ apiKey: z.string().optional(),
9
+ style_prompt: z.string().optional(),
10
+ });
3
11
  export const AudioConfigSchema = z.object({
4
12
  provider: z.enum(['google', 'openai', 'openrouter', 'ollama']).default(DEFAULT_CONFIG.audio.provider),
5
13
  model: z.string().min(1).default(DEFAULT_CONFIG.audio.model),
@@ -8,6 +16,7 @@ export const AudioConfigSchema = z.object({
8
16
  base_url: z.string().optional(),
9
17
  maxDurationSeconds: z.number().default(DEFAULT_CONFIG.audio.maxDurationSeconds),
10
18
  supportedMimeTypes: z.array(z.string()).default(DEFAULT_CONFIG.audio.supportedMimeTypes),
19
+ tts: TtsConfigSchema.optional(),
11
20
  });
12
21
  export const LLMConfigSchema = z.object({
13
22
  provider: z.enum(['openai', 'anthropic', 'openrouter', 'ollama', 'gemini']).default(DEFAULT_CONFIG.llm.provider),
@@ -84,6 +93,11 @@ export const SmithsConfigSchema = z.object({
84
93
  task_timeout_ms: z.number().int().min(1000).default(60000),
85
94
  entries: z.array(SmithEntrySchema).default([]),
86
95
  });
96
+ export const CurrencyConfigSchema = z.object({
97
+ code: z.string().min(1).default('USD'),
98
+ symbol: z.string().min(1).default('$'),
99
+ rate: z.number().positive().default(1.0),
100
+ });
87
101
  // Zod Schema matching MorpheusConfig interface
88
102
  export const ConfigSchema = z.object({
89
103
  agent: z.object({
@@ -110,6 +124,7 @@ export const ConfigSchema = z.object({
110
124
  devkit: DevKitConfigSchema.optional(),
111
125
  smiths: SmithsConfigSchema.optional(),
112
126
  setup: SetupConfigSchema.optional(),
127
+ currency: CurrencyConfigSchema.optional(),
113
128
  verbose_mode: z.boolean().default(true),
114
129
  channels: z.object({
115
130
  telegram: z.object({
package/dist/http/api.js CHANGED
@@ -238,8 +238,9 @@ export function createApiRouter(oracle, chronosWorker) {
238
238
  const offset = parseInt(req.query.offset) || 0;
239
239
  const audit = AuditRepository.getInstance();
240
240
  const events = audit.getBySession(id, { limit, offset });
241
+ const total_count = audit.countBySession(id);
241
242
  const summary = audit.getSessionSummary(id);
242
- res.json({ events, summary });
243
+ res.json({ events, summary, total_count });
243
244
  }
244
245
  catch (err) {
245
246
  res.status(500).json({ error: err.message });
@@ -1,8 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import Database from 'better-sqlite3';
3
- import path from 'path';
4
- import { homedir } from 'os';
5
3
  import fs from 'fs-extra';
4
+ import { PATHS } from '../../config/paths.js';
6
5
  import { z } from 'zod';
7
6
  import { SatiRepository } from '../../runtime/memory/sati/repository.js';
8
7
  import { DisplayManager } from '../../runtime/display.js';
@@ -51,9 +50,9 @@ export function createDangerRouter() {
51
50
  }
52
51
  const { categories } = parsed.data;
53
52
  try {
54
- const memoryDir = path.join(homedir(), '.morpheus', 'memory');
55
- const shortMemoryPath = path.join(memoryDir, 'short-memory.db');
56
- const satiMemoryPath = path.join(memoryDir, 'sati-memory.db');
53
+ const memoryDir = PATHS.memory;
54
+ const shortMemoryPath = PATHS.shortMemoryDb;
55
+ const satiMemoryPath = PATHS.satiDb;
57
56
  const counts = {};
58
57
  // ─── 1. Purge short-memory.db tables based on selected categories ───
59
58
  const needsShortDb = categories.some((c) => ['sessions', 'tasks', 'audit', 'chronos', 'webhooks'].includes(c));
@@ -2,11 +2,11 @@ import { Router } from 'express';
2
2
  import multer from 'multer';
3
3
  import path from 'path';
4
4
  import fs from 'fs-extra';
5
- import { homedir } from 'os';
6
5
  import { LinkRepository } from '../../runtime/subagents/link/repository.js';
7
6
  import { LinkWorker } from '../../runtime/subagents/link/worker.js';
8
7
  import { ConfigManager } from '../../config/manager.js';
9
- const DOCS_PATH = path.join(homedir(), '.morpheus', 'docs');
8
+ import { PATHS } from '../../config/paths.js';
9
+ const DOCS_PATH = PATHS.docs;
10
10
  // Configure multer for file uploads
11
11
  const storage = multer.diskStorage({
12
12
  destination: async (req, file, cb) => {