myceliumail 1.0.5 → 1.0.7

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 (59) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CODEX_SETUP.md +47 -0
  3. package/README.md +68 -2
  4. package/dist/bin/myceliumail.js +8 -0
  5. package/dist/bin/myceliumail.js.map +1 -1
  6. package/dist/commands/activate.d.ts +10 -0
  7. package/dist/commands/activate.d.ts.map +1 -0
  8. package/dist/commands/activate.js +77 -0
  9. package/dist/commands/activate.js.map +1 -0
  10. package/dist/commands/export.d.ts +6 -0
  11. package/dist/commands/export.d.ts.map +1 -0
  12. package/dist/commands/export.js +171 -0
  13. package/dist/commands/export.js.map +1 -0
  14. package/dist/commands/key-import.d.ts.map +1 -1
  15. package/dist/commands/key-import.js +5 -0
  16. package/dist/commands/key-import.js.map +1 -1
  17. package/dist/commands/send.d.ts +1 -0
  18. package/dist/commands/send.d.ts.map +1 -1
  19. package/dist/commands/send.js +30 -6
  20. package/dist/commands/send.js.map +1 -1
  21. package/dist/commands/status.d.ts +10 -0
  22. package/dist/commands/status.d.ts.map +1 -0
  23. package/dist/commands/status.js +93 -0
  24. package/dist/commands/status.js.map +1 -0
  25. package/dist/commands/watch.d.ts +4 -0
  26. package/dist/commands/watch.d.ts.map +1 -1
  27. package/dist/commands/watch.js +69 -0
  28. package/dist/commands/watch.js.map +1 -1
  29. package/dist/lib/config.js +1 -1
  30. package/dist/lib/config.js.map +1 -1
  31. package/dist/lib/crypto.d.ts.map +1 -1
  32. package/dist/lib/crypto.js +5 -4
  33. package/dist/lib/crypto.js.map +1 -1
  34. package/dist/lib/license.d.ts +61 -0
  35. package/dist/lib/license.d.ts.map +1 -0
  36. package/dist/lib/license.js +173 -0
  37. package/dist/lib/license.js.map +1 -0
  38. package/dist/storage/local.d.ts.map +1 -1
  39. package/dist/storage/local.js +5 -2
  40. package/dist/storage/local.js.map +1 -1
  41. package/mcp-server/CHANGELOG.md +68 -0
  42. package/mcp-server/README.md +11 -0
  43. package/mcp-server/package-lock.json +2 -2
  44. package/mcp-server/package.json +5 -4
  45. package/mcp-server/src/lib/license.ts +147 -0
  46. package/mcp-server/src/lib/storage.ts +74 -27
  47. package/mcp-server/src/server.ts +4 -0
  48. package/package.json +1 -1
  49. package/src/bin/myceliumail.ts +10 -0
  50. package/src/commands/activate.ts +85 -0
  51. package/src/commands/export.ts +212 -0
  52. package/src/commands/key-import.ts +7 -0
  53. package/src/commands/send.ts +34 -6
  54. package/src/commands/status.ts +114 -0
  55. package/src/commands/watch.ts +86 -0
  56. package/src/lib/config.ts +1 -1
  57. package/src/lib/crypto.ts +5 -4
  58. package/src/lib/license.ts +215 -0
  59. package/src/storage/local.ts +5 -2
@@ -6,26 +6,112 @@
6
6
 
7
7
  import { Command } from 'commander';
8
8
  import notifier from 'node-notifier';
9
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { homedir } from 'os';
9
12
  import { loadConfig } from '../lib/config.js';
10
13
  import { subscribeToMessages, closeConnection } from '../lib/realtime.js';
11
14
 
15
+ interface InboxStatus {
16
+ status: 0 | 1 | 2; // 0=none, 1=new message, 2=urgent
17
+ count: number;
18
+ lastMessage?: {
19
+ from: string;
20
+ subject: string;
21
+ time: string;
22
+ encrypted: boolean;
23
+ };
24
+ updatedAt: string;
25
+ }
26
+
27
+ const STATUS_FILE_PATH = join(homedir(), '.mycmail', 'inbox_status.json');
28
+
29
+ /**
30
+ * Read current inbox status, or return default
31
+ */
32
+ function readInboxStatus(): InboxStatus {
33
+ try {
34
+ if (existsSync(STATUS_FILE_PATH)) {
35
+ const content = readFileSync(STATUS_FILE_PATH, 'utf-8');
36
+ return JSON.parse(content);
37
+ }
38
+ } catch {
39
+ // Return default if file doesn't exist or is invalid
40
+ }
41
+ return { status: 0, count: 0, updatedAt: new Date().toISOString() };
42
+ }
43
+
44
+ /**
45
+ * Write inbox status to file
46
+ */
47
+ function writeInboxStatus(status: InboxStatus): void {
48
+ const dir = join(homedir(), '.mycmail');
49
+ if (!existsSync(dir)) {
50
+ mkdirSync(dir, { recursive: true });
51
+ }
52
+ writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
53
+ }
54
+
55
+ /**
56
+ * Clear inbox status (set to 0)
57
+ */
58
+ export function clearInboxStatus(): void {
59
+ writeInboxStatus({ status: 0, count: 0, updatedAt: new Date().toISOString() });
60
+ }
61
+
12
62
  export function createWatchCommand(): Command {
13
63
  const command = new Command('watch')
14
64
  .description('Watch for new messages in real-time')
15
65
  .option('-a, --agent <id>', 'Agent ID to watch (default: current agent)')
16
66
  .option('-q, --quiet', 'Suppress console output, only show notifications')
67
+ .option('-s, --status-file', 'Write notification status to ~/.mycmail/inbox_status.json')
68
+ .option('--clear-status', 'Clear the status file and exit')
17
69
  .action(async (options) => {
70
+ // Handle --clear-status flag
71
+ if (options.clearStatus) {
72
+ clearInboxStatus();
73
+ console.log('āœ… Inbox status cleared (set to 0)');
74
+ return;
75
+ }
76
+
18
77
  const config = loadConfig();
19
78
  const agentId = options.agent || config.agentId;
20
79
 
21
80
  if (!options.quiet) {
22
81
  console.log(`\nšŸ„ Watching inbox for ${agentId}...`);
82
+ if (options.statusFile) {
83
+ console.log(`šŸ“ Status file: ${STATUS_FILE_PATH}`);
84
+ // Initialize status file to 0 at start
85
+ clearInboxStatus();
86
+ }
23
87
  console.log('Press Ctrl+C to stop\n');
24
88
  }
25
89
 
26
90
  const channel = subscribeToMessages(
27
91
  agentId,
28
92
  (message) => {
93
+ // Update status file if enabled
94
+ if (options.statusFile) {
95
+ const currentStatus = readInboxStatus();
96
+ // Detect urgency: check for "urgent" in subject (case-insensitive)
97
+ const isUrgent = message.subject?.toLowerCase().includes('urgent');
98
+ const newStatus: InboxStatus = {
99
+ status: isUrgent ? 2 : 1,
100
+ count: currentStatus.count + 1,
101
+ lastMessage: {
102
+ from: message.from_agent,
103
+ subject: message.subject,
104
+ time: message.created_at,
105
+ encrypted: message.encrypted,
106
+ },
107
+ updatedAt: new Date().toISOString(),
108
+ };
109
+ writeInboxStatus(newStatus);
110
+ if (!options.quiet) {
111
+ console.log(`šŸ“ Status file updated (status: ${newStatus.status}, count: ${newStatus.count})`);
112
+ }
113
+ }
114
+
29
115
  // Show console output
30
116
  if (!options.quiet) {
31
117
  const time = new Date(message.created_at).toLocaleTimeString();
package/src/lib/config.ts CHANGED
@@ -56,7 +56,7 @@ export function loadConfig(): Config {
56
56
 
57
57
  // Merge with env taking precedence
58
58
  const config: Config = {
59
- agentId: envAgentId || fileConfig.agentId || 'anonymous',
59
+ agentId: (envAgentId || fileConfig.agentId || 'anonymous').toLowerCase(),
60
60
  supabaseUrl: envSupabaseUrl || fileConfig.supabaseUrl,
61
61
  supabaseKey: envSupabaseKey || fileConfig.supabaseKey,
62
62
  storageMode: envStorageMode || fileConfig.storageMode || 'auto',
package/src/lib/crypto.ts CHANGED
@@ -157,7 +157,7 @@ export function loadKnownKeys(): Record<string, string> {
157
157
  export function saveKnownKey(agentId: string, publicKeyBase64: string): void {
158
158
  ensureKeysDir();
159
159
  const keys = loadKnownKeys();
160
- keys[agentId] = publicKeyBase64;
160
+ keys[agentId.toLowerCase()] = publicKeyBase64;
161
161
  writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
162
162
  }
163
163
 
@@ -166,7 +166,7 @@ export function saveKnownKey(agentId: string, publicKeyBase64: string): void {
166
166
  */
167
167
  export function getKnownKey(agentId: string): string | null {
168
168
  const keys = loadKnownKeys();
169
- return keys[agentId] || null;
169
+ return keys[agentId.toLowerCase()] || null;
170
170
  }
171
171
 
172
172
  /**
@@ -181,8 +181,9 @@ export function getKnownKeys(): Record<string, string> {
181
181
  */
182
182
  export function deleteKnownKey(agentId: string): boolean {
183
183
  const keys = loadKnownKeys();
184
- if (!(agentId in keys)) return false;
185
- delete keys[agentId];
184
+ const normalizedId = agentId.toLowerCase();
185
+ if (!(normalizedId in keys)) return false;
186
+ delete keys[normalizedId];
186
187
  writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
187
188
  return true;
188
189
  }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Myceliumail License Verification
3
+ *
4
+ * Ed25519-based license verification for Pro features.
5
+ * Public key is embedded; private key is kept by treebird for signing.
6
+ */
7
+
8
+ import nacl from 'tweetnacl';
9
+ import util from 'tweetnacl-util';
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { homedir } from 'os';
13
+ import { loadKnownKeys } from './crypto.js';
14
+
15
+ // License storage location
16
+ const LICENSE_DIR = join(homedir(), '.myceliumail');
17
+ const LICENSE_FILE = join(LICENSE_DIR, 'license.key');
18
+
19
+ // Treebird's public key for license verification (Ed25519)
20
+ // Use the same key as Spidersan for unified licensing
21
+ const TREEBIRD_PUBLIC_KEY = 'XqIqSlybZGKkKemgLKKl8P9MepnObhcJcxxZHtgG8/o=';
22
+
23
+ // Free tier limits
24
+ export const FREE_TIER_LIMITS = {
25
+ maxImportedKeys: 5,
26
+ };
27
+
28
+ // Pro features
29
+ export type ProFeature =
30
+ | 'unlimited_keys'
31
+ | 'mcp_server'
32
+ | 'cloud_sync'
33
+ | 'key_backup'
34
+ | 'realtime_watch';
35
+
36
+ export interface LicenseData {
37
+ email: string;
38
+ plan: 'free' | 'pro';
39
+ expiresAt: string; // ISO date
40
+ issuedAt: string; // ISO date
41
+ features: ProFeature[];
42
+ }
43
+
44
+ export interface License {
45
+ data: LicenseData;
46
+ isValid: boolean;
47
+ isExpired: boolean;
48
+ }
49
+
50
+ /**
51
+ * Ensure license directory exists
52
+ */
53
+ function ensureLicenseDir(): void {
54
+ if (!existsSync(LICENSE_DIR)) {
55
+ mkdirSync(LICENSE_DIR, { recursive: true });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Parse a license string into components
61
+ * Format: LICENSE_V1.BASE64_DATA.BASE64_SIGNATURE
62
+ */
63
+ function parseLicenseString(licenseString: string): { version: string; data: string; signature: string } | null {
64
+ const parts = licenseString.trim().split('.');
65
+ if (parts.length !== 3) return null;
66
+
67
+ const [version, data, signature] = parts;
68
+ if (version !== 'LICENSE_V1') return null;
69
+
70
+ return { version, data, signature };
71
+ }
72
+
73
+ /**
74
+ * Verify a license string using Ed25519 detached signature
75
+ */
76
+ export function verifyLicense(licenseString: string): License | null {
77
+ try {
78
+ const parsed = parseLicenseString(licenseString);
79
+ if (!parsed) return null;
80
+
81
+ const dataBytes = util.decodeBase64(parsed.data);
82
+
83
+ // Verify detached Ed25519 signature
84
+ const publicKey = util.decodeBase64(TREEBIRD_PUBLIC_KEY);
85
+ const signatureBytes = util.decodeBase64(parsed.signature);
86
+
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
+ const isValid = (nacl as any).sign.detached.verify(dataBytes, signatureBytes, publicKey);
89
+ if (!isValid) return null;
90
+
91
+ const dataString = util.encodeUTF8(dataBytes);
92
+ const data = JSON.parse(dataString) as LicenseData;
93
+
94
+ const expiresAt = new Date(data.expiresAt);
95
+ const isExpired = expiresAt < new Date();
96
+
97
+ return {
98
+ data,
99
+ isValid: true,
100
+ isExpired,
101
+ };
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Save a license key to disk
109
+ */
110
+ export function saveLicense(licenseString: string): boolean {
111
+ try {
112
+ ensureLicenseDir();
113
+ writeFileSync(LICENSE_FILE, licenseString.trim(), { mode: 0o600 });
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Load the saved license from disk
122
+ */
123
+ export function loadLicense(): License | null {
124
+ if (!existsSync(LICENSE_FILE)) return null;
125
+
126
+ try {
127
+ const licenseString = readFileSync(LICENSE_FILE, 'utf-8');
128
+ return verifyLicense(licenseString);
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Check if user has a valid Pro license
136
+ */
137
+ export function isPro(): boolean {
138
+ const license = loadLicense();
139
+ if (!license) return false;
140
+ if (!license.isValid) return false;
141
+ if (license.isExpired) return false;
142
+ return license.data.plan === 'pro';
143
+ }
144
+
145
+ /**
146
+ * Check if a specific Pro feature is enabled
147
+ */
148
+ export function hasFeature(feature: ProFeature): boolean {
149
+ const license = loadLicense();
150
+ if (!license || !license.isValid || license.isExpired) return false;
151
+ return license.data.features.includes(feature);
152
+ }
153
+
154
+ /**
155
+ * Get license status summary
156
+ */
157
+ export function getLicenseStatus(): {
158
+ plan: 'free' | 'pro';
159
+ email?: string;
160
+ expiresAt?: string;
161
+ features: ProFeature[];
162
+ daysRemaining?: number;
163
+ } {
164
+ const license = loadLicense();
165
+
166
+ if (!license || !license.isValid || license.isExpired) {
167
+ return {
168
+ plan: 'free',
169
+ features: [],
170
+ };
171
+ }
172
+
173
+ const expiresAt = new Date(license.data.expiresAt);
174
+ const now = new Date();
175
+ const daysRemaining = Math.ceil((expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
176
+
177
+ return {
178
+ plan: license.data.plan,
179
+ email: license.data.email,
180
+ expiresAt: license.data.expiresAt,
181
+ features: license.data.features,
182
+ daysRemaining,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Check imported key limit and throw if exceeded (for free tier)
188
+ */
189
+ export function checkKeyLimit(): void {
190
+ if (isPro()) return; // Pro has unlimited
191
+
192
+ const knownKeys = loadKnownKeys();
193
+ const keyCount = Object.keys(knownKeys).length;
194
+
195
+ if (keyCount >= FREE_TIER_LIMITS.maxImportedKeys) {
196
+ console.error(`\nšŸ„ Free tier limit reached: ${keyCount}/${FREE_TIER_LIMITS.maxImportedKeys} imported keys`);
197
+ console.error('');
198
+ console.error(' Options:');
199
+ console.error(' • Remove unused keys from ~/.myceliumail/keys/known_keys.json');
200
+ console.error(' • Upgrade: myceliumail.dev/pro for unlimited keys');
201
+ console.error('');
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Print Pro upsell message (soft sell)
208
+ */
209
+ export function printProUpsell(feature: string): void {
210
+ if (isPro()) return;
211
+
212
+ console.log('');
213
+ console.log(`šŸ’Ž Pro tip: Upgrade for ${feature}`);
214
+ console.log(' myceliumail.dev/pro');
215
+ }
@@ -107,10 +107,12 @@ export async function sendMessage(
107
107
  export async function getInbox(agentId: string, options?: InboxOptions): Promise<Message[]> {
108
108
  const messages = loadMessages();
109
109
 
110
+ const normalizedAgentId = agentId.toLowerCase();
110
111
  let filtered = agentId === 'all'
111
112
  ? messages.filter(m => !m.archived)
112
113
  : messages.filter(m =>
113
- (m.recipient === agentId || m.recipients?.includes(agentId)) && !m.archived
114
+ (m.recipient.toLowerCase() === normalizedAgentId ||
115
+ m.recipients?.some(r => r.toLowerCase() === normalizedAgentId)) && !m.archived
114
116
  );
115
117
 
116
118
  if (options?.unreadOnly) {
@@ -194,8 +196,9 @@ export async function archiveMessage(id: string): Promise<boolean> {
194
196
  */
195
197
  export async function getSent(agentId: string, limit?: number): Promise<Message[]> {
196
198
  const messages = loadMessages();
199
+ const normalizedAgentId = agentId.toLowerCase();
197
200
 
198
- let filtered = messages.filter(m => m.sender === agentId);
201
+ let filtered = messages.filter(m => m.sender.toLowerCase() === normalizedAgentId);
199
202
 
200
203
  filtered.sort((a, b) =>
201
204
  new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()