clay-server 2.30.0 → 2.31.0-beta.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.
Files changed (36) hide show
  1. package/lib/email-accounts.js +299 -0
  2. package/lib/email-mcp-server.js +646 -0
  3. package/lib/project-connection.js +26 -2
  4. package/lib/project-email.js +418 -0
  5. package/lib/project-sessions.js +16 -0
  6. package/lib/project-user-message.js +26 -5
  7. package/lib/project.js +72 -25
  8. package/lib/public/app.js +18 -5
  9. package/lib/public/css/filebrowser.css +80 -2
  10. package/lib/public/css/input.css +196 -0
  11. package/lib/public/css/notifications-center.css +3 -0
  12. package/lib/public/css/sidebar.css +77 -2
  13. package/lib/public/css/sticky-notes.css +0 -48
  14. package/lib/public/css/user-settings.css +85 -0
  15. package/lib/public/icons/email/gmail.svg +7 -0
  16. package/lib/public/icons/email/outlook.svg +35 -0
  17. package/lib/public/icons/email/yahoo.svg +1 -0
  18. package/lib/public/index.html +36 -3
  19. package/lib/public/modules/app-dm.js +4 -9
  20. package/lib/public/modules/app-messages.js +37 -2
  21. package/lib/public/modules/app-panels.js +2 -1
  22. package/lib/public/modules/context-sources.js +527 -1
  23. package/lib/public/modules/filebrowser.js +72 -0
  24. package/lib/public/modules/mate-sidebar.js +7 -0
  25. package/lib/public/modules/sidebar-mobile.js +1 -1
  26. package/lib/public/modules/sidebar.js +144 -2
  27. package/lib/public/modules/sticky-notes.js +1 -91
  28. package/lib/public/modules/terminal.js +0 -12
  29. package/lib/public/modules/theme.js +4 -0
  30. package/lib/public/modules/tools.js +23 -0
  31. package/lib/public/modules/user-settings.js +74 -0
  32. package/lib/sdk-bridge.js +16 -0
  33. package/lib/sdk-message-processor.js +33 -0
  34. package/lib/server-email.js +148 -0
  35. package/lib/server.js +5 -0
  36. 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
- var restoredSources = loadContextSources(slug);
99
- sendTo(ws, { type: "context_sources_state", active: restoredSources });
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, "&lt;").replace(/>/g, "&gt;") + '</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 };
@@ -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 saved = loadContextSources(slug);
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
- saveContextSources(slug, activeIds);
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(ws, tabId);
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