myceliumail 1.0.9 → 1.0.13
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/.mappersan/outbox.json +15 -0
- package/.spidersan/registry.json +39 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +29 -0
- package/README.md +23 -2
- package/dist/bin/myceliumail.js +17 -1
- package/dist/bin/myceliumail.js.map +1 -1
- package/dist/commands/close.d.ts +9 -0
- package/dist/commands/close.d.ts.map +1 -0
- package/dist/commands/close.js +153 -0
- package/dist/commands/close.js.map +1 -0
- package/dist/commands/collab.d.ts +8 -0
- package/dist/commands/collab.d.ts.map +1 -0
- package/dist/commands/collab.js +112 -0
- package/dist/commands/collab.js.map +1 -0
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +105 -26
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/tags.d.ts +6 -0
- package/dist/commands/tags.d.ts.map +1 -0
- package/dist/commands/tags.js +90 -0
- package/dist/commands/tags.js.map +1 -0
- package/dist/commands/wake.d.ts +9 -0
- package/dist/commands/wake.d.ts.map +1 -0
- package/dist/commands/wake.js +198 -0
- package/dist/commands/wake.js.map +1 -0
- package/dist/dashboard/public/app.js +117 -0
- package/dist/dashboard/public/index.html +63 -5
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +31 -2
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/lib/update-check.d.ts.map +1 -1
- package/dist/lib/update-check.js +6 -4
- package/dist/lib/update-check.js.map +1 -1
- package/dist/lib/watson-digest.d.ts +40 -0
- package/dist/lib/watson-digest.d.ts.map +1 -0
- package/dist/lib/watson-digest.js +164 -0
- package/dist/lib/watson-digest.js.map +1 -0
- package/dist/storage/supabase.d.ts +4 -0
- package/dist/storage/supabase.d.ts.map +1 -1
- package/dist/storage/supabase.js +57 -0
- package/dist/storage/supabase.js.map +1 -1
- package/docs/COLLAB_mappersan_mycmail_setup.md +115 -0
- package/docs/COLLAB_wake_close_commands.md +518 -0
- package/docs/CROSS_TOOL_INTEGRATION_PLAN.md +246 -0
- package/docs/JSON_SCHEMA_WAKE_CLOSE.md +246 -0
- package/docs/MYCMAIL_QUICKSTART.md +103 -0
- package/docs/WAKE_AGENTS_SHARED_DOC.md +1215 -0
- package/mcp-server/README.md +75 -69
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +5 -1
- package/mcp-server/postinstall.js +14 -0
- package/mcp-server/src/server.ts +39 -0
- package/mobile-app/README.md +36 -0
- package/mobile-app/app/compose/page.tsx +140 -0
- package/mobile-app/app/favicon.ico +0 -0
- package/mobile-app/app/globals.css +26 -0
- package/mobile-app/app/layout.tsx +42 -0
- package/mobile-app/app/message/[id]/page.tsx +126 -0
- package/mobile-app/app/page.tsx +131 -0
- package/mobile-app/components/MessageCard.tsx +60 -0
- package/mobile-app/eslint.config.mjs +18 -0
- package/mobile-app/lib/supabase.ts +87 -0
- package/mobile-app/next.config.ts +7 -0
- package/mobile-app/package-lock.json +6674 -0
- package/mobile-app/package.json +27 -0
- package/mobile-app/postcss.config.mjs +7 -0
- package/mobile-app/public/file.svg +1 -0
- package/mobile-app/public/globe.svg +1 -0
- package/mobile-app/public/next.svg +1 -0
- package/mobile-app/public/vercel.svg +1 -0
- package/mobile-app/public/window.svg +1 -0
- package/mobile-app/tsconfig.json +34 -0
- package/package.json +2 -1
- package/postinstall.js +14 -0
- package/src/bin/myceliumail.ts +19 -1
- package/src/commands/close.ts +172 -0
- package/src/commands/collab.ts +125 -0
- package/src/commands/inbox.ts +120 -29
- package/src/commands/tags.ts +102 -0
- package/src/commands/wake.ts +228 -0
- package/src/dashboard/public/app.js +117 -0
- package/src/dashboard/public/index.html +63 -5
- package/src/dashboard/routes.ts +31 -2
- package/src/lib/update-check.ts +7 -4
- package/src/lib/watson-digest.ts +217 -0
- package/src/storage/supabase.ts +71 -0
- package/vscode-extension/README.md +107 -0
- package/vscode-extension/package-lock.json +1941 -0
- package/vscode-extension/package.json +117 -0
- package/vscode-extension/src/chatParticipant.ts +179 -0
- package/vscode-extension/src/extension.ts +262 -0
- package/vscode-extension/src/handlers.ts +265 -0
- package/vscode-extension/src/realtime.ts +302 -0
- package/vscode-extension/src/types.ts +41 -0
- package/vscode-extension/tsconfig.json +26 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tags command - List all unique hashtags from messages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { loadConfig } from '../lib/config.js';
|
|
7
|
+
import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
|
|
8
|
+
import * as storage from '../storage/supabase.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract hashtag from subject (e.g., "#wake-feature: Message" -> "wake-feature")
|
|
12
|
+
*/
|
|
13
|
+
function extractTag(subject: string | null): string | null {
|
|
14
|
+
if (!subject) return null;
|
|
15
|
+
const match = subject.match(/^#([a-zA-Z0-9_-]+):/);
|
|
16
|
+
return match ? match[1].toLowerCase() : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createTagsCommand(): Command {
|
|
20
|
+
return new Command('tags')
|
|
21
|
+
.description('List all unique hashtags from your messages')
|
|
22
|
+
.option('--json', 'Output as JSON')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
const agentId = config.agentId;
|
|
26
|
+
|
|
27
|
+
if (agentId === 'anonymous') {
|
|
28
|
+
console.error('❌ Agent ID not configured.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Fetch all messages (up to 500)
|
|
34
|
+
const messages = await storage.getInbox(agentId, { limit: 500 });
|
|
35
|
+
const keyPair = loadKeyPair(agentId);
|
|
36
|
+
|
|
37
|
+
// Count tags
|
|
38
|
+
const tagCounts: Record<string, { count: number, unread: number }> = {};
|
|
39
|
+
|
|
40
|
+
for (const msg of messages) {
|
|
41
|
+
let subject = msg.subject;
|
|
42
|
+
|
|
43
|
+
// Try to decrypt if encrypted
|
|
44
|
+
if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
|
|
45
|
+
try {
|
|
46
|
+
const decrypted = decryptMessage({
|
|
47
|
+
ciphertext: msg.ciphertext,
|
|
48
|
+
nonce: msg.nonce,
|
|
49
|
+
senderPublicKey: msg.senderPublicKey,
|
|
50
|
+
}, keyPair);
|
|
51
|
+
|
|
52
|
+
if (decrypted) {
|
|
53
|
+
const parsed = JSON.parse(decrypted);
|
|
54
|
+
subject = parsed.subject || subject;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Keep original subject
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tag = extractTag(subject);
|
|
62
|
+
if (tag) {
|
|
63
|
+
if (!tagCounts[tag]) {
|
|
64
|
+
tagCounts[tag] = { count: 0, unread: 0 };
|
|
65
|
+
}
|
|
66
|
+
tagCounts[tag].count++;
|
|
67
|
+
if (!msg.read) {
|
|
68
|
+
tagCounts[tag].unread++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tagList = Object.entries(tagCounts)
|
|
74
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
75
|
+
.map(([tag, stats]) => ({ tag, ...stats }));
|
|
76
|
+
|
|
77
|
+
if (options.json) {
|
|
78
|
+
console.log(JSON.stringify({ tags: tagList }, null, 2));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (tagList.length === 0) {
|
|
83
|
+
console.log('📭 No tagged messages found');
|
|
84
|
+
console.log('\n💡 Tag messages by prefixing subject with #tag:');
|
|
85
|
+
console.log(' mycmail send wsan "#wake-feature: Need help"');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('🏷️ Message Tags\n');
|
|
90
|
+
for (const { tag, count, unread } of tagList) {
|
|
91
|
+
const unreadMarker = unread > 0 ? ` (${unread} unread)` : '';
|
|
92
|
+
console.log(` #${tag}: ${count} message${count > 1 ? 's' : ''}${unreadMarker}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\n💡 Filter by tag: mycmail inbox --tag <tag>');
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('❌ Failed to fetch tags:', error);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wake command - Start a new session
|
|
3
|
+
*
|
|
4
|
+
* Shows inbox count, active collabs, and last session time.
|
|
5
|
+
* Designed for agent session lifecycle management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { loadConfig } from '../lib/config.js';
|
|
10
|
+
import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
|
|
11
|
+
import { generateDigest, sendDigestToWatsan } from '../lib/watson-digest.js';
|
|
12
|
+
import * as storage from '../storage/supabase.js';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
|
|
17
|
+
interface SessionData {
|
|
18
|
+
lastWake: string | null;
|
|
19
|
+
lastClose: string | null;
|
|
20
|
+
activeCollabs: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSessionPath(): string {
|
|
24
|
+
return path.join(os.homedir(), '.mycmail', 'session.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadSession(): SessionData {
|
|
28
|
+
const sessionPath = getSessionPath();
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(sessionPath)) {
|
|
31
|
+
return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore errors, return default
|
|
35
|
+
}
|
|
36
|
+
return { lastWake: null, lastClose: null, activeCollabs: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveSession(data: SessionData): void {
|
|
40
|
+
const sessionPath = getSessionPath();
|
|
41
|
+
const dir = path.dirname(sessionPath);
|
|
42
|
+
if (!fs.existsSync(dir)) {
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatTimeSince(date: string | null): string {
|
|
49
|
+
if (!date) return 'Never';
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const then = new Date(date);
|
|
52
|
+
const diffMs = now.getTime() - then.getTime();
|
|
53
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
54
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
55
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
56
|
+
|
|
57
|
+
if (diffMins < 1) return 'Just now';
|
|
58
|
+
if (diffMins < 60) return `${diffMins} minutes ago`;
|
|
59
|
+
if (diffHours < 24) return `${diffHours} hours ago`;
|
|
60
|
+
return `${diffDays} days ago`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractTag(subject: string | null): string | null {
|
|
64
|
+
if (!subject) return null;
|
|
65
|
+
const match = subject.match(/^#([a-zA-Z0-9_-]+):/);
|
|
66
|
+
return match ? match[1].toLowerCase() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TagDigest {
|
|
70
|
+
tag: string;
|
|
71
|
+
count: number;
|
|
72
|
+
unread: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getTagDigest(agentId: string, messages: any[]): Promise<TagDigest[]> {
|
|
76
|
+
const keyPair = loadKeyPair(agentId);
|
|
77
|
+
const tagCounts: Record<string, { count: number; unread: number }> = {};
|
|
78
|
+
|
|
79
|
+
for (const msg of messages) {
|
|
80
|
+
let subject = msg.subject;
|
|
81
|
+
|
|
82
|
+
// Try to decrypt if encrypted
|
|
83
|
+
if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
|
|
84
|
+
try {
|
|
85
|
+
const decrypted = decryptMessage({
|
|
86
|
+
ciphertext: msg.ciphertext,
|
|
87
|
+
nonce: msg.nonce,
|
|
88
|
+
senderPublicKey: msg.senderPublicKey,
|
|
89
|
+
}, keyPair);
|
|
90
|
+
|
|
91
|
+
if (decrypted) {
|
|
92
|
+
const parsed = JSON.parse(decrypted);
|
|
93
|
+
subject = parsed.subject || subject;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Keep original subject
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const tag = extractTag(subject);
|
|
101
|
+
if (tag) {
|
|
102
|
+
if (!tagCounts[tag]) {
|
|
103
|
+
tagCounts[tag] = { count: 0, unread: 0 };
|
|
104
|
+
}
|
|
105
|
+
tagCounts[tag].count++;
|
|
106
|
+
if (!msg.read) {
|
|
107
|
+
tagCounts[tag].unread++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Object.entries(tagCounts)
|
|
113
|
+
.sort((a, b) => b[1].unread - a[1].unread)
|
|
114
|
+
.slice(0, 5)
|
|
115
|
+
.map(([tag, stats]) => ({ tag, ...stats }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createWakeCommand(): Command {
|
|
119
|
+
return new Command('wake')
|
|
120
|
+
.description('Start a new session - check inbox, collabs, and announce presence')
|
|
121
|
+
.option('--json', 'Output as JSON (for scripting)')
|
|
122
|
+
.option('-q, --quiet', 'Minimal output')
|
|
123
|
+
.option('--silent', 'No output (only exit code)')
|
|
124
|
+
.option('--digest', 'Show hashtag digest and active threads')
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
const agentId = config.agentId;
|
|
128
|
+
|
|
129
|
+
if (agentId === 'anonymous') {
|
|
130
|
+
if (!options.silent) {
|
|
131
|
+
console.error('❌ Agent ID not configured.');
|
|
132
|
+
}
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Load session data
|
|
138
|
+
const session = loadSession();
|
|
139
|
+
|
|
140
|
+
// Check if this is a duplicate wake (idempotency)
|
|
141
|
+
if (session.lastWake) {
|
|
142
|
+
const lastWakeTime = new Date(session.lastWake);
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const diffMins = (now.getTime() - lastWakeTime.getTime()) / 60000;
|
|
145
|
+
|
|
146
|
+
// If woken up less than 5 minutes ago, skip re-registration
|
|
147
|
+
if (diffMins < 5 && !options.json) {
|
|
148
|
+
if (!options.silent && !options.quiet) {
|
|
149
|
+
console.log(`⏰ Already woke ${Math.floor(diffMins)} min ago. Use --force to re-wake.`);
|
|
150
|
+
}
|
|
151
|
+
// Still show status but don't re-register
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get inbox count
|
|
156
|
+
const messages = await storage.getInbox(agentId, { limit: 100 });
|
|
157
|
+
const unreadCount = messages.filter(m => !m.read).length;
|
|
158
|
+
const totalCount = messages.length;
|
|
159
|
+
|
|
160
|
+
// Update session
|
|
161
|
+
session.lastWake = new Date().toISOString();
|
|
162
|
+
saveSession(session);
|
|
163
|
+
|
|
164
|
+
// Output based on mode
|
|
165
|
+
if (options.silent) {
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options.json) {
|
|
170
|
+
const output = {
|
|
171
|
+
agentId,
|
|
172
|
+
inbox: {
|
|
173
|
+
total: totalCount,
|
|
174
|
+
unread: unreadCount
|
|
175
|
+
},
|
|
176
|
+
lastClose: session.lastClose,
|
|
177
|
+
activeCollabs: session.activeCollabs,
|
|
178
|
+
wakeTime: session.lastWake
|
|
179
|
+
};
|
|
180
|
+
console.log(JSON.stringify(output, null, 2));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Standard output
|
|
185
|
+
console.log(`\n🌅 Good morning, ${agentId}!\n`);
|
|
186
|
+
console.log(`📬 Inbox: ${unreadCount} unread / ${totalCount} total`);
|
|
187
|
+
console.log(`📋 Active collabs: ${session.activeCollabs.length}`);
|
|
188
|
+
console.log(`🕐 Last close: ${formatTimeSince(session.lastClose)}`);
|
|
189
|
+
|
|
190
|
+
// Show digest if requested
|
|
191
|
+
if (options.digest) {
|
|
192
|
+
const digest = await getTagDigest(agentId, messages);
|
|
193
|
+
if (digest.length > 0) {
|
|
194
|
+
console.log('\n🏷️ Active Threads:');
|
|
195
|
+
for (const { tag, count, unread } of digest) {
|
|
196
|
+
const unreadMarker = unread > 0 ? ` (${unread} new)` : '';
|
|
197
|
+
console.log(` #${tag}: ${count}${unreadMarker}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!options.quiet) {
|
|
203
|
+
console.log('\n💡 Tip: Run \'mycmail inbox\' to read messages');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('\n✅ Session started!\n');
|
|
207
|
+
|
|
208
|
+
// Send digest to watson (unencrypted)
|
|
209
|
+
if (!options.silent) {
|
|
210
|
+
try {
|
|
211
|
+
const wakeDigest = await generateDigest(agentId, 'wake');
|
|
212
|
+
await sendDigestToWatsan(wakeDigest);
|
|
213
|
+
if (!options.quiet) {
|
|
214
|
+
console.log('📊 Digest sent to watson');
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Silent fail - digest is optional
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (!options.silent) {
|
|
223
|
+
console.error('❌ Wake failed:', error);
|
|
224
|
+
}
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -521,3 +521,120 @@ loadInbox();
|
|
|
521
521
|
|
|
522
522
|
// Poll every 30 seconds as fallback (Realtime handles instant updates)
|
|
523
523
|
setInterval(() => loadInbox(true), 30000);
|
|
524
|
+
|
|
525
|
+
// ============== COMPOSE MESSAGE FUNCTIONS ==============
|
|
526
|
+
|
|
527
|
+
let availableAgents = [];
|
|
528
|
+
|
|
529
|
+
// Load available agents for compose dropdown
|
|
530
|
+
async function loadAvailableAgents() {
|
|
531
|
+
try {
|
|
532
|
+
const res = await fetch('/api/config/agents');
|
|
533
|
+
const data = await res.json();
|
|
534
|
+
availableAgents = data.agents || [];
|
|
535
|
+
updateAgentDropdown();
|
|
536
|
+
} catch (err) {
|
|
537
|
+
console.error('Failed to load agents:', err);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function updateAgentDropdown() {
|
|
542
|
+
const select = document.getElementById('compose-from');
|
|
543
|
+
if (!select) return;
|
|
544
|
+
|
|
545
|
+
if (availableAgents.length === 0) {
|
|
546
|
+
select.innerHTML = `<option value="${currentAgentId}">${currentAgentId}</option>`;
|
|
547
|
+
} else {
|
|
548
|
+
select.innerHTML = availableAgents.map(agent =>
|
|
549
|
+
`<option value="${agent}" ${agent === 'treebird' ? 'selected' : ''}>${agent}</option>`
|
|
550
|
+
).join('');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function showComposeModal() {
|
|
555
|
+
loadAvailableAgents();
|
|
556
|
+
document.getElementById('compose-modal').classList.remove('hidden');
|
|
557
|
+
document.getElementById('compose-to').focus();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hideComposeModal() {
|
|
561
|
+
document.getElementById('compose-modal').classList.add('hidden');
|
|
562
|
+
// Clear form
|
|
563
|
+
document.getElementById('compose-to').value = '';
|
|
564
|
+
document.getElementById('compose-subject').value = '';
|
|
565
|
+
document.getElementById('compose-body').value = '';
|
|
566
|
+
document.getElementById('compose-files').value = '';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function sendNewMessage() {
|
|
570
|
+
const from = document.getElementById('compose-from').value;
|
|
571
|
+
const to = document.getElementById('compose-to').value.trim();
|
|
572
|
+
const subject = document.getElementById('compose-subject').value.trim();
|
|
573
|
+
const body = document.getElementById('compose-body').value;
|
|
574
|
+
const fileInput = document.getElementById('compose-files');
|
|
575
|
+
|
|
576
|
+
if (!to) {
|
|
577
|
+
alert('Please enter a recipient');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!subject) {
|
|
581
|
+
alert('Please enter a subject');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (!body.trim()) {
|
|
585
|
+
alert('Please enter a message');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Process attachments
|
|
590
|
+
const attachments = [];
|
|
591
|
+
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
592
|
+
|
|
593
|
+
if (fileInput && fileInput.files.length > 0) {
|
|
594
|
+
for (const file of fileInput.files) {
|
|
595
|
+
if (file.size > MAX_SIZE) {
|
|
596
|
+
alert(`File "${file.name}" exceeds 5MB limit`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const base64 = await readFileAsBase64(file);
|
|
600
|
+
attachments.push({
|
|
601
|
+
name: file.name,
|
|
602
|
+
type: file.type || 'application/octet-stream',
|
|
603
|
+
data: base64,
|
|
604
|
+
size: file.size
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
console.log('Sending message from:', from, 'to:', to);
|
|
611
|
+
const response = await fetch('/api/send', {
|
|
612
|
+
method: 'POST',
|
|
613
|
+
headers: { 'Content-Type': 'application/json' },
|
|
614
|
+
body: JSON.stringify({
|
|
615
|
+
to: to,
|
|
616
|
+
subject: subject,
|
|
617
|
+
body: body,
|
|
618
|
+
from: from,
|
|
619
|
+
attachments: attachments.length > 0 ? attachments : undefined
|
|
620
|
+
})
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const result = await response.json();
|
|
624
|
+
console.log('Send result:', result);
|
|
625
|
+
|
|
626
|
+
if (!response.ok || !result.success) {
|
|
627
|
+
throw new Error(result.error || 'Failed to send');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
hideComposeModal();
|
|
631
|
+
alert('Message sent!' + (attachments.length > 0 ? ` (${attachments.length} attachment(s))` : ''));
|
|
632
|
+
loadInbox(true);
|
|
633
|
+
} catch (err) {
|
|
634
|
+
console.error('Send message error:', err);
|
|
635
|
+
alert('Failed to send message: ' + err.message);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Load agents on startup
|
|
640
|
+
loadAvailableAgents();
|
|
@@ -45,17 +45,75 @@
|
|
|
45
45
|
<div
|
|
46
46
|
class="p-4 border-b border-gray-800 bg-gray-900/50 backdrop-blur flex items-center justify-between">
|
|
47
47
|
<h2 class="text-xl font-semibold text-gray-200">Inbox</h2>
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
<div class="flex gap-2">
|
|
49
|
+
<button onclick="showComposeModal()"
|
|
50
|
+
class="px-3 py-1.5 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-500 transition-colors flex items-center gap-1.5"
|
|
51
|
+
title="Compose new message">
|
|
52
|
+
✉️ Compose
|
|
53
|
+
</button>
|
|
54
|
+
<button onclick="loadInbox(false)"
|
|
55
|
+
class="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
|
|
56
|
+
title="Refresh inbox">
|
|
57
|
+
🔄
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
53
60
|
</div>
|
|
54
61
|
<div id="inbox-list" class="flex-1 overflow-y-auto p-2 space-y-2">
|
|
55
62
|
<!-- Messages will act here -->
|
|
56
63
|
</div>
|
|
57
64
|
</div>
|
|
58
65
|
|
|
66
|
+
<!-- Compose Modal -->
|
|
67
|
+
<div id="compose-modal"
|
|
68
|
+
class="hidden fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
69
|
+
<div
|
|
70
|
+
class="bg-gray-900 rounded-2xl border border-gray-700 shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
71
|
+
<div class="p-6 border-b border-gray-800 flex justify-between items-center">
|
|
72
|
+
<h2 class="text-2xl font-bold text-white">✉️ Compose Message</h2>
|
|
73
|
+
<button onclick="hideComposeModal()"
|
|
74
|
+
class="text-gray-400 hover:text-white text-2xl">×</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="p-6 space-y-4">
|
|
77
|
+
<div class="grid grid-cols-2 gap-4">
|
|
78
|
+
<div>
|
|
79
|
+
<label class="block text-gray-400 mb-2">From:</label>
|
|
80
|
+
<select id="compose-from"
|
|
81
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
|
|
82
|
+
<option value="">Loading agents...</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
<div>
|
|
86
|
+
<label class="block text-gray-400 mb-2">To:</label>
|
|
87
|
+
<input type="text" id="compose-to" placeholder="recipient agent ID"
|
|
88
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div>
|
|
92
|
+
<label class="block text-gray-400 mb-2">Subject:</label>
|
|
93
|
+
<input type="text" id="compose-subject" placeholder="Message subject"
|
|
94
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none">
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<label class="block text-gray-400 mb-2">Message:</label>
|
|
98
|
+
<textarea id="compose-body" rows="8" placeholder="Write your message..."
|
|
99
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none resize-none"></textarea>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<label class="block text-gray-400 mb-2">Attachments (optional):</label>
|
|
103
|
+
<input type="file" id="compose-files" multiple
|
|
104
|
+
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:bg-purple-600 file:text-white file:cursor-pointer">
|
|
105
|
+
</div>
|
|
106
|
+
<div class="flex gap-4 justify-end pt-4">
|
|
107
|
+
<button onclick="hideComposeModal()"
|
|
108
|
+
class="px-6 py-3 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700">Cancel</button>
|
|
109
|
+
<button onclick="sendNewMessage()"
|
|
110
|
+
class="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-500 font-medium">Send
|
|
111
|
+
Message</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
59
117
|
<!-- Message Detail -->
|
|
60
118
|
<div
|
|
61
119
|
class="col-span-8 bg-gray-900 rounded-xl border border-gray-800 flex flex-col overflow-hidden relative">
|
package/src/dashboard/routes.ts
CHANGED
|
@@ -46,9 +46,38 @@ export async function registerRoutes(fastify: FastifyInstance) {
|
|
|
46
46
|
const config = loadConfig();
|
|
47
47
|
const agentId = config.agentId;
|
|
48
48
|
|
|
49
|
-
// GET /api/
|
|
49
|
+
// GET /api/config/agents - List all agent IDs with keys
|
|
50
|
+
fastify.get('/api/config/agents', async (request, reply) => {
|
|
51
|
+
const agents = listOwnKeys();
|
|
52
|
+
// Include config agent if not already in list
|
|
53
|
+
if (agentId && !agents.includes(agentId)) {
|
|
54
|
+
agents.unshift(agentId);
|
|
55
|
+
}
|
|
56
|
+
return { agents };
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// GET /api/inbox - Now supports multi-agent queries
|
|
50
60
|
fastify.get('/api/inbox', async (request, reply) => {
|
|
51
|
-
const
|
|
61
|
+
const queryAgents = (request.query as { agents?: string }).agents;
|
|
62
|
+
|
|
63
|
+
let messages;
|
|
64
|
+
if (queryAgents) {
|
|
65
|
+
// Use specific agents if provided
|
|
66
|
+
const agentIds = queryAgents.split(',').map(s => s.trim());
|
|
67
|
+
messages = await storage.getMultiAgentInbox(agentIds, { limit: 100 });
|
|
68
|
+
} else {
|
|
69
|
+
// Default: get messages for ALL agents with keys
|
|
70
|
+
const allAgentIds = listOwnKeys();
|
|
71
|
+
if (agentId && !allAgentIds.includes(agentId)) {
|
|
72
|
+
allAgentIds.unshift(agentId);
|
|
73
|
+
}
|
|
74
|
+
if (allAgentIds.length > 0) {
|
|
75
|
+
messages = await storage.getMultiAgentInbox(allAgentIds, { limit: 100 });
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback to single agent query
|
|
78
|
+
messages = await storage.getInbox(agentId, { limit: 100 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
52
81
|
|
|
53
82
|
// Decrypt encrypted messages using all available keys
|
|
54
83
|
const decrypted = messages.map(tryDecryptWithAllKeys);
|
package/src/lib/update-check.ts
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
11
15
|
|
|
12
16
|
const CONFIG_DIR = join(homedir(), '.myceliumail');
|
|
13
17
|
const UPDATE_CACHE = join(CONFIG_DIR, 'update-cache.json');
|
|
@@ -24,9 +28,8 @@ interface UpdateCache {
|
|
|
24
28
|
*/
|
|
25
29
|
function getCurrentVersion(): string {
|
|
26
30
|
try {
|
|
27
|
-
// Read from the installed package
|
|
28
|
-
const
|
|
29
|
-
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
31
|
+
// Read from the installed package (2 levels up from dist/lib/)
|
|
32
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
30
33
|
return pkg.version;
|
|
31
34
|
} catch {
|
|
32
35
|
return '0.0.0';
|