myceliumail 1.0.1
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/.context7 +87 -0
- package/.env.example +12 -0
- package/.eslintrc.json +29 -0
- package/CLAUDE.md +35 -0
- package/COMPLETE.md +51 -0
- package/LICENSE +21 -0
- package/MYCELIUMAIL_STARTER_KIT.md +603 -0
- package/NEXT_STEPS.md +96 -0
- package/README.md +229 -0
- package/desktop/README.md +102 -0
- package/desktop/assets/icon.icns +0 -0
- package/desktop/assets/icon.iconset/icon_128x128.png +0 -0
- package/desktop/assets/icon.iconset/icon_128x128@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16.png +0 -0
- package/desktop/assets/icon.iconset/icon_16x16@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256.png +0 -0
- package/desktop/assets/icon.iconset/icon_256x256@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32.png +0 -0
- package/desktop/assets/icon.iconset/icon_32x32@2x.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512.png +0 -0
- package/desktop/assets/icon.iconset/icon_512x512@2x.png +0 -0
- package/desktop/assets/icon.png +0 -0
- package/desktop/assets/tray-icon.png +0 -0
- package/desktop/main.js +257 -0
- package/desktop/package-lock.json +4198 -0
- package/desktop/package.json +48 -0
- package/desktop/preload.js +11 -0
- package/dist/bin/myceliumail.d.ts +8 -0
- package/dist/bin/myceliumail.d.ts.map +1 -0
- package/dist/bin/myceliumail.js +44 -0
- package/dist/bin/myceliumail.js.map +1 -0
- package/dist/commands/broadcast.d.ts +9 -0
- package/dist/commands/broadcast.d.ts.map +1 -0
- package/dist/commands/broadcast.js +59 -0
- package/dist/commands/broadcast.js.map +1 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +18 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/inbox.d.ts +6 -0
- package/dist/commands/inbox.d.ts.map +1 -0
- package/dist/commands/inbox.js +65 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/key-import.d.ts +6 -0
- package/dist/commands/key-import.d.ts.map +1 -0
- package/dist/commands/key-import.js +31 -0
- package/dist/commands/key-import.js.map +1 -0
- package/dist/commands/keygen.d.ts +6 -0
- package/dist/commands/keygen.d.ts.map +1 -0
- package/dist/commands/keygen.js +33 -0
- package/dist/commands/keygen.js.map +1 -0
- package/dist/commands/keys.d.ts +6 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +47 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/read.d.ts +6 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +89 -0
- package/dist/commands/read.js.map +1 -0
- package/dist/commands/send.d.ts +8 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +73 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/watch.d.ts +8 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +88 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/dashboard/public/app.js +523 -0
- package/dist/dashboard/public/index.html +75 -0
- package/dist/dashboard/public/styles.css +68 -0
- package/dist/dashboard/routes.d.ts +3 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +103 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +29 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/lib/config.d.ts +28 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +90 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/crypto.d.ts +72 -0
- package/dist/lib/crypto.d.ts.map +1 -0
- package/dist/lib/crypto.js +169 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/realtime.d.ts +36 -0
- package/dist/lib/realtime.d.ts.map +1 -0
- package/dist/lib/realtime.js +73 -0
- package/dist/lib/realtime.js.map +1 -0
- package/dist/storage/local.d.ts +46 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +160 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/supabase.d.ts +43 -0
- package/dist/storage/supabase.d.ts.map +1 -0
- package/dist/storage/supabase.js +256 -0
- package/dist/storage/supabase.js.map +1 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/docs/20251215_Treebird-Ecosystem_Knowledge-Base_v2.md +292 -0
- package/docs/20251215_Treebird-Ecosystem_Project-Instructions_v2.md +176 -0
- package/docs/AGENT_DELEGATION_WORKFLOW.md +453 -0
- package/docs/AGENT_STARTER_KIT.md +145 -0
- package/docs/ANNOUNCEMENT_DRAFTS.md +55 -0
- package/docs/DASHBOARD_AGENT_HANDOFF.md +429 -0
- package/docs/DASHBOARD_AGENT_PROMPT.md +32 -0
- package/docs/DASHBOARD_BUILD_ROADMAP.md +61 -0
- package/docs/DEPLOYMENT.md +59 -0
- package/docs/MCP_PUBLISHING_ROADMAP.md +113 -0
- package/docs/MCP_STARTER_KIT.md +117 -0
- package/docs/SSAN_MESSAGES_SUMMARY.md +92 -0
- package/docs/STORAGE_ARCHITECTURE.md +114 -0
- package/mcp-server/README.md +143 -0
- package/mcp-server/assets/icon.png +0 -0
- package/mcp-server/myceliumail-mcp-1.0.0.tgz +0 -0
- package/mcp-server/package-lock.json +1142 -0
- package/mcp-server/package.json +49 -0
- package/mcp-server/src/lib/config.ts +21 -0
- package/mcp-server/src/lib/crypto.ts +150 -0
- package/mcp-server/src/lib/storage.ts +257 -0
- package/mcp-server/src/server.ts +387 -0
- package/mcp-server/tsconfig.json +26 -0
- package/package.json +54 -0
- package/src/bin/myceliumail.ts +52 -0
- package/src/commands/broadcast.ts +70 -0
- package/src/commands/dashboard.ts +19 -0
- package/src/commands/inbox.ts +75 -0
- package/src/commands/key-import.ts +35 -0
- package/src/commands/keygen.ts +44 -0
- package/src/commands/keys.ts +55 -0
- package/src/commands/read.ts +97 -0
- package/src/commands/send.ts +89 -0
- package/src/commands/watch.ts +101 -0
- package/src/dashboard/public/app.js +523 -0
- package/src/dashboard/public/index.html +75 -0
- package/src/dashboard/public/styles.css +68 -0
- package/src/dashboard/routes.ts +128 -0
- package/src/dashboard/server.ts +33 -0
- package/src/lib/config.ts +104 -0
- package/src/lib/crypto.ts +210 -0
- package/src/lib/realtime.ts +109 -0
- package/src/storage/local.ts +209 -0
- package/src/storage/supabase.ts +336 -0
- package/src/types/index.ts +53 -0
- package/supabase/migrations/000_myceliumail_setup.sql +93 -0
- package/supabase/migrations/001_enable_realtime.sql +10 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "myceliumail-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Myceliumail - End-to-End Encrypted Messaging for AI Agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"myceliumail-mcp": "./dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/server.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"claude",
|
|
20
|
+
"ai-agents",
|
|
21
|
+
"messaging",
|
|
22
|
+
"encrypted",
|
|
23
|
+
"e2e",
|
|
24
|
+
"myceliumail"
|
|
25
|
+
],
|
|
26
|
+
"author": "Treebird",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/treebird/myceliumail"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/treebird/myceliumail#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/treebird/myceliumail/issues"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
38
|
+
"tweetnacl": "^1.0.3",
|
|
39
|
+
"tweetnacl-util": "^0.15.1",
|
|
40
|
+
"zod": "^3.22.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.10.0",
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail MCP - Config Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function getAgentId(): string {
|
|
6
|
+
return process.env.MYCELIUMAIL_AGENT_ID ||
|
|
7
|
+
process.env.MYCELIUMAIL_AGENT ||
|
|
8
|
+
'anonymous';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getSupabaseUrl(): string | undefined {
|
|
12
|
+
return process.env.SUPABASE_URL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getSupabaseKey(): string | undefined {
|
|
16
|
+
return process.env.SUPABASE_ANON_KEY;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasSupabase(): boolean {
|
|
20
|
+
return !!(getSupabaseUrl() && getSupabaseKey());
|
|
21
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail MCP - Crypto Module
|
|
3
|
+
*
|
|
4
|
+
* NaCl encryption for agent messaging.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import nacl from 'tweetnacl';
|
|
8
|
+
import util from 'tweetnacl-util';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
const KEYS_DIR = join(homedir(), '.myceliumail', 'keys');
|
|
14
|
+
|
|
15
|
+
export interface KeyPair {
|
|
16
|
+
publicKey: Uint8Array;
|
|
17
|
+
secretKey: Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EncryptedMessage {
|
|
21
|
+
ciphertext: string;
|
|
22
|
+
nonce: string;
|
|
23
|
+
senderPublicKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensureKeysDir(): void {
|
|
27
|
+
if (!existsSync(KEYS_DIR)) {
|
|
28
|
+
mkdirSync(KEYS_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateKeyPair(): KeyPair {
|
|
33
|
+
return nacl.box.keyPair();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveKeyPair(agentId: string, keyPair: KeyPair): void {
|
|
37
|
+
ensureKeysDir();
|
|
38
|
+
const serialized = {
|
|
39
|
+
publicKey: util.encodeBase64(keyPair.publicKey),
|
|
40
|
+
secretKey: util.encodeBase64(keyPair.secretKey),
|
|
41
|
+
};
|
|
42
|
+
const path = join(KEYS_DIR, `${agentId}.key.json`);
|
|
43
|
+
writeFileSync(path, JSON.stringify(serialized, null, 2), { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function loadKeyPair(agentId: string): KeyPair | null {
|
|
47
|
+
const path = join(KEYS_DIR, `${agentId}.key.json`);
|
|
48
|
+
if (!existsSync(path)) return null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
52
|
+
return {
|
|
53
|
+
publicKey: util.decodeBase64(data.publicKey),
|
|
54
|
+
secretKey: util.decodeBase64(data.secretKey),
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasKeyPair(agentId: string): boolean {
|
|
62
|
+
return existsSync(join(KEYS_DIR, `${agentId}.key.json`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getPublicKeyBase64(keyPair: KeyPair): string {
|
|
66
|
+
return util.encodeBase64(keyPair.publicKey);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function encryptMessage(
|
|
70
|
+
message: string,
|
|
71
|
+
recipientPublicKey: Uint8Array,
|
|
72
|
+
senderKeyPair: KeyPair
|
|
73
|
+
): EncryptedMessage {
|
|
74
|
+
const messageBytes = util.decodeUTF8(message);
|
|
75
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
76
|
+
|
|
77
|
+
const ciphertext = nacl.box(
|
|
78
|
+
messageBytes,
|
|
79
|
+
nonce,
|
|
80
|
+
recipientPublicKey,
|
|
81
|
+
senderKeyPair.secretKey
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ciphertext: util.encodeBase64(ciphertext),
|
|
86
|
+
nonce: util.encodeBase64(nonce),
|
|
87
|
+
senderPublicKey: util.encodeBase64(senderKeyPair.publicKey),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function decryptMessage(
|
|
92
|
+
encrypted: EncryptedMessage,
|
|
93
|
+
recipientKeyPair: KeyPair
|
|
94
|
+
): string | null {
|
|
95
|
+
try {
|
|
96
|
+
const ciphertext = util.decodeBase64(encrypted.ciphertext);
|
|
97
|
+
const nonce = util.decodeBase64(encrypted.nonce);
|
|
98
|
+
const senderPublicKey = util.decodeBase64(encrypted.senderPublicKey);
|
|
99
|
+
|
|
100
|
+
const decrypted = nacl.box.open(
|
|
101
|
+
ciphertext,
|
|
102
|
+
nonce,
|
|
103
|
+
senderPublicKey,
|
|
104
|
+
recipientKeyPair.secretKey
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!decrypted) return null;
|
|
108
|
+
return util.encodeUTF8(decrypted);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function loadKnownKeys(): Record<string, string> {
|
|
115
|
+
const path = join(KEYS_DIR, 'known_keys.json');
|
|
116
|
+
if (!existsSync(path)) return {};
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function saveKnownKey(agentId: string, publicKeyBase64: string): void {
|
|
125
|
+
ensureKeysDir();
|
|
126
|
+
const keys = loadKnownKeys();
|
|
127
|
+
keys[agentId] = publicKeyBase64;
|
|
128
|
+
writeFileSync(join(KEYS_DIR, 'known_keys.json'), JSON.stringify(keys, null, 2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getKnownKey(agentId: string): string | null {
|
|
132
|
+
const keys = loadKnownKeys();
|
|
133
|
+
return keys[agentId] || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function listOwnKeys(): string[] {
|
|
137
|
+
ensureKeysDir();
|
|
138
|
+
try {
|
|
139
|
+
const files = readdirSync(KEYS_DIR);
|
|
140
|
+
return files
|
|
141
|
+
.filter(f => f.endsWith('.key.json'))
|
|
142
|
+
.map(f => f.replace('.key.json', ''));
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function decodePublicKey(base64: string): Uint8Array {
|
|
149
|
+
return util.decodeBase64(base64);
|
|
150
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail MCP - Storage Module
|
|
3
|
+
*
|
|
4
|
+
* Local JSON storage with optional Supabase sync.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import { getSupabaseUrl, getSupabaseKey, hasSupabase } from './config.js';
|
|
12
|
+
|
|
13
|
+
const DATA_DIR = join(homedir(), '.myceliumail', 'data');
|
|
14
|
+
const MESSAGES_FILE = join(DATA_DIR, 'messages.json');
|
|
15
|
+
|
|
16
|
+
export interface Message {
|
|
17
|
+
id: string;
|
|
18
|
+
sender: string;
|
|
19
|
+
recipient: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
body: string;
|
|
22
|
+
encrypted: boolean;
|
|
23
|
+
ciphertext?: string;
|
|
24
|
+
nonce?: string;
|
|
25
|
+
senderPublicKey?: string;
|
|
26
|
+
read: boolean;
|
|
27
|
+
archived: boolean;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StoredMessage extends Omit<Message, 'createdAt'> {
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureDataDir(): void {
|
|
36
|
+
if (!existsSync(DATA_DIR)) {
|
|
37
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadLocalMessages(): StoredMessage[] {
|
|
42
|
+
if (!existsSync(MESSAGES_FILE)) return [];
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(MESSAGES_FILE, 'utf-8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveLocalMessages(messages: StoredMessage[]): void {
|
|
51
|
+
ensureDataDir();
|
|
52
|
+
writeFileSync(MESSAGES_FILE, JSON.stringify(messages, null, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toMessage(stored: StoredMessage): Message {
|
|
56
|
+
return { ...stored, createdAt: new Date(stored.createdAt) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Supabase helpers
|
|
60
|
+
async function supabaseRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
61
|
+
const url = `${getSupabaseUrl()}/rest/v1${path}`;
|
|
62
|
+
const response = await fetch(url, {
|
|
63
|
+
...options,
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
'apikey': getSupabaseKey()!,
|
|
67
|
+
'Authorization': `Bearer ${getSupabaseKey()}`,
|
|
68
|
+
'Prefer': options.method === 'POST' ? 'return=representation' : 'return=minimal',
|
|
69
|
+
...options.headers,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) throw new Error(await response.text());
|
|
73
|
+
if (response.status === 204) return {} as T;
|
|
74
|
+
return response.json() as Promise<T>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function sendMessage(
|
|
78
|
+
sender: string,
|
|
79
|
+
recipient: string,
|
|
80
|
+
subject: string,
|
|
81
|
+
body: string,
|
|
82
|
+
options?: {
|
|
83
|
+
encrypted?: boolean;
|
|
84
|
+
ciphertext?: string;
|
|
85
|
+
nonce?: string;
|
|
86
|
+
senderPublicKey?: string;
|
|
87
|
+
}
|
|
88
|
+
): Promise<Message> {
|
|
89
|
+
const newMessage: StoredMessage = {
|
|
90
|
+
id: randomUUID(),
|
|
91
|
+
sender,
|
|
92
|
+
recipient,
|
|
93
|
+
subject: options?.encrypted ? '' : subject,
|
|
94
|
+
body: options?.encrypted ? '' : body,
|
|
95
|
+
encrypted: options?.encrypted || false,
|
|
96
|
+
ciphertext: options?.ciphertext,
|
|
97
|
+
nonce: options?.nonce,
|
|
98
|
+
senderPublicKey: options?.senderPublicKey,
|
|
99
|
+
read: false,
|
|
100
|
+
archived: false,
|
|
101
|
+
createdAt: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (hasSupabase()) {
|
|
105
|
+
try {
|
|
106
|
+
const [result] = await supabaseRequest<StoredMessage[]>('/agent_messages', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
sender: newMessage.sender,
|
|
110
|
+
recipient: newMessage.recipient,
|
|
111
|
+
subject: newMessage.subject || null,
|
|
112
|
+
body: newMessage.body || null,
|
|
113
|
+
encrypted: newMessage.encrypted,
|
|
114
|
+
ciphertext: newMessage.ciphertext,
|
|
115
|
+
nonce: newMessage.nonce,
|
|
116
|
+
sender_public_key: newMessage.senderPublicKey,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
return toMessage({
|
|
120
|
+
...newMessage,
|
|
121
|
+
id: (result as unknown as { id: string }).id
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// Fall through to local
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Local storage
|
|
129
|
+
const messages = loadLocalMessages();
|
|
130
|
+
messages.push(newMessage);
|
|
131
|
+
saveLocalMessages(messages);
|
|
132
|
+
return toMessage(newMessage);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function getInbox(
|
|
136
|
+
agentId: string,
|
|
137
|
+
options?: { unreadOnly?: boolean; limit?: number }
|
|
138
|
+
): Promise<Message[]> {
|
|
139
|
+
if (hasSupabase()) {
|
|
140
|
+
try {
|
|
141
|
+
let query = `/agent_messages?recipient=eq.${agentId}&archived=eq.false&order=created_at.desc`;
|
|
142
|
+
if (options?.unreadOnly) query += '&read=eq.false';
|
|
143
|
+
if (options?.limit) query += `&limit=${options.limit}`;
|
|
144
|
+
|
|
145
|
+
const results = await supabaseRequest<Array<{
|
|
146
|
+
id: string; sender: string; recipient: string;
|
|
147
|
+
subject: string; body: string; encrypted: boolean;
|
|
148
|
+
ciphertext: string; nonce: string; sender_public_key: string;
|
|
149
|
+
read: boolean; archived: boolean; created_at: string;
|
|
150
|
+
}>>(query);
|
|
151
|
+
|
|
152
|
+
return results.map(r => ({
|
|
153
|
+
id: r.id,
|
|
154
|
+
sender: r.sender,
|
|
155
|
+
recipient: r.recipient,
|
|
156
|
+
subject: r.subject || '',
|
|
157
|
+
body: r.body || '',
|
|
158
|
+
encrypted: r.encrypted,
|
|
159
|
+
ciphertext: r.ciphertext,
|
|
160
|
+
nonce: r.nonce,
|
|
161
|
+
senderPublicKey: r.sender_public_key,
|
|
162
|
+
read: r.read,
|
|
163
|
+
archived: r.archived,
|
|
164
|
+
createdAt: new Date(r.created_at),
|
|
165
|
+
}));
|
|
166
|
+
} catch {
|
|
167
|
+
// Fall through to local
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Local storage
|
|
172
|
+
const messages = loadLocalMessages();
|
|
173
|
+
let filtered = messages.filter(m => m.recipient === agentId && !m.archived);
|
|
174
|
+
if (options?.unreadOnly) filtered = filtered.filter(m => !m.read);
|
|
175
|
+
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
176
|
+
if (options?.limit) filtered = filtered.slice(0, options.limit);
|
|
177
|
+
return filtered.map(toMessage);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function getMessage(id: string): Promise<Message | null> {
|
|
181
|
+
if (hasSupabase()) {
|
|
182
|
+
try {
|
|
183
|
+
const results = await supabaseRequest<Array<{
|
|
184
|
+
id: string; sender: string; recipient: string;
|
|
185
|
+
subject: string; body: string; encrypted: boolean;
|
|
186
|
+
ciphertext: string; nonce: string; sender_public_key: string;
|
|
187
|
+
read: boolean; archived: boolean; created_at: string;
|
|
188
|
+
}>>(`/agent_messages?id=eq.${id}`);
|
|
189
|
+
|
|
190
|
+
if (results.length > 0) {
|
|
191
|
+
const r = results[0];
|
|
192
|
+
return {
|
|
193
|
+
id: r.id,
|
|
194
|
+
sender: r.sender,
|
|
195
|
+
recipient: r.recipient,
|
|
196
|
+
subject: r.subject || '',
|
|
197
|
+
body: r.body || '',
|
|
198
|
+
encrypted: r.encrypted,
|
|
199
|
+
ciphertext: r.ciphertext,
|
|
200
|
+
nonce: r.nonce,
|
|
201
|
+
senderPublicKey: r.sender_public_key,
|
|
202
|
+
read: r.read,
|
|
203
|
+
archived: r.archived,
|
|
204
|
+
createdAt: new Date(r.created_at),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Fall through
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const messages = loadLocalMessages();
|
|
213
|
+
const found = messages.find(m => m.id === id);
|
|
214
|
+
return found ? toMessage(found) : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function markAsRead(id: string): Promise<boolean> {
|
|
218
|
+
if (hasSupabase()) {
|
|
219
|
+
try {
|
|
220
|
+
await supabaseRequest(`/agent_messages?id=eq.${id}`, {
|
|
221
|
+
method: 'PATCH',
|
|
222
|
+
body: JSON.stringify({ read: true }),
|
|
223
|
+
});
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
// Fall through
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const messages = loadLocalMessages();
|
|
231
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
232
|
+
if (idx === -1) return false;
|
|
233
|
+
messages[idx].read = true;
|
|
234
|
+
saveLocalMessages(messages);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function archiveMessage(id: string): Promise<boolean> {
|
|
239
|
+
if (hasSupabase()) {
|
|
240
|
+
try {
|
|
241
|
+
await supabaseRequest(`/agent_messages?id=eq.${id}`, {
|
|
242
|
+
method: 'PATCH',
|
|
243
|
+
body: JSON.stringify({ archived: true }),
|
|
244
|
+
});
|
|
245
|
+
return true;
|
|
246
|
+
} catch {
|
|
247
|
+
// Fall through
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const messages = loadLocalMessages();
|
|
252
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
253
|
+
if (idx === -1) return false;
|
|
254
|
+
messages[idx].archived = true;
|
|
255
|
+
saveLocalMessages(messages);
|
|
256
|
+
return true;
|
|
257
|
+
}
|