myceliumail 1.0.5 → 1.0.6
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 +13 -0
- package/CODEX_SETUP.md +36 -0
- package/README.md +20 -1
- package/dist/bin/myceliumail.js +4 -0
- package/dist/bin/myceliumail.js.map +1 -1
- 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/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/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/README.md +11 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +5 -4
- package/mcp-server/src/lib/storage.ts +74 -27
- package/package.json +1 -1
- package/src/bin/myceliumail.ts +4 -0
- package/src/commands/export.ts +212 -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/storage/local.ts +5 -2
package/src/commands/send.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* send command - Send a message to another agent
|
|
3
3
|
*
|
|
4
4
|
* Messages are encrypted by default. Use --plaintext to send unencrypted.
|
|
5
|
+
* Message body can be provided via -m flag, stdin pipe, or defaults to subject.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Command } from 'commander';
|
|
@@ -14,16 +15,36 @@ import {
|
|
|
14
15
|
} from '../lib/crypto.js';
|
|
15
16
|
import * as storage from '../storage/supabase.js';
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Read from stdin if data is being piped
|
|
20
|
+
*/
|
|
21
|
+
async function readStdin(): Promise<string | null> {
|
|
22
|
+
// Check if stdin is a TTY (interactive terminal) - if so, no piped data
|
|
23
|
+
if (process.stdin.isTTY) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let data = '';
|
|
29
|
+
process.stdin.setEncoding('utf8');
|
|
30
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
31
|
+
process.stdin.on('end', () => { resolve(data.trim() || null); });
|
|
32
|
+
// Timeout after 100ms if no data
|
|
33
|
+
setTimeout(() => resolve(null), 100);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
export function createSendCommand(): Command {
|
|
18
38
|
return new Command('send')
|
|
19
39
|
.description('Send a message to another agent (encrypted by default)')
|
|
20
40
|
.argument('<recipient>', 'Recipient agent ID')
|
|
21
41
|
.argument('<subject>', 'Message subject')
|
|
22
|
-
.option('-m, --message <body>', 'Message body (or
|
|
42
|
+
.option('-m, --message <body>', 'Message body (or pipe via stdin)')
|
|
23
43
|
.option('-p, --plaintext', 'Send unencrypted (not recommended)')
|
|
24
44
|
.action(async (recipient: string, subject: string, options) => {
|
|
25
45
|
const config = loadConfig();
|
|
26
46
|
const sender = config.agentId;
|
|
47
|
+
const normalizedRecipient = recipient.toLowerCase();
|
|
27
48
|
|
|
28
49
|
if (sender === 'anonymous') {
|
|
29
50
|
console.error('❌ Agent ID not configured.');
|
|
@@ -31,14 +52,20 @@ export function createSendCommand(): Command {
|
|
|
31
52
|
process.exit(1);
|
|
32
53
|
}
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
// Try to get body from: 1) -m option, 2) stdin pipe, 3) subject
|
|
56
|
+
let body = options.message;
|
|
57
|
+
if (!body) {
|
|
58
|
+
const stdinData = await readStdin();
|
|
59
|
+
body = stdinData || subject;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
let messageOptions;
|
|
36
63
|
let encrypted = false;
|
|
37
64
|
|
|
38
65
|
// Encrypt by default unless --plaintext is specified
|
|
39
66
|
if (!options.plaintext) {
|
|
40
67
|
const senderKeyPair = loadKeyPair(sender);
|
|
41
|
-
const recipientPubKeyB64 = getKnownKey(
|
|
68
|
+
const recipientPubKeyB64 = getKnownKey(normalizedRecipient);
|
|
42
69
|
|
|
43
70
|
if (senderKeyPair && recipientPubKeyB64) {
|
|
44
71
|
try {
|
|
@@ -62,7 +89,7 @@ export function createSendCommand(): Command {
|
|
|
62
89
|
console.warn('⚠️ No keypair found. Run: mycmail keygen');
|
|
63
90
|
}
|
|
64
91
|
if (!recipientPubKeyB64) {
|
|
65
|
-
console.warn(`⚠️ No public key for ${
|
|
92
|
+
console.warn(`⚠️ No public key for ${normalizedRecipient}. Run: mycmail key-import ${normalizedRecipient} <key>`);
|
|
66
93
|
}
|
|
67
94
|
console.warn(' Sending as plaintext (use -p to suppress this warning)\n');
|
|
68
95
|
}
|
|
@@ -71,13 +98,13 @@ export function createSendCommand(): Command {
|
|
|
71
98
|
try {
|
|
72
99
|
const message = await storage.sendMessage(
|
|
73
100
|
sender,
|
|
74
|
-
|
|
101
|
+
normalizedRecipient,
|
|
75
102
|
subject,
|
|
76
103
|
body,
|
|
77
104
|
messageOptions
|
|
78
105
|
);
|
|
79
106
|
|
|
80
|
-
console.log(`\n✅ Message sent to ${
|
|
107
|
+
console.log(`\n✅ Message sent to ${normalizedRecipient}`);
|
|
81
108
|
console.log(` ID: ${message.id}`);
|
|
82
109
|
console.log(` Subject: ${subject}`);
|
|
83
110
|
console.log(` ${encrypted ? '🔐 Encrypted' : '📨 Plaintext'}`);
|
|
@@ -87,3 +114,4 @@ export function createSendCommand(): Command {
|
|
|
87
114
|
}
|
|
88
115
|
});
|
|
89
116
|
}
|
|
117
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Command
|
|
3
|
+
*
|
|
4
|
+
* Check the current inbox notification status from the status file.
|
|
5
|
+
* This allows agents to quickly check if they have new mail without
|
|
6
|
+
* running the watch command.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
interface InboxStatus {
|
|
15
|
+
status: 0 | 1 | 2; // 0=none, 1=new message, 2=urgent
|
|
16
|
+
count: number;
|
|
17
|
+
lastMessage?: {
|
|
18
|
+
from: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
time: string;
|
|
21
|
+
encrypted: boolean;
|
|
22
|
+
};
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STATUS_FILE_PATH = join(homedir(), '.mycmail', 'inbox_status.json');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read current inbox status
|
|
30
|
+
*/
|
|
31
|
+
function readInboxStatus(): InboxStatus | null {
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(STATUS_FILE_PATH)) {
|
|
34
|
+
const content = readFileSync(STATUS_FILE_PATH, 'utf-8');
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Return null if file doesn't exist or is invalid
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clear inbox status (set to 0)
|
|
45
|
+
*/
|
|
46
|
+
function clearInboxStatus(): void {
|
|
47
|
+
const dir = join(homedir(), '.mycmail');
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const status: InboxStatus = { status: 0, count: 0, updatedAt: new Date().toISOString() };
|
|
52
|
+
writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createStatusCommand(): Command {
|
|
56
|
+
const command = new Command('status')
|
|
57
|
+
.description('Check inbox notification status (0=none, 1=new, 2=urgent)')
|
|
58
|
+
.option('--clear', 'Clear the status (acknowledge messages)')
|
|
59
|
+
.option('--json', 'Output as JSON')
|
|
60
|
+
.option('--number-only', 'Output only the status number (0, 1, or 2)')
|
|
61
|
+
.action(async (options) => {
|
|
62
|
+
if (options.clear) {
|
|
63
|
+
clearInboxStatus();
|
|
64
|
+
if (!options.numberOnly) {
|
|
65
|
+
console.log('✅ Status cleared');
|
|
66
|
+
} else {
|
|
67
|
+
console.log('0');
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const status = readInboxStatus();
|
|
73
|
+
|
|
74
|
+
if (!status) {
|
|
75
|
+
if (options.json) {
|
|
76
|
+
console.log(JSON.stringify({ status: 0, count: 0, message: 'No status file found' }));
|
|
77
|
+
} else if (options.numberOnly) {
|
|
78
|
+
console.log('0');
|
|
79
|
+
} else {
|
|
80
|
+
console.log('📭 No status file found. Run `mycmail watch --status-file` to enable.');
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.numberOnly) {
|
|
86
|
+
console.log(status.status.toString());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(JSON.stringify(status, null, 2));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Human-readable output
|
|
96
|
+
const statusEmoji = status.status === 0 ? '📭' : status.status === 1 ? '📬' : '🚨';
|
|
97
|
+
const statusText = status.status === 0 ? 'No new messages' : status.status === 1 ? 'New message(s)' : 'URGENT message(s)';
|
|
98
|
+
|
|
99
|
+
console.log(`\n${statusEmoji} ${statusText}`);
|
|
100
|
+
console.log(` Count: ${status.count}`);
|
|
101
|
+
|
|
102
|
+
if (status.lastMessage) {
|
|
103
|
+
console.log(` Last: ${status.lastMessage.from} - "${status.lastMessage.subject}"`);
|
|
104
|
+
if (status.lastMessage.encrypted) {
|
|
105
|
+
console.log(` 🔒 Message is encrypted`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(` Updated: ${new Date(status.updatedAt).toLocaleString()}`);
|
|
110
|
+
console.log();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return command;
|
|
114
|
+
}
|
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
|
}
|
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()
|