clay-server 3.3.0-beta.1 → 3.3.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.
@@ -9,31 +9,6 @@ var smtp = require("./smtp");
9
9
  var { CONFIG_DIR } = require("./config");
10
10
 
11
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
12
  // --- Audit log (Server SMTP only) ---
38
13
 
39
14
  function appendAuditLog(entry) {
@@ -185,7 +160,6 @@ function formatTimeAgo(date) {
185
160
  function attachEmail(ctx) {
186
161
  var slug = ctx.slug;
187
162
  var send = ctx.send;
188
- var sendTo = ctx.sendTo;
189
163
  var clients = ctx.clients;
190
164
  var loadContextSources = ctx.loadContextSources;
191
165
  var getUserIdForWs = ctx.getUserIdForWs;
@@ -327,17 +301,6 @@ function attachEmail(ctx) {
327
301
  }
328
302
  }
329
303
  }
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
304
  return result;
342
305
  }
343
306
 
@@ -376,28 +339,6 @@ function attachEmail(ctx) {
376
339
  };
377
340
  }
378
341
 
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
342
  function destroy() {
402
343
  if (pollTimer) {
403
344
  clearInterval(pollTimer);
@@ -421,10 +362,8 @@ function attachEmail(ctx) {
421
362
  }
422
363
 
423
364
  return {
424
- handleEmailMessage: handleEmailMessage,
425
365
  getEmailContext: getEmailContext,
426
366
  getCheckedEmailAccounts: getCheckedEmailAccounts,
427
- getEmailDefaults: getEmailDefaults,
428
367
  createMcpDeps: createMcpDeps,
429
368
  hasEmailCapability: hasEmailCapability,
430
369
  destroy: destroy,
@@ -5,6 +5,7 @@ var crypto = require("crypto");
5
5
  var { execFileSync, spawn } = require("child_process");
6
6
  var { fsAsUser } = require("./os-users");
7
7
  var usersModule = require("./users");
8
+ var gitCli = require("./git-cli");
8
9
 
9
10
  var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
10
11
  var MIME_TYPES = {
@@ -47,6 +48,7 @@ function attachHTTP(ctx) {
47
48
  var slug = ctx.slug;
48
49
  var project = ctx.project;
49
50
  var sm = ctx.sm;
51
+ var gitAttribution = ctx.gitAttribution;
50
52
  var send = ctx.send;
51
53
  var imagesDir = ctx.imagesDir;
52
54
  var osUsers = ctx.osUsers;
@@ -643,7 +645,108 @@ function attachHTTP(ctx) {
643
645
  return true;
644
646
  }
645
647
 
646
- // Git dirty check
648
+ // Git panel status and actions
649
+ var isGitPanelRequest = urlPath === "/api/git/status" ||
650
+ urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
651
+ urlPath.indexOf("/api/git/session-diff?") === 0;
652
+ if (isGitPanelRequest && usersModule.isMultiUser()) {
653
+ var gitRequestUser = req._clayUser;
654
+ var gitPermissions = gitRequestUser ? usersModule.getEffectivePermissions(gitRequestUser, osUsers) : null;
655
+ if (!gitPermissions || !gitPermissions.fileBrowser) {
656
+ res.writeHead(403, { "Content-Type": "application/json" });
657
+ res.end('{"error":"Git access is not permitted"}');
658
+ return true;
659
+ }
660
+ }
661
+
662
+ if (req.method === "GET" && urlPath === "/api/git/status") {
663
+ var gitUserInfo = getOsUserInfoForReq(req);
664
+ try {
665
+ var gitStatus = gitCli.getStatus(cwd, gitUserInfo);
666
+ if (gitAttribution) {
667
+ var visibleGitSessions = sm.sessions;
668
+ if (usersModule.isMultiUser() && req._clayUser) {
669
+ visibleGitSessions = new Map();
670
+ sm.sessions.forEach(function (session, id) {
671
+ if (usersModule.canAccessSession(req._clayUser.id, session, { visibility: "public" })) {
672
+ visibleGitSessions.set(id, session);
673
+ }
674
+ });
675
+ }
676
+ gitAttribution.decorateStatus(gitStatus, visibleGitSessions);
677
+ }
678
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
679
+ res.end(JSON.stringify(gitStatus));
680
+ } catch (e) {
681
+ res.writeHead(500, { "Content-Type": "application/json" });
682
+ res.end(JSON.stringify({ error: e.message || "Unable to read Git status" }));
683
+ }
684
+ return true;
685
+ }
686
+
687
+ if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
688
+ var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
689
+ var gitDiffPath = gitDiffParams.get("path");
690
+ if (!gitDiffPath) {
691
+ res.writeHead(400, { "Content-Type": "application/json" });
692
+ res.end('{"error":"Missing file path"}');
693
+ return true;
694
+ }
695
+ try {
696
+ var gitDiffResult = gitCli.getFileDiff(cwd, gitDiffPath, getOsUserInfoForReq(req));
697
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
698
+ res.end(JSON.stringify(gitDiffResult));
699
+ } catch (e) {
700
+ res.writeHead(400, { "Content-Type": "application/json" });
701
+ res.end(JSON.stringify({ error: e.message || "Unable to read file diff" }));
702
+ }
703
+ return true;
704
+ }
705
+
706
+ if (req.method === "GET" && urlPath.indexOf("/api/git/session-diff?") === 0) {
707
+ var sessionDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
708
+ var sessionDiffKey = sessionDiffParams.get("session");
709
+ var sessionDiffPath = sessionDiffParams.get("path");
710
+ if (!gitAttribution || !sessionDiffKey || !sessionDiffPath) {
711
+ res.writeHead(400, { "Content-Type": "application/json" });
712
+ res.end('{"error":"Missing session comparison"}');
713
+ return true;
714
+ }
715
+ try {
716
+ var requestedSessionVisible = false;
717
+ sm.sessions.forEach(function (session) {
718
+ var key = session.cliSessionId || ("local:" + session.localId);
719
+ if (key !== sessionDiffKey) return;
720
+ requestedSessionVisible = !usersModule.isMultiUser() ||
721
+ (req._clayUser && usersModule.canAccessSession(req._clayUser.id, session, { visibility: "public" }));
722
+ });
723
+ if (!requestedSessionVisible) throw new Error("Session comparison is unavailable");
724
+ var sessionDiff = gitAttribution.getSessionBaselineDiff(sessionDiffKey, sessionDiffPath, getOsUserInfoForReq(req));
725
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
726
+ res.end(JSON.stringify(sessionDiff));
727
+ } catch (e) {
728
+ res.writeHead(400, { "Content-Type": "application/json" });
729
+ res.end(JSON.stringify({ error: e.message || "Unable to compare session changes" }));
730
+ }
731
+ return true;
732
+ }
733
+
734
+ if (req.method === "POST" && urlPath === "/api/git/action") {
735
+ var gitActionUserInfo = getOsUserInfoForReq(req);
736
+ parseJsonBody(req).then(function (body) {
737
+ return gitCli.runAction(cwd, body, gitActionUserInfo);
738
+ }).then(function (output) {
739
+ var updatedStatus = gitCli.getStatus(cwd, gitActionUserInfo);
740
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
741
+ res.end(JSON.stringify({ ok: true, output: output, status: updatedStatus }));
742
+ }).catch(function (err) {
743
+ res.writeHead(400, { "Content-Type": "application/json" });
744
+ res.end(JSON.stringify({ error: err.userMessage || err.message || "Git action failed" }));
745
+ });
746
+ return true;
747
+ }
748
+
749
+ // Legacy dirty check used by the Ralph Loop flow
647
750
  if (req.method === "GET" && urlPath === "/api/git-dirty") {
648
751
  try {
649
752
  var out = execFileSync("git", ["status", "--porcelain"], { cwd: cwd, encoding: "utf8", timeout: 5000 });
@@ -104,7 +104,6 @@ function attachSessions(ctx) {
104
104
  var getLatestVersion = ctx.getLatestVersion;
105
105
  var setLatestVersion = ctx.setLatestVersion;
106
106
  var loadContextSources = ctx.loadContextSources;
107
- var saveContextSources = ctx.saveContextSources;
108
107
 
109
108
  // Resolve the active user's Claude open-mode preference ('gui' or 'tui').
110
109
  // Multi-user mode reads per-user storage; single-user mode falls back to
@@ -516,7 +515,7 @@ function attachSessions(ctx) {
516
515
  // Reuse an existing blank GUI session instead of stacking another
517
516
  // one. A vendor-less blank is acceptable: nothing has happened in it,
518
517
  // so stamping the requested vendor is equivalent to a fresh create.
519
- var reusable = sm.findReusableBlankSession({
518
+ var reusable = msg.forceNew === true ? null : sm.findReusableBlankSession({
520
519
  vendor: sessionOpts.vendor || null,
521
520
  ownerId: sessionOpts.ownerId || null,
522
521
  });
@@ -542,15 +541,6 @@ function attachSessions(ctx) {
542
541
  }
543
542
  send({ type: "last_vendor", vendor: msg.vendor });
544
543
  }
545
- // Apply project-level email defaults to new session
546
- if (typeof ctx._email === "object" && ctx._email.getEmailDefaults) {
547
- var emailDefaults = ctx._email.getEmailDefaults();
548
- if (emailDefaults.length > 0) {
549
- var defaultSources = emailDefaults.map(function (id) { return "email:" + id; });
550
- saveContextSources(slug, newSess.localId, defaultSources);
551
- sendTo(ws, { type: "context_sources_state", active: defaultSources });
552
- }
553
- }
554
544
  var nsPresKey = ws._clayUser ? ws._clayUser.id : "_default";
555
545
  if (!ws._clayPane) userPresence.setPresence(slug, nsPresKey, newSess.localId, null);
556
546
  if (usersModule.isMultiUser() && !ws._clayPane) {
@@ -57,6 +57,7 @@ function attachUserMessage(ctx) {
57
57
  var imagesDir = ctx.imagesDir;
58
58
 
59
59
  var onProcessingChanged = ctx.onProcessingChanged;
60
+ var gitAttribution = ctx.gitAttribution;
60
61
 
61
62
  var _loop = ctx._loop;
62
63
  var browserState = ctx.browserState;
@@ -293,6 +294,7 @@ function attachUserMessage(ctx) {
293
294
 
294
295
  var session = getSessionForWs(ws);
295
296
  if (!session) return true;
297
+ if (gitAttribution) gitAttribution.beginTurn(session);
296
298
 
297
299
  // Bind vendor to session on first message (if not already set)
298
300
  if (!session.vendor && msg.vendor) {
package/lib/project.js CHANGED
@@ -3,6 +3,7 @@ var path = require("path");
3
3
  var os = require("os");
4
4
  var crypto = require("crypto");
5
5
  var { createSessionManager } = require("./sessions");
6
+ var { attachGitSessionAttribution } = require("./git-session-attribution");
6
7
  var { createSDKBridge, createMessageQueue } = require("./sdk-bridge");
7
8
  var { createTerminalManager } = require("./terminal-manager");
8
9
  var { createNotesManager } = require("./notes");
@@ -172,7 +173,9 @@ function createProjectContext(opts) {
172
173
  // --- YOKE adapters (multi-vendor, lazy init) ---
173
174
  var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug, osUsers: osUsers });
174
175
  var adapters = _yokeState.adapters;
175
- var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
176
+ // A new project has no remembered vendor. Select the first installed
177
+ // provider by the shared Claude -> Codex -> Kiro preference order.
178
+ var defaultVendor = yoke.resolveDefaultVendor(adapters);
176
179
  var adapter = adapters[defaultVendor] || null;
177
180
 
178
181
  // Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
@@ -399,6 +402,7 @@ function createProjectContext(opts) {
399
402
  var stopAllDirWatches = _fileWatch.stopAllDirWatches;
400
403
 
401
404
  // --- Session manager ---
405
+ var _gitAttribution = null;
402
406
  var sm = createSessionManager({
403
407
  cwd: cwd,
404
408
  send: send,
@@ -417,7 +421,17 @@ function createProjectContext(opts) {
417
421
  fn(ws, filterFn);
418
422
  }
419
423
  },
420
- onSessionDone: onSessionDone,
424
+ onSessionDone: function (session) {
425
+ if (_gitAttribution) _gitAttribution.finishTurn(session);
426
+ onSessionDone();
427
+ },
428
+ });
429
+ _gitAttribution = attachGitSessionAttribution({
430
+ cwd: cwd,
431
+ getOsUserInfoForSession: function (session) {
432
+ var linuxUser = getLinuxUserForSession(session);
433
+ return linuxUser ? resolveOsUserInfo(linuxUser) : null;
434
+ },
421
435
  });
422
436
  sm.availableVendors = Object.keys(adapters);
423
437
  sm.defaultVendor = defaultVendor;
@@ -1152,9 +1166,6 @@ function createProjectContext(opts) {
1152
1166
  return;
1153
1167
  }
1154
1168
 
1155
- // --- Email defaults (project-level) ---
1156
- if (_email.handleEmailMessage(ws, msg)) return;
1157
-
1158
1169
  // --- MCP bridge (remote MCP servers via extension) ---
1159
1170
  if (_mcp.handleMcpMessage(ws, msg)) return;
1160
1171
 
@@ -1408,6 +1419,7 @@ function createProjectContext(opts) {
1408
1419
  saveImageFile: saveImageFile,
1409
1420
  imagesDir: imagesDir,
1410
1421
  onProcessingChanged: onProcessingChanged,
1422
+ gitAttribution: _gitAttribution,
1411
1423
  _loop: _loop,
1412
1424
  browserState: browserState,
1413
1425
  sendExtensionCommandAny: sendExtensionCommandAny,
@@ -1619,6 +1631,7 @@ function createProjectContext(opts) {
1619
1631
  slug: slug,
1620
1632
  project: title || project,
1621
1633
  sm: sm,
1634
+ gitAttribution: _gitAttribution,
1622
1635
  send: send,
1623
1636
  imagesDir: imagesDir,
1624
1637
  osUsers: osUsers,
package/lib/public/app.js CHANGED
@@ -25,8 +25,9 @@ import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEna
25
25
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent, setScheduleBtnDisabled, setScheduleDelayMs, clearScheduleDelay } from './modules/input.js';
26
26
  import { initQrCode, triggerShare } from './modules/qrcode.js';
27
27
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
28
+ import { initGitPanel } from './modules/git-panel.js';
28
29
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
29
- import { initContextSources, initEmailDefaultsModal, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
30
+ import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
30
31
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible, createNote } from './modules/sticky-notes.js';
31
32
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme, getChatLayout } from './modules/theme.js';
32
33
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
@@ -324,6 +325,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
324
325
  currentFullText: "",
325
326
  matePreThinkingEl: null,
326
327
  // panels
328
+ fileViewerFullscreen: false,
329
+ markdownSlidesActive: false,
330
+ markdownSlideIndex: 0,
331
+ markdownSlideCount: 0,
332
+ markdownSlideLevel: 1,
333
+ markdownSlidePreferredLevel: 1,
334
+ markdownSlideLevelExplicit: false,
327
335
  currentModel: "",
328
336
  currentModels: [],
329
337
  // Project's last-used vendor; seeds the sidebar's "New session" button.
@@ -875,6 +883,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
875
883
  if (!_perms.fileBrowser) {
876
884
  var fbBtn = document.getElementById("file-browser-btn");
877
885
  if (fbBtn) fbBtn.style.display = "none";
886
+ var gitBtn = document.getElementById("git-sidebar-btn");
887
+ if (gitBtn) gitBtn.style.display = "none";
878
888
  }
879
889
  if (!_perms.skills) {
880
890
  var sBtn = document.getElementById("skills-btn");
@@ -963,6 +973,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
963
973
  fileTreeEl: $("file-tree"),
964
974
  fileViewerEl: $("file-viewer"),
965
975
  });
976
+ initGitPanel();
966
977
 
967
978
  // --- Terminal ---
968
979
  initTerminal({
@@ -1047,8 +1058,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1047
1058
 
1048
1059
  // Close archive / scheduler panel when switching to other sidebar panels
1049
1060
  var fileBrowserBtn = $("file-browser-btn");
1061
+ var gitSidebarBtn = $("git-sidebar-btn");
1050
1062
  var terminalSidebarBtn = $("terminal-sidebar-btn");
1051
1063
  if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1064
+ if (gitSidebarBtn) gitSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1052
1065
  if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1053
1066
 
1054
1067
  // --- Ralph Loop UI (delegated to app-loop-ui.js + app-loop-wizard.js) ---
@@ -1114,7 +1127,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1114
1127
 
1115
1128
  // --- MCP Servers ---
1116
1129
  initMcp();
1117
- initEmailDefaultsModal();
1118
1130
 
1119
1131
  // --- Skills ---
1120
1132
  initSkills();