morpheus-cli 0.9.24 → 0.9.31
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 +97 -2
- package/dist/channels/telegram.js +165 -120
- package/dist/cli/commands/restart.js +12 -0
- package/dist/config/manager.js +34 -2
- package/dist/config/paths.js +2 -0
- package/dist/config/schemas.js +6 -0
- package/dist/runtime/__tests__/gws-sync.test.js +69 -0
- package/dist/runtime/gws-sync.js +102 -0
- package/dist/runtime/hash-utils.js +15 -0
- package/dist/runtime/hot-reload.js +5 -1
- package/dist/runtime/oracle.js +1 -0
- package/dist/runtime/scaffold.js +4 -0
- package/dist/runtime/skills/loader.js +7 -52
- package/dist/runtime/skills/registry.js +5 -1
- package/dist/runtime/skills/schema.js +1 -1
- package/dist/runtime/skills/tool.js +8 -1
- package/dist/runtime/subagents/apoc.js +154 -1
- package/dist/runtime/subagents/devkit-instrument.js +13 -0
- package/dist/types/config.js +3 -0
- package/dist/ui/assets/{AuditDashboard-CfYKdOEt.js → AuditDashboard-tH9QZTl4.js} +1 -1
- package/dist/ui/assets/{Chat-CYev7-CJ.js → Chat-Cd0uYF8g.js} +1 -1
- package/dist/ui/assets/{Chronos-5KR8aZud.js → Chronos-CWwHYdBl.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-NFwIYI7B.js → ConfirmationModal-CxvFe-We.js} +1 -1
- package/dist/ui/assets/{Dashboard-hsjB56la.js → Dashboard-CNNMxl53.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-BfV370Vv.js → DeleteConfirmationModal-AqGQSh1X.js} +1 -1
- package/dist/ui/assets/{Documents-BNo2tMfG.js → Documents-BfRYOK88.js} +1 -1
- package/dist/ui/assets/{Logs-1hBpMPZE.js → Logs-DhFo4cio.js} +1 -1
- package/dist/ui/assets/{MCPManager-CvPRHn4C.js → MCPManager-BMhxbhni.js} +1 -1
- package/dist/ui/assets/{ModelPricing-BbwJFdz4.js → ModelPricing-Dvl0R_HR.js} +1 -1
- package/dist/ui/assets/{Notifications-C_MA51Gf.js → Notifications-CawvBid4.js} +1 -1
- package/dist/ui/assets/{SatiMemories-Cd9xn98_.js → SatiMemories-yyVrJGdc.js} +1 -1
- package/dist/ui/assets/{SessionAudit-BTABenGk.js → SessionAudit-joq0ntdJ.js} +1 -1
- package/dist/ui/assets/{Settings-DRVx4ICA.js → Settings-B6SMPn41.js} +7 -7
- package/dist/ui/assets/{Skills-DS9p1-S8.js → Skills-B5yhTHyn.js} +1 -1
- package/dist/ui/assets/{Smiths-CMCZaAF_.js → Smiths-Dug63YED.js} +1 -1
- package/dist/ui/assets/{Tasks-Cvt4sTcs.js → Tasks-D8HPLkg0.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-qhSUMeCw.js → TrinityDatabases-D0qEKmwJ.js} +1 -1
- package/dist/ui/assets/{UsageStats-Cy9HKYOp.js → UsageStats-CHWALN70.js} +1 -1
- package/dist/ui/assets/{WebhookManager-ByqkTyqs.js → WebhookManager-T2ef90p8.js} +1 -1
- package/dist/ui/assets/{agents-svEaAPka.js → agents-BVnfnJ1X.js} +1 -1
- package/dist/ui/assets/{audit-gxRPR5Jb.js → audit-BErc_ye8.js} +1 -1
- package/dist/ui/assets/{chronos-ZrBE4yA4.js → chronos-CAv__H3B.js} +1 -1
- package/dist/ui/assets/{config-B1i6Xxwk.js → config-CPFW7PTY.js} +1 -1
- package/dist/ui/assets/{index-DyKlGDg1.js → index-BvsF1a9j.js} +2 -2
- package/dist/ui/assets/{mcp-DSddQR1h.js → mcp-BaHwY4DW.js} +1 -1
- package/dist/ui/assets/{skills-DIuMjpPF.js → skills-lbjIRO8d.js} +1 -1
- package/dist/ui/assets/{stats-CxlRAO2g.js → stats-C8KAfpHO.js} +1 -1
- package/dist/ui/assets/{useCurrency-BkHiWfcT.js → useCurrency-Ch0lsvGj.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
|
|
|
14
14
|
- Chronos temporal scheduler for recurring and one-time Oracle executions.
|
|
15
15
|
- Smith remote agent system for DevKit execution on isolated machines via WebSocket.
|
|
16
16
|
- Multi-channel output via ChannelRegistry (Telegram, Discord) with per-job routing.
|
|
17
|
-
-
|
|
17
|
+
- Google Workspace automation via 102 built-in skills (Gmail, Drive, Sheets, Calendar, Docs, Tasks).
|
|
18
|
+
- Rich operational visibility in UI (chat traces, tasks, usage, logs, real-time activity visualization).
|
|
18
19
|
|
|
19
20
|
## Multi-Agent Roles
|
|
20
21
|
- `Oracle`: orchestration and routing. Decides direct answer vs async delegation.
|
|
@@ -325,6 +326,7 @@ Adding a new channel requires only implementing `IChannelAdapter` (`channel`, `s
|
|
|
325
326
|
## Web UI
|
|
326
327
|
|
|
327
328
|
The dashboard includes:
|
|
329
|
+
- **Dashboard** with real-time 3D agent visualization and Matrix-style activity feed (live agent events via SSE)
|
|
328
330
|
- Chat with session management and browser notifications
|
|
329
331
|
- Tasks page (stats, filters, details, retry, pagination)
|
|
330
332
|
- Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
|
|
@@ -335,7 +337,7 @@ The dashboard includes:
|
|
|
335
337
|
- Chronos scheduler (create/edit/delete jobs, execution history)
|
|
336
338
|
- Smiths management (add/edit/delete, real-time status, ping)
|
|
337
339
|
- Audit dashboard (session audit, tool call tracking, cost breakdowns with currency conversion)
|
|
338
|
-
- Webhooks and notification inbox
|
|
340
|
+
- Webhooks and notification inbox (with per-webhook API key toggle for public/secured webhooks)
|
|
339
341
|
- Logs viewer
|
|
340
342
|
- Danger Zone (Settings → reset sessions, tasks, jobs, audit, or factory reset)
|
|
341
343
|
- Display currency setting (Settings → Interface): converts USD costs to BRL, EUR, CAD, JPY, GBP, AUD, CHF, ARS, or any custom currency
|
|
@@ -430,6 +432,10 @@ audio:
|
|
|
430
432
|
voice: Kore # voice name (provider-specific)
|
|
431
433
|
style_prompt: "" # optional tone/style prefix (Gemini only)
|
|
432
434
|
|
|
435
|
+
gws: # Google Workspace integration
|
|
436
|
+
enabled: true # sync and use GWS skills (default: true)
|
|
437
|
+
service_account_json: "" # path to Google Service Account JSON key
|
|
438
|
+
|
|
433
439
|
currency: # display currency for cost dashboards
|
|
434
440
|
code: USD # ISO 4217 code (e.g. BRL, EUR)
|
|
435
441
|
symbol: $ # symbol shown in UI
|
|
@@ -519,6 +525,8 @@ Generic Morpheus overrides (selected):
|
|
|
519
525
|
| `MORPHEUS_DISCORD_ALLOWED_USERS` | `channels.discord.allowedUsers` |
|
|
520
526
|
| `MORPHEUS_UI_ENABLED` | `ui.enabled` |
|
|
521
527
|
| `MORPHEUS_UI_PORT` | `ui.port` |
|
|
528
|
+
| `MORPHEUS_GWS_ENABLED` | `gws.enabled` |
|
|
529
|
+
| `MORPHEUS_GWS_SERVICE_ACCOUNT_JSON` | `gws.service_account_json` |
|
|
522
530
|
| `MORPHEUS_LOGGING_ENABLED` | `logging.enabled` |
|
|
523
531
|
| `MORPHEUS_LOGGING_LEVEL` | `logging.level` |
|
|
524
532
|
| `MORPHEUS_LOGGING_RETENTION` | `logging.retention` |
|
|
@@ -619,6 +627,87 @@ Copy them to your skills directory:
|
|
|
619
627
|
cp -r examples/skills/* ~/.morpheus/skills/
|
|
620
628
|
```
|
|
621
629
|
|
|
630
|
+
## Google Workspace (GWS) Integration
|
|
631
|
+
|
|
632
|
+
Morpheus integrates with Google Workspace via the `gws` CLI tool. 102 built-in skills provide structured instructions for automating Gmail, Drive, Sheets, Calendar, Docs, Tasks, and more.
|
|
633
|
+
|
|
634
|
+
### Prerequisites
|
|
635
|
+
|
|
636
|
+
1. **Install the `gws` CLI:**
|
|
637
|
+
```bash
|
|
638
|
+
npm install -g @nicholasgriffintn/google-workspace-cli
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
2. **Create a Google Service Account:**
|
|
642
|
+
- Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
643
|
+
- Create or select a project
|
|
644
|
+
- Enable desired APIs (Gmail, Drive, Sheets, Calendar, Docs, etc.)
|
|
645
|
+
- IAM & Admin → Service Accounts → Create Service Account
|
|
646
|
+
- Grant the service account [Domain-Wide Delegation](https://developers.google.com/workspace/guides/create-credentials#service-account) if needed
|
|
647
|
+
- Create a JSON key and download it
|
|
648
|
+
|
|
649
|
+
3. **Configure Morpheus:**
|
|
650
|
+
|
|
651
|
+
Add to `~/.morpheus/zaion.yaml`:
|
|
652
|
+
```yaml
|
|
653
|
+
gws:
|
|
654
|
+
enabled: true
|
|
655
|
+
service_account_json: /path/to/your-service-account-key.json
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Or via environment variable:
|
|
659
|
+
```bash
|
|
660
|
+
MORPHEUS_GWS_SERVICE_ACCOUNT_JSON=/path/to/your-service-account-key.json
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### How It Works
|
|
664
|
+
|
|
665
|
+
GWS skills are synced automatically at startup from the built-in `gws-skills/skills/` directory to `~/.morpheus/skills/`. Skills are organized in four categories:
|
|
666
|
+
|
|
667
|
+
| Category | Count | Description |
|
|
668
|
+
|---|---|---|
|
|
669
|
+
| **Services** | 17 | Core API skills (`gws-gmail`, `gws-drive`, `gws-sheets`, `gws-calendar`, `gws-docs`, `gws-tasks`, etc.) |
|
|
670
|
+
| **Helpers** | 22 | Shortcut skills for common operations (`gws-gmail-send`, `gws-drive-upload`, `gws-sheets-append`, `gws-calendar-insert`) |
|
|
671
|
+
| **Personas** | 10 | Role-based skill bundles (`persona-exec-assistant`, `persona-project-manager`, `persona-hr-coordinator`) |
|
|
672
|
+
| **Recipes** | 42+ | Multi-step task sequences (`recipe-create-doc-from-template`, `recipe-send-team-announcement`, `recipe-plan-weekly-schedule`) |
|
|
673
|
+
|
|
674
|
+
### Usage Examples
|
|
675
|
+
|
|
676
|
+
Just ask Morpheus naturally — Apoc auto-detects GWS tasks and loads the right skills:
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
"Send an email to alice@example.com with the weekly report"
|
|
680
|
+
"Create a Google Sheet named 'Q2 Budget' with columns for category, amount, and notes"
|
|
681
|
+
"List my unread Gmail messages"
|
|
682
|
+
"Add an event to my calendar tomorrow at 10 AM: Team standup"
|
|
683
|
+
"Upload specs/spec.md to my Google Drive"
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Oracle can also explicitly load skills via `load_skill` when needed.
|
|
687
|
+
|
|
688
|
+
### Skill Customization
|
|
689
|
+
|
|
690
|
+
GWS skills use smart sync with MD5 hashing:
|
|
691
|
+
- **New skills**: Automatically copied on startup
|
|
692
|
+
- **Unmodified skills**: Updated to latest version
|
|
693
|
+
- **User-customized skills**: Preserved (your edits are never overwritten)
|
|
694
|
+
|
|
695
|
+
To customize a skill, edit the `SKILL.md` file in `~/.morpheus/skills/<skill-name>/`. Your changes will be preserved across updates.
|
|
696
|
+
|
|
697
|
+
### Docker
|
|
698
|
+
|
|
699
|
+
Add the service account JSON as a volume mount:
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
docker run -d \
|
|
703
|
+
--name morpheus-agent \
|
|
704
|
+
-p 3333:3333 \
|
|
705
|
+
-e MORPHEUS_GWS_SERVICE_ACCOUNT_JSON=/root/.morpheus/gws/credentials.json \
|
|
706
|
+
-v /path/to/your-key.json:/root/.morpheus/gws/credentials.json:ro \
|
|
707
|
+
-v morpheus_data:/root/.morpheus \
|
|
708
|
+
morpheus
|
|
709
|
+
```
|
|
710
|
+
|
|
622
711
|
## Link — Document RAG
|
|
623
712
|
|
|
624
713
|
Link is a documentation specialist subagent that provides RAG (Retrieval-Augmented Generation) over user documents.
|
|
@@ -810,8 +899,14 @@ src/
|
|
|
810
899
|
runtime/
|
|
811
900
|
container.ts # ServiceContainer — DI composition root
|
|
812
901
|
oracle.ts # Orchestration brain
|
|
902
|
+
gws-sync.ts # Smart sync of built-in GWS skills
|
|
903
|
+
display.ts # Activity event emission for real-time visualization
|
|
813
904
|
ports/ # Port interfaces (INotifier, ITaskEnqueuer, IChatHistory, ILLMProviderFactory, IAuditEmitter)
|
|
814
905
|
adapters/ # Concrete adapter implementations for ports
|
|
906
|
+
skills/
|
|
907
|
+
registry.ts # SkillRegistry singleton — manages loaded skills
|
|
908
|
+
loader.ts # Discovers and parses SKILL.md files
|
|
909
|
+
tool.ts # load_skill tool for Oracle
|
|
815
910
|
providers/
|
|
816
911
|
factory.ts # ProviderFactory — strategy-based LLM creation
|
|
817
912
|
strategies.ts # IProviderStrategy + per-provider implementations
|
|
@@ -266,13 +266,42 @@ export class TelegramAdapter {
|
|
|
266
266
|
// Send "typing" status
|
|
267
267
|
await ctx.sendChatAction('typing');
|
|
268
268
|
const sessionId = await this.getSessionForUser(userId);
|
|
269
|
-
// Process with Agent
|
|
270
|
-
const
|
|
269
|
+
// Process with Agent - with a 30s timeout to avoid blocking the bot's update loop.
|
|
270
|
+
const CHAT_TIMEOUT_MS = 30000;
|
|
271
|
+
let timeoutTriggered = false;
|
|
272
|
+
const chatPromise = this.oracle.chat(text, undefined, false, {
|
|
271
273
|
origin_channel: 'telegram',
|
|
272
274
|
session_id: sessionId,
|
|
273
275
|
origin_message_id: String(ctx.message.message_id),
|
|
274
276
|
origin_user_id: userId,
|
|
275
277
|
});
|
|
278
|
+
const response = await Promise.race([
|
|
279
|
+
chatPromise,
|
|
280
|
+
new Promise((resolve) => setTimeout(() => {
|
|
281
|
+
timeoutTriggered = true;
|
|
282
|
+
resolve(null);
|
|
283
|
+
}, CHAT_TIMEOUT_MS))
|
|
284
|
+
]);
|
|
285
|
+
if (timeoutTriggered) {
|
|
286
|
+
await ctx.reply("⏳ I'm still working on that, it's taking a bit longer than usual. I'll send the response as soon as it's ready!");
|
|
287
|
+
// Wait for actual response in background
|
|
288
|
+
const finalResponse = await chatPromise;
|
|
289
|
+
if (finalResponse) {
|
|
290
|
+
const rich = await toTelegramRichText(finalResponse);
|
|
291
|
+
for (const chunk of rich.chunks) {
|
|
292
|
+
try {
|
|
293
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
297
|
+
if (plain)
|
|
298
|
+
await ctx.reply(plain);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this.display.log(`Responded to @${user} (delayed): ${finalResponse.slice(0, 50)}...`, { source: 'Telegram' });
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
276
305
|
if (response) {
|
|
277
306
|
const rich = await toTelegramRichText(response);
|
|
278
307
|
for (const chunk of rich.chunks) {
|
|
@@ -285,7 +314,7 @@ export class TelegramAdapter {
|
|
|
285
314
|
await ctx.reply(plain);
|
|
286
315
|
}
|
|
287
316
|
}
|
|
288
|
-
this.display.log(`Responded to @${user}: ${response}
|
|
317
|
+
this.display.log(`Responded to @${user}: ${response.slice(0, 50)}...`, { source: 'Telegram' });
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
320
|
catch (error) {
|
|
@@ -389,126 +418,33 @@ export class TelegramAdapter {
|
|
|
389
418
|
await ctx.reply(`🎤 Transcription: "${text}"`);
|
|
390
419
|
await ctx.sendChatAction('typing');
|
|
391
420
|
const sessionId = await this.getSessionForUser(userId);
|
|
392
|
-
// Process with Agent
|
|
393
|
-
const
|
|
421
|
+
// Process with Agent - with a 30s timeout to avoid blocking the bot's update loop.
|
|
422
|
+
const CHAT_TIMEOUT_MS = 30000;
|
|
423
|
+
let timeoutTriggered = false;
|
|
424
|
+
const chatPromise = this.oracle.chat(text, usage, true, {
|
|
394
425
|
origin_channel: 'telegram',
|
|
395
426
|
session_id: sessionId,
|
|
396
427
|
origin_message_id: String(ctx.message.message_id),
|
|
397
428
|
origin_user_id: userId,
|
|
398
429
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
this.display.startActivity('telephonist', 'Synthesizing TTS...');
|
|
414
|
-
const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
|
|
415
|
-
getUsableApiKey(config.audio.apiKey) ||
|
|
416
|
-
(config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
|
|
417
|
-
? getUsableApiKey(config.llm.api_key) : undefined);
|
|
418
|
-
const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
|
|
419
|
-
ttsFilePath = ttsResult.filePath;
|
|
420
|
-
const ttsDurationMs = Date.now() - ttsStart;
|
|
421
|
-
this.display.endActivity('telephonist', true);
|
|
422
|
-
// OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
|
|
423
|
-
const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
|
|
424
|
-
if (isOgg) {
|
|
425
|
-
await ctx.replyWithVoice({ source: ttsFilePath });
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
await ctx.replyWithAudio({ source: ttsFilePath });
|
|
429
|
-
}
|
|
430
|
-
this.display.log(`Responded to @${user} (TTS audio)`, { source: 'Telegram' });
|
|
431
|
-
// Audit TTS success
|
|
432
|
-
try {
|
|
433
|
-
const auditSessionId = await this.getSessionForUser(userId);
|
|
434
|
-
AuditRepository.getInstance().insert({
|
|
435
|
-
session_id: auditSessionId,
|
|
436
|
-
event_type: 'telephonist',
|
|
437
|
-
agent: 'telephonist',
|
|
438
|
-
provider: ttsConfig.provider,
|
|
439
|
-
model: ttsConfig.model,
|
|
440
|
-
input_tokens: ttsResult.usage.input_tokens || null,
|
|
441
|
-
output_tokens: ttsResult.usage.output_tokens || null,
|
|
442
|
-
duration_ms: ttsDurationMs,
|
|
443
|
-
status: 'success',
|
|
444
|
-
metadata: {
|
|
445
|
-
operation: 'tts',
|
|
446
|
-
characters: response.length,
|
|
447
|
-
voice: ttsConfig.voice,
|
|
448
|
-
user,
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// Audit failure never breaks the main flow
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
catch (ttsError) {
|
|
457
|
-
this.display.endActivity('telephonist', false);
|
|
458
|
-
const ttsDetail = ttsError?.message || String(ttsError);
|
|
459
|
-
this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
|
|
460
|
-
// Audit TTS failure
|
|
461
|
-
try {
|
|
462
|
-
const auditSessionId = await this.getSessionForUser(userId);
|
|
463
|
-
AuditRepository.getInstance().insert({
|
|
464
|
-
session_id: auditSessionId,
|
|
465
|
-
event_type: 'telephonist',
|
|
466
|
-
agent: 'telephonist',
|
|
467
|
-
provider: ttsConfig.provider,
|
|
468
|
-
model: ttsConfig.model,
|
|
469
|
-
duration_ms: Date.now() - ttsStart,
|
|
470
|
-
status: 'error',
|
|
471
|
-
metadata: { operation: 'tts', error: ttsDetail, user },
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
// Audit failure never breaks the main flow
|
|
476
|
-
}
|
|
477
|
-
// Fallback to text
|
|
478
|
-
const rich = await toTelegramRichText(response);
|
|
479
|
-
for (const chunk of rich.chunks) {
|
|
480
|
-
try {
|
|
481
|
-
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
485
|
-
if (plain)
|
|
486
|
-
await ctx.reply(plain);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
finally {
|
|
491
|
-
// Cleanup TTS temp file
|
|
492
|
-
if (ttsFilePath) {
|
|
493
|
-
await fs.promises.unlink(ttsFilePath).catch(() => { });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
// Text path (TTS disabled)
|
|
499
|
-
const rich = await toTelegramRichText(response);
|
|
500
|
-
for (const chunk of rich.chunks) {
|
|
501
|
-
try {
|
|
502
|
-
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
506
|
-
if (plain)
|
|
507
|
-
await ctx.reply(plain);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
|
|
430
|
+
const response = await Promise.race([
|
|
431
|
+
chatPromise,
|
|
432
|
+
new Promise((resolve) => setTimeout(() => {
|
|
433
|
+
timeoutTriggered = true;
|
|
434
|
+
resolve(null);
|
|
435
|
+
}, CHAT_TIMEOUT_MS))
|
|
436
|
+
]);
|
|
437
|
+
if (timeoutTriggered) {
|
|
438
|
+
await ctx.reply("⏳ I'm still working on that, it's taking a bit longer than usual. I'll send the response as soon as it's ready!");
|
|
439
|
+
// Wait for actual response in background
|
|
440
|
+
const finalResponse = await chatPromise;
|
|
441
|
+
if (finalResponse) {
|
|
442
|
+
await this.handleAgentResponse(ctx, finalResponse, config, user, userId);
|
|
511
443
|
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (response) {
|
|
447
|
+
await this.handleAgentResponse(ctx, response, config, user, userId);
|
|
512
448
|
}
|
|
513
449
|
}
|
|
514
450
|
catch (error) {
|
|
@@ -876,11 +812,17 @@ export class TelegramAdapter {
|
|
|
876
812
|
ctx.answerCbQuery(`Error: ${error.message}`).catch(() => { });
|
|
877
813
|
}
|
|
878
814
|
});
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
815
|
+
// Global error handler to prevent crashes on long-running tasks or network errors
|
|
816
|
+
this.bot.catch((err, ctx) => {
|
|
817
|
+
const updateId = ctx?.update?.update_id;
|
|
818
|
+
const errorMsg = err?.message || String(err);
|
|
819
|
+
this.display.log(`Unhandled Telegram error (Update: ${updateId}): ${errorMsg}`, { source: 'Telegram', level: 'error' });
|
|
820
|
+
// If it's a timeout error, notify the user if possible
|
|
821
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('90000')) {
|
|
822
|
+
ctx.reply("⏳ Sorry, that request is taking longer than expected. I'm still working on it, but the connection timed out. I'll notify you once it's done.").catch(() => { });
|
|
882
823
|
}
|
|
883
824
|
});
|
|
825
|
+
this.launchBot();
|
|
884
826
|
this.isConnected = true;
|
|
885
827
|
// Check if there's a restart notification to send
|
|
886
828
|
this.checkAndSendRestartNotification().catch((err) => {
|
|
@@ -1955,4 +1897,107 @@ How can I assist you today?`;
|
|
|
1955
1897
|
await ctx.reply('An error occurred while retrieving the list of MCP servers\\. Please check the logs for more details\\.', { parse_mode: 'MarkdownV2' });
|
|
1956
1898
|
}
|
|
1957
1899
|
}
|
|
1900
|
+
launchBot() {
|
|
1901
|
+
if (!this.bot)
|
|
1902
|
+
return;
|
|
1903
|
+
this.bot.launch().catch((err) => {
|
|
1904
|
+
this.display.log(`Telegram bot error: ${err.message || String(err)}`, { source: 'Telegram', level: 'error' });
|
|
1905
|
+
if (this.isConnected) {
|
|
1906
|
+
this.display.log('Attempting to restart Telegram bot in 5 seconds...', { source: 'Telegram', level: 'info' });
|
|
1907
|
+
setTimeout(() => {
|
|
1908
|
+
if (this.isConnected)
|
|
1909
|
+
this.launchBot();
|
|
1910
|
+
}, 5000);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
async handleAgentResponse(ctx, response, config, user, userId) {
|
|
1915
|
+
const ttsConfig = config.audio.tts;
|
|
1916
|
+
if (ttsConfig?.enabled && this.ttsTelephonist?.synthesize) {
|
|
1917
|
+
// TTS path: synthesize and send as voice message
|
|
1918
|
+
let ttsFilePath = null;
|
|
1919
|
+
const ttsStart = Date.now();
|
|
1920
|
+
try {
|
|
1921
|
+
this.display.startActivity('telephonist', 'Synthesizing TTS...');
|
|
1922
|
+
const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
|
|
1923
|
+
getUsableApiKey(config.audio.apiKey) ||
|
|
1924
|
+
(config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
|
|
1925
|
+
? getUsableApiKey(config.llm.api_key) : undefined);
|
|
1926
|
+
const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
|
|
1927
|
+
ttsFilePath = ttsResult.filePath;
|
|
1928
|
+
const ttsDurationMs = Date.now() - ttsStart;
|
|
1929
|
+
this.display.endActivity('telephonist', true);
|
|
1930
|
+
// OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
|
|
1931
|
+
const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
|
|
1932
|
+
if (isOgg) {
|
|
1933
|
+
await ctx.replyWithVoice({ source: ttsFilePath });
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
await ctx.replyWithAudio({ source: ttsFilePath });
|
|
1937
|
+
}
|
|
1938
|
+
this.display.log(`Responded to @${user} (TTS audio)`, { source: 'Telegram' });
|
|
1939
|
+
// Audit TTS success
|
|
1940
|
+
try {
|
|
1941
|
+
const auditSessionId = await this.getSessionForUser(userId);
|
|
1942
|
+
AuditRepository.getInstance().insert({
|
|
1943
|
+
session_id: auditSessionId,
|
|
1944
|
+
event_type: 'telephonist',
|
|
1945
|
+
agent: 'telephonist',
|
|
1946
|
+
provider: ttsConfig.provider,
|
|
1947
|
+
model: ttsConfig.model,
|
|
1948
|
+
input_tokens: ttsResult.usage.input_tokens || null,
|
|
1949
|
+
output_tokens: ttsResult.usage.output_tokens || null,
|
|
1950
|
+
duration_ms: ttsDurationMs,
|
|
1951
|
+
status: 'success',
|
|
1952
|
+
metadata: {
|
|
1953
|
+
operation: 'tts',
|
|
1954
|
+
characters: response.length,
|
|
1955
|
+
voice: ttsConfig.voice,
|
|
1956
|
+
user,
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
// Audit failure never breaks the main flow
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
catch (ttsError) {
|
|
1965
|
+
this.display.endActivity('telephonist', false);
|
|
1966
|
+
const ttsDetail = ttsError?.message || String(ttsError);
|
|
1967
|
+
this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
|
|
1968
|
+
// Fallback to text
|
|
1969
|
+
const rich = await toTelegramRichText(response);
|
|
1970
|
+
for (const chunk of rich.chunks) {
|
|
1971
|
+
try {
|
|
1972
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
1976
|
+
if (plain)
|
|
1977
|
+
await ctx.reply(plain);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
finally {
|
|
1982
|
+
if (ttsFilePath) {
|
|
1983
|
+
await fs.promises.unlink(ttsFilePath).catch(() => { });
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
// Text path
|
|
1989
|
+
const rich = await toTelegramRichText(response);
|
|
1990
|
+
for (const chunk of rich.chunks) {
|
|
1991
|
+
try {
|
|
1992
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
1993
|
+
}
|
|
1994
|
+
catch {
|
|
1995
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
1996
|
+
if (plain)
|
|
1997
|
+
await ctx.reply(plain);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.display.log(`Responded to @${user}`, { source: 'Telegram' });
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1958
2003
|
}
|
|
@@ -9,6 +9,7 @@ import { renderBanner } from '../utils/render.js';
|
|
|
9
9
|
import { TelegramAdapter } from '../../channels/telegram.js';
|
|
10
10
|
import { ChannelRegistry } from '../../channels/registry.js';
|
|
11
11
|
import { PATHS } from '../../config/paths.js';
|
|
12
|
+
import { SkillRegistry } from '../../runtime/skills/index.js';
|
|
12
13
|
import { Oracle } from '../../runtime/oracle.js';
|
|
13
14
|
import { ProviderError } from '../../runtime/errors.js';
|
|
14
15
|
import { HttpServer } from '../../http/server.js';
|
|
@@ -87,6 +88,17 @@ export const restartCommand = new Command('restart')
|
|
|
87
88
|
if (options.ui) {
|
|
88
89
|
display.log(chalk.blue(`Web UI enabled to port ${options.port}`), { source: 'Zaion' });
|
|
89
90
|
}
|
|
91
|
+
// Initialize SkillRegistry before Oracle (so skills are available in system prompt)
|
|
92
|
+
try {
|
|
93
|
+
const skillRegistry = SkillRegistry.getInstance();
|
|
94
|
+
await skillRegistry.load();
|
|
95
|
+
const loadedSkills = skillRegistry.getAll();
|
|
96
|
+
const enabledCount = skillRegistry.getEnabled().length;
|
|
97
|
+
display.log(chalk.green(`✓ Skills loaded: ${loadedSkills.length} total, ${enabledCount} enabled`), { source: 'Skills' });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
display.log(chalk.yellow(`Skills initialization warning: ${err.message}`), { source: 'Skills' });
|
|
101
|
+
}
|
|
90
102
|
// Initialize Oracle
|
|
91
103
|
const oracle = new Oracle(config);
|
|
92
104
|
try {
|
package/dist/config/manager.js
CHANGED
|
@@ -343,6 +343,11 @@ export class ConfigManager {
|
|
|
343
343
|
max_active_jobs: resolveNumeric('MORPHEUS_CHRONOS_MAX_ACTIVE_JOBS', config.chronos.max_active_jobs, 100),
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
|
+
// Apply precedence to GWS config
|
|
347
|
+
const gwsConfig = {
|
|
348
|
+
service_account_json: resolveString('MORPHEUS_GWS_SERVICE_ACCOUNT_JSON', config.gws?.service_account_json, ''),
|
|
349
|
+
enabled: resolveBoolean('MORPHEUS_GWS_ENABLED', config.gws?.enabled, true),
|
|
350
|
+
};
|
|
346
351
|
// Apply precedence to DevKit config
|
|
347
352
|
// Migration: if devkit is absent but apoc.working_dir exists, migrate it
|
|
348
353
|
const rawDevKit = config.devkit ?? {};
|
|
@@ -409,6 +414,22 @@ export class ConfigManager {
|
|
|
409
414
|
async save(newConfig) {
|
|
410
415
|
// Deep merge or overwrite? simpler to overwrite for now or merge top level
|
|
411
416
|
let updated = { ...this.config, ...newConfig };
|
|
417
|
+
// If GWS credentials string is provided, save it to file
|
|
418
|
+
if (newConfig.gws?.service_account_json_content) {
|
|
419
|
+
const content = newConfig.gws.service_account_json_content;
|
|
420
|
+
try {
|
|
421
|
+
// Validate JSON
|
|
422
|
+
JSON.parse(content);
|
|
423
|
+
await fs.ensureDir(PATHS.gws);
|
|
424
|
+
await fs.writeFile(PATHS.gwsCredentials, content, 'utf8');
|
|
425
|
+
// Update the path in config and remove the raw content
|
|
426
|
+
newConfig.gws.service_account_json = PATHS.gwsCredentials;
|
|
427
|
+
delete newConfig.gws.service_account_json_content;
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
throw new Error(`Invalid Google Service Account JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
412
433
|
// Encrypt API keys before saving if MORPHEUS_SECRET is set
|
|
413
434
|
updated = this.encryptAgentApiKeys(updated);
|
|
414
435
|
// Validate before saving
|
|
@@ -524,7 +545,7 @@ export class ConfigManager {
|
|
|
524
545
|
sandbox_dir: process.cwd(),
|
|
525
546
|
readonly_mode: false,
|
|
526
547
|
allowed_shell_commands: [],
|
|
527
|
-
allowed_paths: [PATHS.docs, PATHS.skills],
|
|
548
|
+
allowed_paths: [PATHS.docs, PATHS.skills, PATHS.gws],
|
|
528
549
|
enable_filesystem: true,
|
|
529
550
|
enable_shell: true,
|
|
530
551
|
enable_git: true,
|
|
@@ -535,12 +556,23 @@ export class ConfigManager {
|
|
|
535
556
|
const merged = { ...defaults, ...this.config.devkit };
|
|
536
557
|
// Ensure allowed_paths has default if empty or undefined
|
|
537
558
|
if (!merged.allowed_paths?.length) {
|
|
538
|
-
merged.allowed_paths = [PATHS.docs, PATHS.skills];
|
|
559
|
+
merged.allowed_paths = [PATHS.docs, PATHS.skills, PATHS.gws];
|
|
560
|
+
}
|
|
561
|
+
else if (!merged.allowed_paths.includes(PATHS.gws)) {
|
|
562
|
+
// Force inclusion of GWS path if it's not there
|
|
563
|
+
merged.allowed_paths.push(PATHS.gws);
|
|
539
564
|
}
|
|
540
565
|
return merged;
|
|
541
566
|
}
|
|
542
567
|
return defaults;
|
|
543
568
|
}
|
|
569
|
+
getGwsConfig() {
|
|
570
|
+
const defaults = { enabled: true };
|
|
571
|
+
if (this.config.gws) {
|
|
572
|
+
return { ...defaults, ...this.config.gws };
|
|
573
|
+
}
|
|
574
|
+
return defaults;
|
|
575
|
+
}
|
|
544
576
|
/**
|
|
545
577
|
* Returns encryption status for all agent API keys.
|
|
546
578
|
*/
|
package/dist/config/paths.js
CHANGED
|
@@ -16,6 +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
|
+
gws: path.join(MORPHEUS_ROOT, 'gws'),
|
|
20
|
+
gwsCredentials: path.join(MORPHEUS_ROOT, 'gws', 'credentials.json'),
|
|
19
21
|
shortMemoryDb: path.join(MORPHEUS_ROOT, 'memory', 'short-memory.db'),
|
|
20
22
|
trinityDb: path.join(MORPHEUS_ROOT, 'memory', 'trinity.db'),
|
|
21
23
|
satiDb: path.join(MORPHEUS_ROOT, 'memory', 'sati-memory.db'),
|
package/dist/config/schemas.js
CHANGED
|
@@ -98,6 +98,11 @@ export const CurrencyConfigSchema = z.object({
|
|
|
98
98
|
symbol: z.string().min(1).default('$'),
|
|
99
99
|
rate: z.number().positive().default(1.0),
|
|
100
100
|
});
|
|
101
|
+
export const GwsConfigSchema = z.object({
|
|
102
|
+
service_account_json: z.string().optional(),
|
|
103
|
+
service_account_json_content: z.string().optional(),
|
|
104
|
+
enabled: z.boolean().default(true),
|
|
105
|
+
});
|
|
101
106
|
// Zod Schema matching MorpheusConfig interface
|
|
102
107
|
export const ConfigSchema = z.object({
|
|
103
108
|
agent: z.object({
|
|
@@ -123,6 +128,7 @@ export const ConfigSchema = z.object({
|
|
|
123
128
|
chronos: ChronosConfigSchema.optional(),
|
|
124
129
|
devkit: DevKitConfigSchema.optional(),
|
|
125
130
|
smiths: SmithsConfigSchema.optional(),
|
|
131
|
+
gws: GwsConfigSchema.optional(),
|
|
126
132
|
setup: SetupConfigSchema.optional(),
|
|
127
133
|
currency: CurrencyConfigSchema.optional(),
|
|
128
134
|
verbose_mode: z.boolean().default(true),
|