myceliumail 1.0.4 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CODEX_SETUP.md +36 -0
  3. package/README.md +20 -1
  4. package/dist/bin/myceliumail.js +4 -0
  5. package/dist/bin/myceliumail.js.map +1 -1
  6. package/dist/commands/export.d.ts +6 -0
  7. package/dist/commands/export.d.ts.map +1 -0
  8. package/dist/commands/export.js +171 -0
  9. package/dist/commands/export.js.map +1 -0
  10. package/dist/commands/send.d.ts +1 -0
  11. package/dist/commands/send.d.ts.map +1 -1
  12. package/dist/commands/send.js +30 -6
  13. package/dist/commands/send.js.map +1 -1
  14. package/dist/commands/status.d.ts +10 -0
  15. package/dist/commands/status.d.ts.map +1 -0
  16. package/dist/commands/status.js +93 -0
  17. package/dist/commands/status.js.map +1 -0
  18. package/dist/commands/watch.d.ts +4 -0
  19. package/dist/commands/watch.d.ts.map +1 -1
  20. package/dist/commands/watch.js +69 -0
  21. package/dist/commands/watch.js.map +1 -1
  22. package/dist/lib/config.js +1 -1
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/crypto.d.ts.map +1 -1
  25. package/dist/lib/crypto.js +5 -4
  26. package/dist/lib/crypto.js.map +1 -1
  27. package/dist/storage/local.d.ts.map +1 -1
  28. package/dist/storage/local.js +5 -2
  29. package/dist/storage/local.js.map +1 -1
  30. package/dist/storage/supabase.d.ts +3 -3
  31. package/dist/storage/supabase.d.ts.map +1 -1
  32. package/dist/storage/supabase.js +56 -5
  33. package/dist/storage/supabase.js.map +1 -1
  34. package/mcp-server/README.md +11 -0
  35. package/mcp-server/package-lock.json +2 -2
  36. package/mcp-server/package.json +5 -4
  37. package/mcp-server/src/lib/storage.ts +74 -27
  38. package/package.json +1 -1
  39. package/src/bin/myceliumail.ts +4 -0
  40. package/src/commands/export.ts +212 -0
  41. package/src/commands/send.ts +34 -6
  42. package/src/commands/status.ts +114 -0
  43. package/src/commands/watch.ts +86 -0
  44. package/src/lib/config.ts +1 -1
  45. package/src/lib/crypto.ts +5 -4
  46. package/src/storage/local.ts +5 -2
  47. package/src/storage/supabase.ts +67 -5
@@ -0,0 +1,212 @@
1
+ /**
2
+ * export command - Export messages for RAG/backup
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { writeFileSync } from 'fs';
7
+ import { loadConfig } from '../lib/config.js';
8
+ import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
9
+ import * as storage from '../storage/supabase.js';
10
+ import type { Message } from '../types/index.js';
11
+
12
+ interface ExportOptions {
13
+ format: 'jsonl' | 'json' | 'md';
14
+ output?: string;
15
+ from?: string;
16
+ since?: string;
17
+ limit?: string;
18
+ decrypt?: boolean;
19
+ }
20
+
21
+ interface ExportedMessage {
22
+ id: string;
23
+ from: string;
24
+ to: string;
25
+ subject: string;
26
+ content: string;
27
+ encrypted: boolean;
28
+ created_at: string;
29
+ }
30
+
31
+ /**
32
+ * Decrypt a message if possible
33
+ */
34
+ function tryDecrypt(msg: Message, keyPair: { publicKey: Uint8Array; secretKey: Uint8Array } | null): { subject: string; content: string } {
35
+ if (!msg.encrypted) {
36
+ return { subject: msg.subject, content: msg.body };
37
+ }
38
+
39
+ if (!keyPair || !msg.ciphertext || !msg.nonce || !msg.senderPublicKey) {
40
+ return { subject: msg.subject, content: '[Encrypted - No keys available]' };
41
+ }
42
+
43
+ try {
44
+ const decrypted = decryptMessage({
45
+ ciphertext: msg.ciphertext,
46
+ nonce: msg.nonce,
47
+ senderPublicKey: msg.senderPublicKey,
48
+ }, keyPair);
49
+
50
+ if (decrypted) {
51
+ const parsed = JSON.parse(decrypted);
52
+ return {
53
+ subject: parsed.subject || msg.subject,
54
+ content: parsed.body || parsed.message || decrypted,
55
+ };
56
+ }
57
+ } catch {
58
+ // Decryption failed
59
+ }
60
+
61
+ return { subject: msg.subject, content: '[Encrypted - Decryption failed]' };
62
+ }
63
+
64
+ /**
65
+ * Format messages as JSONL (one JSON object per line)
66
+ */
67
+ function formatJsonl(messages: ExportedMessage[]): string {
68
+ return messages.map(m => JSON.stringify(m)).join('\n');
69
+ }
70
+
71
+ /**
72
+ * Format messages as JSON array with metadata
73
+ */
74
+ function formatJson(messages: ExportedMessage[], agentId: string): string {
75
+ return JSON.stringify({
76
+ exported_at: new Date().toISOString(),
77
+ agent_id: agentId,
78
+ message_count: messages.length,
79
+ messages,
80
+ }, null, 2);
81
+ }
82
+
83
+ /**
84
+ * Format messages as Markdown
85
+ */
86
+ function formatMarkdown(messages: ExportedMessage[], agentId: string): string {
87
+ const lines: string[] = [
88
+ `# Myceliumail Archive - ${agentId}`,
89
+ `Exported: ${new Date().toLocaleString()}`,
90
+ `Total messages: ${messages.length}`,
91
+ '',
92
+ ];
93
+
94
+ for (const msg of messages) {
95
+ const date = new Date(msg.created_at).toLocaleString();
96
+ const encMarker = msg.encrypted ? ' šŸ”' : '';
97
+ lines.push('---');
98
+ lines.push(`## šŸ“¬ From: ${msg.from}${encMarker} | ${date}`);
99
+ lines.push(`**Subject:** ${msg.subject}`);
100
+ lines.push('');
101
+ lines.push(msg.content);
102
+ lines.push('');
103
+ }
104
+
105
+ return lines.join('\n');
106
+ }
107
+
108
+ export function createExportCommand(): Command {
109
+ return new Command('export')
110
+ .description('Export messages for RAG/backup (JSONL, JSON, or Markdown)')
111
+ .option('-f, --format <format>', 'Output format: jsonl, json, md', 'jsonl')
112
+ .option('-o, --output <file>', 'Output file (default: stdout)')
113
+ .option('--from <agent>', 'Filter by sender')
114
+ .option('--since <date>', 'Filter messages after date (ISO 8601)')
115
+ .option('-l, --limit <n>', 'Max messages to export', '100')
116
+ .option('-d, --decrypt', 'Attempt to decrypt encrypted messages')
117
+ .action(async (options: ExportOptions) => {
118
+ const config = loadConfig();
119
+ const agentId = config.agentId;
120
+
121
+ if (agentId === 'anonymous') {
122
+ console.error('āŒ Agent ID not configured.');
123
+ console.error('Set MYCELIUMAIL_AGENT_ID or configure ~/.myceliumail/config.json');
124
+ process.exit(1);
125
+ }
126
+
127
+ // Validate format
128
+ const format = options.format.toLowerCase() as 'jsonl' | 'json' | 'md';
129
+ if (!['jsonl', 'json', 'md'].includes(format)) {
130
+ console.error('āŒ Invalid format. Use: jsonl, json, or md');
131
+ process.exit(1);
132
+ }
133
+
134
+ try {
135
+ // Fetch messages
136
+ const messages = await storage.getInbox(agentId, {
137
+ limit: parseInt(options.limit || '100', 10),
138
+ });
139
+
140
+ if (messages.length === 0) {
141
+ console.error('šŸ“­ No messages to export');
142
+ process.exit(0);
143
+ }
144
+
145
+ // Load keys for decryption if requested
146
+ const keyPair = options.decrypt ? loadKeyPair(agentId) : null;
147
+
148
+ // Filter by sender if specified
149
+ let filtered = messages;
150
+ if (options.from) {
151
+ const fromLower = options.from.toLowerCase();
152
+ filtered = filtered.filter(m => m.sender.toLowerCase() === fromLower);
153
+ }
154
+
155
+ // Filter by date if specified
156
+ if (options.since) {
157
+ const sinceDate = new Date(options.since);
158
+ if (!isNaN(sinceDate.getTime())) {
159
+ filtered = filtered.filter(m => m.createdAt >= sinceDate);
160
+ }
161
+ }
162
+
163
+ if (filtered.length === 0) {
164
+ console.error('šŸ“­ No messages match filters');
165
+ process.exit(0);
166
+ }
167
+
168
+ // Transform to export format
169
+ const exported: ExportedMessage[] = filtered.map(msg => {
170
+ const { subject, content } = options.decrypt
171
+ ? tryDecrypt(msg, keyPair)
172
+ : { subject: msg.subject, content: msg.encrypted ? '[Encrypted]' : msg.body };
173
+
174
+ return {
175
+ id: msg.id,
176
+ from: msg.sender,
177
+ to: msg.recipient as string,
178
+ subject,
179
+ content,
180
+ encrypted: msg.encrypted,
181
+ created_at: msg.createdAt.toISOString(),
182
+ };
183
+ });
184
+
185
+ // Format output
186
+ let output: string;
187
+ switch (format) {
188
+ case 'jsonl':
189
+ output = formatJsonl(exported);
190
+ break;
191
+ case 'json':
192
+ output = formatJson(exported, agentId);
193
+ break;
194
+ case 'md':
195
+ output = formatMarkdown(exported, agentId);
196
+ break;
197
+ }
198
+
199
+ // Write to file or stdout
200
+ if (options.output) {
201
+ writeFileSync(options.output, output);
202
+ console.error(`āœ… Exported ${filtered.length} messages to ${options.output}`);
203
+ } else {
204
+ console.log(output);
205
+ }
206
+
207
+ } catch (error) {
208
+ console.error('āŒ Export failed:', error);
209
+ process.exit(1);
210
+ }
211
+ });
212
+ }
@@ -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 provide via stdin)')
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
- const body = options.message || subject;
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(recipient);
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 ${recipient}. Run: mycmail key-import ${recipient} <key>`);
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
- recipient,
101
+ normalizedRecipient,
75
102
  subject,
76
103
  body,
77
104
  messageOptions
78
105
  );
79
106
 
80
- console.log(`\nāœ… Message sent to ${recipient}`);
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
+ }
@@ -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
  }
@@ -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()