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,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.2",
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
+ }