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.
@@ -0,0 +1,199 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import { homedir } from 'os';
4
+ import fs from 'fs-extra';
5
+ import { randomUUID } from 'crypto';
6
+ export class WebhookRepository {
7
+ static instance = null;
8
+ db;
9
+ constructor() {
10
+ const dbPath = path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
11
+ fs.ensureDirSync(path.dirname(dbPath));
12
+ this.db = new Database(dbPath, { timeout: 5000 });
13
+ this.db.pragma('journal_mode = WAL');
14
+ this.db.pragma('foreign_keys = ON');
15
+ this.ensureTables();
16
+ }
17
+ static getInstance() {
18
+ if (!WebhookRepository.instance) {
19
+ WebhookRepository.instance = new WebhookRepository();
20
+ }
21
+ return WebhookRepository.instance;
22
+ }
23
+ ensureTables() {
24
+ this.db.exec(`
25
+ CREATE TABLE IF NOT EXISTS webhooks (
26
+ id TEXT PRIMARY KEY,
27
+ name TEXT NOT NULL UNIQUE,
28
+ api_key TEXT NOT NULL UNIQUE,
29
+ prompt TEXT NOT NULL,
30
+ enabled INTEGER NOT NULL DEFAULT 1,
31
+ notification_channels TEXT NOT NULL DEFAULT '["ui"]',
32
+ created_at INTEGER NOT NULL,
33
+ last_triggered_at INTEGER,
34
+ trigger_count INTEGER NOT NULL DEFAULT 0
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS idx_webhooks_name ON webhooks(name);
38
+ CREATE INDEX IF NOT EXISTS idx_webhooks_api_key ON webhooks(api_key);
39
+
40
+ CREATE TABLE IF NOT EXISTS webhook_notifications (
41
+ id TEXT PRIMARY KEY,
42
+ webhook_id TEXT NOT NULL,
43
+ webhook_name TEXT NOT NULL,
44
+ status TEXT NOT NULL DEFAULT 'pending',
45
+ payload TEXT NOT NULL,
46
+ result TEXT,
47
+ read INTEGER NOT NULL DEFAULT 0,
48
+ created_at INTEGER NOT NULL,
49
+ completed_at INTEGER,
50
+ FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE CASCADE
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_webhook_notifications_webhook_id
54
+ ON webhook_notifications(webhook_id);
55
+ CREATE INDEX IF NOT EXISTS idx_webhook_notifications_read
56
+ ON webhook_notifications(read);
57
+ CREATE INDEX IF NOT EXISTS idx_webhook_notifications_created_at
58
+ ON webhook_notifications(created_at DESC);
59
+ `);
60
+ }
61
+ // ─── Webhook CRUD ────────────────────────────────────────────────────────────
62
+ createWebhook(data) {
63
+ const id = randomUUID();
64
+ const api_key = randomUUID();
65
+ const now = Date.now();
66
+ this.db.prepare(`
67
+ INSERT INTO webhooks (id, name, api_key, prompt, enabled, notification_channels, created_at)
68
+ VALUES (?, ?, ?, ?, 1, ?, ?)
69
+ `).run(id, data.name, api_key, data.prompt, JSON.stringify(data.notification_channels), now);
70
+ return this.getWebhookById(id);
71
+ }
72
+ listWebhooks() {
73
+ const rows = this.db.prepare('SELECT * FROM webhooks ORDER BY created_at DESC').all();
74
+ return rows.map(this.deserializeWebhook);
75
+ }
76
+ getWebhookById(id) {
77
+ const row = this.db.prepare('SELECT * FROM webhooks WHERE id = ?').get(id);
78
+ return row ? this.deserializeWebhook(row) : null;
79
+ }
80
+ getWebhookByName(name) {
81
+ const row = this.db.prepare('SELECT * FROM webhooks WHERE name = ?').get(name);
82
+ return row ? this.deserializeWebhook(row) : null;
83
+ }
84
+ /**
85
+ * Looks up a webhook by name, then validates the api_key and enabled status.
86
+ * Returns null if not found, disabled, or api_key mismatch (caller decides error code).
87
+ */
88
+ getAndValidateWebhook(name, api_key) {
89
+ const row = this.db.prepare('SELECT * FROM webhooks WHERE name = ? AND enabled = 1').get(name);
90
+ if (!row)
91
+ return null;
92
+ const wh = this.deserializeWebhook(row);
93
+ if (wh.api_key !== api_key)
94
+ return null;
95
+ return wh;
96
+ }
97
+ updateWebhook(id, data) {
98
+ const existing = this.getWebhookById(id);
99
+ if (!existing)
100
+ return null;
101
+ const name = data.name ?? existing.name;
102
+ const prompt = data.prompt ?? existing.prompt;
103
+ const enabled = data.enabled !== undefined ? (data.enabled ? 1 : 0) : (existing.enabled ? 1 : 0);
104
+ const notification_channels = JSON.stringify(data.notification_channels ?? existing.notification_channels);
105
+ this.db.prepare(`
106
+ UPDATE webhooks
107
+ SET name = ?, prompt = ?, enabled = ?, notification_channels = ?
108
+ WHERE id = ?
109
+ `).run(name, prompt, enabled, notification_channels, id);
110
+ return this.getWebhookById(id);
111
+ }
112
+ deleteWebhook(id) {
113
+ const result = this.db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
114
+ return result.changes > 0;
115
+ }
116
+ recordTrigger(webhookId) {
117
+ this.db.prepare(`
118
+ UPDATE webhooks
119
+ SET trigger_count = trigger_count + 1, last_triggered_at = ?
120
+ WHERE id = ?
121
+ `).run(Date.now(), webhookId);
122
+ }
123
+ deserializeWebhook(row) {
124
+ return {
125
+ id: row.id,
126
+ name: row.name,
127
+ api_key: row.api_key,
128
+ prompt: row.prompt,
129
+ enabled: Boolean(row.enabled),
130
+ notification_channels: JSON.parse(row.notification_channels || '["ui"]'),
131
+ created_at: row.created_at,
132
+ last_triggered_at: row.last_triggered_at ?? null,
133
+ trigger_count: row.trigger_count ?? 0,
134
+ };
135
+ }
136
+ // ─── Notification CRUD ───────────────────────────────────────────────────────
137
+ createNotification(data) {
138
+ const id = randomUUID();
139
+ this.db.prepare(`
140
+ INSERT INTO webhook_notifications
141
+ (id, webhook_id, webhook_name, status, payload, read, created_at)
142
+ VALUES (?, ?, ?, 'pending', ?, 0, ?)
143
+ `).run(id, data.webhook_id, data.webhook_name, data.payload, Date.now());
144
+ return this.getNotificationById(id);
145
+ }
146
+ updateNotificationResult(id, status, result) {
147
+ this.db.prepare(`
148
+ UPDATE webhook_notifications
149
+ SET status = ?, result = ?, completed_at = ?
150
+ WHERE id = ?
151
+ `).run(status, result, Date.now(), id);
152
+ }
153
+ listNotifications(filters) {
154
+ let query = 'SELECT * FROM webhook_notifications WHERE 1=1';
155
+ const params = [];
156
+ if (filters?.webhookId) {
157
+ query += ' AND webhook_id = ?';
158
+ params.push(filters.webhookId);
159
+ }
160
+ if (filters?.unreadOnly) {
161
+ query += ' AND read = 0';
162
+ }
163
+ query += ' ORDER BY created_at DESC LIMIT 500';
164
+ const rows = this.db.prepare(query).all(...params);
165
+ return rows.map(this.deserializeNotification);
166
+ }
167
+ getNotificationById(id) {
168
+ const row = this.db.prepare('SELECT * FROM webhook_notifications WHERE id = ?').get(id);
169
+ return row ? this.deserializeNotification(row) : null;
170
+ }
171
+ markNotificationsRead(ids) {
172
+ const stmt = this.db.prepare('UPDATE webhook_notifications SET read = 1 WHERE id = ?');
173
+ const tx = this.db.transaction((list) => {
174
+ for (const id of list)
175
+ stmt.run(id);
176
+ });
177
+ tx(ids);
178
+ }
179
+ countUnread() {
180
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM webhook_notifications WHERE read = 0').get();
181
+ return row?.cnt ?? 0;
182
+ }
183
+ deserializeNotification(row) {
184
+ return {
185
+ id: row.id,
186
+ webhook_id: row.webhook_id,
187
+ webhook_name: row.webhook_name,
188
+ status: row.status,
189
+ payload: row.payload,
190
+ result: row.result ?? null,
191
+ read: Boolean(row.read),
192
+ created_at: row.created_at,
193
+ completed_at: row.completed_at ?? null,
194
+ };
195
+ }
196
+ close() {
197
+ this.db.close();
198
+ }
199
+ }
@@ -0,0 +1 @@
1
+ export {};