myceliumail 1.0.2 → 1.0.3
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/.eslintrc.json +29 -0
- package/COMPLETE.md +51 -0
- package/MYCELIUMAIL_STARTER_KIT.md +603 -0
- package/NEXT_STEPS.md +96 -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.js +2 -0
- package/dist/bin/myceliumail.js.map +1 -1
- package/dist/commands/key-announce.d.ts +6 -0
- package/dist/commands/key-announce.d.ts.map +1 -0
- package/dist/commands/key-announce.js +63 -0
- package/dist/commands/key-announce.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/LESSONS_LEARNED.md +127 -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 +55 -0
- package/mcp-server/src/lib/crypto.ts +150 -0
- package/mcp-server/src/lib/storage.ts +267 -0
- package/mcp-server/src/server.ts +387 -0
- package/mcp-server/tsconfig.json +26 -0
- package/package.json +3 -3
- package/src/bin/myceliumail.ts +54 -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-announce.ts +70 -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.7",
|
|
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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Myceliumail MCP - Config Module
|
|
3
|
+
*
|
|
4
|
+
* Reads configuration from:
|
|
5
|
+
* 1. Environment variables (highest priority)
|
|
6
|
+
* 2. ~/.myceliumail/config.json (fallback)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
const CONFIG_FILE = join(homedir(), '.myceliumail', 'config.json');
|
|
14
|
+
|
|
15
|
+
interface FileConfig {
|
|
16
|
+
agent_id?: string;
|
|
17
|
+
supabase_url?: string;
|
|
18
|
+
supabase_key?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let cachedFileConfig: FileConfig | null = null;
|
|
22
|
+
|
|
23
|
+
function loadFileConfig(): FileConfig {
|
|
24
|
+
if (cachedFileConfig) return cachedFileConfig;
|
|
25
|
+
|
|
26
|
+
if (existsSync(CONFIG_FILE)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
29
|
+
cachedFileConfig = JSON.parse(raw);
|
|
30
|
+
return cachedFileConfig!;
|
|
31
|
+
} catch {
|
|
32
|
+
// Invalid config file - ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getAgentId(): string {
|
|
39
|
+
return process.env.MYCELIUMAIL_AGENT_ID ||
|
|
40
|
+
process.env.MYCELIUMAIL_AGENT ||
|
|
41
|
+
loadFileConfig().agent_id ||
|
|
42
|
+
'anonymous';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getSupabaseUrl(): string | undefined {
|
|
46
|
+
return process.env.SUPABASE_URL || loadFileConfig().supabase_url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getSupabaseKey(): string | undefined {
|
|
50
|
+
return process.env.SUPABASE_ANON_KEY || loadFileConfig().supabase_key;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hasSupabase(): boolean {
|
|
54
|
+
return !!(getSupabaseUrl() && getSupabaseKey());
|
|
55
|
+
}
|
|
@@ -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,267 @@
|
|
|
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
|
+
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
...options,
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'apikey': getSupabaseKey()!,
|
|
68
|
+
'Authorization': `Bearer ${getSupabaseKey()}`,
|
|
69
|
+
'Prefer': options.method === 'POST' ? 'return=representation' : 'return=minimal',
|
|
70
|
+
...options.headers,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const text = await response.text();
|
|
76
|
+
console.error(`Supabase request failed (${response.status}): ${text}`);
|
|
77
|
+
throw new Error(text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (response.status === 204) return {} as T;
|
|
81
|
+
return response.json() as Promise<T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function sendMessage(
|
|
85
|
+
sender: string,
|
|
86
|
+
recipient: string,
|
|
87
|
+
subject: string,
|
|
88
|
+
body: string,
|
|
89
|
+
options?: {
|
|
90
|
+
encrypted?: boolean;
|
|
91
|
+
ciphertext?: string;
|
|
92
|
+
nonce?: string;
|
|
93
|
+
senderPublicKey?: string;
|
|
94
|
+
}
|
|
95
|
+
): Promise<Message> {
|
|
96
|
+
const newMessage: StoredMessage = {
|
|
97
|
+
id: randomUUID(),
|
|
98
|
+
sender,
|
|
99
|
+
recipient,
|
|
100
|
+
subject: options?.encrypted ? '' : subject,
|
|
101
|
+
body: options?.encrypted ? '' : body,
|
|
102
|
+
encrypted: options?.encrypted || false,
|
|
103
|
+
ciphertext: options?.ciphertext,
|
|
104
|
+
nonce: options?.nonce,
|
|
105
|
+
senderPublicKey: options?.senderPublicKey,
|
|
106
|
+
read: false,
|
|
107
|
+
archived: false,
|
|
108
|
+
createdAt: new Date().toISOString(),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (hasSupabase()) {
|
|
112
|
+
try {
|
|
113
|
+
const [result] = await supabaseRequest<StoredMessage[]>('/agent_messages', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
from_agent: newMessage.sender,
|
|
117
|
+
to_agent: newMessage.recipient,
|
|
118
|
+
subject: newMessage.subject || null,
|
|
119
|
+
message: newMessage.body || null,
|
|
120
|
+
encrypted: newMessage.encrypted,
|
|
121
|
+
ciphertext: newMessage.ciphertext,
|
|
122
|
+
nonce: newMessage.nonce,
|
|
123
|
+
sender_public_key: newMessage.senderPublicKey,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
return toMessage({
|
|
127
|
+
...newMessage,
|
|
128
|
+
id: (result as unknown as { id: string }).id
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error('sendMessage failed, falling back to local:', err);
|
|
132
|
+
// Fall through to local
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Local storage
|
|
137
|
+
const messages = loadLocalMessages();
|
|
138
|
+
messages.push(newMessage);
|
|
139
|
+
saveLocalMessages(messages);
|
|
140
|
+
return toMessage(newMessage);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getInbox(
|
|
144
|
+
agentId: string,
|
|
145
|
+
options?: { unreadOnly?: boolean; limit?: number }
|
|
146
|
+
): Promise<Message[]> {
|
|
147
|
+
if (hasSupabase()) {
|
|
148
|
+
try {
|
|
149
|
+
let query = `/agent_messages?to_agent=eq.${agentId}&order=created_at.desc`;
|
|
150
|
+
if (options?.unreadOnly) query += '&read=eq.false';
|
|
151
|
+
if (options?.limit) query += `&limit=${options.limit}`;
|
|
152
|
+
|
|
153
|
+
const results = await supabaseRequest<Array<{
|
|
154
|
+
id: string; from_agent: string; to_agent: string;
|
|
155
|
+
subject: string; message: string; encrypted: boolean;
|
|
156
|
+
ciphertext: string; nonce: string; sender_public_key: string;
|
|
157
|
+
read: boolean; created_at: string;
|
|
158
|
+
}>>(query);
|
|
159
|
+
|
|
160
|
+
return results.map(r => ({
|
|
161
|
+
id: r.id,
|
|
162
|
+
sender: r.from_agent,
|
|
163
|
+
recipient: r.to_agent,
|
|
164
|
+
subject: r.subject || '',
|
|
165
|
+
body: r.message || '',
|
|
166
|
+
encrypted: r.encrypted,
|
|
167
|
+
ciphertext: r.ciphertext,
|
|
168
|
+
nonce: r.nonce,
|
|
169
|
+
senderPublicKey: r.sender_public_key,
|
|
170
|
+
read: r.read,
|
|
171
|
+
archived: false,
|
|
172
|
+
createdAt: new Date(r.created_at),
|
|
173
|
+
}));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error('getInbox failed, falling back to local:', err);
|
|
176
|
+
// Fall through to local
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Local storage
|
|
181
|
+
const messages = loadLocalMessages();
|
|
182
|
+
let filtered = messages.filter(m => m.recipient === agentId && !m.archived);
|
|
183
|
+
if (options?.unreadOnly) filtered = filtered.filter(m => !m.read);
|
|
184
|
+
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
185
|
+
if (options?.limit) filtered = filtered.slice(0, options.limit);
|
|
186
|
+
return filtered.map(toMessage);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function getMessage(id: string): Promise<Message | null> {
|
|
190
|
+
if (hasSupabase()) {
|
|
191
|
+
try {
|
|
192
|
+
const results = await supabaseRequest<Array<{
|
|
193
|
+
id: string; from_agent: string; to_agent: string;
|
|
194
|
+
subject: string; message: string; encrypted: boolean;
|
|
195
|
+
ciphertext: string; nonce: string; sender_public_key: string;
|
|
196
|
+
read: boolean; created_at: string;
|
|
197
|
+
}>>(`/agent_messages?id=eq.${id}`);
|
|
198
|
+
|
|
199
|
+
if (results.length > 0) {
|
|
200
|
+
const r = results[0];
|
|
201
|
+
return {
|
|
202
|
+
id: r.id,
|
|
203
|
+
sender: r.from_agent,
|
|
204
|
+
recipient: r.to_agent,
|
|
205
|
+
subject: r.subject || '',
|
|
206
|
+
body: r.message || '',
|
|
207
|
+
encrypted: r.encrypted,
|
|
208
|
+
ciphertext: r.ciphertext,
|
|
209
|
+
nonce: r.nonce,
|
|
210
|
+
senderPublicKey: r.sender_public_key,
|
|
211
|
+
read: r.read,
|
|
212
|
+
archived: false,
|
|
213
|
+
createdAt: new Date(r.created_at),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('getMessage failed, falling back to local:', err);
|
|
218
|
+
// Fall through
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const messages = loadLocalMessages();
|
|
223
|
+
const found = messages.find(m => m.id === id);
|
|
224
|
+
return found ? toMessage(found) : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function markAsRead(id: string): Promise<boolean> {
|
|
228
|
+
if (hasSupabase()) {
|
|
229
|
+
try {
|
|
230
|
+
await supabaseRequest(`/agent_messages?id=eq.${id}`, {
|
|
231
|
+
method: 'PATCH',
|
|
232
|
+
body: JSON.stringify({ read: true }),
|
|
233
|
+
});
|
|
234
|
+
return true;
|
|
235
|
+
} catch {
|
|
236
|
+
// Fall through
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const messages = loadLocalMessages();
|
|
241
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
242
|
+
if (idx === -1) return false;
|
|
243
|
+
messages[idx].read = true;
|
|
244
|
+
saveLocalMessages(messages);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function archiveMessage(id: string): Promise<boolean> {
|
|
249
|
+
if (hasSupabase()) {
|
|
250
|
+
try {
|
|
251
|
+
await supabaseRequest(`/agent_messages?id=eq.${id}`, {
|
|
252
|
+
method: 'PATCH',
|
|
253
|
+
body: JSON.stringify({ archived: true }),
|
|
254
|
+
});
|
|
255
|
+
return true;
|
|
256
|
+
} catch {
|
|
257
|
+
// Fall through
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const messages = loadLocalMessages();
|
|
262
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
263
|
+
if (idx === -1) return false;
|
|
264
|
+
messages[idx].archived = true;
|
|
265
|
+
saveLocalMessages(messages);
|
|
266
|
+
return true;
|
|
267
|
+
}
|