myceliumail 1.0.2 → 1.0.3
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/.context7 +87 -0
- package/.eslintrc.json +29 -0
- package/COMPLETE.md +51 -0
- package/MYCELIUMAIL_STARTER_KIT.md +603 -0
- package/NEXT_STEPS.md +96 -0
- package/desktop/README.md +102 -0
- package/desktop/assets/icon.icns +0 -0
- package/desktop/assets/icon.iconset/icon_128x128.png +0 -0
- package/desktop/assets/icon.iconset/icon_128x128@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512@2x.png +0 -0
- package/desktop/assets/icon.png +0 -0
- package/desktop/assets/tray-icon.png +0 -0
- package/desktop/main.js +257 -0
- package/desktop/package-lock.json +4198 -0
- package/desktop/package.json +48 -0
- package/desktop/preload.js +11 -0
- package/dist/bin/myceliumail.js +2 -0
- package/dist/bin/myceliumail.js.map +1 -1
- package/dist/commands/key-announce.d.ts +6 -0
- package/dist/commands/key-announce.d.ts.map +1 -0
- package/dist/commands/key-announce.js +63 -0
- package/dist/commands/key-announce.js.map +1 -0
- package/docs/20251215_Treebird-Ecosystem_Knowledge-Base_v2.md +292 -0
- package/docs/20251215_Treebird-Ecosystem_Project-Instructions_v2.md +176 -0
- package/docs/AGENT_DELEGATION_WORKFLOW.md +453 -0
- package/docs/AGENT_STARTER_KIT.md +145 -0
- package/docs/ANNOUNCEMENT_DRAFTS.md +55 -0
- package/docs/DASHBOARD_AGENT_HANDOFF.md +429 -0
- package/docs/DASHBOARD_AGENT_PROMPT.md +32 -0
- package/docs/DASHBOARD_BUILD_ROADMAP.md +61 -0
- package/docs/DEPLOYMENT.md +59 -0
- package/docs/LESSONS_LEARNED.md +127 -0
- package/docs/MCP_PUBLISHING_ROADMAP.md +113 -0
- package/docs/MCP_STARTER_KIT.md +117 -0
- package/docs/SSAN_MESSAGES_SUMMARY.md +92 -0
- package/docs/STORAGE_ARCHITECTURE.md +114 -0
- package/mcp-server/README.md +143 -0
- package/mcp-server/assets/icon.png +0 -0
- package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
- package/mcp-server/package-lock.json +1142 -0
- package/mcp-server/package.json +49 -0
- package/mcp-server/src/lib/config.ts +55 -0
- package/mcp-server/src/lib/crypto.ts +150 -0
- package/mcp-server/src/lib/storage.ts +267 -0
- package/mcp-server/src/server.ts +387 -0
- package/mcp-server/tsconfig.json +26 -0
- package/package.json +3 -3
- package/src/bin/myceliumail.ts +54 -0
- package/src/commands/broadcast.ts +70 -0
- package/src/commands/dashboard.ts +19 -0
- package/src/commands/inbox.ts +75 -0
- package/src/commands/key-announce.ts +70 -0
- package/src/commands/key-import.ts +35 -0
- package/src/commands/keygen.ts +44 -0
- package/src/commands/keys.ts +55 -0
- package/src/commands/read.ts +97 -0
- package/src/commands/send.ts +89 -0
- package/src/commands/watch.ts +101 -0
- package/src/dashboard/public/app.js +523 -0
- package/src/dashboard/public/index.html +75 -0
- package/src/dashboard/public/styles.css +68 -0
- package/src/dashboard/routes.ts +128 -0
- package/src/dashboard/server.ts +33 -0
- package/src/lib/config.ts +104 -0
- package/src/lib/crypto.ts +210 -0
- package/src/lib/realtime.ts +109 -0
- package/src/storage/local.ts +209 -0
- package/src/storage/supabase.ts +336 -0
- package/src/types/index.ts +53 -0
- package/supabase/migrations/000_myceliumail_setup.sql +93 -0
- package/supabase/migrations/001_enable_realtime.sql +10 -0
- 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');
|