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.
Files changed (51) hide show
  1. package/README.md +97 -2
  2. package/dist/channels/telegram.js +165 -120
  3. package/dist/cli/commands/restart.js +12 -0
  4. package/dist/config/manager.js +34 -2
  5. package/dist/config/paths.js +2 -0
  6. package/dist/config/schemas.js +6 -0
  7. package/dist/runtime/__tests__/gws-sync.test.js +69 -0
  8. package/dist/runtime/gws-sync.js +102 -0
  9. package/dist/runtime/hash-utils.js +15 -0
  10. package/dist/runtime/hot-reload.js +5 -1
  11. package/dist/runtime/oracle.js +1 -0
  12. package/dist/runtime/scaffold.js +4 -0
  13. package/dist/runtime/skills/loader.js +7 -52
  14. package/dist/runtime/skills/registry.js +5 -1
  15. package/dist/runtime/skills/schema.js +1 -1
  16. package/dist/runtime/skills/tool.js +8 -1
  17. package/dist/runtime/subagents/apoc.js +154 -1
  18. package/dist/runtime/subagents/devkit-instrument.js +13 -0
  19. package/dist/types/config.js +3 -0
  20. package/dist/ui/assets/{AuditDashboard-CfYKdOEt.js → AuditDashboard-tH9QZTl4.js} +1 -1
  21. package/dist/ui/assets/{Chat-CYev7-CJ.js → Chat-Cd0uYF8g.js} +1 -1
  22. package/dist/ui/assets/{Chronos-5KR8aZud.js → Chronos-CWwHYdBl.js} +1 -1
  23. package/dist/ui/assets/{ConfirmationModal-NFwIYI7B.js → ConfirmationModal-CxvFe-We.js} +1 -1
  24. package/dist/ui/assets/{Dashboard-hsjB56la.js → Dashboard-CNNMxl53.js} +1 -1
  25. package/dist/ui/assets/{DeleteConfirmationModal-BfV370Vv.js → DeleteConfirmationModal-AqGQSh1X.js} +1 -1
  26. package/dist/ui/assets/{Documents-BNo2tMfG.js → Documents-BfRYOK88.js} +1 -1
  27. package/dist/ui/assets/{Logs-1hBpMPZE.js → Logs-DhFo4cio.js} +1 -1
  28. package/dist/ui/assets/{MCPManager-CvPRHn4C.js → MCPManager-BMhxbhni.js} +1 -1
  29. package/dist/ui/assets/{ModelPricing-BbwJFdz4.js → ModelPricing-Dvl0R_HR.js} +1 -1
  30. package/dist/ui/assets/{Notifications-C_MA51Gf.js → Notifications-CawvBid4.js} +1 -1
  31. package/dist/ui/assets/{SatiMemories-Cd9xn98_.js → SatiMemories-yyVrJGdc.js} +1 -1
  32. package/dist/ui/assets/{SessionAudit-BTABenGk.js → SessionAudit-joq0ntdJ.js} +1 -1
  33. package/dist/ui/assets/{Settings-DRVx4ICA.js → Settings-B6SMPn41.js} +7 -7
  34. package/dist/ui/assets/{Skills-DS9p1-S8.js → Skills-B5yhTHyn.js} +1 -1
  35. package/dist/ui/assets/{Smiths-CMCZaAF_.js → Smiths-Dug63YED.js} +1 -1
  36. package/dist/ui/assets/{Tasks-Cvt4sTcs.js → Tasks-D8HPLkg0.js} +1 -1
  37. package/dist/ui/assets/{TrinityDatabases-qhSUMeCw.js → TrinityDatabases-D0qEKmwJ.js} +1 -1
  38. package/dist/ui/assets/{UsageStats-Cy9HKYOp.js → UsageStats-CHWALN70.js} +1 -1
  39. package/dist/ui/assets/{WebhookManager-ByqkTyqs.js → WebhookManager-T2ef90p8.js} +1 -1
  40. package/dist/ui/assets/{agents-svEaAPka.js → agents-BVnfnJ1X.js} +1 -1
  41. package/dist/ui/assets/{audit-gxRPR5Jb.js → audit-BErc_ye8.js} +1 -1
  42. package/dist/ui/assets/{chronos-ZrBE4yA4.js → chronos-CAv__H3B.js} +1 -1
  43. package/dist/ui/assets/{config-B1i6Xxwk.js → config-CPFW7PTY.js} +1 -1
  44. package/dist/ui/assets/{index-DyKlGDg1.js → index-BvsF1a9j.js} +2 -2
  45. package/dist/ui/assets/{mcp-DSddQR1h.js → mcp-BaHwY4DW.js} +1 -1
  46. package/dist/ui/assets/{skills-DIuMjpPF.js → skills-lbjIRO8d.js} +1 -1
  47. package/dist/ui/assets/{stats-CxlRAO2g.js → stats-C8KAfpHO.js} +1 -1
  48. package/dist/ui/assets/{useCurrency-BkHiWfcT.js → useCurrency-Ch0lsvGj.js} +1 -1
  49. package/dist/ui/index.html +1 -1
  50. package/dist/ui/sw.js +1 -1
  51. 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
- - Rich operational visibility in UI (chat traces, tasks, usage, logs).
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 response = await this.oracle.chat(text, undefined, false, {
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}`, { source: 'Telegram' });
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 response = await this.oracle.chat(text, usage, true, {
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
- // if (listeningMsg) {
400
- // try {
401
- // await ctx.telegram.deleteMessage(ctx.chat.id, listeningMsg.message_id);
402
- // } catch (e) {
403
- // // Ignore delete error
404
- // }
405
- // }
406
- if (response) {
407
- const ttsConfig = config.audio.tts;
408
- if (ttsConfig?.enabled && this.ttsTelephonist?.synthesize) {
409
- // TTS path: synthesize and send as voice message
410
- let ttsFilePath = null;
411
- const ttsStart = Date.now();
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
- this.bot.launch().catch((err) => {
880
- if (this.isConnected) {
881
- this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
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 {
@@ -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
  */
@@ -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'),
@@ -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),