morpheus-cli 0.4.4 → 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 +23 -0
- 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/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;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { WebhookRepository } from './repository.js';
|
|
2
|
+
import { DisplayManager } from '../display.js';
|
|
3
|
+
export class WebhookDispatcher {
|
|
4
|
+
static telegramAdapter = null;
|
|
5
|
+
static oracle = null;
|
|
6
|
+
display = DisplayManager.getInstance();
|
|
7
|
+
/**
|
|
8
|
+
* Called at boot time after TelegramAdapter.connect() succeeds,
|
|
9
|
+
* so Telegram notifications can be dispatched from any trigger.
|
|
10
|
+
*/
|
|
11
|
+
static setTelegramAdapter(adapter) {
|
|
12
|
+
WebhookDispatcher.telegramAdapter = adapter;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Called at boot time with the Oracle instance so webhooks can use
|
|
16
|
+
* the full Oracle (MCPs, apoc_delegate, memory, etc.).
|
|
17
|
+
*/
|
|
18
|
+
static setOracle(oracle) {
|
|
19
|
+
WebhookDispatcher.oracle = oracle;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Main orchestration method — runs in background (fire-and-forget).
|
|
23
|
+
* 1. Builds the agent prompt from webhook.prompt + payload
|
|
24
|
+
* 2. Sends to Oracle (which can use MCPs or delegate to Apoc)
|
|
25
|
+
* 3. Persists result to DB
|
|
26
|
+
* 4. Dispatches to configured channels
|
|
27
|
+
*/
|
|
28
|
+
async dispatch(webhook, payload, notificationId) {
|
|
29
|
+
const repo = WebhookRepository.getInstance();
|
|
30
|
+
const oracle = WebhookDispatcher.oracle;
|
|
31
|
+
if (!oracle) {
|
|
32
|
+
const errMsg = 'Oracle not available — webhook cannot be processed.';
|
|
33
|
+
this.display.log(errMsg, { source: 'Webhooks', level: 'error' });
|
|
34
|
+
repo.updateNotificationResult(notificationId, 'failed', errMsg);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const message = this.buildPrompt(webhook.prompt, payload);
|
|
38
|
+
let result;
|
|
39
|
+
let status;
|
|
40
|
+
try {
|
|
41
|
+
result = await oracle.chat(message);
|
|
42
|
+
status = 'completed';
|
|
43
|
+
this.display.log(`Webhook "${webhook.name}" completed (notification: ${notificationId})`, { source: 'Webhooks', level: 'success' });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
result = `Execution error: ${err.message}`;
|
|
47
|
+
status = 'failed';
|
|
48
|
+
this.display.log(`Webhook "${webhook.name}" failed: ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
49
|
+
}
|
|
50
|
+
// Persist result
|
|
51
|
+
repo.updateNotificationResult(notificationId, status, result);
|
|
52
|
+
// Dispatch to configured channels
|
|
53
|
+
for (const channel of webhook.notification_channels) {
|
|
54
|
+
if (channel === 'telegram') {
|
|
55
|
+
await this.sendTelegram(webhook.name, result, status);
|
|
56
|
+
}
|
|
57
|
+
// 'ui' channel is handled by UI polling — nothing extra needed here
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Combines the user-authored webhook prompt with the received payload.
|
|
62
|
+
*/
|
|
63
|
+
buildPrompt(webhookPrompt, payload) {
|
|
64
|
+
const payloadStr = JSON.stringify(payload, null, 2);
|
|
65
|
+
return `${webhookPrompt}
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
RECEIVED WEBHOOK PAYLOAD:
|
|
69
|
+
\`\`\`json
|
|
70
|
+
${payloadStr}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
Analyze the payload above and follow the instructions provided. Be concise and actionable in your response.`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sends a formatted Telegram message to all allowed users.
|
|
77
|
+
* Silently skips if the adapter is not connected.
|
|
78
|
+
*/
|
|
79
|
+
async sendTelegram(webhookName, result, status) {
|
|
80
|
+
const adapter = WebhookDispatcher.telegramAdapter;
|
|
81
|
+
if (!adapter) {
|
|
82
|
+
this.display.log('Telegram notification skipped — adapter not connected.', { source: 'Webhooks', level: 'warning' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const icon = status === 'completed' ? '✅' : '❌';
|
|
87
|
+
const truncated = result.length > 3500 ? result.slice(0, 3500) + '…' : result;
|
|
88
|
+
const message = `${icon} *Webhook: ${webhookName}*\n\n${truncated}`;
|
|
89
|
+
await adapter.sendMessage(message);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
this.display.log(`Failed to send Telegram notification for webhook "${webhookName}": ${err.message}`, { source: 'Webhooks', level: 'error' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|