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/mcp-server/src/server.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { z } from 'zod';
|
|
|
14
14
|
import * as crypto from './lib/crypto.js';
|
|
15
15
|
import * as storage from './lib/storage.js';
|
|
16
16
|
import { getAgentId } from './lib/config.js';
|
|
17
|
+
import { requireProLicense } from './lib/license.js';
|
|
17
18
|
|
|
18
19
|
// Create the MCP server
|
|
19
20
|
const server = new McpServer({
|
|
@@ -379,6 +380,9 @@ server.tool(
|
|
|
379
380
|
|
|
380
381
|
// Start the server
|
|
381
382
|
async function main() {
|
|
383
|
+
// Verify Pro license before starting
|
|
384
|
+
requireProLicense();
|
|
385
|
+
|
|
382
386
|
const transport = new StdioServerTransport();
|
|
383
387
|
await server.connect(transport);
|
|
384
388
|
console.error('Myceliumail MCP server running');
|
package/package.json
CHANGED
package/src/bin/myceliumail.ts
CHANGED
|
@@ -23,6 +23,9 @@ import { createReadCommand } from '../commands/read.js';
|
|
|
23
23
|
import { createDashboardCommand } from '../commands/dashboard.js';
|
|
24
24
|
import { createBroadcastCommand } from '../commands/broadcast.js';
|
|
25
25
|
import { createWatchCommand } from '../commands/watch.js';
|
|
26
|
+
import { createExportCommand } from '../commands/export.js';
|
|
27
|
+
import { createStatusCommand } from '../commands/status.js';
|
|
28
|
+
import { createActivateCommand, createLicenseStatusCommand } from '../commands/activate.js';
|
|
26
29
|
|
|
27
30
|
const program = new Command();
|
|
28
31
|
|
|
@@ -49,6 +52,13 @@ program.addCommand(createReadCommand());
|
|
|
49
52
|
program.addCommand(createDashboardCommand());
|
|
50
53
|
program.addCommand(createBroadcastCommand());
|
|
51
54
|
program.addCommand(createWatchCommand());
|
|
55
|
+
program.addCommand(createExportCommand());
|
|
56
|
+
program.addCommand(createStatusCommand());
|
|
57
|
+
|
|
58
|
+
// License management
|
|
59
|
+
program.addCommand(createActivateCommand());
|
|
60
|
+
program.addCommand(createLicenseStatusCommand());
|
|
52
61
|
|
|
53
62
|
// Parse and run
|
|
54
63
|
program.parse();
|
|
64
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Activation Command
|
|
3
|
+
*
|
|
4
|
+
* Activates a Pro license key.
|
|
5
|
+
* Usage: mycmail activate <license-key>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { verifyLicense, saveLicense, getLicenseStatus, FREE_TIER_LIMITS } from '../lib/license.js';
|
|
10
|
+
import { loadKnownKeys } from '../lib/crypto.js';
|
|
11
|
+
|
|
12
|
+
export function createActivateCommand(): Command {
|
|
13
|
+
return new Command('activate')
|
|
14
|
+
.description('Activate a Pro license key')
|
|
15
|
+
.argument('<license-key>', 'Your license key from myceliumail.dev/pro')
|
|
16
|
+
.action(async (licenseKey: string) => {
|
|
17
|
+
console.log('🍄 Activating license...\n');
|
|
18
|
+
|
|
19
|
+
// Verify the license
|
|
20
|
+
const license = verifyLicense(licenseKey);
|
|
21
|
+
|
|
22
|
+
if (!license) {
|
|
23
|
+
console.error('❌ Invalid license key');
|
|
24
|
+
console.error(' Please check your key and try again.');
|
|
25
|
+
console.error(' Get a license at: myceliumail.dev/pro');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (license.isExpired) {
|
|
30
|
+
console.error('❌ This license has expired');
|
|
31
|
+
console.error(` Expired on: ${new Date(license.data.expiresAt).toLocaleDateString()}`);
|
|
32
|
+
console.error(' Renew at: myceliumail.dev/pro');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Save the license
|
|
37
|
+
const saved = saveLicense(licenseKey);
|
|
38
|
+
if (!saved) {
|
|
39
|
+
console.error('❌ Failed to save license');
|
|
40
|
+
console.error(' Please check file permissions for ~/.myceliumail/');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Show success
|
|
45
|
+
const status = getLicenseStatus();
|
|
46
|
+
console.log('✅ Pro License activated!\n');
|
|
47
|
+
console.log(` Email: ${status.email}`);
|
|
48
|
+
console.log(` Plan: ${status.plan.toUpperCase()}`);
|
|
49
|
+
console.log(` Expires: ${new Date(status.expiresAt!).toLocaleDateString()}`);
|
|
50
|
+
console.log(` Features: ${status.features.join(', ')}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('🍄 Thank you for supporting Myceliumail!');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createLicenseStatusCommand(): Command {
|
|
57
|
+
return new Command('license')
|
|
58
|
+
.description('Show license status and plan details')
|
|
59
|
+
.action(async () => {
|
|
60
|
+
const status = getLicenseStatus();
|
|
61
|
+
const knownKeys = loadKnownKeys();
|
|
62
|
+
const keyCount = Object.keys(knownKeys).length;
|
|
63
|
+
|
|
64
|
+
console.log('🍄 Myceliumail License Status\n');
|
|
65
|
+
|
|
66
|
+
if (status.plan === 'pro') {
|
|
67
|
+
console.log(` Plan: 💎 Pro`);
|
|
68
|
+
console.log(` Email: ${status.email}`);
|
|
69
|
+
console.log(` Expires: ${new Date(status.expiresAt!).toLocaleDateString()} (${status.daysRemaining} days)`);
|
|
70
|
+
console.log(` Features: ${status.features.join(', ')}`);
|
|
71
|
+
console.log(` Keys: ${keyCount} imported (unlimited)`);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(` Plan: Free`);
|
|
74
|
+
console.log(` Keys: ${keyCount}/${FREE_TIER_LIMITS.maxImportedKeys} imported`);
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(' 💎 Upgrade to Pro for:');
|
|
77
|
+
console.log(' • Unlimited imported keys');
|
|
78
|
+
console.log(' • MCP Server integration');
|
|
79
|
+
console.log(' • Cloud key backup/restore');
|
|
80
|
+
console.log(' • Real-time notifications');
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(' Get Pro: myceliumail.dev/pro');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { saveKnownKey, getKnownKey } from '../lib/crypto.js';
|
|
7
|
+
import { checkKeyLimit } from '../lib/license.js';
|
|
7
8
|
|
|
8
9
|
export function createKeyImportCommand(): Command {
|
|
9
10
|
return new Command('key-import')
|
|
@@ -26,6 +27,11 @@ export function createKeyImportCommand(): Command {
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// Check key limit for free tier (only for new keys)
|
|
31
|
+
if (!existing) {
|
|
32
|
+
checkKeyLimit();
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
saveKnownKey(agentId, publicKey);
|
|
30
36
|
|
|
31
37
|
console.log(`✅ Imported public key for ${agentId}`);
|
|
@@ -33,3 +39,4 @@ export function createKeyImportCommand(): Command {
|
|
|
33
39
|
console.log('\n🔐 You can now send encrypted messages to this agent');
|
|
34
40
|
});
|
|
35
41
|
}
|
|
42
|
+
|
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
|
+
}
|