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.
- package/CHANGELOG.md +50 -0
- package/CODEX_SETUP.md +47 -0
- package/README.md +68 -2
- package/dist/bin/myceliumail.js +8 -0
- package/dist/bin/myceliumail.js.map +1 -1
- package/dist/commands/activate.d.ts +10 -0
- package/dist/commands/activate.d.ts.map +1 -0
- package/dist/commands/activate.js +77 -0
- package/dist/commands/activate.js.map +1 -0
- package/dist/commands/export.d.ts +6 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +171 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/key-import.d.ts.map +1 -1
- package/dist/commands/key-import.js +5 -0
- package/dist/commands/key-import.js.map +1 -1
- package/dist/commands/send.d.ts +1 -0
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +30 -6
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +93 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +69 -0
- package/dist/commands/watch.js.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crypto.d.ts.map +1 -1
- package/dist/lib/crypto.js +5 -4
- package/dist/lib/crypto.js.map +1 -1
- package/dist/lib/license.d.ts +61 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +173 -0
- package/dist/lib/license.js.map +1 -0
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +5 -2
- package/dist/storage/local.js.map +1 -1
- package/mcp-server/CHANGELOG.md +68 -0
- package/mcp-server/README.md +11 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +5 -4
- package/mcp-server/src/lib/license.ts +147 -0
- package/mcp-server/src/lib/storage.ts +74 -27
- package/mcp-server/src/server.ts +4 -0
- package/package.json +1 -1
- package/src/bin/myceliumail.ts +10 -0
- package/src/commands/activate.ts +85 -0
- package/src/commands/export.ts +212 -0
- package/src/commands/key-import.ts +7 -0
- package/src/commands/send.ts +34 -6
- package/src/commands/status.ts +114 -0
- package/src/commands/watch.ts +86 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/crypto.ts +5 -4
- package/src/lib/license.ts +215 -0
- package/src/storage/local.ts +5 -2
package/src/commands/watch.ts
CHANGED
|
@@ -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
|
-
|
|
185
|
-
|
|
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
|
+
}
|
package/src/storage/local.ts
CHANGED
|
@@ -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 ===
|
|
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 ===
|
|
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()
|