clay-server 2.30.0 → 2.31.0
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/lib/email-accounts.js +299 -0
- package/lib/email-mcp-server.js +646 -0
- package/lib/project-connection.js +26 -2
- package/lib/project-email.js +418 -0
- package/lib/project-sessions.js +16 -0
- package/lib/project-user-message.js +26 -5
- package/lib/project.js +72 -25
- package/lib/public/app.js +18 -5
- package/lib/public/css/filebrowser.css +80 -2
- package/lib/public/css/input.css +196 -0
- package/lib/public/css/notifications-center.css +3 -0
- package/lib/public/css/sidebar.css +77 -2
- package/lib/public/css/sticky-notes.css +0 -48
- package/lib/public/css/user-settings.css +85 -0
- package/lib/public/icons/email/gmail.svg +7 -0
- package/lib/public/icons/email/outlook.svg +35 -0
- package/lib/public/icons/email/yahoo.svg +1 -0
- package/lib/public/index.html +36 -3
- package/lib/public/modules/app-dm.js +4 -9
- package/lib/public/modules/app-messages.js +37 -2
- package/lib/public/modules/app-panels.js +2 -1
- package/lib/public/modules/context-sources.js +527 -1
- package/lib/public/modules/filebrowser.js +72 -0
- package/lib/public/modules/mate-sidebar.js +7 -0
- package/lib/public/modules/sidebar-mobile.js +1 -1
- package/lib/public/modules/sidebar.js +144 -2
- package/lib/public/modules/sticky-notes.js +1 -91
- package/lib/public/modules/terminal.js +0 -12
- package/lib/public/modules/theme.js +4 -0
- package/lib/public/modules/tools.js +23 -0
- package/lib/public/modules/user-settings.js +74 -0
- package/lib/sdk-bridge.js +16 -0
- package/lib/sdk-message-processor.js +33 -0
- package/lib/server-email.js +148 -0
- package/lib/server.js +5 -0
- package/package.json +3 -2
|
@@ -2,6 +2,7 @@ var fs = require("fs");
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var usersModule = require("./users");
|
|
4
4
|
var userPresence = require("./user-presence");
|
|
5
|
+
var emailAccounts = require("./email-accounts");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Attach connection/disconnection handlers to a project context.
|
|
@@ -95,8 +96,11 @@ function attachConnection(ctx) {
|
|
|
95
96
|
}
|
|
96
97
|
sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
|
|
97
98
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
// Context sources sent after session is resolved (per-session storage)
|
|
100
|
+
// Send email accounts list for context sources picker
|
|
101
|
+
var emailUserId = (wsUser && wsUser.id) || "default";
|
|
102
|
+
var emailAccountsList = emailAccounts.listAccounts(emailUserId);
|
|
103
|
+
sendTo(ws, { type: "email_accounts_list", accounts: emailAccountsList, providers: emailAccounts.PROVIDER_PRESETS });
|
|
100
104
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
101
105
|
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
102
106
|
_loop.sendConnectionState(ws);
|
|
@@ -177,6 +181,9 @@ function attachConnection(ctx) {
|
|
|
177
181
|
}
|
|
178
182
|
ws._clayActiveSession = active.localId;
|
|
179
183
|
sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null });
|
|
184
|
+
// Send per-session context sources
|
|
185
|
+
var sessionSources = loadContextSources(slug, active.localId);
|
|
186
|
+
sendTo(ws, { type: "context_sources_state", active: sessionSources });
|
|
180
187
|
|
|
181
188
|
var total = active.history.length;
|
|
182
189
|
var fromIndex = 0;
|
|
@@ -220,6 +227,23 @@ function attachConnection(ctx) {
|
|
|
220
227
|
|
|
221
228
|
if (active) {
|
|
222
229
|
userPresence.setPresence(slug, presenceKey, active.localId, storedPresence ? storedPresence.mateDm : null);
|
|
230
|
+
// For auto-created sessions, apply project email defaults
|
|
231
|
+
if (autoCreated) {
|
|
232
|
+
var _emailMod = ctx._email;
|
|
233
|
+
var _saveCtx = ctx.saveContextSources;
|
|
234
|
+
if (_emailMod && _emailMod.getEmailDefaults && _saveCtx) {
|
|
235
|
+
var emailDefs = _emailMod.getEmailDefaults();
|
|
236
|
+
if (emailDefs.length > 0) {
|
|
237
|
+
var defSources = emailDefs.map(function (id) { return "email:" + id; });
|
|
238
|
+
_saveCtx(slug, active.localId, defSources);
|
|
239
|
+
sendTo(ws, { type: "context_sources_state", active: defSources });
|
|
240
|
+
} else {
|
|
241
|
+
sendTo(ws, { type: "context_sources_state", active: [] });
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
sendTo(ws, { type: "context_sources_state", active: [] });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
223
247
|
}
|
|
224
248
|
if (storedPresence && storedPresence.mateDm && !isMate) {
|
|
225
249
|
sendTo(ws, { type: "restore_mate_dm", mateId: storedPresence.mateDm });
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Project-level email module.
|
|
2
|
+
// Handles email context injection and unread polling.
|
|
3
|
+
// Follows the attachXxx(ctx) pattern.
|
|
4
|
+
|
|
5
|
+
var fs = require("fs");
|
|
6
|
+
var path = require("path");
|
|
7
|
+
var emailAccounts = require("./email-accounts");
|
|
8
|
+
var smtp = require("./smtp");
|
|
9
|
+
var { CONFIG_DIR } = require("./config");
|
|
10
|
+
|
|
11
|
+
var AUDIT_LOG_PATH = path.join(CONFIG_DIR, "email-audit.jsonl");
|
|
12
|
+
var EMAIL_DEFAULTS_DIR = path.join(CONFIG_DIR, "email-defaults");
|
|
13
|
+
|
|
14
|
+
// --- Project-level email defaults ---
|
|
15
|
+
// Stores which email accounts should be auto-enabled for every new session.
|
|
16
|
+
|
|
17
|
+
function loadEmailDefaults(slug) {
|
|
18
|
+
try {
|
|
19
|
+
var filePath = path.join(EMAIL_DEFAULTS_DIR, slug + ".json");
|
|
20
|
+
var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
21
|
+
return data.accounts || [];
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveEmailDefaults(slug, accountIds) {
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(EMAIL_DEFAULTS_DIR, { recursive: true });
|
|
30
|
+
var filePath = path.join(EMAIL_DEFAULTS_DIR, slug + ".json");
|
|
31
|
+
fs.writeFileSync(filePath, JSON.stringify({ accounts: accountIds }), "utf8");
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error("[email] Failed to save defaults:", e.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Audit log (Server SMTP only) ---
|
|
38
|
+
|
|
39
|
+
function appendAuditLog(entry) {
|
|
40
|
+
try {
|
|
41
|
+
fs.mkdirSync(path.dirname(AUDIT_LOG_PATH), { recursive: true });
|
|
42
|
+
fs.appendFileSync(AUDIT_LOG_PATH, JSON.stringify(entry) + "\n");
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error("[email] Failed to write audit log:", e.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Unread count fetching ---
|
|
49
|
+
|
|
50
|
+
function fetchUnreadCount(account) {
|
|
51
|
+
var ImapFlow;
|
|
52
|
+
try { ImapFlow = require("imapflow").ImapFlow; } catch (e) {
|
|
53
|
+
return Promise.resolve(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var client = new ImapFlow({
|
|
57
|
+
host: account.imap.host,
|
|
58
|
+
port: account.imap.port || 993,
|
|
59
|
+
secure: account.imap.tls !== false,
|
|
60
|
+
auth: { user: account.email, pass: account.appPassword },
|
|
61
|
+
logger: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return client.connect().then(function () {
|
|
65
|
+
return client.status("INBOX", { unseen: true });
|
|
66
|
+
}).then(function (status) {
|
|
67
|
+
var count = status.unseen || 0;
|
|
68
|
+
return client.logout().then(function () {
|
|
69
|
+
return count;
|
|
70
|
+
});
|
|
71
|
+
}).catch(function () {
|
|
72
|
+
try { client.close(); } catch (e) {}
|
|
73
|
+
return 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Email context builder (for injecting into user messages) ---
|
|
78
|
+
|
|
79
|
+
function buildEmailContext(accounts, limit) {
|
|
80
|
+
if (!accounts || accounts.length === 0) return Promise.resolve("");
|
|
81
|
+
|
|
82
|
+
var perAccount = Math.max(Math.floor((limit || 10) / accounts.length), 3);
|
|
83
|
+
|
|
84
|
+
var ImapFlow;
|
|
85
|
+
try { ImapFlow = require("imapflow").ImapFlow; } catch (e) {
|
|
86
|
+
return Promise.resolve("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var promises = accounts.map(function (account) {
|
|
90
|
+
var client = new ImapFlow({
|
|
91
|
+
host: account.imap.host,
|
|
92
|
+
port: account.imap.port || 993,
|
|
93
|
+
secure: account.imap.tls !== false,
|
|
94
|
+
auth: { user: account.email, pass: account.appPassword },
|
|
95
|
+
logger: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return client.connect().then(function () {
|
|
99
|
+
return client.getMailboxLock("INBOX");
|
|
100
|
+
}).then(function (lock) {
|
|
101
|
+
return client.search({ seen: false }, { uid: true }).then(function (uids) {
|
|
102
|
+
if (!uids || uids.length === 0) {
|
|
103
|
+
lock.release();
|
|
104
|
+
return client.logout().then(function () {
|
|
105
|
+
return { email: account.email, unread: 0, messages: [] };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var recentUids = uids.slice(-perAccount).reverse();
|
|
110
|
+
|
|
111
|
+
var fetchIter = client.fetch(recentUids, {
|
|
112
|
+
uid: true,
|
|
113
|
+
envelope: true,
|
|
114
|
+
flags: true,
|
|
115
|
+
}, { uid: true });
|
|
116
|
+
var messages = [];
|
|
117
|
+
|
|
118
|
+
function collect() {
|
|
119
|
+
return fetchIter.next().then(function (result) {
|
|
120
|
+
if (result.done) return;
|
|
121
|
+
var msg = result.value;
|
|
122
|
+
var env = msg.envelope || {};
|
|
123
|
+
var fromAddr = env.from && env.from[0] ? (env.from[0].address || "") : "";
|
|
124
|
+
var date = env.date || new Date();
|
|
125
|
+
var ago = formatTimeAgo(date);
|
|
126
|
+
messages.push({
|
|
127
|
+
from: fromAddr,
|
|
128
|
+
subject: env.subject || "(no subject)",
|
|
129
|
+
ago: ago,
|
|
130
|
+
});
|
|
131
|
+
return collect();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return collect().then(function () {
|
|
136
|
+
lock.release();
|
|
137
|
+
return client.logout().then(function () {
|
|
138
|
+
return { email: account.email, unread: uids.length, messages: messages };
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}).catch(function () {
|
|
143
|
+
try { client.close(); } catch (e) {}
|
|
144
|
+
return { email: account.email, unread: 0, messages: [], error: true };
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return Promise.all(promises).then(function (results) {
|
|
149
|
+
var parts = [];
|
|
150
|
+
for (var i = 0; i < results.length; i++) {
|
|
151
|
+
var r = results[i];
|
|
152
|
+
if (r.messages.length === 0 && !r.error) continue;
|
|
153
|
+
var header = "--- Email Context: " + r.email + " (" + r.unread + " unread) ---";
|
|
154
|
+
var lines = [];
|
|
155
|
+
for (var j = 0; j < r.messages.length; j++) {
|
|
156
|
+
var m = r.messages[j];
|
|
157
|
+
lines.push((j + 1) + ". From: " + m.from + " | Subject: " + m.subject + " | " + m.ago);
|
|
158
|
+
}
|
|
159
|
+
if (lines.length > 0) {
|
|
160
|
+
parts.push(header + "\n" + lines.join("\n"));
|
|
161
|
+
} else {
|
|
162
|
+
parts.push(header + "\n(unable to fetch messages)");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return parts.join("\n\n");
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatTimeAgo(date) {
|
|
170
|
+
var now = Date.now();
|
|
171
|
+
var d = date instanceof Date ? date : new Date(date);
|
|
172
|
+
var diff = now - d.getTime();
|
|
173
|
+
if (diff < 0) diff = 0;
|
|
174
|
+
var minutes = Math.floor(diff / 60000);
|
|
175
|
+
if (minutes < 1) return "just now";
|
|
176
|
+
if (minutes < 60) return minutes + "m ago";
|
|
177
|
+
var hours = Math.floor(minutes / 60);
|
|
178
|
+
if (hours < 24) return hours + "h ago";
|
|
179
|
+
var days = Math.floor(hours / 24);
|
|
180
|
+
return days + "d ago";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Module attachment ---
|
|
184
|
+
|
|
185
|
+
function attachEmail(ctx) {
|
|
186
|
+
var slug = ctx.slug;
|
|
187
|
+
var send = ctx.send;
|
|
188
|
+
var sendTo = ctx.sendTo;
|
|
189
|
+
var clients = ctx.clients;
|
|
190
|
+
var loadContextSources = ctx.loadContextSources;
|
|
191
|
+
var getUserIdForWs = ctx.getUserIdForWs;
|
|
192
|
+
|
|
193
|
+
// Unread count cache: { accountId: { count, fetchedAt } }
|
|
194
|
+
var unreadCache = {};
|
|
195
|
+
var POLL_INTERVAL_ACTIVE = 2 * 60 * 1000; // 2 minutes for checked accounts
|
|
196
|
+
var POLL_INTERVAL_IDLE = 10 * 60 * 1000; // 10 minutes for unchecked
|
|
197
|
+
var pollTimer = null;
|
|
198
|
+
|
|
199
|
+
function getCheckedEmailAccounts(userId, sessionId) {
|
|
200
|
+
var sources = loadContextSources(slug, sessionId);
|
|
201
|
+
var accounts = [];
|
|
202
|
+
for (var i = 0; i < sources.length; i++) {
|
|
203
|
+
if (sources[i].startsWith("email:")) {
|
|
204
|
+
var accountId = sources[i].split(":")[1];
|
|
205
|
+
var acc = emailAccounts.getAccountDecrypted(userId, accountId);
|
|
206
|
+
if (acc) accounts.push(acc);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return accounts;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getCheckedEmailAccountsByEmail(userId, sessionId) {
|
|
213
|
+
var sources = loadContextSources(slug, sessionId);
|
|
214
|
+
var emailIds = [];
|
|
215
|
+
for (var i = 0; i < sources.length; i++) {
|
|
216
|
+
if (sources[i].startsWith("email:")) {
|
|
217
|
+
emailIds.push(sources[i].split(":")[1]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (emailIds.length === 0) return [];
|
|
221
|
+
var allAccounts = emailAccounts.listAccounts(userId);
|
|
222
|
+
var checked = [];
|
|
223
|
+
for (var j = 0; j < allAccounts.length; j++) {
|
|
224
|
+
if (emailIds.indexOf(allAccounts[j].id) !== -1) {
|
|
225
|
+
var dec = emailAccounts.getAccountDecrypted(userId, allAccounts[j].id);
|
|
226
|
+
if (dec) checked.push(dec);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return checked;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Poll unread counts and push updates
|
|
233
|
+
function pollUnreadCounts() {
|
|
234
|
+
// Find first connected client to get userId
|
|
235
|
+
var userId = null;
|
|
236
|
+
for (var ws of clients) {
|
|
237
|
+
if (ws.readyState === 1) {
|
|
238
|
+
userId = (ws._clayUser && ws._clayUser.id) || "default";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!userId) return;
|
|
243
|
+
|
|
244
|
+
var allAccounts = emailAccounts.listAccounts(userId);
|
|
245
|
+
if (allAccounts.length === 0) return;
|
|
246
|
+
|
|
247
|
+
// Collect checked email IDs across all connected clients' sessions
|
|
248
|
+
var checkedIds = {};
|
|
249
|
+
for (var ws2 of clients) {
|
|
250
|
+
if (ws2.readyState !== 1) continue;
|
|
251
|
+
var sid = ws2._clayActiveSession || null;
|
|
252
|
+
var sources = loadContextSources(slug, sid);
|
|
253
|
+
for (var i = 0; i < sources.length; i++) {
|
|
254
|
+
if (sources[i].startsWith("email:")) {
|
|
255
|
+
checkedIds[sources[i].split(":")[1]] = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var toFetch = [];
|
|
261
|
+
for (var j = 0; j < allAccounts.length; j++) {
|
|
262
|
+
var acc = allAccounts[j];
|
|
263
|
+
var isChecked = !!checkedIds[acc.id];
|
|
264
|
+
var interval = isChecked ? POLL_INTERVAL_ACTIVE : POLL_INTERVAL_IDLE;
|
|
265
|
+
var cached = unreadCache[acc.id];
|
|
266
|
+
if (!cached || (Date.now() - cached.fetchedAt) > interval) {
|
|
267
|
+
toFetch.push(acc);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (toFetch.length === 0) return;
|
|
272
|
+
|
|
273
|
+
var fetchPromises = toFetch.map(function (acc) {
|
|
274
|
+
var decrypted = emailAccounts.getAccountDecrypted(userId, acc.id);
|
|
275
|
+
if (!decrypted) return Promise.resolve(null);
|
|
276
|
+
return fetchUnreadCount(decrypted).then(function (count) {
|
|
277
|
+
return { id: acc.id, count: count };
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
Promise.all(fetchPromises).then(function (results) {
|
|
282
|
+
var changed = false;
|
|
283
|
+
for (var k = 0; k < results.length; k++) {
|
|
284
|
+
if (!results[k]) continue;
|
|
285
|
+
var prev = unreadCache[results[k].id];
|
|
286
|
+
unreadCache[results[k].id] = { count: results[k].count, fetchedAt: Date.now() };
|
|
287
|
+
if (!prev || prev.count !== results[k].count) changed = true;
|
|
288
|
+
}
|
|
289
|
+
if (changed) {
|
|
290
|
+
var updates = {};
|
|
291
|
+
var ukeys = Object.keys(unreadCache);
|
|
292
|
+
for (var m = 0; m < ukeys.length; m++) {
|
|
293
|
+
updates[ukeys[m]] = unreadCache[ukeys[m]].count;
|
|
294
|
+
}
|
|
295
|
+
var msg = JSON.stringify({ type: "email_unread_update", unread: updates });
|
|
296
|
+
for (var ws of clients) {
|
|
297
|
+
if (ws.readyState === 1) ws.send(msg);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}).catch(function () {});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Start polling
|
|
304
|
+
pollTimer = setInterval(pollUnreadCounts, 60000); // Check every minute
|
|
305
|
+
// Initial poll after 5 seconds
|
|
306
|
+
setTimeout(pollUnreadCounts, 5000);
|
|
307
|
+
|
|
308
|
+
// Get email context for message injection
|
|
309
|
+
function getEmailContext(userId, sessionId) {
|
|
310
|
+
var checked = getCheckedEmailAccountsByEmail(userId, sessionId);
|
|
311
|
+
if (checked.length === 0) return Promise.resolve("");
|
|
312
|
+
return buildEmailContext(checked, 10);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Collect checked email accounts across all connected clients' active sessions
|
|
316
|
+
function getAllCheckedAccounts(userId) {
|
|
317
|
+
var seen = {};
|
|
318
|
+
var result = [];
|
|
319
|
+
for (var ws of clients) {
|
|
320
|
+
if (ws.readyState !== 1) continue;
|
|
321
|
+
var sid = ws._clayActiveSession || null;
|
|
322
|
+
var checked = getCheckedEmailAccountsByEmail(userId, sid);
|
|
323
|
+
for (var ci = 0; ci < checked.length; ci++) {
|
|
324
|
+
if (!seen[checked[ci].id]) {
|
|
325
|
+
seen[checked[ci].id] = true;
|
|
326
|
+
result.push(checked[ci]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Fallback: if no session-level sources found, check project email defaults
|
|
331
|
+
if (result.length === 0) {
|
|
332
|
+
var defaults = loadEmailDefaults(slug);
|
|
333
|
+
for (var di = 0; di < defaults.length; di++) {
|
|
334
|
+
var dec = emailAccounts.getAccountDecrypted(userId, defaults[di]);
|
|
335
|
+
if (dec && !seen[dec.id]) {
|
|
336
|
+
seen[dec.id] = true;
|
|
337
|
+
result.push(dec);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Resolve the active userId from connected clients at call time
|
|
345
|
+
function getActiveUserId() {
|
|
346
|
+
for (var ws of clients) {
|
|
347
|
+
if (ws.readyState === 1 && ws._clayUser && ws._clayUser.id) {
|
|
348
|
+
return ws._clayUser.id;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return "default";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Create MCP server dependencies (userId resolved dynamically per call)
|
|
355
|
+
function createMcpDeps() {
|
|
356
|
+
return {
|
|
357
|
+
getAccountForTool: function (email) {
|
|
358
|
+
return emailAccounts.getAccountByEmailDecrypted(getActiveUserId(), email);
|
|
359
|
+
},
|
|
360
|
+
getCheckedAccounts: function () {
|
|
361
|
+
return getAllCheckedAccounts(getActiveUserId());
|
|
362
|
+
},
|
|
363
|
+
getServerSmtp: function () {
|
|
364
|
+
if (!smtp.isSmtpConfigured()) return null;
|
|
365
|
+
return {
|
|
366
|
+
sendMail: function (to, subject, body) {
|
|
367
|
+
return smtp.sendMail(to, subject, '<pre style="font-family:system-ui,sans-serif;white-space:pre-wrap">' + body.replace(/</g, "<").replace(/>/g, ">") + '</pre>');
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
appendAuditLog: function (entry) {
|
|
372
|
+
entry.userId = getActiveUserId();
|
|
373
|
+
entry.projectSlug = slug;
|
|
374
|
+
appendAuditLog(entry);
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleEmailMessage(ws, msg) {
|
|
380
|
+
if (msg.type === "email_defaults_get") {
|
|
381
|
+
var defaults = loadEmailDefaults(slug);
|
|
382
|
+
sendTo(ws, { type: "email_defaults", accounts: defaults });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
if (msg.type === "email_defaults_save") {
|
|
386
|
+
var accountIds = msg.accounts || [];
|
|
387
|
+
saveEmailDefaults(slug, accountIds);
|
|
388
|
+
// Broadcast to all clients on this project
|
|
389
|
+
var _defMsg = JSON.stringify({ type: "email_defaults", accounts: accountIds });
|
|
390
|
+
for (var c of clients) { if (c.readyState === 1) c.send(_defMsg); }
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Get default email account IDs for new sessions
|
|
397
|
+
function getEmailDefaults() {
|
|
398
|
+
return loadEmailDefaults(slug);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function destroy() {
|
|
402
|
+
if (pollTimer) {
|
|
403
|
+
clearInterval(pollTimer);
|
|
404
|
+
pollTimer = null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
handleEmailMessage: handleEmailMessage,
|
|
410
|
+
getEmailContext: getEmailContext,
|
|
411
|
+
getCheckedEmailAccounts: getCheckedEmailAccounts,
|
|
412
|
+
getEmailDefaults: getEmailDefaults,
|
|
413
|
+
createMcpDeps: createMcpDeps,
|
|
414
|
+
destroy: destroy,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = { attachEmail: attachEmail, appendAuditLog: appendAuditLog };
|
package/lib/project-sessions.js
CHANGED
|
@@ -65,6 +65,8 @@ function attachSessions(ctx) {
|
|
|
65
65
|
var setUpdateChannel = ctx.setUpdateChannel;
|
|
66
66
|
var getLatestVersion = ctx.getLatestVersion;
|
|
67
67
|
var setLatestVersion = ctx.setLatestVersion;
|
|
68
|
+
var loadContextSources = ctx.loadContextSources;
|
|
69
|
+
var saveContextSources = ctx.saveContextSources;
|
|
68
70
|
|
|
69
71
|
function handleSessionsMessage(ws, msg) {
|
|
70
72
|
|
|
@@ -96,6 +98,15 @@ function attachSessions(ctx) {
|
|
|
96
98
|
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
97
99
|
var newSess = sm.createSession(sessionOpts, ws);
|
|
98
100
|
ws._clayActiveSession = newSess.localId;
|
|
101
|
+
// Apply project-level email defaults to new session
|
|
102
|
+
if (typeof ctx._email === "object" && ctx._email.getEmailDefaults) {
|
|
103
|
+
var emailDefaults = ctx._email.getEmailDefaults();
|
|
104
|
+
if (emailDefaults.length > 0) {
|
|
105
|
+
var defaultSources = emailDefaults.map(function (id) { return "email:" + id; });
|
|
106
|
+
saveContextSources(slug, newSess.localId, defaultSources);
|
|
107
|
+
sendTo(ws, { type: "context_sources_state", active: defaultSources });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
99
110
|
var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
100
111
|
userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
|
|
101
112
|
if (usersModule.isMultiUser()) {
|
|
@@ -233,6 +244,11 @@ function attachSessions(ctx) {
|
|
|
233
244
|
ws._clayActiveSession = msg.id;
|
|
234
245
|
sm.switchSession(msg.id, ws, hydrateImageRefs);
|
|
235
246
|
}
|
|
247
|
+
// Send per-session context sources
|
|
248
|
+
if (typeof loadContextSources === "function") {
|
|
249
|
+
var switchedSources = loadContextSources(slug, msg.id);
|
|
250
|
+
sendTo(ws, { type: "context_sources_state", active: switchedSources });
|
|
251
|
+
}
|
|
236
252
|
var swPresKey = ws._clayUser ? ws._clayUser.id : "_default";
|
|
237
253
|
userPresence.setPresence(slug, swPresKey, msg.id, null);
|
|
238
254
|
}
|
|
@@ -70,6 +70,7 @@ function attachUserMessage(ctx) {
|
|
|
70
70
|
var saveContextSources = ctx.saveContextSources;
|
|
71
71
|
|
|
72
72
|
var getSDK = ctx.getSDK;
|
|
73
|
+
var _email = ctx._email;
|
|
73
74
|
|
|
74
75
|
// --------------- Sticky notes ---------------
|
|
75
76
|
|
|
@@ -181,11 +182,12 @@ function attachUserMessage(ctx) {
|
|
|
181
182
|
tm.close(msg.id);
|
|
182
183
|
send({ type: "term_list", terminals: tm.list() });
|
|
183
184
|
// Remove closed terminal from context sources
|
|
184
|
-
var
|
|
185
|
+
var _termSessionId = ws._clayActiveSession || null;
|
|
186
|
+
var saved = loadContextSources(slug, _termSessionId);
|
|
185
187
|
var termKey = "term:" + msg.id;
|
|
186
188
|
var filtered = saved.filter(function(id) { return id !== termKey; });
|
|
187
189
|
if (filtered.length !== saved.length) {
|
|
188
|
-
saveContextSources(slug, filtered);
|
|
190
|
+
saveContextSources(slug, _termSessionId, filtered);
|
|
189
191
|
send({ type: "context_sources_state", active: filtered });
|
|
190
192
|
}
|
|
191
193
|
}
|
|
@@ -203,7 +205,8 @@ function attachUserMessage(ctx) {
|
|
|
203
205
|
// --- Context Sources ---
|
|
204
206
|
if (msg.type === "context_sources_save") {
|
|
205
207
|
var activeIds = msg.active || [];
|
|
206
|
-
|
|
208
|
+
var _saveSessionId = ws._clayActiveSession || null;
|
|
209
|
+
saveContextSources(slug, _saveSessionId, activeIds);
|
|
207
210
|
return true;
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -366,7 +369,7 @@ function attachUserMessage(ctx) {
|
|
|
366
369
|
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
367
370
|
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
|
368
371
|
var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
|
|
369
|
-
var ctxSources = loadContextSources(slug);
|
|
372
|
+
var ctxSources = loadContextSources(slug, session.localId);
|
|
370
373
|
if (ctxSources.length > 0) {
|
|
371
374
|
if (!session._termContextCursors) session._termContextCursors = {};
|
|
372
375
|
var termContextParts = [];
|
|
@@ -447,6 +450,16 @@ function attachUserMessage(ctx) {
|
|
|
447
450
|
}
|
|
448
451
|
}
|
|
449
452
|
|
|
453
|
+
// Collect email context (async: requires IMAP fetch for checked email accounts)
|
|
454
|
+
var emailSources = ctxSources.filter(function(id) { return id.startsWith("email:"); });
|
|
455
|
+
var emailContextPromise;
|
|
456
|
+
if (emailSources.length > 0 && _email) {
|
|
457
|
+
var emailUserId = (ws._clayUser && ws._clayUser.id) || "default";
|
|
458
|
+
emailContextPromise = _email.getEmailContext(emailUserId, session.localId).catch(function () { return ""; });
|
|
459
|
+
} else {
|
|
460
|
+
emailContextPromise = Promise.resolve("");
|
|
461
|
+
}
|
|
462
|
+
|
|
450
463
|
// Collect browser tab context (async: requires round-trip to client extension)
|
|
451
464
|
var _browserTabList = browserState._browserTabList;
|
|
452
465
|
var tabSources = ctxSources.filter(function(id) {
|
|
@@ -476,11 +489,17 @@ function attachUserMessage(ctx) {
|
|
|
476
489
|
sm.broadcastSessionList();
|
|
477
490
|
}
|
|
478
491
|
|
|
492
|
+
// Wait for email context, then proceed with browser tab context and dispatch
|
|
493
|
+
emailContextPromise.then(function (emailCtxText) {
|
|
494
|
+
if (emailCtxText) {
|
|
495
|
+
fullText = emailCtxText + "\n\n" + fullText;
|
|
496
|
+
}
|
|
497
|
+
|
|
479
498
|
if (tabSources.length > 0) {
|
|
480
499
|
// Request tab context from all active browser tab sources
|
|
481
500
|
var tabPromises = tabSources.map(function(srcId) {
|
|
482
501
|
var tabId = parseInt(srcId.split(":")[1], 10);
|
|
483
|
-
return requestTabContext(
|
|
502
|
+
return requestTabContext(tabId);
|
|
484
503
|
});
|
|
485
504
|
Promise.all(tabPromises).then(function(results) {
|
|
486
505
|
var tabContextParts = [];
|
|
@@ -623,6 +642,8 @@ function attachUserMessage(ctx) {
|
|
|
623
642
|
dispatchToSdk(fullText);
|
|
624
643
|
}
|
|
625
644
|
|
|
645
|
+
}); // emailContextPromise.then
|
|
646
|
+
|
|
626
647
|
return true;
|
|
627
648
|
}
|
|
628
649
|
|