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.
- package/.context7 +87 -0
- package/.eslintrc.json +29 -0
- package/.github/workflows/publish.yml +108 -0
- package/CHANGELOG.md +85 -0
- package/README.md +295 -162
- 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/AGENT_STARTER_KIT.md +145 -0
- package/docs/DEPLOYMENT.md +59 -0
- package/docs/LESSONS_LEARNED.md +127 -0
- package/docs/MCP_STARTER_KIT.md +117 -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 +1141 -0
- package/mcp-server/package.json +50 -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 +13 -4
- 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,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Myceliumail MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes Myceliumail messaging as MCP tools for Claude Desktop
|
|
7
|
+
* and other MCP-compatible clients.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
import * as crypto from './lib/crypto.js';
|
|
15
|
+
import * as storage from './lib/storage.js';
|
|
16
|
+
import { getAgentId } from './lib/config.js';
|
|
17
|
+
|
|
18
|
+
// Create the MCP server
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: 'myceliumail',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Tool: check_inbox
|
|
25
|
+
server.tool(
|
|
26
|
+
'check_inbox',
|
|
27
|
+
'Check your Myceliumail inbox for messages',
|
|
28
|
+
{
|
|
29
|
+
unread_only: z.boolean().optional().describe('Only show unread messages'),
|
|
30
|
+
limit: z.number().optional().describe('Maximum number of messages to return'),
|
|
31
|
+
},
|
|
32
|
+
async ({ unread_only, limit }) => {
|
|
33
|
+
const agentId = getAgentId();
|
|
34
|
+
const messages = await storage.getInbox(agentId, {
|
|
35
|
+
unreadOnly: unread_only,
|
|
36
|
+
limit: limit || 10,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (messages.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: '📭 No messages in inbox' }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const formatted = messages.map(msg => {
|
|
46
|
+
const status = msg.read ? ' ' : '● ';
|
|
47
|
+
const encrypted = msg.encrypted ? '🔐 ' : '';
|
|
48
|
+
return `${status}${encrypted}[${msg.id.slice(0, 8)}] From: ${msg.sender} | ${msg.subject || '(no subject)'} | ${msg.createdAt.toLocaleString()}`;
|
|
49
|
+
}).join('\n');
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: `📬 Inbox (${messages.length} messages):\n\n${formatted}`
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Tool: read_message
|
|
61
|
+
server.tool(
|
|
62
|
+
'read_message',
|
|
63
|
+
'Read a specific message by ID',
|
|
64
|
+
{
|
|
65
|
+
message_id: z.string().describe('Message ID (can be partial)'),
|
|
66
|
+
},
|
|
67
|
+
async ({ message_id }) => {
|
|
68
|
+
const agentId = getAgentId();
|
|
69
|
+
let message = await storage.getMessage(message_id);
|
|
70
|
+
|
|
71
|
+
// Try partial ID match
|
|
72
|
+
if (!message) {
|
|
73
|
+
const inbox = await storage.getInbox(agentId, { limit: 100 });
|
|
74
|
+
message = inbox.find(m => m.id.startsWith(message_id)) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!message) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: `❌ Message not found: ${message_id}` }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mark as read
|
|
84
|
+
await storage.markAsRead(message.id);
|
|
85
|
+
|
|
86
|
+
// Decrypt if needed
|
|
87
|
+
let subject = message.subject;
|
|
88
|
+
let body = message.body;
|
|
89
|
+
|
|
90
|
+
if (message.encrypted && message.ciphertext && message.nonce && message.senderPublicKey) {
|
|
91
|
+
const keyPair = crypto.loadKeyPair(agentId);
|
|
92
|
+
if (keyPair) {
|
|
93
|
+
try {
|
|
94
|
+
const decrypted = crypto.decryptMessage({
|
|
95
|
+
ciphertext: message.ciphertext,
|
|
96
|
+
nonce: message.nonce,
|
|
97
|
+
senderPublicKey: message.senderPublicKey,
|
|
98
|
+
}, keyPair);
|
|
99
|
+
if (decrypted) {
|
|
100
|
+
const parsed = JSON.parse(decrypted);
|
|
101
|
+
subject = parsed.subject || subject;
|
|
102
|
+
body = parsed.body || body;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
body = '[Failed to decrypt]';
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
body = '[Cannot decrypt - no keypair]';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const encrypted = message.encrypted ? '\n🔐 Encrypted: Yes' : '';
|
|
113
|
+
const text = `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
114
|
+
From: ${message.sender}
|
|
115
|
+
To: ${message.recipient}
|
|
116
|
+
Date: ${message.createdAt.toLocaleString()}
|
|
117
|
+
Subject: ${subject}${encrypted}
|
|
118
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
119
|
+
|
|
120
|
+
${body}
|
|
121
|
+
|
|
122
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
123
|
+
ID: ${message.id}`;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Tool: send_message
|
|
132
|
+
server.tool(
|
|
133
|
+
'send_message',
|
|
134
|
+
'Send a message to another agent',
|
|
135
|
+
{
|
|
136
|
+
recipient: z.string().describe('Recipient agent ID'),
|
|
137
|
+
subject: z.string().describe('Message subject'),
|
|
138
|
+
body: z.string().describe('Message body'),
|
|
139
|
+
encrypt: z.boolean().optional().describe('Encrypt the message (requires key exchange)'),
|
|
140
|
+
},
|
|
141
|
+
async ({ recipient, subject, body, encrypt }) => {
|
|
142
|
+
const sender = getAgentId();
|
|
143
|
+
|
|
144
|
+
if (sender === 'anonymous') {
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: 'text', text: '❌ Agent ID not configured. Set MYCELIUMAIL_AGENT_ID environment variable.' }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let messageOptions;
|
|
151
|
+
|
|
152
|
+
if (encrypt) {
|
|
153
|
+
const senderKeyPair = crypto.loadKeyPair(sender);
|
|
154
|
+
if (!senderKeyPair) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: 'text', text: '❌ No keypair found. Use generate_keys first.' }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const recipientPubKeyB64 = crypto.getKnownKey(recipient);
|
|
161
|
+
if (!recipientPubKeyB64) {
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: 'text', text: `❌ No public key found for ${recipient}. Use import_key first.` }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const recipientPubKey = crypto.decodePublicKey(recipientPubKeyB64);
|
|
168
|
+
const payload = JSON.stringify({ subject, body });
|
|
169
|
+
const encrypted = crypto.encryptMessage(payload, recipientPubKey, senderKeyPair);
|
|
170
|
+
|
|
171
|
+
messageOptions = {
|
|
172
|
+
encrypted: true,
|
|
173
|
+
ciphertext: encrypted.ciphertext,
|
|
174
|
+
nonce: encrypted.nonce,
|
|
175
|
+
senderPublicKey: encrypted.senderPublicKey,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const message = await storage.sendMessage(sender, recipient, subject, body, messageOptions);
|
|
181
|
+
const encInfo = encrypt ? ' (🔐 encrypted)' : '';
|
|
182
|
+
return {
|
|
183
|
+
content: [{
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: `✅ Message sent to ${recipient}${encInfo}\nID: ${message.id}`
|
|
186
|
+
}],
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: `❌ Failed to send: ${error}` }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Tool: reply_message
|
|
197
|
+
server.tool(
|
|
198
|
+
'reply_message',
|
|
199
|
+
'Reply to a message',
|
|
200
|
+
{
|
|
201
|
+
message_id: z.string().describe('ID of message to reply to'),
|
|
202
|
+
body: z.string().describe('Reply message body'),
|
|
203
|
+
encrypt: z.boolean().optional().describe('Encrypt the reply'),
|
|
204
|
+
},
|
|
205
|
+
async ({ message_id, body, encrypt }) => {
|
|
206
|
+
const agentId = getAgentId();
|
|
207
|
+
|
|
208
|
+
// Find original message
|
|
209
|
+
let original = await storage.getMessage(message_id);
|
|
210
|
+
if (!original) {
|
|
211
|
+
const inbox = await storage.getInbox(agentId, { limit: 100 });
|
|
212
|
+
original = inbox.find(m => m.id.startsWith(message_id)) || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!original) {
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: 'text', text: `❌ Message not found: ${message_id}` }],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Send reply to original sender
|
|
222
|
+
const subject = original.subject.startsWith('Re: ')
|
|
223
|
+
? original.subject
|
|
224
|
+
: `Re: ${original.subject}`;
|
|
225
|
+
|
|
226
|
+
let messageOptions;
|
|
227
|
+
if (encrypt) {
|
|
228
|
+
const senderKeyPair = crypto.loadKeyPair(agentId);
|
|
229
|
+
const recipientPubKeyB64 = crypto.getKnownKey(original.sender);
|
|
230
|
+
|
|
231
|
+
if (senderKeyPair && recipientPubKeyB64) {
|
|
232
|
+
const recipientPubKey = crypto.decodePublicKey(recipientPubKeyB64);
|
|
233
|
+
const payload = JSON.stringify({ subject, body });
|
|
234
|
+
const encrypted = crypto.encryptMessage(payload, recipientPubKey, senderKeyPair);
|
|
235
|
+
messageOptions = {
|
|
236
|
+
encrypted: true,
|
|
237
|
+
ciphertext: encrypted.ciphertext,
|
|
238
|
+
nonce: encrypted.nonce,
|
|
239
|
+
senderPublicKey: encrypted.senderPublicKey,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const message = await storage.sendMessage(agentId, original.sender, subject, body, messageOptions);
|
|
245
|
+
const encInfo = encrypt && messageOptions ? ' (🔐 encrypted)' : '';
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
content: [{
|
|
249
|
+
type: 'text',
|
|
250
|
+
text: `✅ Reply sent to ${original.sender}${encInfo}\nID: ${message.id}`
|
|
251
|
+
}],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Tool: generate_keys
|
|
257
|
+
server.tool(
|
|
258
|
+
'generate_keys',
|
|
259
|
+
'Generate encryption keypair for this agent',
|
|
260
|
+
{
|
|
261
|
+
force: z.boolean().optional().describe('Overwrite existing keypair'),
|
|
262
|
+
},
|
|
263
|
+
async ({ force }) => {
|
|
264
|
+
const agentId = getAgentId();
|
|
265
|
+
|
|
266
|
+
if (crypto.hasKeyPair(agentId) && !force) {
|
|
267
|
+
const existing = crypto.loadKeyPair(agentId);
|
|
268
|
+
if (existing) {
|
|
269
|
+
const pubKey = crypto.getPublicKeyBase64(existing);
|
|
270
|
+
return {
|
|
271
|
+
content: [{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: `⚠️ Keypair already exists for ${agentId}\n\n📧 Your public key:\n${pubKey}\n\nUse force=true to regenerate.`
|
|
274
|
+
}],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const keyPair = crypto.generateKeyPair();
|
|
280
|
+
crypto.saveKeyPair(agentId, keyPair);
|
|
281
|
+
const publicKey = crypto.getPublicKeyBase64(keyPair);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: 'text',
|
|
286
|
+
text: `🔐 Keypair generated for ${agentId}\n\n📧 Your public key (share with other agents):\n${publicKey}`
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Tool: list_keys
|
|
293
|
+
server.tool(
|
|
294
|
+
'list_keys',
|
|
295
|
+
'List all known encryption keys',
|
|
296
|
+
{},
|
|
297
|
+
async () => {
|
|
298
|
+
const agentId = getAgentId();
|
|
299
|
+
const ownKeys = crypto.listOwnKeys();
|
|
300
|
+
const knownKeys = crypto.loadKnownKeys();
|
|
301
|
+
|
|
302
|
+
let output = '🔐 Encryption Keys\n\n── Your Keys ──\n';
|
|
303
|
+
|
|
304
|
+
if (ownKeys.length === 0) {
|
|
305
|
+
output += 'No keypairs. Use generate_keys to create one.\n';
|
|
306
|
+
} else {
|
|
307
|
+
for (const id of ownKeys) {
|
|
308
|
+
const kp = crypto.loadKeyPair(id);
|
|
309
|
+
if (kp) {
|
|
310
|
+
const marker = id === agentId ? ' (active)' : '';
|
|
311
|
+
output += `${id}${marker}: ${crypto.getPublicKeyBase64(kp).slice(0, 20)}...\n`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
output += '\n── Peer Keys ──\n';
|
|
317
|
+
const peers = Object.entries(knownKeys);
|
|
318
|
+
if (peers.length === 0) {
|
|
319
|
+
output += 'No peer keys. Use import_key to add one.\n';
|
|
320
|
+
} else {
|
|
321
|
+
for (const [id, key] of peers) {
|
|
322
|
+
output += `${id}: ${key.slice(0, 20)}...\n`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: 'text', text: output }],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Tool: import_key
|
|
333
|
+
server.tool(
|
|
334
|
+
'import_key',
|
|
335
|
+
"Import another agent's public key for encrypted messaging",
|
|
336
|
+
{
|
|
337
|
+
agent_id: z.string().describe('Agent ID to import key for'),
|
|
338
|
+
public_key: z.string().describe('Base64 encoded public key'),
|
|
339
|
+
},
|
|
340
|
+
async ({ agent_id, public_key }) => {
|
|
341
|
+
if (public_key.length < 40) {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: 'text', text: '❌ Invalid key format. Expected base64 NaCl public key.' }],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
crypto.saveKnownKey(agent_id, public_key);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
content: [{
|
|
351
|
+
type: 'text',
|
|
352
|
+
text: `✅ Imported public key for ${agent_id}\n\n🔐 You can now send encrypted messages to this agent.`
|
|
353
|
+
}],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Tool: archive_message
|
|
359
|
+
server.tool(
|
|
360
|
+
'archive_message',
|
|
361
|
+
'Archive a message (remove from inbox)',
|
|
362
|
+
{
|
|
363
|
+
message_id: z.string().describe('Message ID to archive'),
|
|
364
|
+
},
|
|
365
|
+
async ({ message_id }) => {
|
|
366
|
+
const success = await storage.archiveMessage(message_id);
|
|
367
|
+
|
|
368
|
+
if (success) {
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: 'text', text: `✅ Message archived: ${message_id}` }],
|
|
371
|
+
};
|
|
372
|
+
} else {
|
|
373
|
+
return {
|
|
374
|
+
content: [{ type: 'text', text: `❌ Message not found: ${message_id}` }],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Start the server
|
|
381
|
+
async function main() {
|
|
382
|
+
const transport = new StdioServerTransport();
|
|
383
|
+
await server.connect(transport);
|
|
384
|
+
console.error('Myceliumail MCP server running');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022"
|
|
8
|
+
],
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"resolveJsonModule": true
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myceliumail",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "End-to-End Encrypted Messaging for AI Agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mycmail": "dist/bin/myceliumail.js",
|
|
9
|
-
"myceliumail": "dist/bin/myceliumail.js"
|
|
8
|
+
"mycmail": "./dist/bin/myceliumail.js",
|
|
9
|
+
"myceliumail": "./dist/bin/myceliumail.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
@@ -25,8 +25,17 @@
|
|
|
25
25
|
"e2e",
|
|
26
26
|
"cli"
|
|
27
27
|
],
|
|
28
|
-
"author": "Treebird",
|
|
28
|
+
"author": "Treebird <treebird@treebird.dev>",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/treebird7/myceliumail"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/treebird7/myceliumail#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/treebird7/myceliumail/issues"
|
|
37
|
+
},
|
|
38
|
+
"readme": "README.md",
|
|
30
39
|
"dependencies": {
|
|
31
40
|
"@fastify/cors": "^11.2.0",
|
|
32
41
|
"@fastify/static": "^8.3.0",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Myceliumail CLI
|
|
5
|
+
*
|
|
6
|
+
* End-to-End Encrypted Messaging for AI Agents
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Load environment variables from .env file
|
|
10
|
+
import 'dotenv/config';
|
|
11
|
+
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { loadConfig } from '../lib/config.js';
|
|
14
|
+
|
|
15
|
+
// Import commands
|
|
16
|
+
import { createKeygenCommand } from '../commands/keygen.js';
|
|
17
|
+
import { createKeysCommand } from '../commands/keys.js';
|
|
18
|
+
import { createKeyImportCommand } from '../commands/key-import.js';
|
|
19
|
+
import { createKeyAnnounceCommand } from '../commands/key-announce.js';
|
|
20
|
+
import { createSendCommand } from '../commands/send.js';
|
|
21
|
+
import { createInboxCommand } from '../commands/inbox.js';
|
|
22
|
+
import { createReadCommand } from '../commands/read.js';
|
|
23
|
+
import { createDashboardCommand } from '../commands/dashboard.js';
|
|
24
|
+
import { createBroadcastCommand } from '../commands/broadcast.js';
|
|
25
|
+
import { createWatchCommand } from '../commands/watch.js';
|
|
26
|
+
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('mycmail')
|
|
31
|
+
.description('🍄 Myceliumail - End-to-End Encrypted Messaging for AI Agents')
|
|
32
|
+
.version('1.0.0');
|
|
33
|
+
|
|
34
|
+
// Show current agent in help
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
program.addHelpText('after', `
|
|
37
|
+
Current agent: ${config.agentId}
|
|
38
|
+
Config: ~/.myceliumail/config.json
|
|
39
|
+
`);
|
|
40
|
+
|
|
41
|
+
// Register commands
|
|
42
|
+
program.addCommand(createKeygenCommand());
|
|
43
|
+
program.addCommand(createKeysCommand());
|
|
44
|
+
program.addCommand(createKeyImportCommand());
|
|
45
|
+
program.addCommand(createKeyAnnounceCommand());
|
|
46
|
+
program.addCommand(createSendCommand());
|
|
47
|
+
program.addCommand(createInboxCommand());
|
|
48
|
+
program.addCommand(createReadCommand());
|
|
49
|
+
program.addCommand(createDashboardCommand());
|
|
50
|
+
program.addCommand(createBroadcastCommand());
|
|
51
|
+
program.addCommand(createWatchCommand());
|
|
52
|
+
|
|
53
|
+
// Parse and run
|
|
54
|
+
program.parse();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* broadcast command - Send a message to all known agents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { loadConfig } from '../lib/config.js';
|
|
7
|
+
import { getKnownKeys } from '../lib/crypto.js';
|
|
8
|
+
import * as storage from '../storage/supabase.js';
|
|
9
|
+
|
|
10
|
+
// Known agent aliases
|
|
11
|
+
const AGENT_ALIASES: Record<string, string> = {
|
|
12
|
+
'mycs': 'mycsan',
|
|
13
|
+
'treeb': 'treebird',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function resolveAlias(agent: string): string {
|
|
17
|
+
return AGENT_ALIASES[agent] || agent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createBroadcastCommand(): Command {
|
|
21
|
+
return new Command('broadcast')
|
|
22
|
+
.description('Send a message to all known agents')
|
|
23
|
+
.argument('<subject>', 'Message subject/body')
|
|
24
|
+
.option('-m, --message <body>', 'Message body (optional)')
|
|
25
|
+
.action(async (subject: string, options) => {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
const sender = config.agentId;
|
|
28
|
+
|
|
29
|
+
if (sender === 'anonymous') {
|
|
30
|
+
console.error('❌ Agent ID not configured.');
|
|
31
|
+
console.error('Set MYCELIUMAIL_AGENT_ID or configure ~/.myceliumail/config.json');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const body = options.message || subject;
|
|
36
|
+
|
|
37
|
+
// Get all known agents from keys
|
|
38
|
+
const knownKeys = getKnownKeys();
|
|
39
|
+
const recipients = Object.keys(knownKeys).filter(agent => agent !== sender);
|
|
40
|
+
|
|
41
|
+
if (recipients.length === 0) {
|
|
42
|
+
console.error('❌ No known agents to broadcast to.');
|
|
43
|
+
console.error('Import agent keys first:');
|
|
44
|
+
console.error(' mycmail key-import <agent> <their-public-key>');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`📢 Broadcasting to ${recipients.length} agents...`);
|
|
49
|
+
console.log(` Recipients: ${recipients.join(', ')}\n`);
|
|
50
|
+
|
|
51
|
+
let sent = 0;
|
|
52
|
+
let failed = 0;
|
|
53
|
+
|
|
54
|
+
for (const recipient of recipients) {
|
|
55
|
+
try {
|
|
56
|
+
await storage.sendMessage(sender, recipient, subject, body);
|
|
57
|
+
console.log(` ✅ ${recipient}`);
|
|
58
|
+
sent++;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.log(` ❌ ${recipient}: ${error}`);
|
|
61
|
+
failed++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n📬 Broadcast complete: ${sent} sent, ${failed} failed`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Export alias resolver for use in send command
|
|
70
|
+
export { resolveAlias, AGENT_ALIASES };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { startDashboard } from '../dashboard/server.js';
|
|
3
|
+
|
|
4
|
+
export function createDashboardCommand(): Command {
|
|
5
|
+
return new Command('dashboard')
|
|
6
|
+
.description('Start web dashboard on localhost:3737')
|
|
7
|
+
.option('-p, --port <port>', 'Port to run on', '3737')
|
|
8
|
+
.action(async (options) => {
|
|
9
|
+
const port = parseInt(options.port, 10);
|
|
10
|
+
console.log(`\n🍄 Starting Myceliumail Dashboard...`);
|
|
11
|
+
await startDashboard(port);
|
|
12
|
+
// Help text
|
|
13
|
+
console.log(`\n ➜ http://localhost:${port}`);
|
|
14
|
+
console.log('\nPress Ctrl+C to stop');
|
|
15
|
+
|
|
16
|
+
// Keep process alive
|
|
17
|
+
await new Promise(() => { });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inbox command - List incoming messages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { loadConfig } from '../lib/config.js';
|
|
7
|
+
import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
|
|
8
|
+
import * as storage from '../storage/supabase.js';
|
|
9
|
+
|
|
10
|
+
export function createInboxCommand(): Command {
|
|
11
|
+
return new Command('inbox')
|
|
12
|
+
.description('List incoming messages')
|
|
13
|
+
.option('-u, --unread', 'Show only unread messages')
|
|
14
|
+
.option('-l, --limit <n>', 'Limit number of messages', '10')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const agentId = config.agentId;
|
|
18
|
+
|
|
19
|
+
if (agentId === 'anonymous') {
|
|
20
|
+
console.error('❌ Agent ID not configured.');
|
|
21
|
+
console.error('Set MYCELIUMAIL_AGENT_ID or configure ~/.myceliumail/config.json');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const messages = await storage.getInbox(agentId, {
|
|
27
|
+
unreadOnly: options.unread,
|
|
28
|
+
limit: parseInt(options.limit, 10),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (messages.length === 0) {
|
|
32
|
+
console.log('📭 No messages');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`📬 Inbox for ${agentId} (${messages.length} messages)\n`);
|
|
37
|
+
|
|
38
|
+
const keyPair = loadKeyPair(agentId);
|
|
39
|
+
|
|
40
|
+
for (const msg of messages) {
|
|
41
|
+
const readMarker = msg.read ? ' ' : '● ';
|
|
42
|
+
const encryptedMarker = msg.encrypted ? '🔐 ' : '';
|
|
43
|
+
const date = msg.createdAt.toLocaleString();
|
|
44
|
+
|
|
45
|
+
let displaySubject = msg.subject;
|
|
46
|
+
|
|
47
|
+
// Try to decrypt if encrypted and we have keys
|
|
48
|
+
if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
|
|
49
|
+
try {
|
|
50
|
+
const decrypted = decryptMessage({
|
|
51
|
+
ciphertext: msg.ciphertext,
|
|
52
|
+
nonce: msg.nonce,
|
|
53
|
+
senderPublicKey: msg.senderPublicKey,
|
|
54
|
+
}, keyPair);
|
|
55
|
+
|
|
56
|
+
if (decrypted) {
|
|
57
|
+
const parsed = JSON.parse(decrypted);
|
|
58
|
+
displaySubject = parsed.subject || '[Decrypted]';
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
displaySubject = '[Encrypted]';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`${readMarker}${encryptedMarker}${msg.id.slice(0, 8)} | From: ${msg.sender} | ${displaySubject}`);
|
|
66
|
+
console.log(` ${date}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('\n💡 Use: mycmail read <id> to read a message');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('❌ Failed to fetch inbox:', error);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|