morpheus-cli 0.4.3 → 0.4.6
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 +127 -0
- package/dist/channels/telegram.js +133 -26
- package/dist/cli/commands/start.js +3 -0
- package/dist/config/schemas.js +4 -0
- package/dist/http/middleware/auth.js +1 -1
- package/dist/http/server.js +8 -0
- package/dist/http/webhooks-router.js +181 -0
- package/dist/runtime/tools/analytics-tools.js +77 -41
- package/dist/runtime/tools/diagnostic-tools.js +72 -43
- package/dist/runtime/webhooks/dispatcher.js +95 -0
- package/dist/runtime/webhooks/repository.js +199 -0
- package/dist/runtime/webhooks/types.js +1 -0
- package/dist/ui/assets/index-LemKVRjC.js +112 -0
- package/dist/ui/assets/index-TCQ7VNYO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CjlkpcsE.js +0 -109
- package/dist/ui/assets/index-LrqT6MpO.css +0 -1
package/README.md
CHANGED
|
@@ -242,6 +242,31 @@ Morpheus features a dedicated middleware system called **Sati** (Mindfulness) th
|
|
|
242
242
|
- **Data Privacy**: Stored in a local, independent SQLite database (`santi-memory.db`), ensuring sensitive data is handled securely and reducing context window usage.
|
|
243
243
|
- **Memory Management**: View and manage your long-term memories through the Web UI or via API endpoints.
|
|
244
244
|
|
|
245
|
+
### 🪝 Webhooks & Notifications
|
|
246
|
+
|
|
247
|
+
Morpheus includes a **Webhook System** that lets any external service (GitHub Actions, CI/CD pipelines, monitoring tools, etc.) trigger Oracle and receive the result asynchronously.
|
|
248
|
+
|
|
249
|
+
**How it works:**
|
|
250
|
+
1. Create a webhook via the Web UI or API — give it a name (slug) and a prompt.
|
|
251
|
+
2. You receive a unique `api_key` for that webhook.
|
|
252
|
+
3. Trigger it from anywhere by posting JSON to `POST /api/webhooks/trigger/<name>` with the `x-api-key` header.
|
|
253
|
+
4. Morpheus runs Oracle with your prompt + the received payload in the background.
|
|
254
|
+
5. The result is saved as a **Notification** and optionally pushed to Telegram.
|
|
255
|
+
|
|
256
|
+
**Example — trigger from GitHub Actions:**
|
|
257
|
+
```yaml
|
|
258
|
+
- name: Notify Morpheus of deployment
|
|
259
|
+
run: |
|
|
260
|
+
curl -s -X POST https://your-morpheus-host/api/webhooks/trigger/deploy-done \
|
|
261
|
+
-H "x-api-key: ${{ secrets.MORPHEUS_WEBHOOK_KEY }}" \
|
|
262
|
+
-H "Content-Type: application/json" \
|
|
263
|
+
-d '{"workflow":"${{ github.workflow }}","status":"success","ref":"${{ github.ref }}"}'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Security:** Each webhook has its own `api_key` (UUID). The key is sent in the `x-api-key` header — never in the URL — to prevent leakage in server logs. Management endpoints remain protected by `THE_ARCHITECT_PASS`.
|
|
267
|
+
|
|
268
|
+
**Notification channels:** `ui` (Web UI inbox with unread badge) and/or `telegram` (proactive push message).
|
|
269
|
+
|
|
245
270
|
### 📊 Usage Analytics
|
|
246
271
|
Track your token usage across different providers and models directly from the Web UI. View detailed breakdowns of input/output tokens and message counts to monitor costs and activity.
|
|
247
272
|
|
|
@@ -920,6 +945,106 @@ Restart the Morpheus agent.
|
|
|
920
945
|
}
|
|
921
946
|
```
|
|
922
947
|
|
|
948
|
+
### Webhook Endpoints
|
|
949
|
+
|
|
950
|
+
All management endpoints require `x-architect-pass` authentication. The trigger endpoint is **public** — authenticated only by the per-webhook `x-api-key` header.
|
|
951
|
+
|
|
952
|
+
#### POST `/api/webhooks/trigger/:webhook_name`
|
|
953
|
+
Trigger a webhook and queue an Oracle agent execution in the background.
|
|
954
|
+
|
|
955
|
+
* **Authentication:** `x-api-key: <webhook_api_key>` header (no `x-architect-pass` required).
|
|
956
|
+
* **Parameters:** `webhook_name` — the slug of the webhook to trigger.
|
|
957
|
+
* **Body:** Any JSON payload (forwarded to the agent as context).
|
|
958
|
+
* **Response (202 Accepted):**
|
|
959
|
+
```json
|
|
960
|
+
{
|
|
961
|
+
"accepted": true,
|
|
962
|
+
"notification_id": "uuid-..."
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
* **Errors:** `401` for missing/invalid api_key; `404` for webhook not found or disabled.
|
|
966
|
+
|
|
967
|
+
#### GET `/api/webhooks`
|
|
968
|
+
List all configured webhooks.
|
|
969
|
+
|
|
970
|
+
* **Authentication:** `x-architect-pass` header.
|
|
971
|
+
* **Response:**
|
|
972
|
+
```json
|
|
973
|
+
[
|
|
974
|
+
{
|
|
975
|
+
"id": "uuid",
|
|
976
|
+
"name": "deploy-done",
|
|
977
|
+
"api_key": "uuid",
|
|
978
|
+
"prompt": "Analyze the deployment result...",
|
|
979
|
+
"enabled": true,
|
|
980
|
+
"notification_channels": ["ui", "telegram"],
|
|
981
|
+
"created_at": 1700000000000,
|
|
982
|
+
"last_triggered_at": 1700001000000,
|
|
983
|
+
"trigger_count": 42
|
|
984
|
+
}
|
|
985
|
+
]
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
#### POST `/api/webhooks`
|
|
989
|
+
Create a new webhook.
|
|
990
|
+
|
|
991
|
+
* **Authentication:** `x-architect-pass` header.
|
|
992
|
+
* **Body:**
|
|
993
|
+
```json
|
|
994
|
+
{
|
|
995
|
+
"name": "deploy-done",
|
|
996
|
+
"prompt": "A deployment just finished. Analyze the payload and summarize what happened.",
|
|
997
|
+
"notification_channels": ["ui", "telegram"],
|
|
998
|
+
"enabled": true
|
|
999
|
+
}
|
|
1000
|
+
```
|
|
1001
|
+
* **Response (201):** The created webhook object including the generated `api_key`.
|
|
1002
|
+
|
|
1003
|
+
#### PUT `/api/webhooks/:id`
|
|
1004
|
+
Update an existing webhook (prompt, channels, enabled status).
|
|
1005
|
+
|
|
1006
|
+
* **Authentication:** `x-architect-pass` header.
|
|
1007
|
+
* **Note:** The `name` (slug) and `api_key` fields are immutable via this endpoint.
|
|
1008
|
+
|
|
1009
|
+
#### DELETE `/api/webhooks/:id`
|
|
1010
|
+
Delete a webhook and all its associated notifications.
|
|
1011
|
+
|
|
1012
|
+
* **Authentication:** `x-architect-pass` header.
|
|
1013
|
+
|
|
1014
|
+
#### GET `/api/webhooks/notifications`
|
|
1015
|
+
List webhook execution notifications.
|
|
1016
|
+
|
|
1017
|
+
* **Authentication:** `x-architect-pass` header.
|
|
1018
|
+
* **Query Parameters:** `unreadOnly=true` to filter unread notifications.
|
|
1019
|
+
* **Response:**
|
|
1020
|
+
```json
|
|
1021
|
+
[
|
|
1022
|
+
{
|
|
1023
|
+
"id": "uuid",
|
|
1024
|
+
"webhook_id": "uuid",
|
|
1025
|
+
"webhook_name": "deploy-done",
|
|
1026
|
+
"status": "completed",
|
|
1027
|
+
"payload": "{\"ref\":\"main\"}",
|
|
1028
|
+
"result": "Deployment of main to production succeeded...",
|
|
1029
|
+
"read": false,
|
|
1030
|
+
"created_at": 1700001000000,
|
|
1031
|
+
"completed_at": 1700001005000
|
|
1032
|
+
}
|
|
1033
|
+
]
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
#### POST `/api/webhooks/notifications/read`
|
|
1037
|
+
Mark notifications as read.
|
|
1038
|
+
|
|
1039
|
+
* **Authentication:** `x-architect-pass` header.
|
|
1040
|
+
* **Body:** `{ "ids": ["uuid1", "uuid2"] }`
|
|
1041
|
+
|
|
1042
|
+
#### GET `/api/webhooks/notifications/unread-count`
|
|
1043
|
+
Get the count of unread notifications (used by the sidebar badge).
|
|
1044
|
+
|
|
1045
|
+
* **Authentication:** `x-architect-pass` header.
|
|
1046
|
+
* **Response:** `{ "count": 3 }`
|
|
1047
|
+
|
|
923
1048
|
## Testing
|
|
924
1049
|
|
|
925
1050
|
We use **Vitest** for testing.
|
|
@@ -957,8 +1082,10 @@ npm run test:watch
|
|
|
957
1082
|
|
|
958
1083
|
- [x] **Web Dashboard**: Local UI for management and logs.
|
|
959
1084
|
- [x] **MCP Support**: Full integration with Model Context Protocol.
|
|
1085
|
+
- [x] **Webhook System**: External triggers with Oracle execution and multi-channel notifications.
|
|
960
1086
|
- [ ] **Discord Adapter**: Support for Discord interactions.
|
|
961
1087
|
- [ ] **Plugin System**: Extend functionality via external modules.
|
|
1088
|
+
- [ ] **Webhook Retry Logic**: Exponential backoff for failed Oracle executions.
|
|
962
1089
|
|
|
963
1090
|
## 🕵️ Privacy Protection
|
|
964
1091
|
|
|
@@ -356,6 +356,29 @@ export class TelegramAdapter {
|
|
|
356
356
|
await fs.writeFile(filePath, buffer);
|
|
357
357
|
return filePath;
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Sends a proactive message to all allowed Telegram users.
|
|
361
|
+
* Used by the webhook notification system to push results.
|
|
362
|
+
*/
|
|
363
|
+
async sendMessage(text) {
|
|
364
|
+
if (!this.isConnected || !this.bot) {
|
|
365
|
+
this.display.log('Cannot send message: Telegram bot not connected.', { source: 'Telegram', level: 'warning' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const allowedUsers = this.config.get().channels.telegram.allowedUsers;
|
|
369
|
+
if (allowedUsers.length === 0) {
|
|
370
|
+
this.display.log('No allowed Telegram users configured — skipping notification.', { source: 'Telegram', level: 'warning' });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
for (const userId of allowedUsers) {
|
|
374
|
+
try {
|
|
375
|
+
await this.bot.telegram.sendMessage(userId, text, { parse_mode: 'Markdown' });
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
this.display.log(`Failed to send message to Telegram user ${userId}: ${err.message}`, { source: 'Telegram', level: 'error' });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
359
382
|
async disconnect() {
|
|
360
383
|
if (!this.isConnected || !this.bot) {
|
|
361
384
|
return;
|
|
@@ -522,44 +545,83 @@ How can I assist you today?`;
|
|
|
522
545
|
}
|
|
523
546
|
}
|
|
524
547
|
async handleDoctorCommand(ctx, user) {
|
|
525
|
-
// Implementação simplificada do diagnóstico
|
|
526
548
|
const config = this.config.get();
|
|
527
549
|
let response = '*Morpheus Doctor*\n\n';
|
|
528
550
|
// Verificar versão do Node.js
|
|
529
551
|
const nodeVersion = process.version;
|
|
530
552
|
const majorVersion = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
|
|
531
553
|
if (majorVersion >= 18) {
|
|
532
|
-
response += '✅ Node.js
|
|
554
|
+
response += '✅ Node.js: ' + nodeVersion + '\n';
|
|
533
555
|
}
|
|
534
556
|
else {
|
|
535
|
-
response += '❌ Node.js
|
|
557
|
+
response += '❌ Node.js: ' + nodeVersion + ' (Required: >=18)\n';
|
|
536
558
|
}
|
|
537
|
-
// Verificar configuração
|
|
538
559
|
if (config) {
|
|
539
560
|
response += '✅ Configuration: Valid\n';
|
|
540
|
-
//
|
|
561
|
+
// Helper para verificar API key de um provider
|
|
562
|
+
const hasApiKey = (provider, apiKey) => {
|
|
563
|
+
if (apiKey)
|
|
564
|
+
return true;
|
|
565
|
+
if (provider === 'openai')
|
|
566
|
+
return !!process.env.OPENAI_API_KEY;
|
|
567
|
+
if (provider === 'anthropic')
|
|
568
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
569
|
+
if (provider === 'gemini' || provider === 'google')
|
|
570
|
+
return !!process.env.GOOGLE_API_KEY;
|
|
571
|
+
if (provider === 'openrouter')
|
|
572
|
+
return !!process.env.OPENROUTER_API_KEY;
|
|
573
|
+
return false; // ollama and others don't need keys
|
|
574
|
+
};
|
|
575
|
+
// Oracle (LLM)
|
|
541
576
|
const llmProvider = config.llm?.provider;
|
|
542
577
|
if (llmProvider && llmProvider !== 'ollama') {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
578
|
+
if (hasApiKey(llmProvider, config.llm?.api_key)) {
|
|
579
|
+
response += `✅ Oracle API key (${llmProvider})\n`;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
response += `❌ Oracle API key missing (${llmProvider})\n`;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Sati
|
|
586
|
+
const sati = config.sati;
|
|
587
|
+
const satiProvider = sati?.provider || llmProvider;
|
|
588
|
+
if (satiProvider && satiProvider !== 'ollama') {
|
|
589
|
+
if (hasApiKey(satiProvider, sati?.api_key ?? config.llm?.api_key)) {
|
|
590
|
+
response += `✅ Sati API key (${satiProvider})\n`;
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
response += `❌ Sati API key missing (${satiProvider})\n`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Apoc
|
|
597
|
+
const apoc = config.apoc;
|
|
598
|
+
const apocProvider = apoc?.provider || llmProvider;
|
|
599
|
+
if (apocProvider && apocProvider !== 'ollama') {
|
|
600
|
+
if (hasApiKey(apocProvider, apoc?.api_key ?? config.llm?.api_key)) {
|
|
601
|
+
response += `✅ Apoc API key (${apocProvider})\n`;
|
|
550
602
|
}
|
|
551
603
|
else {
|
|
552
|
-
response += `❌
|
|
604
|
+
response += `❌ Apoc API key missing (${apocProvider})\n`;
|
|
553
605
|
}
|
|
554
606
|
}
|
|
555
|
-
//
|
|
607
|
+
// Telegram token
|
|
556
608
|
if (config.channels?.telegram?.enabled) {
|
|
557
609
|
const hasTelegramToken = config.channels.telegram?.token || process.env.TELEGRAM_BOT_TOKEN;
|
|
558
610
|
if (hasTelegramToken) {
|
|
559
|
-
response += '✅ Telegram
|
|
611
|
+
response += '✅ Telegram token\n';
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
response += '❌ Telegram token missing\n';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Audio API key
|
|
618
|
+
if (config.audio?.enabled) {
|
|
619
|
+
const audioKey = config.audio?.apiKey || process.env.GOOGLE_API_KEY;
|
|
620
|
+
if (audioKey) {
|
|
621
|
+
response += '✅ Audio API key\n';
|
|
560
622
|
}
|
|
561
623
|
else {
|
|
562
|
-
response += '❌
|
|
624
|
+
response += '❌ Audio API key missing\n';
|
|
563
625
|
}
|
|
564
626
|
}
|
|
565
627
|
}
|
|
@@ -570,29 +632,46 @@ How can I assist you today?`;
|
|
|
570
632
|
}
|
|
571
633
|
async handleStatsCommand(ctx, user) {
|
|
572
634
|
try {
|
|
573
|
-
// Criar instância temporária do histórico para obter estatísticas
|
|
574
635
|
const history = new SQLiteChatMessageHistory({
|
|
575
636
|
sessionId: "default",
|
|
576
|
-
databasePath: undefined,
|
|
577
|
-
limit: 100,
|
|
637
|
+
databasePath: undefined,
|
|
638
|
+
limit: 100,
|
|
578
639
|
});
|
|
579
640
|
const stats = await history.getGlobalUsageStats();
|
|
580
641
|
const groupedStats = await history.getUsageStatsByProviderAndModel();
|
|
642
|
+
// Totals from global stats
|
|
643
|
+
const totalTokens = stats.totalInputTokens + stats.totalOutputTokens;
|
|
644
|
+
// Aggregate audio seconds and cost from grouped stats
|
|
645
|
+
const totalAudioSeconds = groupedStats.reduce((sum, s) => sum + (s.totalAudioSeconds || 0), 0);
|
|
646
|
+
const totalCost = stats.totalEstimatedCostUsd;
|
|
581
647
|
let response = '*Token Usage Statistics*\n\n';
|
|
582
|
-
response += `
|
|
583
|
-
response += `
|
|
584
|
-
response += `Total Tokens: ${
|
|
648
|
+
response += `Input Tokens: ${stats.totalInputTokens.toLocaleString()}\n`;
|
|
649
|
+
response += `Output Tokens: ${stats.totalOutputTokens.toLocaleString()}\n`;
|
|
650
|
+
response += `Total Tokens: ${totalTokens.toLocaleString()}\n`;
|
|
651
|
+
if (totalAudioSeconds > 0) {
|
|
652
|
+
response += `Audio Processed: ${totalAudioSeconds.toFixed(1)}s\n`;
|
|
653
|
+
}
|
|
654
|
+
if (totalCost != null) {
|
|
655
|
+
response += `Estimated Cost: $${totalCost.toFixed(4)}\n`;
|
|
656
|
+
}
|
|
657
|
+
response += '\n';
|
|
585
658
|
if (groupedStats.length > 0) {
|
|
586
|
-
response += '*
|
|
659
|
+
response += '*By Provider/Model:*\n';
|
|
587
660
|
for (const stat of groupedStats) {
|
|
588
|
-
response +=
|
|
661
|
+
response += `\n*${stat.provider}/${stat.model}*\n`;
|
|
662
|
+
response += ` Tokens: ${stat.totalTokens.toLocaleString()} (${stat.messageCount} msgs)\n`;
|
|
663
|
+
if (stat.totalAudioSeconds > 0) {
|
|
664
|
+
response += ` Audio: ${stat.totalAudioSeconds.toFixed(1)}s\n`;
|
|
665
|
+
}
|
|
666
|
+
if (stat.estimatedCostUsd != null) {
|
|
667
|
+
response += ` Cost: $${stat.estimatedCostUsd.toFixed(4)}\n`;
|
|
668
|
+
}
|
|
589
669
|
}
|
|
590
670
|
}
|
|
591
671
|
else {
|
|
592
672
|
response += 'No detailed usage statistics available.';
|
|
593
673
|
}
|
|
594
674
|
await ctx.reply(response, { parse_mode: 'Markdown' });
|
|
595
|
-
// Fechar conexão com o banco de dados
|
|
596
675
|
history.close();
|
|
597
676
|
}
|
|
598
677
|
catch (error) {
|
|
@@ -632,11 +711,39 @@ How can I assist you today?`;
|
|
|
632
711
|
response += `*Agent:*\n`;
|
|
633
712
|
response += `- Name: ${config.agent.name}\n`;
|
|
634
713
|
response += `- Personality: ${config.agent.personality}\n\n`;
|
|
635
|
-
response += `*LLM:*\n`;
|
|
714
|
+
response += `*Oracle (LLM):*\n`;
|
|
636
715
|
response += `- Provider: ${config.llm.provider}\n`;
|
|
637
716
|
response += `- Model: ${config.llm.model}\n`;
|
|
638
717
|
response += `- Temperature: ${config.llm.temperature}\n`;
|
|
639
718
|
response += `- Context Window: ${config.llm.context_window || 100}\n\n`;
|
|
719
|
+
// Sati config (falls back to llm if not set)
|
|
720
|
+
const sati = config.sati;
|
|
721
|
+
response += `*Sati (Memory):*\n`;
|
|
722
|
+
if (sati?.provider) {
|
|
723
|
+
response += `- Provider: ${sati.provider}\n`;
|
|
724
|
+
response += `- Model: ${sati.model || config.llm.model}\n`;
|
|
725
|
+
response += `- Temperature: ${sati.temperature ?? config.llm.temperature}\n`;
|
|
726
|
+
response += `- Memory Limit: ${sati.memory_limit ?? 1000}\n`;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
response += `- Inherits Oracle config\n`;
|
|
730
|
+
}
|
|
731
|
+
response += '\n';
|
|
732
|
+
// Apoc config (falls back to llm if not set)
|
|
733
|
+
const apoc = config.apoc;
|
|
734
|
+
response += `*Apoc (DevTools):*\n`;
|
|
735
|
+
if (apoc?.provider) {
|
|
736
|
+
response += `- Provider: ${apoc.provider}\n`;
|
|
737
|
+
response += `- Model: ${apoc.model || config.llm.model}\n`;
|
|
738
|
+
response += `- Temperature: ${apoc.temperature ?? 0.2}\n`;
|
|
739
|
+
if (apoc.working_dir)
|
|
740
|
+
response += `- Working Dir: ${apoc.working_dir}\n`;
|
|
741
|
+
response += `- Timeout: ${apoc.timeout_ms ?? 30000}ms\n`;
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
response += `- Inherits Oracle config\n`;
|
|
745
|
+
}
|
|
746
|
+
response += '\n';
|
|
640
747
|
response += `*Channels:*\n`;
|
|
641
748
|
response += `- Telegram Enabled: ${config.channels.telegram.enabled}\n`;
|
|
642
749
|
response += `- Discord Enabled: ${config.channels.discord.enabled}\n\n`;
|
|
@@ -8,6 +8,7 @@ import { writePid, readPid, isProcessRunning, clearPid, checkStalePid, killProce
|
|
|
8
8
|
import { ConfigManager } from '../../config/manager.js';
|
|
9
9
|
import { renderBanner } from '../utils/render.js';
|
|
10
10
|
import { TelegramAdapter } from '../../channels/telegram.js';
|
|
11
|
+
import { WebhookDispatcher } from '../../runtime/webhooks/dispatcher.js';
|
|
11
12
|
import { PATHS } from '../../config/paths.js';
|
|
12
13
|
import { Oracle } from '../../runtime/oracle.js';
|
|
13
14
|
import { ProviderError } from '../../runtime/errors.js';
|
|
@@ -136,6 +137,8 @@ export const startCommand = new Command('start')
|
|
|
136
137
|
const telegram = new TelegramAdapter(oracle);
|
|
137
138
|
try {
|
|
138
139
|
await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
|
|
140
|
+
// Wire Telegram adapter to webhook dispatcher for proactive notifications
|
|
141
|
+
WebhookDispatcher.setTelegramAdapter(telegram);
|
|
139
142
|
adapters.push(telegram);
|
|
140
143
|
}
|
|
141
144
|
catch (e) {
|
package/dist/config/schemas.js
CHANGED
|
@@ -26,6 +26,9 @@ export const ApocConfigSchema = LLMConfigSchema.extend({
|
|
|
26
26
|
working_dir: z.string().optional(),
|
|
27
27
|
timeout_ms: z.number().int().positive().optional(),
|
|
28
28
|
});
|
|
29
|
+
export const WebhookConfigSchema = z.object({
|
|
30
|
+
telegram_notify_all: z.boolean().optional(),
|
|
31
|
+
}).optional();
|
|
29
32
|
// Zod Schema matching MorpheusConfig interface
|
|
30
33
|
export const ConfigSchema = z.object({
|
|
31
34
|
agent: z.object({
|
|
@@ -35,6 +38,7 @@ export const ConfigSchema = z.object({
|
|
|
35
38
|
llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
|
|
36
39
|
sati: SatiConfigSchema.optional(),
|
|
37
40
|
apoc: ApocConfigSchema.optional(),
|
|
41
|
+
webhooks: WebhookConfigSchema,
|
|
38
42
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
39
43
|
memory: z.object({
|
|
40
44
|
limit: z.number().int().positive().optional(),
|
|
@@ -10,7 +10,7 @@ export const authMiddleware = (req, res, next) => {
|
|
|
10
10
|
// If password is not configured (using default), log a warning
|
|
11
11
|
if (!process.env.THE_ARCHITECT_PASS) {
|
|
12
12
|
const display = DisplayManager.getInstance();
|
|
13
|
-
display.log('Using default password for dashboard access. For security, set THE_ARCHITECT_PASS environment variable.', { source: 'http', level: 'warning' });
|
|
13
|
+
// display.log('Using default password for dashboard access. For security, set THE_ARCHITECT_PASS environment variable.', { source: 'http', level: 'warning' });
|
|
14
14
|
}
|
|
15
15
|
const providedPass = req.headers[AUTH_HEADER];
|
|
16
16
|
if (providedPass === architectPass) {
|
package/dist/http/server.js
CHANGED
|
@@ -6,7 +6,9 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { ConfigManager } from '../config/manager.js';
|
|
7
7
|
import { DisplayManager } from '../runtime/display.js';
|
|
8
8
|
import { createApiRouter } from './api.js';
|
|
9
|
+
import { createWebhooksRouter } from './webhooks-router.js';
|
|
9
10
|
import { authMiddleware } from './middleware/auth.js';
|
|
11
|
+
import { WebhookDispatcher } from '../runtime/webhooks/dispatcher.js';
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
13
|
const __dirname = path.dirname(__filename);
|
|
12
14
|
export class HttpServer {
|
|
@@ -16,6 +18,8 @@ export class HttpServer {
|
|
|
16
18
|
constructor(oracle) {
|
|
17
19
|
this.app = express();
|
|
18
20
|
this.oracle = oracle;
|
|
21
|
+
// Wire Oracle into the webhook dispatcher so triggers use the full agent
|
|
22
|
+
WebhookDispatcher.setOracle(oracle);
|
|
19
23
|
this.setupMiddleware();
|
|
20
24
|
this.setupRoutes();
|
|
21
25
|
}
|
|
@@ -45,6 +49,10 @@ export class HttpServer {
|
|
|
45
49
|
uptime: process.uptime()
|
|
46
50
|
});
|
|
47
51
|
});
|
|
52
|
+
// Webhooks router — mounted BEFORE the auth-guarded /api block.
|
|
53
|
+
// The trigger endpoint is public (validated via x-api-key header internally).
|
|
54
|
+
// All other webhook management endpoints apply authMiddleware internally.
|
|
55
|
+
this.app.use('/api/webhooks', createWebhooksRouter());
|
|
48
56
|
this.app.use('/api', authMiddleware, createApiRouter(this.oracle));
|
|
49
57
|
// Serve static frontend from compiled output
|
|
50
58
|
const uiPath = path.resolve(__dirname, '../ui');
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { WebhookRepository } from '../runtime/webhooks/repository.js';
|
|
4
|
+
import { WebhookDispatcher } from '../runtime/webhooks/dispatcher.js';
|
|
5
|
+
import { authMiddleware } from './middleware/auth.js';
|
|
6
|
+
import { DisplayManager } from '../runtime/display.js';
|
|
7
|
+
const CreateWebhookSchema = z.object({
|
|
8
|
+
name: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(100)
|
|
12
|
+
.regex(/^[a-z0-9-_]+$/, 'Name must be a slug: lowercase letters, numbers, hyphens, underscores only'),
|
|
13
|
+
prompt: z.string().min(1).max(10_000),
|
|
14
|
+
notification_channels: z.array(z.enum(['ui', 'telegram'])).min(1).default(['ui']),
|
|
15
|
+
});
|
|
16
|
+
const UpdateWebhookSchema = z.object({
|
|
17
|
+
name: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.max(100)
|
|
21
|
+
.regex(/^[a-z0-9-_]+$/, 'Name must be a slug')
|
|
22
|
+
.optional(),
|
|
23
|
+
prompt: z.string().min(1).max(10_000).optional(),
|
|
24
|
+
enabled: z.boolean().optional(),
|
|
25
|
+
notification_channels: z.array(z.enum(['ui', 'telegram'])).min(1).optional(),
|
|
26
|
+
});
|
|
27
|
+
const MarkReadSchema = z.object({
|
|
28
|
+
ids: z.array(z.string().uuid()).min(1),
|
|
29
|
+
});
|
|
30
|
+
export function createWebhooksRouter() {
|
|
31
|
+
const router = Router();
|
|
32
|
+
const repo = WebhookRepository.getInstance();
|
|
33
|
+
const display = DisplayManager.getInstance();
|
|
34
|
+
const dispatcher = new WebhookDispatcher();
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// PUBLIC TRIGGER ENDPOINT — no authMiddleware, api_key validated via header
|
|
37
|
+
// Must be declared BEFORE router.use(authMiddleware) below
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
router.post('/trigger/:webhook_name', async (req, res) => {
|
|
40
|
+
const { webhook_name } = req.params;
|
|
41
|
+
const apiKey = req.headers['x-api-key'];
|
|
42
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
43
|
+
return res.status(401).json({ error: 'Missing x-api-key header' });
|
|
44
|
+
}
|
|
45
|
+
const webhook = repo.getAndValidateWebhook(webhook_name, apiKey);
|
|
46
|
+
if (!webhook) {
|
|
47
|
+
// Intentionally ambiguous — don't reveal whether the name exists or is disabled
|
|
48
|
+
return res.status(401).json({ error: 'Invalid webhook name or api key' });
|
|
49
|
+
}
|
|
50
|
+
const payload = req.body ?? {};
|
|
51
|
+
// Create pending notification immediately
|
|
52
|
+
const notification = repo.createNotification({
|
|
53
|
+
webhook_id: webhook.id,
|
|
54
|
+
webhook_name: webhook.name,
|
|
55
|
+
payload: JSON.stringify(payload),
|
|
56
|
+
});
|
|
57
|
+
// Increment trigger counter
|
|
58
|
+
repo.recordTrigger(webhook.id);
|
|
59
|
+
display.log(`Webhook "${webhook.name}" triggered (notification: ${notification.id})`, { source: 'Webhooks' });
|
|
60
|
+
// Fire-and-forget — respond immediately with 202
|
|
61
|
+
setImmediate(() => {
|
|
62
|
+
dispatcher.dispatch(webhook, payload, notification.id).catch((err) => {
|
|
63
|
+
display.log(`Unhandled webhook dispatch error for "${webhook.name}": ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
return res.status(202).json({
|
|
67
|
+
accepted: true,
|
|
68
|
+
notification_id: notification.id,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// NOTIFICATION ENDPOINTS — declared before /:id to prevent route conflict
|
|
73
|
+
// (still public here, authMiddleware applied below protects management routes)
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
router.get('/notifications', authMiddleware, (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const { webhookId, unreadOnly } = req.query;
|
|
78
|
+
const notifications = repo.listNotifications({
|
|
79
|
+
webhookId: typeof webhookId === 'string' ? webhookId : undefined,
|
|
80
|
+
unreadOnly: unreadOnly === 'true',
|
|
81
|
+
});
|
|
82
|
+
res.json(notifications);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
res.status(500).json({ error: err.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
router.post('/notifications/read', authMiddleware, (req, res) => {
|
|
89
|
+
const parsed = MarkReadSchema.safeParse(req.body);
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
repo.markNotificationsRead(parsed.data.ids);
|
|
95
|
+
res.json({ success: true });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
res.status(500).json({ error: err.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
router.get('/notifications/unread-count', authMiddleware, (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const count = repo.countUnread();
|
|
104
|
+
res.json({ count });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
res.status(500).json({ error: err.message });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// WEBHOOK MANAGEMENT ENDPOINTS — all require authMiddleware
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
router.use(authMiddleware);
|
|
114
|
+
// GET /api/webhooks — list all webhooks
|
|
115
|
+
router.get('/', (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const webhooks = repo.listWebhooks();
|
|
118
|
+
res.json(webhooks);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
res.status(500).json({ error: err.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// POST /api/webhooks — create a webhook
|
|
125
|
+
router.post('/', (req, res) => {
|
|
126
|
+
const parsed = CreateWebhookSchema.safeParse(req.body);
|
|
127
|
+
if (!parsed.success) {
|
|
128
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const webhook = repo.createWebhook(parsed.data);
|
|
132
|
+
res.status(201).json(webhook);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// SQLite UNIQUE constraint error
|
|
136
|
+
if (err.message?.includes('UNIQUE constraint failed: webhooks.name')) {
|
|
137
|
+
return res.status(409).json({ error: `A webhook with name "${parsed.data.name}" already exists` });
|
|
138
|
+
}
|
|
139
|
+
res.status(500).json({ error: err.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// GET /api/webhooks/:id — get one webhook
|
|
143
|
+
router.get('/:id', (req, res) => {
|
|
144
|
+
const webhook = repo.getWebhookById(req.params.id);
|
|
145
|
+
if (!webhook)
|
|
146
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
147
|
+
res.json(webhook);
|
|
148
|
+
});
|
|
149
|
+
// PUT /api/webhooks/:id — update a webhook
|
|
150
|
+
router.put('/:id', (req, res) => {
|
|
151
|
+
const parsed = UpdateWebhookSchema.safeParse(req.body);
|
|
152
|
+
if (!parsed.success) {
|
|
153
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const updated = repo.updateWebhook(req.params.id, parsed.data);
|
|
157
|
+
if (!updated)
|
|
158
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
159
|
+
res.json(updated);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err.message?.includes('UNIQUE constraint failed: webhooks.name')) {
|
|
163
|
+
return res.status(409).json({ error: `A webhook with that name already exists` });
|
|
164
|
+
}
|
|
165
|
+
res.status(500).json({ error: err.message });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// DELETE /api/webhooks/:id — delete a webhook
|
|
169
|
+
router.delete('/:id', (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const deleted = repo.deleteWebhook(req.params.id);
|
|
172
|
+
if (!deleted)
|
|
173
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
174
|
+
res.json({ success: true });
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
res.status(500).json({ error: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return router;
|
|
181
|
+
}
|