myceliumail 1.0.2 → 1.0.4

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 (69) hide show
  1. package/.context7 +87 -0
  2. package/.eslintrc.json +29 -0
  3. package/.github/workflows/publish.yml +108 -0
  4. package/CHANGELOG.md +85 -0
  5. package/README.md +295 -162
  6. package/desktop/README.md +102 -0
  7. package/desktop/assets/icon.icns +0 -0
  8. package/desktop/assets/icon.iconset/icon_128x128.png +0 -0
  9. package/desktop/assets/icon.iconset/icon_128x128@2x.png +0 -0
  10. package/desktop/assets/icon.iconset/icon_16x16.png +0 -0
  11. package/desktop/assets/icon.iconset/icon_16x16@2x.png +0 -0
  12. package/desktop/assets/icon.iconset/icon_256x256.png +0 -0
  13. package/desktop/assets/icon.iconset/icon_256x256@2x.png +0 -0
  14. package/desktop/assets/icon.iconset/icon_32x32.png +0 -0
  15. package/desktop/assets/icon.iconset/icon_32x32@2x.png +0 -0
  16. package/desktop/assets/icon.iconset/icon_512x512.png +0 -0
  17. package/desktop/assets/icon.iconset/icon_512x512@2x.png +0 -0
  18. package/desktop/assets/icon.png +0 -0
  19. package/desktop/assets/tray-icon.png +0 -0
  20. package/desktop/main.js +257 -0
  21. package/desktop/package-lock.json +4198 -0
  22. package/desktop/package.json +48 -0
  23. package/desktop/preload.js +11 -0
  24. package/dist/bin/myceliumail.js +2 -0
  25. package/dist/bin/myceliumail.js.map +1 -1
  26. package/dist/commands/key-announce.d.ts +6 -0
  27. package/dist/commands/key-announce.d.ts.map +1 -0
  28. package/dist/commands/key-announce.js +63 -0
  29. package/dist/commands/key-announce.js.map +1 -0
  30. package/docs/AGENT_STARTER_KIT.md +145 -0
  31. package/docs/DEPLOYMENT.md +59 -0
  32. package/docs/LESSONS_LEARNED.md +127 -0
  33. package/docs/MCP_STARTER_KIT.md +117 -0
  34. package/mcp-server/README.md +143 -0
  35. package/mcp-server/assets/icon.png +0 -0
  36. package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
  37. package/mcp-server/package-lock.json +1141 -0
  38. package/mcp-server/package.json +50 -0
  39. package/mcp-server/src/lib/config.ts +55 -0
  40. package/mcp-server/src/lib/crypto.ts +150 -0
  41. package/mcp-server/src/lib/storage.ts +267 -0
  42. package/mcp-server/src/server.ts +387 -0
  43. package/mcp-server/tsconfig.json +26 -0
  44. package/package.json +13 -4
  45. package/src/bin/myceliumail.ts +54 -0
  46. package/src/commands/broadcast.ts +70 -0
  47. package/src/commands/dashboard.ts +19 -0
  48. package/src/commands/inbox.ts +75 -0
  49. package/src/commands/key-announce.ts +70 -0
  50. package/src/commands/key-import.ts +35 -0
  51. package/src/commands/keygen.ts +44 -0
  52. package/src/commands/keys.ts +55 -0
  53. package/src/commands/read.ts +97 -0
  54. package/src/commands/send.ts +89 -0
  55. package/src/commands/watch.ts +101 -0
  56. package/src/dashboard/public/app.js +523 -0
  57. package/src/dashboard/public/index.html +75 -0
  58. package/src/dashboard/public/styles.css +68 -0
  59. package/src/dashboard/routes.ts +128 -0
  60. package/src/dashboard/server.ts +33 -0
  61. package/src/lib/config.ts +104 -0
  62. package/src/lib/crypto.ts +210 -0
  63. package/src/lib/realtime.ts +109 -0
  64. package/src/storage/local.ts +209 -0
  65. package/src/storage/supabase.ts +336 -0
  66. package/src/types/index.ts +53 -0
  67. package/supabase/migrations/000_myceliumail_setup.sql +93 -0
  68. package/supabase/migrations/001_enable_realtime.sql +10 -0
  69. package/tsconfig.json +28 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Local JSON Storage Adapter
3
+ *
4
+ * Stores messages in a local JSON file for offline/testing use.
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import { randomUUID } from 'crypto';
11
+ import type { Message, InboxOptions } from '../types/index.js';
12
+
13
+ const DATA_DIR = join(homedir(), '.myceliumail', 'data');
14
+ const MESSAGES_FILE = join(DATA_DIR, 'messages.json');
15
+
16
+ interface StoredMessage extends Omit<Message, 'createdAt'> {
17
+ createdAt: string;
18
+ }
19
+
20
+ /**
21
+ * Ensure data directory exists
22
+ */
23
+ function ensureDataDir(): void {
24
+ if (!existsSync(DATA_DIR)) {
25
+ mkdirSync(DATA_DIR, { recursive: true });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Load all messages from storage
31
+ */
32
+ function loadMessages(): StoredMessage[] {
33
+ if (!existsSync(MESSAGES_FILE)) return [];
34
+ try {
35
+ return JSON.parse(readFileSync(MESSAGES_FILE, 'utf-8'));
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Save messages to storage
43
+ */
44
+ function saveMessages(messages: StoredMessage[]): void {
45
+ ensureDataDir();
46
+ writeFileSync(MESSAGES_FILE, JSON.stringify(messages, null, 2));
47
+ }
48
+
49
+ /**
50
+ * Convert stored message to Message type
51
+ */
52
+ function toMessage(stored: StoredMessage): Message {
53
+ return {
54
+ ...stored,
55
+ createdAt: new Date(stored.createdAt),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Send a message (store locally)
61
+ */
62
+ export async function sendMessage(
63
+ sender: string,
64
+ recipient: string | string[],
65
+ subject: string,
66
+ body: string,
67
+ options?: {
68
+ encrypted?: boolean;
69
+ ciphertext?: string;
70
+ nonce?: string;
71
+ senderPublicKey?: string;
72
+ attachments?: { name: string; type: string; data: string; size: number }[];
73
+ }
74
+ ): Promise<Message> {
75
+ const messages = loadMessages();
76
+
77
+ // Handle multi-recipient
78
+ const recipientList = Array.isArray(recipient) ? recipient : [recipient];
79
+ const primaryRecipient = recipientList[0];
80
+
81
+ const newMessage: StoredMessage = {
82
+ id: randomUUID(),
83
+ sender,
84
+ recipient: primaryRecipient,
85
+ recipients: recipientList.length > 1 ? recipientList : undefined,
86
+ subject: options?.encrypted ? '' : subject,
87
+ body: options?.encrypted ? '' : body,
88
+ encrypted: options?.encrypted || false,
89
+ ciphertext: options?.ciphertext,
90
+ nonce: options?.nonce,
91
+ senderPublicKey: options?.senderPublicKey,
92
+ attachments: options?.attachments,
93
+ read: false,
94
+ archived: false,
95
+ createdAt: new Date().toISOString(),
96
+ };
97
+
98
+ messages.push(newMessage);
99
+ saveMessages(messages);
100
+
101
+ return toMessage(newMessage);
102
+ }
103
+
104
+ /**
105
+ * Get inbox messages for an agent (use 'all' for all messages)
106
+ */
107
+ export async function getInbox(agentId: string, options?: InboxOptions): Promise<Message[]> {
108
+ const messages = loadMessages();
109
+
110
+ let filtered = agentId === 'all'
111
+ ? messages.filter(m => !m.archived)
112
+ : messages.filter(m =>
113
+ (m.recipient === agentId || m.recipients?.includes(agentId)) && !m.archived
114
+ );
115
+
116
+ if (options?.unreadOnly) {
117
+ filtered = filtered.filter(m => !m.read);
118
+ }
119
+
120
+ // Sort by date descending (newest first)
121
+ filtered.sort((a, b) =>
122
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
123
+ );
124
+
125
+ if (options?.limit) {
126
+ filtered = filtered.slice(0, options.limit);
127
+ }
128
+
129
+ return filtered.map(toMessage);
130
+ }
131
+
132
+ /**
133
+ * Get a specific message by ID
134
+ */
135
+ export async function getMessage(id: string): Promise<Message | null> {
136
+ const messages = loadMessages();
137
+ const found = messages.find(m => m.id === id);
138
+ return found ? toMessage(found) : null;
139
+ }
140
+
141
+ /**
142
+ * Mark message as read by an agent
143
+ */
144
+ export async function markAsRead(id: string, agentId?: string): Promise<boolean> {
145
+ const messages = loadMessages();
146
+ const index = messages.findIndex(m => m.id === id);
147
+ if (index === -1) return false;
148
+
149
+ // Initialize readBy if not present
150
+ if (!messages[index].readBy) {
151
+ messages[index].readBy = [];
152
+ }
153
+
154
+ // Add agent to readBy if provided and not already there
155
+ if (agentId && !messages[index].readBy!.includes(agentId)) {
156
+ messages[index].readBy!.push(agentId);
157
+ }
158
+
159
+ // Legacy compat
160
+ messages[index].read = true;
161
+
162
+ saveMessages(messages);
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Delete a message
168
+ */
169
+ export async function deleteMessage(id: string): Promise<boolean> {
170
+ const messages = loadMessages();
171
+ const index = messages.findIndex(m => m.id === id);
172
+ if (index === -1) return false;
173
+
174
+ messages.splice(index, 1);
175
+ saveMessages(messages);
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Archive a message
181
+ */
182
+ export async function archiveMessage(id: string): Promise<boolean> {
183
+ const messages = loadMessages();
184
+ const index = messages.findIndex(m => m.id === id);
185
+ if (index === -1) return false;
186
+
187
+ messages[index].archived = true;
188
+ saveMessages(messages);
189
+ return true;
190
+ }
191
+
192
+ /**
193
+ * Get sent messages
194
+ */
195
+ export async function getSent(agentId: string, limit?: number): Promise<Message[]> {
196
+ const messages = loadMessages();
197
+
198
+ let filtered = messages.filter(m => m.sender === agentId);
199
+
200
+ filtered.sort((a, b) =>
201
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
202
+ );
203
+
204
+ if (limit) {
205
+ filtered = filtered.slice(0, limit);
206
+ }
207
+
208
+ return filtered.map(toMessage);
209
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Supabase Storage Adapter
3
+ *
4
+ * Stores messages in Supabase PostgreSQL for cloud sync.
5
+ * Falls back to local storage if Supabase is not configured.
6
+ */
7
+
8
+ import { loadConfig, hasSupabaseConfig } from '../lib/config.js';
9
+ import type { Message, InboxOptions } from '../types/index.js';
10
+ import * as local from './local.js';
11
+
12
+ // Simple fetch-based Supabase client (no dependencies)
13
+ interface SupabaseClient {
14
+ url: string;
15
+ key: string;
16
+ }
17
+
18
+ function createClient(): SupabaseClient | null {
19
+ const config = loadConfig();
20
+
21
+ // Force local storage if mode is 'local'
22
+ if (config.storageMode === 'local') {
23
+ return null;
24
+ }
25
+
26
+ // Check if Supabase is configured
27
+ if (!hasSupabaseConfig(config)) {
28
+ // Error if mode is 'supabase' but not configured
29
+ if (config.storageMode === 'supabase') {
30
+ console.error('❌ MYCELIUMAIL_STORAGE=supabase but Supabase not configured!');
31
+ console.error(' Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.');
32
+ }
33
+ return null;
34
+ }
35
+
36
+ return {
37
+ url: config.supabaseUrl!,
38
+ key: config.supabaseKey!,
39
+ };
40
+ }
41
+
42
+ async function supabaseRequest<T>(
43
+ client: SupabaseClient,
44
+ path: string,
45
+ options: RequestInit = {}
46
+ ): Promise<T> {
47
+ const url = `${client.url}/rest/v1${path}`;
48
+
49
+ // Create abort controller for timeout
50
+ const controller = new AbortController();
51
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
52
+
53
+ try {
54
+ const response = await fetch(url, {
55
+ ...options,
56
+ signal: controller.signal,
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'apikey': client.key,
60
+ 'Authorization': `Bearer ${client.key}`,
61
+ 'Prefer': options.method === 'POST' ? 'return=representation' : 'return=minimal',
62
+ ...options.headers,
63
+ },
64
+ });
65
+
66
+ clearTimeout(timeoutId);
67
+
68
+ if (!response.ok) {
69
+ const error = await response.text();
70
+ throw new Error(`Supabase error: ${error}`);
71
+ }
72
+
73
+ if (response.status === 204) return {} as T;
74
+ return response.json() as Promise<T>;
75
+ } catch (error) {
76
+ clearTimeout(timeoutId);
77
+ if (error instanceof Error && error.name === 'AbortError') {
78
+ throw new Error('Supabase request timed out after 10 seconds');
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Send a message via Supabase
86
+ */
87
+ export async function sendMessage(
88
+ sender: string,
89
+ recipient: string | string[],
90
+ subject: string,
91
+ body: string,
92
+ options?: {
93
+ encrypted?: boolean;
94
+ ciphertext?: string;
95
+ nonce?: string;
96
+ senderPublicKey?: string;
97
+ attachments?: { name: string; type: string; data: string; size: number }[];
98
+ }
99
+ ): Promise<Message> {
100
+ const client = createClient();
101
+
102
+ // Fall back to local if no Supabase
103
+ if (!client) {
104
+ return local.sendMessage(sender, recipient, subject, body, options);
105
+ }
106
+
107
+ const payload = {
108
+ from_agent: sender,
109
+ to_agent: recipient,
110
+ subject: options?.encrypted ? '🔒 [Encrypted Message]' : subject,
111
+ message: options?.encrypted ? JSON.stringify({
112
+ ciphertext: options?.ciphertext,
113
+ nonce: options?.nonce,
114
+ sender_public_key: options?.senderPublicKey,
115
+ }) : body,
116
+ encrypted: options?.encrypted || false,
117
+ };
118
+
119
+ const [result] = await supabaseRequest<Array<{
120
+ id: string;
121
+ from_agent: string;
122
+ to_agent: string;
123
+ subject: string;
124
+ message: string;
125
+ encrypted: boolean;
126
+ read: boolean;
127
+ created_at: string;
128
+ }>>(client, '/agent_messages', {
129
+ method: 'POST',
130
+ body: JSON.stringify(payload),
131
+ });
132
+
133
+ // Parse encrypted message if needed
134
+ let parsedMessage = result.message;
135
+ let ciphertext, nonce, senderPublicKey;
136
+ if (result.encrypted && result.message) {
137
+ try {
138
+ const encrypted = JSON.parse(result.message);
139
+ ciphertext = encrypted.ciphertext;
140
+ nonce = encrypted.nonce;
141
+ senderPublicKey = encrypted.sender_public_key;
142
+ } catch {
143
+ // Not JSON, treat as plaintext
144
+ }
145
+ }
146
+
147
+ return {
148
+ id: result.id,
149
+ sender: result.from_agent,
150
+ recipient: result.to_agent,
151
+ subject: result.subject || '',
152
+ body: result.encrypted ? '' : result.message,
153
+ encrypted: result.encrypted,
154
+ ciphertext,
155
+ nonce,
156
+ senderPublicKey,
157
+ read: result.read,
158
+ archived: false, // Not in response
159
+ createdAt: new Date(result.created_at),
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Get inbox messages from Supabase
165
+ */
166
+ export async function getInbox(agentId: string, options?: InboxOptions): Promise<Message[]> {
167
+ const client = createClient();
168
+
169
+ if (!client) {
170
+ return local.getInbox(agentId, options);
171
+ }
172
+
173
+ let query = `/agent_messages?to_agent=eq.${agentId}&order=created_at.desc`;
174
+
175
+ if (options?.unreadOnly) {
176
+ query += '&read=eq.false';
177
+ }
178
+
179
+ if (options?.limit) {
180
+ query += `&limit=${options.limit}`;
181
+ }
182
+
183
+ const results = await supabaseRequest<Array<{
184
+ id: string;
185
+ from_agent: string;
186
+ to_agent: string;
187
+ subject: string;
188
+ message: string;
189
+ encrypted: boolean;
190
+ read: boolean;
191
+ created_at: string;
192
+ }>>(client, query);
193
+
194
+ return results.map(r => {
195
+ // Parse encrypted message
196
+ let ciphertext, nonce, senderPublicKey, body = r.message;
197
+ if (r.encrypted && r.message) {
198
+ try {
199
+ const enc = JSON.parse(r.message);
200
+ ciphertext = enc.ciphertext;
201
+ nonce = enc.nonce;
202
+ senderPublicKey = enc.sender_public_key;
203
+ body = '';
204
+ } catch { }
205
+ }
206
+ return {
207
+ id: r.id,
208
+ sender: r.from_agent,
209
+ recipient: r.to_agent,
210
+ subject: r.subject || '',
211
+ body,
212
+ encrypted: r.encrypted,
213
+ ciphertext,
214
+ nonce,
215
+ senderPublicKey,
216
+ read: r.read,
217
+ archived: false,
218
+ createdAt: new Date(r.created_at),
219
+ };
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Get a specific message
225
+ */
226
+ export async function getMessage(id: string): Promise<Message | null> {
227
+ const client = createClient();
228
+
229
+ if (!client) {
230
+ return local.getMessage(id);
231
+ }
232
+
233
+ const results = await supabaseRequest<Array<{
234
+ id: string;
235
+ from_agent: string;
236
+ to_agent: string;
237
+ subject: string;
238
+ message: string;
239
+ encrypted: boolean;
240
+ read: boolean;
241
+ created_at: string;
242
+ }>>(client, `/agent_messages?id=eq.${id}`);
243
+
244
+ if (results.length === 0) return null;
245
+
246
+ const r = results[0];
247
+
248
+ // Parse encrypted message
249
+ let ciphertext, nonce, senderPublicKey, body = r.message;
250
+ if (r.encrypted && r.message) {
251
+ try {
252
+ const enc = JSON.parse(r.message);
253
+ ciphertext = enc.ciphertext;
254
+ nonce = enc.nonce;
255
+ senderPublicKey = enc.sender_public_key;
256
+ body = '';
257
+ } catch { }
258
+ }
259
+
260
+ return {
261
+ id: r.id,
262
+ sender: r.from_agent,
263
+ recipient: r.to_agent,
264
+ subject: r.subject || '',
265
+ body,
266
+ encrypted: r.encrypted,
267
+ ciphertext,
268
+ nonce,
269
+ senderPublicKey,
270
+ read: r.read,
271
+ archived: false,
272
+ createdAt: new Date(r.created_at),
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Mark message as read
278
+ */
279
+ export async function markAsRead(id: string, agentId?: string): Promise<boolean> {
280
+ const client = createClient();
281
+
282
+ if (!client) {
283
+ return local.markAsRead(id, agentId);
284
+ }
285
+
286
+ try {
287
+ await supabaseRequest(client, `/agent_messages?id=eq.${id}`, {
288
+ method: 'PATCH',
289
+ body: JSON.stringify({ read: true }),
290
+ });
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Archive a message
299
+ */
300
+ export async function archiveMessage(id: string): Promise<boolean> {
301
+ const client = createClient();
302
+
303
+ if (!client) {
304
+ return local.archiveMessage(id);
305
+ }
306
+
307
+ try {
308
+ await supabaseRequest(client, `/agent_messages?id=eq.${id}`, {
309
+ method: 'PATCH',
310
+ body: JSON.stringify({ archived: true }),
311
+ });
312
+ return true;
313
+ } catch {
314
+ return false;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Delete a message
320
+ */
321
+ export async function deleteMessage(id: string): Promise<boolean> {
322
+ const client = createClient();
323
+
324
+ if (!client) {
325
+ return local.deleteMessage(id);
326
+ }
327
+
328
+ try {
329
+ await supabaseRequest(client, `/agent_messages?id=eq.${id}`, {
330
+ method: 'DELETE',
331
+ });
332
+ return true;
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Myceliumail Type Definitions
3
+ */
4
+
5
+ export interface Attachment {
6
+ name: string; // filename
7
+ type: string; // MIME type
8
+ data: string; // base64 encoded
9
+ size: number; // original size in bytes
10
+ }
11
+
12
+ export interface Message {
13
+ id: string;
14
+ sender: string;
15
+ recipient: string; // Primary recipient (for backwards compat)
16
+ recipients?: string[]; // Multi-recipient support
17
+ subject: string;
18
+ body: string;
19
+ encrypted: boolean;
20
+ ciphertext?: string;
21
+ nonce?: string;
22
+ senderPublicKey?: string;
23
+ read?: boolean; // Legacy - kept for backwards compat
24
+ readBy?: string[]; // Array of agents who read this
25
+ archived: boolean;
26
+ attachments?: Attachment[];
27
+ createdAt: Date;
28
+ }
29
+
30
+ export interface Channel {
31
+ name: string;
32
+ description?: string;
33
+ isPublic: boolean;
34
+ createdBy: string;
35
+ createdAt: Date;
36
+ }
37
+
38
+ export interface Agent {
39
+ id: string;
40
+ publicKey?: string;
41
+ status?: string;
42
+ lastSeen?: Date;
43
+ }
44
+
45
+ export interface SendOptions {
46
+ encrypt?: boolean;
47
+ priority?: 'low' | 'normal' | 'high' | 'urgent';
48
+ }
49
+
50
+ export interface InboxOptions {
51
+ unreadOnly?: boolean;
52
+ limit?: number;
53
+ }
@@ -0,0 +1,93 @@
1
+ -- Myceliumail: Agent Messaging with E2E Encryption
2
+ -- Apply to Supabase to enable cloud message storage
3
+
4
+ -- Agent messages table with encryption support
5
+ CREATE TABLE IF NOT EXISTS agent_messages (
6
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7
+ sender text NOT NULL,
8
+ recipient text NOT NULL,
9
+ subject text,
10
+ body text,
11
+
12
+ -- Encryption fields (NaCl box)
13
+ encrypted boolean DEFAULT false,
14
+ ciphertext text, -- base64 encrypted payload
15
+ nonce text, -- base64 nonce
16
+ sender_public_key text, -- base64 sender's public key
17
+
18
+ -- Message state
19
+ read boolean DEFAULT false,
20
+ archived boolean DEFAULT false,
21
+
22
+ -- Metadata
23
+ message_type text DEFAULT 'direct', -- direct, channel, broadcast, system
24
+ thread_id uuid, -- for threaded conversations
25
+ reply_to uuid, -- parent message
26
+ priority text DEFAULT 'normal', -- low, normal, high, urgent
27
+
28
+ created_at timestamptz DEFAULT now(),
29
+ updated_at timestamptz DEFAULT now()
30
+ );
31
+
32
+ -- Indexes for common queries
33
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_recipient
34
+ ON agent_messages(recipient, read, archived);
35
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_sender
36
+ ON agent_messages(sender, created_at);
37
+ CREATE INDEX IF NOT EXISTS idx_agent_messages_thread
38
+ ON agent_messages(thread_id) WHERE thread_id IS NOT NULL;
39
+
40
+ -- Agent public keys registry
41
+ CREATE TABLE IF NOT EXISTS agent_keys (
42
+ agent_id text PRIMARY KEY,
43
+ public_key text NOT NULL, -- base64 encoded
44
+ created_at timestamptz DEFAULT now(),
45
+ updated_at timestamptz DEFAULT now()
46
+ );
47
+
48
+ -- Channels table
49
+ CREATE TABLE IF NOT EXISTS channels (
50
+ name text PRIMARY KEY,
51
+ description text,
52
+ is_public boolean DEFAULT true,
53
+ created_by text NOT NULL,
54
+ created_at timestamptz DEFAULT now()
55
+ );
56
+
57
+ -- Channel membership
58
+ CREATE TABLE IF NOT EXISTS channel_members (
59
+ channel_name text REFERENCES channels(name) ON DELETE CASCADE,
60
+ agent_id text,
61
+ joined_at timestamptz DEFAULT now(),
62
+ notify_level text DEFAULT 'all', -- all, mentions, none
63
+ PRIMARY KEY (channel_name, agent_id)
64
+ );
65
+
66
+ -- RLS Policies
67
+ ALTER TABLE agent_messages ENABLE ROW LEVEL SECURITY;
68
+ ALTER TABLE agent_keys ENABLE ROW LEVEL SECURITY;
69
+
70
+ -- Allow all authenticated access (agents auth via service key)
71
+ CREATE POLICY "Agents can read their own messages"
72
+ ON agent_messages FOR SELECT
73
+ USING (true);
74
+
75
+ CREATE POLICY "Agents can insert messages"
76
+ ON agent_messages FOR INSERT
77
+ WITH CHECK (true);
78
+
79
+ CREATE POLICY "Agents can update their own received messages"
80
+ ON agent_messages FOR UPDATE
81
+ USING (true);
82
+
83
+ CREATE POLICY "Anyone can read public keys"
84
+ ON agent_keys FOR SELECT
85
+ USING (true);
86
+
87
+ CREATE POLICY "Agents can insert their own key"
88
+ ON agent_keys FOR INSERT
89
+ WITH CHECK (true);
90
+
91
+ CREATE POLICY "Agents can update their own key"
92
+ ON agent_keys FOR UPDATE
93
+ USING (true);
@@ -0,0 +1,10 @@
1
+ -- Enable Realtime for agent_messages table
2
+ -- This allows the mycmail watch command to receive push notifications
3
+
4
+ -- Enable Realtime publication for agent_messages table
5
+ ALTER PUBLICATION supabase_realtime ADD TABLE agent_messages;
6
+
7
+ -- Note: By default, Realtime listens to all events (INSERT, UPDATE, DELETE).
8
+ -- If you want to limit to specific events, use:
9
+ -- ALTER PUBLICATION supabase_realtime ADD TABLE ONLY agent_messages
10
+ -- WITH (publish = 'insert');