myceliumail 1.0.9 → 1.0.13

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 (96) hide show
  1. package/.mappersan/outbox.json +15 -0
  2. package/.spidersan/registry.json +39 -0
  3. package/CHANGELOG.md +29 -0
  4. package/CLAUDE.md +29 -0
  5. package/README.md +23 -2
  6. package/dist/bin/myceliumail.js +17 -1
  7. package/dist/bin/myceliumail.js.map +1 -1
  8. package/dist/commands/close.d.ts +9 -0
  9. package/dist/commands/close.d.ts.map +1 -0
  10. package/dist/commands/close.js +153 -0
  11. package/dist/commands/close.js.map +1 -0
  12. package/dist/commands/collab.d.ts +8 -0
  13. package/dist/commands/collab.d.ts.map +1 -0
  14. package/dist/commands/collab.js +112 -0
  15. package/dist/commands/collab.js.map +1 -0
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +105 -26
  18. package/dist/commands/inbox.js.map +1 -1
  19. package/dist/commands/tags.d.ts +6 -0
  20. package/dist/commands/tags.d.ts.map +1 -0
  21. package/dist/commands/tags.js +90 -0
  22. package/dist/commands/tags.js.map +1 -0
  23. package/dist/commands/wake.d.ts +9 -0
  24. package/dist/commands/wake.d.ts.map +1 -0
  25. package/dist/commands/wake.js +198 -0
  26. package/dist/commands/wake.js.map +1 -0
  27. package/dist/dashboard/public/app.js +117 -0
  28. package/dist/dashboard/public/index.html +63 -5
  29. package/dist/dashboard/routes.d.ts.map +1 -1
  30. package/dist/dashboard/routes.js +31 -2
  31. package/dist/dashboard/routes.js.map +1 -1
  32. package/dist/lib/update-check.d.ts.map +1 -1
  33. package/dist/lib/update-check.js +6 -4
  34. package/dist/lib/update-check.js.map +1 -1
  35. package/dist/lib/watson-digest.d.ts +40 -0
  36. package/dist/lib/watson-digest.d.ts.map +1 -0
  37. package/dist/lib/watson-digest.js +164 -0
  38. package/dist/lib/watson-digest.js.map +1 -0
  39. package/dist/storage/supabase.d.ts +4 -0
  40. package/dist/storage/supabase.d.ts.map +1 -1
  41. package/dist/storage/supabase.js +57 -0
  42. package/dist/storage/supabase.js.map +1 -1
  43. package/docs/COLLAB_mappersan_mycmail_setup.md +115 -0
  44. package/docs/COLLAB_wake_close_commands.md +518 -0
  45. package/docs/CROSS_TOOL_INTEGRATION_PLAN.md +246 -0
  46. package/docs/JSON_SCHEMA_WAKE_CLOSE.md +246 -0
  47. package/docs/MYCMAIL_QUICKSTART.md +103 -0
  48. package/docs/WAKE_AGENTS_SHARED_DOC.md +1215 -0
  49. package/mcp-server/README.md +75 -69
  50. package/mcp-server/package-lock.json +2 -2
  51. package/mcp-server/package.json +5 -1
  52. package/mcp-server/postinstall.js +14 -0
  53. package/mcp-server/src/server.ts +39 -0
  54. package/mobile-app/README.md +36 -0
  55. package/mobile-app/app/compose/page.tsx +140 -0
  56. package/mobile-app/app/favicon.ico +0 -0
  57. package/mobile-app/app/globals.css +26 -0
  58. package/mobile-app/app/layout.tsx +42 -0
  59. package/mobile-app/app/message/[id]/page.tsx +126 -0
  60. package/mobile-app/app/page.tsx +131 -0
  61. package/mobile-app/components/MessageCard.tsx +60 -0
  62. package/mobile-app/eslint.config.mjs +18 -0
  63. package/mobile-app/lib/supabase.ts +87 -0
  64. package/mobile-app/next.config.ts +7 -0
  65. package/mobile-app/package-lock.json +6674 -0
  66. package/mobile-app/package.json +27 -0
  67. package/mobile-app/postcss.config.mjs +7 -0
  68. package/mobile-app/public/file.svg +1 -0
  69. package/mobile-app/public/globe.svg +1 -0
  70. package/mobile-app/public/next.svg +1 -0
  71. package/mobile-app/public/vercel.svg +1 -0
  72. package/mobile-app/public/window.svg +1 -0
  73. package/mobile-app/tsconfig.json +34 -0
  74. package/package.json +2 -1
  75. package/postinstall.js +14 -0
  76. package/src/bin/myceliumail.ts +19 -1
  77. package/src/commands/close.ts +172 -0
  78. package/src/commands/collab.ts +125 -0
  79. package/src/commands/inbox.ts +120 -29
  80. package/src/commands/tags.ts +102 -0
  81. package/src/commands/wake.ts +228 -0
  82. package/src/dashboard/public/app.js +117 -0
  83. package/src/dashboard/public/index.html +63 -5
  84. package/src/dashboard/routes.ts +31 -2
  85. package/src/lib/update-check.ts +7 -4
  86. package/src/lib/watson-digest.ts +217 -0
  87. package/src/storage/supabase.ts +71 -0
  88. package/vscode-extension/README.md +107 -0
  89. package/vscode-extension/package-lock.json +1941 -0
  90. package/vscode-extension/package.json +117 -0
  91. package/vscode-extension/src/chatParticipant.ts +179 -0
  92. package/vscode-extension/src/extension.ts +262 -0
  93. package/vscode-extension/src/handlers.ts +265 -0
  94. package/vscode-extension/src/realtime.ts +302 -0
  95. package/vscode-extension/src/types.ts +41 -0
  96. package/vscode-extension/tsconfig.json +26 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * tags command - List all unique hashtags from 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
+ /**
11
+ * Extract hashtag from subject (e.g., "#wake-feature: Message" -> "wake-feature")
12
+ */
13
+ function extractTag(subject: string | null): string | null {
14
+ if (!subject) return null;
15
+ const match = subject.match(/^#([a-zA-Z0-9_-]+):/);
16
+ return match ? match[1].toLowerCase() : null;
17
+ }
18
+
19
+ export function createTagsCommand(): Command {
20
+ return new Command('tags')
21
+ .description('List all unique hashtags from your messages')
22
+ .option('--json', 'Output as JSON')
23
+ .action(async (options) => {
24
+ const config = loadConfig();
25
+ const agentId = config.agentId;
26
+
27
+ if (agentId === 'anonymous') {
28
+ console.error('❌ Agent ID not configured.');
29
+ process.exit(1);
30
+ }
31
+
32
+ try {
33
+ // Fetch all messages (up to 500)
34
+ const messages = await storage.getInbox(agentId, { limit: 500 });
35
+ const keyPair = loadKeyPair(agentId);
36
+
37
+ // Count tags
38
+ const tagCounts: Record<string, { count: number, unread: number }> = {};
39
+
40
+ for (const msg of messages) {
41
+ let subject = msg.subject;
42
+
43
+ // Try to decrypt if encrypted
44
+ if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
45
+ try {
46
+ const decrypted = decryptMessage({
47
+ ciphertext: msg.ciphertext,
48
+ nonce: msg.nonce,
49
+ senderPublicKey: msg.senderPublicKey,
50
+ }, keyPair);
51
+
52
+ if (decrypted) {
53
+ const parsed = JSON.parse(decrypted);
54
+ subject = parsed.subject || subject;
55
+ }
56
+ } catch {
57
+ // Keep original subject
58
+ }
59
+ }
60
+
61
+ const tag = extractTag(subject);
62
+ if (tag) {
63
+ if (!tagCounts[tag]) {
64
+ tagCounts[tag] = { count: 0, unread: 0 };
65
+ }
66
+ tagCounts[tag].count++;
67
+ if (!msg.read) {
68
+ tagCounts[tag].unread++;
69
+ }
70
+ }
71
+ }
72
+
73
+ const tagList = Object.entries(tagCounts)
74
+ .sort((a, b) => b[1].count - a[1].count)
75
+ .map(([tag, stats]) => ({ tag, ...stats }));
76
+
77
+ if (options.json) {
78
+ console.log(JSON.stringify({ tags: tagList }, null, 2));
79
+ return;
80
+ }
81
+
82
+ if (tagList.length === 0) {
83
+ console.log('📭 No tagged messages found');
84
+ console.log('\n💡 Tag messages by prefixing subject with #tag:');
85
+ console.log(' mycmail send wsan "#wake-feature: Need help"');
86
+ return;
87
+ }
88
+
89
+ console.log('🏷️ Message Tags\n');
90
+ for (const { tag, count, unread } of tagList) {
91
+ const unreadMarker = unread > 0 ? ` (${unread} unread)` : '';
92
+ console.log(` #${tag}: ${count} message${count > 1 ? 's' : ''}${unreadMarker}`);
93
+ }
94
+
95
+ console.log('\n💡 Filter by tag: mycmail inbox --tag <tag>');
96
+
97
+ } catch (error) {
98
+ console.error('❌ Failed to fetch tags:', error);
99
+ process.exit(1);
100
+ }
101
+ });
102
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * wake command - Start a new session
3
+ *
4
+ * Shows inbox count, active collabs, and last session time.
5
+ * Designed for agent session lifecycle management.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { loadConfig } from '../lib/config.js';
10
+ import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
11
+ import { generateDigest, sendDigestToWatsan } from '../lib/watson-digest.js';
12
+ import * as storage from '../storage/supabase.js';
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import * as os from 'os';
16
+
17
+ interface SessionData {
18
+ lastWake: string | null;
19
+ lastClose: string | null;
20
+ activeCollabs: string[];
21
+ }
22
+
23
+ function getSessionPath(): string {
24
+ return path.join(os.homedir(), '.mycmail', 'session.json');
25
+ }
26
+
27
+ function loadSession(): SessionData {
28
+ const sessionPath = getSessionPath();
29
+ try {
30
+ if (fs.existsSync(sessionPath)) {
31
+ return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
32
+ }
33
+ } catch {
34
+ // Ignore errors, return default
35
+ }
36
+ return { lastWake: null, lastClose: null, activeCollabs: [] };
37
+ }
38
+
39
+ function saveSession(data: SessionData): void {
40
+ const sessionPath = getSessionPath();
41
+ const dir = path.dirname(sessionPath);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2));
46
+ }
47
+
48
+ function formatTimeSince(date: string | null): string {
49
+ if (!date) return 'Never';
50
+ const now = new Date();
51
+ const then = new Date(date);
52
+ const diffMs = now.getTime() - then.getTime();
53
+ const diffMins = Math.floor(diffMs / 60000);
54
+ const diffHours = Math.floor(diffMins / 60);
55
+ const diffDays = Math.floor(diffHours / 24);
56
+
57
+ if (diffMins < 1) return 'Just now';
58
+ if (diffMins < 60) return `${diffMins} minutes ago`;
59
+ if (diffHours < 24) return `${diffHours} hours ago`;
60
+ return `${diffDays} days ago`;
61
+ }
62
+
63
+ function extractTag(subject: string | null): string | null {
64
+ if (!subject) return null;
65
+ const match = subject.match(/^#([a-zA-Z0-9_-]+):/);
66
+ return match ? match[1].toLowerCase() : null;
67
+ }
68
+
69
+ interface TagDigest {
70
+ tag: string;
71
+ count: number;
72
+ unread: number;
73
+ }
74
+
75
+ async function getTagDigest(agentId: string, messages: any[]): Promise<TagDigest[]> {
76
+ const keyPair = loadKeyPair(agentId);
77
+ const tagCounts: Record<string, { count: number; unread: number }> = {};
78
+
79
+ for (const msg of messages) {
80
+ let subject = msg.subject;
81
+
82
+ // Try to decrypt if encrypted
83
+ if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
84
+ try {
85
+ const decrypted = decryptMessage({
86
+ ciphertext: msg.ciphertext,
87
+ nonce: msg.nonce,
88
+ senderPublicKey: msg.senderPublicKey,
89
+ }, keyPair);
90
+
91
+ if (decrypted) {
92
+ const parsed = JSON.parse(decrypted);
93
+ subject = parsed.subject || subject;
94
+ }
95
+ } catch {
96
+ // Keep original subject
97
+ }
98
+ }
99
+
100
+ const tag = extractTag(subject);
101
+ if (tag) {
102
+ if (!tagCounts[tag]) {
103
+ tagCounts[tag] = { count: 0, unread: 0 };
104
+ }
105
+ tagCounts[tag].count++;
106
+ if (!msg.read) {
107
+ tagCounts[tag].unread++;
108
+ }
109
+ }
110
+ }
111
+
112
+ return Object.entries(tagCounts)
113
+ .sort((a, b) => b[1].unread - a[1].unread)
114
+ .slice(0, 5)
115
+ .map(([tag, stats]) => ({ tag, ...stats }));
116
+ }
117
+
118
+ export function createWakeCommand(): Command {
119
+ return new Command('wake')
120
+ .description('Start a new session - check inbox, collabs, and announce presence')
121
+ .option('--json', 'Output as JSON (for scripting)')
122
+ .option('-q, --quiet', 'Minimal output')
123
+ .option('--silent', 'No output (only exit code)')
124
+ .option('--digest', 'Show hashtag digest and active threads')
125
+ .action(async (options) => {
126
+ const config = loadConfig();
127
+ const agentId = config.agentId;
128
+
129
+ if (agentId === 'anonymous') {
130
+ if (!options.silent) {
131
+ console.error('❌ Agent ID not configured.');
132
+ }
133
+ process.exit(1);
134
+ }
135
+
136
+ try {
137
+ // Load session data
138
+ const session = loadSession();
139
+
140
+ // Check if this is a duplicate wake (idempotency)
141
+ if (session.lastWake) {
142
+ const lastWakeTime = new Date(session.lastWake);
143
+ const now = new Date();
144
+ const diffMins = (now.getTime() - lastWakeTime.getTime()) / 60000;
145
+
146
+ // If woken up less than 5 minutes ago, skip re-registration
147
+ if (diffMins < 5 && !options.json) {
148
+ if (!options.silent && !options.quiet) {
149
+ console.log(`⏰ Already woke ${Math.floor(diffMins)} min ago. Use --force to re-wake.`);
150
+ }
151
+ // Still show status but don't re-register
152
+ }
153
+ }
154
+
155
+ // Get inbox count
156
+ const messages = await storage.getInbox(agentId, { limit: 100 });
157
+ const unreadCount = messages.filter(m => !m.read).length;
158
+ const totalCount = messages.length;
159
+
160
+ // Update session
161
+ session.lastWake = new Date().toISOString();
162
+ saveSession(session);
163
+
164
+ // Output based on mode
165
+ if (options.silent) {
166
+ process.exit(0);
167
+ }
168
+
169
+ if (options.json) {
170
+ const output = {
171
+ agentId,
172
+ inbox: {
173
+ total: totalCount,
174
+ unread: unreadCount
175
+ },
176
+ lastClose: session.lastClose,
177
+ activeCollabs: session.activeCollabs,
178
+ wakeTime: session.lastWake
179
+ };
180
+ console.log(JSON.stringify(output, null, 2));
181
+ return;
182
+ }
183
+
184
+ // Standard output
185
+ console.log(`\n🌅 Good morning, ${agentId}!\n`);
186
+ console.log(`📬 Inbox: ${unreadCount} unread / ${totalCount} total`);
187
+ console.log(`📋 Active collabs: ${session.activeCollabs.length}`);
188
+ console.log(`🕐 Last close: ${formatTimeSince(session.lastClose)}`);
189
+
190
+ // Show digest if requested
191
+ if (options.digest) {
192
+ const digest = await getTagDigest(agentId, messages);
193
+ if (digest.length > 0) {
194
+ console.log('\n🏷️ Active Threads:');
195
+ for (const { tag, count, unread } of digest) {
196
+ const unreadMarker = unread > 0 ? ` (${unread} new)` : '';
197
+ console.log(` #${tag}: ${count}${unreadMarker}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ if (!options.quiet) {
203
+ console.log('\n💡 Tip: Run \'mycmail inbox\' to read messages');
204
+ }
205
+
206
+ console.log('\n✅ Session started!\n');
207
+
208
+ // Send digest to watson (unencrypted)
209
+ if (!options.silent) {
210
+ try {
211
+ const wakeDigest = await generateDigest(agentId, 'wake');
212
+ await sendDigestToWatsan(wakeDigest);
213
+ if (!options.quiet) {
214
+ console.log('📊 Digest sent to watson');
215
+ }
216
+ } catch {
217
+ // Silent fail - digest is optional
218
+ }
219
+ }
220
+
221
+ } catch (error) {
222
+ if (!options.silent) {
223
+ console.error('❌ Wake failed:', error);
224
+ }
225
+ process.exit(1);
226
+ }
227
+ });
228
+ }
@@ -521,3 +521,120 @@ loadInbox();
521
521
 
522
522
  // Poll every 30 seconds as fallback (Realtime handles instant updates)
523
523
  setInterval(() => loadInbox(true), 30000);
524
+
525
+ // ============== COMPOSE MESSAGE FUNCTIONS ==============
526
+
527
+ let availableAgents = [];
528
+
529
+ // Load available agents for compose dropdown
530
+ async function loadAvailableAgents() {
531
+ try {
532
+ const res = await fetch('/api/config/agents');
533
+ const data = await res.json();
534
+ availableAgents = data.agents || [];
535
+ updateAgentDropdown();
536
+ } catch (err) {
537
+ console.error('Failed to load agents:', err);
538
+ }
539
+ }
540
+
541
+ function updateAgentDropdown() {
542
+ const select = document.getElementById('compose-from');
543
+ if (!select) return;
544
+
545
+ if (availableAgents.length === 0) {
546
+ select.innerHTML = `<option value="${currentAgentId}">${currentAgentId}</option>`;
547
+ } else {
548
+ select.innerHTML = availableAgents.map(agent =>
549
+ `<option value="${agent}" ${agent === 'treebird' ? 'selected' : ''}>${agent}</option>`
550
+ ).join('');
551
+ }
552
+ }
553
+
554
+ function showComposeModal() {
555
+ loadAvailableAgents();
556
+ document.getElementById('compose-modal').classList.remove('hidden');
557
+ document.getElementById('compose-to').focus();
558
+ }
559
+
560
+ function hideComposeModal() {
561
+ document.getElementById('compose-modal').classList.add('hidden');
562
+ // Clear form
563
+ document.getElementById('compose-to').value = '';
564
+ document.getElementById('compose-subject').value = '';
565
+ document.getElementById('compose-body').value = '';
566
+ document.getElementById('compose-files').value = '';
567
+ }
568
+
569
+ async function sendNewMessage() {
570
+ const from = document.getElementById('compose-from').value;
571
+ const to = document.getElementById('compose-to').value.trim();
572
+ const subject = document.getElementById('compose-subject').value.trim();
573
+ const body = document.getElementById('compose-body').value;
574
+ const fileInput = document.getElementById('compose-files');
575
+
576
+ if (!to) {
577
+ alert('Please enter a recipient');
578
+ return;
579
+ }
580
+ if (!subject) {
581
+ alert('Please enter a subject');
582
+ return;
583
+ }
584
+ if (!body.trim()) {
585
+ alert('Please enter a message');
586
+ return;
587
+ }
588
+
589
+ // Process attachments
590
+ const attachments = [];
591
+ const MAX_SIZE = 5 * 1024 * 1024; // 5MB
592
+
593
+ if (fileInput && fileInput.files.length > 0) {
594
+ for (const file of fileInput.files) {
595
+ if (file.size > MAX_SIZE) {
596
+ alert(`File "${file.name}" exceeds 5MB limit`);
597
+ return;
598
+ }
599
+ const base64 = await readFileAsBase64(file);
600
+ attachments.push({
601
+ name: file.name,
602
+ type: file.type || 'application/octet-stream',
603
+ data: base64,
604
+ size: file.size
605
+ });
606
+ }
607
+ }
608
+
609
+ try {
610
+ console.log('Sending message from:', from, 'to:', to);
611
+ const response = await fetch('/api/send', {
612
+ method: 'POST',
613
+ headers: { 'Content-Type': 'application/json' },
614
+ body: JSON.stringify({
615
+ to: to,
616
+ subject: subject,
617
+ body: body,
618
+ from: from,
619
+ attachments: attachments.length > 0 ? attachments : undefined
620
+ })
621
+ });
622
+
623
+ const result = await response.json();
624
+ console.log('Send result:', result);
625
+
626
+ if (!response.ok || !result.success) {
627
+ throw new Error(result.error || 'Failed to send');
628
+ }
629
+
630
+ hideComposeModal();
631
+ alert('Message sent!' + (attachments.length > 0 ? ` (${attachments.length} attachment(s))` : ''));
632
+ loadInbox(true);
633
+ } catch (err) {
634
+ console.error('Send message error:', err);
635
+ alert('Failed to send message: ' + err.message);
636
+ }
637
+ }
638
+
639
+ // Load agents on startup
640
+ loadAvailableAgents();
@@ -45,17 +45,75 @@
45
45
  <div
46
46
  class="p-4 border-b border-gray-800 bg-gray-900/50 backdrop-blur flex items-center justify-between">
47
47
  <h2 class="text-xl font-semibold text-gray-200">Inbox</h2>
48
- <button onclick="loadInbox(false)"
49
- class="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
50
- title="Refresh inbox">
51
- 🔄
52
- </button>
48
+ <div class="flex gap-2">
49
+ <button onclick="showComposeModal()"
50
+ class="px-3 py-1.5 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-500 transition-colors flex items-center gap-1.5"
51
+ title="Compose new message">
52
+ ✉️ Compose
53
+ </button>
54
+ <button onclick="loadInbox(false)"
55
+ class="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
56
+ title="Refresh inbox">
57
+ 🔄
58
+ </button>
59
+ </div>
53
60
  </div>
54
61
  <div id="inbox-list" class="flex-1 overflow-y-auto p-2 space-y-2">
55
62
  <!-- Messages will act here -->
56
63
  </div>
57
64
  </div>
58
65
 
66
+ <!-- Compose Modal -->
67
+ <div id="compose-modal"
68
+ class="hidden fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
69
+ <div
70
+ class="bg-gray-900 rounded-2xl border border-gray-700 shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
71
+ <div class="p-6 border-b border-gray-800 flex justify-between items-center">
72
+ <h2 class="text-2xl font-bold text-white">✉️ Compose Message</h2>
73
+ <button onclick="hideComposeModal()"
74
+ class="text-gray-400 hover:text-white text-2xl">&times;</button>
75
+ </div>
76
+ <div class="p-6 space-y-4">
77
+ <div class="grid grid-cols-2 gap-4">
78
+ <div>
79
+ <label class="block text-gray-400 mb-2">From:</label>
80
+ <select id="compose-from"
81
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
82
+ <option value="">Loading agents...</option>
83
+ </select>
84
+ </div>
85
+ <div>
86
+ <label class="block text-gray-400 mb-2">To:</label>
87
+ <input type="text" id="compose-to" placeholder="recipient agent ID"
88
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
89
+ </div>
90
+ </div>
91
+ <div>
92
+ <label class="block text-gray-400 mb-2">Subject:</label>
93
+ <input type="text" id="compose-subject" placeholder="Message subject"
94
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
95
+ </div>
96
+ <div>
97
+ <label class="block text-gray-400 mb-2">Message:</label>
98
+ <textarea id="compose-body" rows="8" placeholder="Write your message..."
99
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none resize-none"></textarea>
100
+ </div>
101
+ <div>
102
+ <label class="block text-gray-400 mb-2">Attachments (optional):</label>
103
+ <input type="file" id="compose-files" multiple
104
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-purple-600 file:text-white file:cursor-pointer">
105
+ </div>
106
+ <div class="flex gap-4 justify-end pt-4">
107
+ <button onclick="hideComposeModal()"
108
+ class="px-6 py-3 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700">Cancel</button>
109
+ <button onclick="sendNewMessage()"
110
+ class="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-500 font-medium">Send
111
+ Message</button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
59
117
  <!-- Message Detail -->
60
118
  <div
61
119
  class="col-span-8 bg-gray-900 rounded-xl border border-gray-800 flex flex-col overflow-hidden relative">
@@ -46,9 +46,38 @@ export async function registerRoutes(fastify: FastifyInstance) {
46
46
  const config = loadConfig();
47
47
  const agentId = config.agentId;
48
48
 
49
- // GET /api/inbox
49
+ // GET /api/config/agents - List all agent IDs with keys
50
+ fastify.get('/api/config/agents', async (request, reply) => {
51
+ const agents = listOwnKeys();
52
+ // Include config agent if not already in list
53
+ if (agentId && !agents.includes(agentId)) {
54
+ agents.unshift(agentId);
55
+ }
56
+ return { agents };
57
+ });
58
+
59
+ // GET /api/inbox - Now supports multi-agent queries
50
60
  fastify.get('/api/inbox', async (request, reply) => {
51
- const messages = await storage.getInbox(agentId, { limit: 100 });
61
+ const queryAgents = (request.query as { agents?: string }).agents;
62
+
63
+ let messages;
64
+ if (queryAgents) {
65
+ // Use specific agents if provided
66
+ const agentIds = queryAgents.split(',').map(s => s.trim());
67
+ messages = await storage.getMultiAgentInbox(agentIds, { limit: 100 });
68
+ } else {
69
+ // Default: get messages for ALL agents with keys
70
+ const allAgentIds = listOwnKeys();
71
+ if (agentId && !allAgentIds.includes(agentId)) {
72
+ allAgentIds.unshift(agentId);
73
+ }
74
+ if (allAgentIds.length > 0) {
75
+ messages = await storage.getMultiAgentInbox(allAgentIds, { limit: 100 });
76
+ } else {
77
+ // Fallback to single agent query
78
+ messages = await storage.getInbox(agentId, { limit: 100 });
79
+ }
80
+ }
52
81
 
53
82
  // Decrypt encrypted messages using all available keys
54
83
  const decrypted = messages.map(tryDecryptWithAllKeys);
@@ -6,8 +6,12 @@
6
6
  */
7
7
 
8
8
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
9
- import { join } from 'path';
9
+ import { join, dirname } from 'path';
10
10
  import { homedir } from 'os';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
11
15
 
12
16
  const CONFIG_DIR = join(homedir(), '.myceliumail');
13
17
  const UPDATE_CACHE = join(CONFIG_DIR, 'update-cache.json');
@@ -24,9 +28,8 @@ interface UpdateCache {
24
28
  */
25
29
  function getCurrentVersion(): string {
26
30
  try {
27
- // Read from the installed package
28
- const packagePath = new URL('../package.json', import.meta.url);
29
- const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
31
+ // Read from the installed package (2 levels up from dist/lib/)
32
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
30
33
  return pkg.version;
31
34
  } catch {
32
35
  return '0.0.0';