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.
- package/README.md +48 -17
- package/dist/channels/discord.js +93 -6
- package/dist/channels/telegram.js +109 -9
- package/dist/cli/commands/start.js +15 -0
- package/dist/config/manager.js +20 -1
- package/dist/config/paths.js +4 -0
- package/dist/config/schemas.js +15 -0
- package/dist/http/api.js +2 -1
- package/dist/http/routers/danger.js +4 -5
- package/dist/http/routers/link.js +2 -2
- package/dist/runtime/__tests__/telephonist-tts.test.js +84 -0
- package/dist/runtime/adapters/AuditRepositoryAdapter.js +6 -0
- package/dist/runtime/adapters/ChannelNotifierAdapter.js +9 -0
- package/dist/runtime/adapters/LangChainProviderAdapter.js +9 -0
- package/dist/runtime/adapters/SQLiteChatHistoryAdapter.js +15 -0
- package/dist/runtime/adapters/SQLiteTaskEnqueuerAdapter.js +6 -0
- package/dist/runtime/adapters/index.js +5 -0
- package/dist/runtime/audit/repository.js +6 -2
- package/dist/runtime/chronos/repository.js +2 -2
- package/dist/runtime/container.js +50 -0
- package/dist/runtime/hot-reload.js +6 -9
- package/dist/runtime/memory/backfill-embeddings.js +2 -3
- package/dist/runtime/memory/sati/repository.js +3 -3
- package/dist/runtime/memory/sqlite.js +3 -3
- package/dist/runtime/memory/trinity-db.js +2 -2
- package/dist/runtime/ports/IAuditEmitter.js +1 -0
- package/dist/runtime/ports/IChatHistory.js +1 -0
- package/dist/runtime/ports/ILLMProviderFactory.js +1 -0
- package/dist/runtime/ports/INotifier.js +1 -0
- package/dist/runtime/ports/ITaskEnqueuer.js +1 -0
- package/dist/runtime/ports/index.js +1 -0
- package/dist/runtime/providers/factory.js +8 -52
- package/dist/runtime/providers/strategies.js +66 -0
- package/dist/runtime/setup/repository.js +2 -2
- package/dist/runtime/subagents/apoc.js +2 -2
- package/dist/runtime/subagents/link/link.js +2 -2
- package/dist/runtime/subagents/link/repository.js +2 -2
- package/dist/runtime/subagents/link/worker.js +3 -3
- package/dist/runtime/subagents/neo.js +2 -2
- package/dist/runtime/subagents/trinity/trinity.js +2 -2
- package/dist/runtime/tasks/repository.js +2 -2
- package/dist/runtime/telephonist.js +160 -0
- package/dist/runtime/tools/delegation-utils.js +5 -7
- package/dist/runtime/tools/morpheus-tools.js +6 -7
- package/dist/runtime/tools/smith-tool.js +5 -7
- package/dist/runtime/webhooks/repository.js +2 -2
- package/dist/types/config.js +6 -0
- package/dist/ui/assets/AuditDashboard-Cu33zb_7.js +1 -0
- package/dist/ui/assets/{Chat-UVoDlqqM.js → Chat-mt1j5V55.js} +1 -1
- package/dist/ui/assets/{Chronos-Dfs_pOsc.js → Chronos-Bq_h41cw.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-BBIjVef7.js → ConfirmationModal-CxLP8iC6.js} +1 -1
- package/dist/ui/assets/{Dashboard-BdSQDB14.js → Dashboard-D0LAlHtG.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-Du85q5u2.js → DeleteConfirmationModal-kZ_c3sFk.js} +1 -1
- package/dist/ui/assets/{Documents-DguILrI8.js → Documents-nlQNoUcq.js} +1 -1
- package/dist/ui/assets/{Logs-BDup2FET.js → Logs-C1tlg574.js} +1 -1
- package/dist/ui/assets/{MCPManager-WBdh1rum.js → MCPManager-Do7isizG.js} +1 -1
- package/dist/ui/assets/ModelPricing-BeJ7oXBA.js +1 -0
- package/dist/ui/assets/{Notifications-BslO2Ect.js → Notifications-Cg5CMlY0.js} +1 -1
- package/dist/ui/assets/{SatiMemories-DzaLaZ6M.js → SatiMemories-D9l6s8Pc.js} +1 -1
- package/dist/ui/assets/SessionAudit-Da1ySlYg.js +9 -0
- package/dist/ui/assets/Settings-DpXwpEhO.js +49 -0
- package/dist/ui/assets/{Skills-BnDg1HCb.js → Skills-DaqCY8QH.js} +1 -1
- package/dist/ui/assets/Smiths-DA-x4KFT.js +1 -0
- package/dist/ui/assets/Switch-CJTE4ZQm.js +1 -0
- package/dist/ui/assets/{Tasks-BuoNCvI-.js → Tasks-DU49M9U-.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-DYHJunk7.js → TrinityDatabases-CoKzKTL-.js} +1 -1
- package/dist/ui/assets/{UsageStats-BpGXaHgW.js → UsageStats-cds352Pj.js} +1 -1
- package/dist/ui/assets/{WebhookManager-D2muhYy9.js → WebhookManager-DdAdHQUk.js} +1 -1
- package/dist/ui/assets/{agents-CgqJea9n.js → agents-B1z_dlQC.js} +1 -1
- package/dist/ui/assets/{audit-Dc3YW0-4.js → audit-BAhaGrKY.js} +1 -1
- package/dist/ui/assets/{chronos-CZvGhZQB.js → chronos-DGD_Md9M.js} +1 -1
- package/dist/ui/assets/config-BwTXe5M2.js +1 -0
- package/dist/ui/assets/{index-Bta9YXEm.js → index-BcX5O7kY.js} +2 -2
- package/dist/ui/assets/{mcp-vIffcwd6.js → mcp-BlkruPaA.js} +1 -1
- package/dist/ui/assets/{skills-wANsorUj.js → skills-CtCb-52u.js} +1 -1
- package/dist/ui/assets/{stats-xnlA4NwX.js → stats-BiPI2kaw.js} +1 -1
- package/dist/ui/assets/useCurrency-BCdG-pHx.js +1 -0
- package/dist/ui/index.html +1 -1
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/AuditDashboard-BVyKnpVm.js +0 -1
- package/dist/ui/assets/ModelPricing-BQPw0r6z.js +0 -1
- package/dist/ui/assets/SessionAudit-CBDThjBi.js +0 -9
- package/dist/ui/assets/Settings-JPTCA7C7.js +0 -49
- package/dist/ui/assets/Smiths-DR6g_o3D.js +0 -1
- 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/
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
795
|
-
repository.ts
|
|
796
|
-
parser.ts
|
|
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
|
|
799
|
-
connection.ts
|
|
800
|
-
delegator.ts
|
|
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/
|
package/dist/channels/discord.js
CHANGED
|
@@ -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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
396
|
-
|
|
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
|
-
|
|
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
|
|
402
|
-
|
|
403
|
-
|
|
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();
|
package/dist/config/manager.js
CHANGED
|
@@ -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() {
|
package/dist/config/paths.js
CHANGED
|
@@ -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
|
};
|
package/dist/config/schemas.js
CHANGED
|
@@ -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 =
|
|
55
|
-
const shortMemoryPath =
|
|
56
|
-
const satiMemoryPath =
|
|
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
|
-
|
|
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) => {
|