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.
Files changed (79) hide show
  1. package/.context7 +87 -0
  2. package/.eslintrc.json +29 -0
  3. package/COMPLETE.md +51 -0
  4. package/MYCELIUMAIL_STARTER_KIT.md +603 -0
  5. package/NEXT_STEPS.md +96 -0
  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/20251215_Treebird-Ecosystem_Knowledge-Base_v2.md +292 -0
  31. package/docs/20251215_Treebird-Ecosystem_Project-Instructions_v2.md +176 -0
  32. package/docs/AGENT_DELEGATION_WORKFLOW.md +453 -0
  33. package/docs/AGENT_STARTER_KIT.md +145 -0
  34. package/docs/ANNOUNCEMENT_DRAFTS.md +55 -0
  35. package/docs/DASHBOARD_AGENT_HANDOFF.md +429 -0
  36. package/docs/DASHBOARD_AGENT_PROMPT.md +32 -0
  37. package/docs/DASHBOARD_BUILD_ROADMAP.md +61 -0
  38. package/docs/DEPLOYMENT.md +59 -0
  39. package/docs/LESSONS_LEARNED.md +127 -0
  40. package/docs/MCP_PUBLISHING_ROADMAP.md +113 -0
  41. package/docs/MCP_STARTER_KIT.md +117 -0
  42. package/docs/SSAN_MESSAGES_SUMMARY.md +92 -0
  43. package/docs/STORAGE_ARCHITECTURE.md +114 -0
  44. package/mcp-server/README.md +143 -0
  45. package/mcp-server/assets/icon.png +0 -0
  46. package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
  47. package/mcp-server/package-lock.json +1142 -0
  48. package/mcp-server/package.json +49 -0
  49. package/mcp-server/src/lib/config.ts +55 -0
  50. package/mcp-server/src/lib/crypto.ts +150 -0
  51. package/mcp-server/src/lib/storage.ts +267 -0
  52. package/mcp-server/src/server.ts +387 -0
  53. package/mcp-server/tsconfig.json +26 -0
  54. package/package.json +3 -3
  55. package/src/bin/myceliumail.ts +54 -0
  56. package/src/commands/broadcast.ts +70 -0
  57. package/src/commands/dashboard.ts +19 -0
  58. package/src/commands/inbox.ts +75 -0
  59. package/src/commands/key-announce.ts +70 -0
  60. package/src/commands/key-import.ts +35 -0
  61. package/src/commands/keygen.ts +44 -0
  62. package/src/commands/keys.ts +55 -0
  63. package/src/commands/read.ts +97 -0
  64. package/src/commands/send.ts +89 -0
  65. package/src/commands/watch.ts +101 -0
  66. package/src/dashboard/public/app.js +523 -0
  67. package/src/dashboard/public/index.html +75 -0
  68. package/src/dashboard/public/styles.css +68 -0
  69. package/src/dashboard/routes.ts +128 -0
  70. package/src/dashboard/server.ts +33 -0
  71. package/src/lib/config.ts +104 -0
  72. package/src/lib/crypto.ts +210 -0
  73. package/src/lib/realtime.ts +109 -0
  74. package/src/storage/local.ts +209 -0
  75. package/src/storage/supabase.ts +336 -0
  76. package/src/types/index.ts +53 -0
  77. package/supabase/migrations/000_myceliumail_setup.sql +93 -0
  78. package/supabase/migrations/001_enable_realtime.sql +10 -0
  79. package/tsconfig.json +28 -0
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "myceliumail-mcp",
3
+ "version": "1.0.7",
4
+ "description": "MCP server for Myceliumail - End-to-End Encrypted Messaging for AI Agents",
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "myceliumail-mcp": "./dist/server.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/server.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "claude",
20
+ "ai-agents",
21
+ "messaging",
22
+ "encrypted",
23
+ "e2e",
24
+ "myceliumail"
25
+ ],
26
+ "author": "Treebird",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/treebird/myceliumail"
31
+ },
32
+ "homepage": "https://github.com/treebird/myceliumail#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/treebird/myceliumail/issues"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.0",
38
+ "tweetnacl": "^1.0.3",
39
+ "tweetnacl-util": "^0.15.1",
40
+ "zod": "^3.22.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.10.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ }
49
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Myceliumail MCP - Config Module
3
+ *
4
+ * Reads configuration from:
5
+ * 1. Environment variables (highest priority)
6
+ * 2. ~/.myceliumail/config.json (fallback)
7
+ */
8
+
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ const CONFIG_FILE = join(homedir(), '.myceliumail', 'config.json');
14
+
15
+ interface FileConfig {
16
+ agent_id?: string;
17
+ supabase_url?: string;
18
+ supabase_key?: string;
19
+ }
20
+
21
+ let cachedFileConfig: FileConfig | null = null;
22
+
23
+ function loadFileConfig(): FileConfig {
24
+ if (cachedFileConfig) return cachedFileConfig;
25
+
26
+ if (existsSync(CONFIG_FILE)) {
27
+ try {
28
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
29
+ cachedFileConfig = JSON.parse(raw);
30
+ return cachedFileConfig!;
31
+ } catch {
32
+ // Invalid config file - ignore
33
+ }
34
+ }
35
+ return {};
36
+ }
37
+
38
+ export function getAgentId(): string {
39
+ return process.env.MYCELIUMAIL_AGENT_ID ||
40
+ process.env.MYCELIUMAIL_AGENT ||
41
+ loadFileConfig().agent_id ||
42
+ 'anonymous';
43
+ }
44
+
45
+ export function getSupabaseUrl(): string | undefined {
46
+ return process.env.SUPABASE_URL || loadFileConfig().supabase_url;
47
+ }
48
+
49
+ export function getSupabaseKey(): string | undefined {
50
+ return process.env.SUPABASE_ANON_KEY || loadFileConfig().supabase_key;
51
+ }
52
+
53
+ export function hasSupabase(): boolean {
54
+ return !!(getSupabaseUrl() && getSupabaseKey());
55
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Myceliumail MCP - Crypto Module
3
+ *
4
+ * NaCl encryption for agent messaging.
5
+ */
6
+
7
+ import nacl from 'tweetnacl';
8
+ import util from 'tweetnacl-util';
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ const KEYS_DIR = join(homedir(), '.myceliumail', 'keys');
14
+
15
+ export interface KeyPair {
16
+ publicKey: Uint8Array;
17
+ secretKey: Uint8Array;
18
+ }
19
+
20
+ export interface EncryptedMessage {
21
+ ciphertext: string;
22
+ nonce: string;
23
+ senderPublicKey: string;
24
+ }
25
+
26
+ function ensureKeysDir(): void {
27
+ if (!existsSync(KEYS_DIR)) {
28
+ mkdirSync(KEYS_DIR, { recursive: true });
29
+ }
30
+ }
31
+
32
+ export function generateKeyPair(): KeyPair {
33
+ return nacl.box.keyPair();
34
+ }
35
+
36
+ export function saveKeyPair(agentId: string, keyPair: KeyPair): void {
37
+ ensureKeysDir();
38
+ const serialized = {
39
+ publicKey: util.encodeBase64(keyPair.publicKey),
40
+ secretKey: util.encodeBase64(keyPair.secretKey),
41
+ };
42
+ const path = join(KEYS_DIR, `${agentId}.key.json`);
43
+ writeFileSync(path, JSON.stringify(serialized, null, 2), { mode: 0o600 });
44
+ }
45
+
46
+ export function loadKeyPair(agentId: string): KeyPair | null {
47
+ const path = join(KEYS_DIR, `${agentId}.key.json`);
48
+ if (!existsSync(path)) return null;
49
+
50
+ try {
51
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
52
+ return {
53
+ publicKey: util.decodeBase64(data.publicKey),
54
+ secretKey: util.decodeBase64(data.secretKey),
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ export function hasKeyPair(agentId: string): boolean {
62
+ return existsSync(join(KEYS_DIR, `${agentId}.key.json`));
63
+ }
64
+
65
+ export function getPublicKeyBase64(keyPair: KeyPair): string {
66
+ return util.encodeBase64(keyPair.publicKey);
67
+ }
68
+
69
+ export function encryptMessage(
70
+ message: string,
71
+ recipientPublicKey: Uint8Array,
72
+ senderKeyPair: KeyPair
73
+ ): EncryptedMessage {
74
+ const messageBytes = util.decodeUTF8(message);
75
+ const nonce = nacl.randomBytes(nacl.box.nonceLength);
76
+
77
+ const ciphertext = nacl.box(
78
+ messageBytes,
79
+ nonce,
80
+ recipientPublicKey,
81
+ senderKeyPair.secretKey
82
+ );
83
+
84
+ return {
85
+ ciphertext: util.encodeBase64(ciphertext),
86
+ nonce: util.encodeBase64(nonce),
87
+ senderPublicKey: util.encodeBase64(senderKeyPair.publicKey),
88
+ };
89
+ }
90
+
91
+ export function decryptMessage(
92
+ encrypted: EncryptedMessage,
93
+ recipientKeyPair: KeyPair
94
+ ): string | null {
95
+ try {
96
+ const ciphertext = util.decodeBase64(encrypted.ciphertext);
97
+ const nonce = util.decodeBase64(encrypted.nonce);
98
+ const senderPublicKey = util.decodeBase64(encrypted.senderPublicKey);
99
+
100
+ const decrypted = nacl.box.open(
101
+ ciphertext,
102
+ nonce,
103
+ senderPublicKey,
104
+ recipientKeyPair.secretKey
105
+ );
106
+
107
+ if (!decrypted) return null;
108
+ return util.encodeUTF8(decrypted);
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ export function loadKnownKeys(): Record<string, string> {
115
+ const path = join(KEYS_DIR, 'known_keys.json');
116
+ if (!existsSync(path)) return {};
117
+ try {
118
+ return JSON.parse(readFileSync(path, 'utf-8'));
119
+ } catch {
120
+ return {};
121
+ }
122
+ }
123
+
124
+ export function saveKnownKey(agentId: string, publicKeyBase64: string): void {
125
+ ensureKeysDir();
126
+ const keys = loadKnownKeys();
127
+ keys[agentId] = publicKeyBase64;
128
+ writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
129
+ }
130
+
131
+ export function getKnownKey(agentId: string): string | null {
132
+ const keys = loadKnownKeys();
133
+ return keys[agentId] || null;
134
+ }
135
+
136
+ export function listOwnKeys(): string[] {
137
+ ensureKeysDir();
138
+ try {
139
+ const files = readdirSync(KEYS_DIR);
140
+ return files
141
+ .filter(f => f.endsWith('.key.json'))
142
+ .map(f => f.replace('.key.json', ''));
143
+ } catch {
144
+ return [];
145
+ }
146
+ }
147
+
148
+ export function decodePublicKey(base64: string): Uint8Array {
149
+ return util.decodeBase64(base64);
150
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Myceliumail MCP - Storage Module
3
+ *
4
+ * Local JSON storage with optional Supabase sync.
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 { getSupabaseUrl, getSupabaseKey, hasSupabase } from './config.js';
12
+
13
+ const DATA_DIR = join(homedir(), '.myceliumail', 'data');
14
+ const MESSAGES_FILE = join(DATA_DIR, 'messages.json');
15
+
16
+ export interface Message {
17
+ id: string;
18
+ sender: string;
19
+ recipient: string;
20
+ subject: string;
21
+ body: string;
22
+ encrypted: boolean;
23
+ ciphertext?: string;
24
+ nonce?: string;
25
+ senderPublicKey?: string;
26
+ read: boolean;
27
+ archived: boolean;
28
+ createdAt: Date;
29
+ }
30
+
31
+ interface StoredMessage extends Omit<Message, 'createdAt'> {
32
+ createdAt: string;
33
+ }
34
+
35
+ function ensureDataDir(): void {
36
+ if (!existsSync(DATA_DIR)) {
37
+ mkdirSync(DATA_DIR, { recursive: true });
38
+ }
39
+ }
40
+
41
+ function loadLocalMessages(): StoredMessage[] {
42
+ if (!existsSync(MESSAGES_FILE)) return [];
43
+ try {
44
+ return JSON.parse(readFileSync(MESSAGES_FILE, 'utf-8'));
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
49
+
50
+ function saveLocalMessages(messages: StoredMessage[]): void {
51
+ ensureDataDir();
52
+ writeFileSync(MESSAGES_FILE, JSON.stringify(messages, null, 2));
53
+ }
54
+
55
+ function toMessage(stored: StoredMessage): Message {
56
+ return { ...stored, createdAt: new Date(stored.createdAt) };
57
+ }
58
+
59
+ // Supabase helpers
60
+ async function supabaseRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
61
+ const url = `${getSupabaseUrl()}/rest/v1${path}`;
62
+
63
+ const response = await fetch(url, {
64
+ ...options,
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'apikey': getSupabaseKey()!,
68
+ 'Authorization': `Bearer ${getSupabaseKey()}`,
69
+ 'Prefer': options.method === 'POST' ? 'return=representation' : 'return=minimal',
70
+ ...options.headers,
71
+ },
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const text = await response.text();
76
+ console.error(`Supabase request failed (${response.status}): ${text}`);
77
+ throw new Error(text);
78
+ }
79
+
80
+ if (response.status === 204) return {} as T;
81
+ return response.json() as Promise<T>;
82
+ }
83
+
84
+ export async function sendMessage(
85
+ sender: string,
86
+ recipient: string,
87
+ subject: string,
88
+ body: string,
89
+ options?: {
90
+ encrypted?: boolean;
91
+ ciphertext?: string;
92
+ nonce?: string;
93
+ senderPublicKey?: string;
94
+ }
95
+ ): Promise<Message> {
96
+ const newMessage: StoredMessage = {
97
+ id: randomUUID(),
98
+ sender,
99
+ recipient,
100
+ subject: options?.encrypted ? '' : subject,
101
+ body: options?.encrypted ? '' : body,
102
+ encrypted: options?.encrypted || false,
103
+ ciphertext: options?.ciphertext,
104
+ nonce: options?.nonce,
105
+ senderPublicKey: options?.senderPublicKey,
106
+ read: false,
107
+ archived: false,
108
+ createdAt: new Date().toISOString(),
109
+ };
110
+
111
+ if (hasSupabase()) {
112
+ try {
113
+ const [result] = await supabaseRequest<StoredMessage[]>('/agent_messages', {
114
+ method: 'POST',
115
+ body: JSON.stringify({
116
+ from_agent: newMessage.sender,
117
+ to_agent: newMessage.recipient,
118
+ subject: newMessage.subject || null,
119
+ message: newMessage.body || null,
120
+ encrypted: newMessage.encrypted,
121
+ ciphertext: newMessage.ciphertext,
122
+ nonce: newMessage.nonce,
123
+ sender_public_key: newMessage.senderPublicKey,
124
+ }),
125
+ });
126
+ return toMessage({
127
+ ...newMessage,
128
+ id: (result as unknown as { id: string }).id
129
+ });
130
+ } catch (err) {
131
+ console.error('sendMessage failed, falling back to local:', err);
132
+ // Fall through to local
133
+ }
134
+ }
135
+
136
+ // Local storage
137
+ const messages = loadLocalMessages();
138
+ messages.push(newMessage);
139
+ saveLocalMessages(messages);
140
+ return toMessage(newMessage);
141
+ }
142
+
143
+ export async function getInbox(
144
+ agentId: string,
145
+ options?: { unreadOnly?: boolean; limit?: number }
146
+ ): Promise<Message[]> {
147
+ if (hasSupabase()) {
148
+ try {
149
+ let query = `/agent_messages?to_agent=eq.${agentId}&order=created_at.desc`;
150
+ if (options?.unreadOnly) query += '&read=eq.false';
151
+ if (options?.limit) query += `&limit=${options.limit}`;
152
+
153
+ const results = await supabaseRequest<Array<{
154
+ id: string; from_agent: string; to_agent: string;
155
+ subject: string; message: string; encrypted: boolean;
156
+ ciphertext: string; nonce: string; sender_public_key: string;
157
+ read: boolean; created_at: string;
158
+ }>>(query);
159
+
160
+ return results.map(r => ({
161
+ id: r.id,
162
+ sender: r.from_agent,
163
+ recipient: r.to_agent,
164
+ subject: r.subject || '',
165
+ body: r.message || '',
166
+ encrypted: r.encrypted,
167
+ ciphertext: r.ciphertext,
168
+ nonce: r.nonce,
169
+ senderPublicKey: r.sender_public_key,
170
+ read: r.read,
171
+ archived: false,
172
+ createdAt: new Date(r.created_at),
173
+ }));
174
+ } catch (err) {
175
+ console.error('getInbox failed, falling back to local:', err);
176
+ // Fall through to local
177
+ }
178
+ }
179
+
180
+ // Local storage
181
+ const messages = loadLocalMessages();
182
+ let filtered = messages.filter(m => m.recipient === agentId && !m.archived);
183
+ if (options?.unreadOnly) filtered = filtered.filter(m => !m.read);
184
+ filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
185
+ if (options?.limit) filtered = filtered.slice(0, options.limit);
186
+ return filtered.map(toMessage);
187
+ }
188
+
189
+ export async function getMessage(id: string): Promise<Message | null> {
190
+ if (hasSupabase()) {
191
+ try {
192
+ const results = await supabaseRequest<Array<{
193
+ id: string; from_agent: string; to_agent: string;
194
+ subject: string; message: string; encrypted: boolean;
195
+ ciphertext: string; nonce: string; sender_public_key: string;
196
+ read: boolean; created_at: string;
197
+ }>>(`/agent_messages?id=eq.${id}`);
198
+
199
+ if (results.length > 0) {
200
+ const r = results[0];
201
+ return {
202
+ id: r.id,
203
+ sender: r.from_agent,
204
+ recipient: r.to_agent,
205
+ subject: r.subject || '',
206
+ body: r.message || '',
207
+ encrypted: r.encrypted,
208
+ ciphertext: r.ciphertext,
209
+ nonce: r.nonce,
210
+ senderPublicKey: r.sender_public_key,
211
+ read: r.read,
212
+ archived: false,
213
+ createdAt: new Date(r.created_at),
214
+ };
215
+ }
216
+ } catch (err) {
217
+ console.error('getMessage failed, falling back to local:', err);
218
+ // Fall through
219
+ }
220
+ }
221
+
222
+ const messages = loadLocalMessages();
223
+ const found = messages.find(m => m.id === id);
224
+ return found ? toMessage(found) : null;
225
+ }
226
+
227
+ export async function markAsRead(id: string): Promise<boolean> {
228
+ if (hasSupabase()) {
229
+ try {
230
+ await supabaseRequest(`/agent_messages?id=eq.${id}`, {
231
+ method: 'PATCH',
232
+ body: JSON.stringify({ read: true }),
233
+ });
234
+ return true;
235
+ } catch {
236
+ // Fall through
237
+ }
238
+ }
239
+
240
+ const messages = loadLocalMessages();
241
+ const idx = messages.findIndex(m => m.id === id);
242
+ if (idx === -1) return false;
243
+ messages[idx].read = true;
244
+ saveLocalMessages(messages);
245
+ return true;
246
+ }
247
+
248
+ export async function archiveMessage(id: string): Promise<boolean> {
249
+ if (hasSupabase()) {
250
+ try {
251
+ await supabaseRequest(`/agent_messages?id=eq.${id}`, {
252
+ method: 'PATCH',
253
+ body: JSON.stringify({ archived: true }),
254
+ });
255
+ return true;
256
+ } catch {
257
+ // Fall through
258
+ }
259
+ }
260
+
261
+ const messages = loadLocalMessages();
262
+ const idx = messages.findIndex(m => m.id === id);
263
+ if (idx === -1) return false;
264
+ messages[idx].archived = true;
265
+ saveLocalMessages(messages);
266
+ return true;
267
+ }