clay-server 2.10.0 → 2.11.0-beta.10

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.
@@ -93,7 +93,11 @@ function loadUsersTab(body) {
93
93
  }
94
94
 
95
95
  function renderUsersTab(body) {
96
- var html = '<div class="admin-user-list">';
96
+ var html = '<div class="admin-section-header">' +
97
+ '<div class="admin-header-btns">' +
98
+ '<button class="admin-action-btn" id="admin-add-user">' + iconHtml("user-plus") + ' Add User</button>' +
99
+ '</div></div>';
100
+ html += '<div class="admin-user-list">';
97
101
  for (var i = 0; i < cachedUsers.length; i++) {
98
102
  var u = cachedUsers[i];
99
103
  var isMe = meInfo && meInfo.user && meInfo.user.id === u.id;
@@ -117,6 +121,14 @@ function renderUsersTab(body) {
117
121
  body.innerHTML = html;
118
122
  refreshIcons(body);
119
123
 
124
+ // Bind add user button
125
+ var addUserBtn = body.querySelector("#admin-add-user");
126
+ if (addUserBtn) {
127
+ addUserBtn.addEventListener("click", function () {
128
+ showAddUserModal(body);
129
+ });
130
+ }
131
+
120
132
  // Bind remove buttons
121
133
  var removeBtns = body.querySelectorAll(".admin-remove-btn");
122
134
  for (var j = 0; j < removeBtns.length; j++) {
@@ -131,6 +143,124 @@ function renderUsersTab(body) {
131
143
  }
132
144
  }
133
145
 
146
+ function showAddUserModal(body) {
147
+ var modal = document.createElement("div");
148
+ modal.className = "admin-modal-overlay";
149
+ var html = '<div class="admin-modal">' +
150
+ '<div class="admin-modal-header">' +
151
+ '<h3>Add User</h3>' +
152
+ '<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
153
+ '</div>' +
154
+ '<div class="admin-modal-body">' +
155
+ '<p class="admin-modal-desc">Create a new user account. A temporary 6-digit PIN will be generated automatically. The user must change it on first login.</p>' +
156
+ '<div class="admin-smtp-row"><label>Username</label>' +
157
+ '<input type="text" class="admin-smtp-input" id="admin-new-username" placeholder="username" autocomplete="off" maxlength="100"></div>' +
158
+ '<div class="admin-smtp-row"><label>Display Name</label>' +
159
+ '<input type="text" class="admin-smtp-input" id="admin-new-displayname" placeholder="Display Name (optional)" autocomplete="off" maxlength="30"></div>' +
160
+ '<div class="admin-smtp-row"><label>Email</label>' +
161
+ '<input type="email" class="admin-smtp-input" id="admin-new-email" placeholder="user@example.com (optional)" autocomplete="off"></div>' +
162
+ '<div class="admin-smtp-row"><label>Role</label>' +
163
+ '<select class="admin-smtp-input" id="admin-new-role"><option value="user">User</option><option value="admin">Admin</option></select></div>' +
164
+ '<div class="admin-smtp-error" id="admin-new-user-error"></div>' +
165
+ '</div>' +
166
+ '<div class="admin-modal-footer">' +
167
+ '<button class="admin-modal-save" id="admin-new-user-create">Create User</button>' +
168
+ '<button class="admin-modal-cancel">Cancel</button>' +
169
+ '</div></div>';
170
+ modal.innerHTML = html;
171
+ document.body.appendChild(modal);
172
+ refreshIcons(modal);
173
+
174
+ var usernameInput = modal.querySelector("#admin-new-username");
175
+ var displayNameInput = modal.querySelector("#admin-new-displayname");
176
+ var emailInput = modal.querySelector("#admin-new-email");
177
+ var roleSelect = modal.querySelector("#admin-new-role");
178
+ var createBtn = modal.querySelector("#admin-new-user-create");
179
+ var errorEl = modal.querySelector("#admin-new-user-error");
180
+
181
+ modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
182
+ modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
183
+ modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
184
+
185
+ usernameInput.focus();
186
+ usernameInput.addEventListener("keydown", function (e) {
187
+ if (e.key === "Enter") createBtn.click();
188
+ });
189
+
190
+ createBtn.addEventListener("click", function () {
191
+ var username = usernameInput.value.trim();
192
+ if (!username) {
193
+ errorEl.textContent = "Username is required";
194
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
195
+ return;
196
+ }
197
+ createBtn.disabled = true;
198
+ createBtn.textContent = "Creating...";
199
+ errorEl.textContent = "";
200
+ errorEl.className = "admin-smtp-error";
201
+ apiPost("/api/admin/users", {
202
+ username: username,
203
+ displayName: displayNameInput.value.trim() || username,
204
+ email: emailInput.value.trim() || null,
205
+ role: roleSelect.value,
206
+ }).then(function (data) {
207
+ if (data.ok) {
208
+ modal.remove();
209
+ showTempPinModal(data.user, data.tempPin);
210
+ loadUsersTab(body);
211
+ } else {
212
+ errorEl.textContent = data.error || "Failed to create user";
213
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
214
+ createBtn.disabled = false;
215
+ createBtn.textContent = "Create User";
216
+ }
217
+ }).catch(function () {
218
+ errorEl.textContent = "Failed to create user";
219
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
220
+ createBtn.disabled = false;
221
+ createBtn.textContent = "Create User";
222
+ });
223
+ });
224
+ }
225
+
226
+ function showTempPinModal(user, tempPin) {
227
+ var modal = document.createElement("div");
228
+ modal.className = "admin-modal-overlay";
229
+ var html = '<div class="admin-modal">' +
230
+ '<div class="admin-modal-header">' +
231
+ '<h3>User Created</h3>' +
232
+ '<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
233
+ '</div>' +
234
+ '<div class="admin-modal-body">' +
235
+ '<p class="admin-modal-desc">Account for <strong>' + escapeHtml(user.displayName || user.username) + '</strong> has been created. Share these credentials with the user:</p>' +
236
+ '<div class="admin-temp-pin-box">' +
237
+ '<div class="admin-temp-pin-row"><span class="admin-temp-pin-label">Username</span><code class="admin-temp-pin-value">' + escapeHtml(user.username) + '</code></div>' +
238
+ '<div class="admin-temp-pin-row"><span class="admin-temp-pin-label">Temporary PIN</span><code class="admin-temp-pin-value admin-temp-pin-highlight">' + escapeHtml(tempPin) + '</code></div>' +
239
+ '</div>' +
240
+ '<p class="admin-modal-desc" style="margin-top:12px;color:var(--text-secondary);font-size:12px;">This PIN is one-time use. The user will be prompted to set a new PIN on first login.</p>' +
241
+ '</div>' +
242
+ '<div class="admin-modal-footer">' +
243
+ '<button class="admin-modal-save" id="admin-copy-credentials">Copy Credentials</button>' +
244
+ '<button class="admin-modal-cancel">Close</button>' +
245
+ '</div></div>';
246
+ modal.innerHTML = html;
247
+ document.body.appendChild(modal);
248
+ refreshIcons(modal);
249
+
250
+ modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
251
+ modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
252
+ modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
253
+
254
+ modal.querySelector("#admin-copy-credentials").addEventListener("click", function () {
255
+ var text = "Username: " + user.username + "\nTemporary PIN: " + tempPin;
256
+ copyToClipboard(text).then(function () {
257
+ showToast("Credentials copied to clipboard");
258
+ }).catch(function () {
259
+ showToast("Username: " + user.username + " / PIN: " + tempPin);
260
+ });
261
+ });
262
+ }
263
+
134
264
  function removeUser(userId, body) {
135
265
  apiDelete("/api/admin/users/" + userId).then(function (data) {
136
266
  if (data.ok) {
@@ -340,9 +470,9 @@ function renderSmtpTab(body, cfg) {
340
470
 
341
471
  var emailEnabled = !!(cfg && cfg.emailLoginEnabled);
342
472
  if (hasConfig) {
343
- html += '<div class="admin-smtp-status admin-smtp-status-ok">' + iconHtml("check-circle") + ' SMTP configured</div>';
473
+ html += '<div class="admin-smtp-status admin-smtp-status-ok">' + iconHtml("check-circle") + ' SMTP configured. Invite links and one-time login codes are sent via email.</div>';
344
474
  } else {
345
- html += '<div class="admin-smtp-status admin-smtp-status-off">' + iconHtml("mail-x") + ' SMTP not configured using PIN-based login</div>';
475
+ html += '<div class="admin-smtp-status admin-smtp-status-off">' + iconHtml("mail-x") + ' SMTP not configured. Users log in with a PIN instead of email codes.</div>';
346
476
  }
347
477
 
348
478
  html += '<div class="admin-smtp-fields">';
@@ -371,13 +501,12 @@ function renderSmtpTab(body, cfg) {
371
501
  '<label>From Address</label>' +
372
502
  '<input type="text" id="smtp-from" class="admin-smtp-input" placeholder="Clay <noreply@example.com>" value="' + escapeHtml((cfg && cfg.from) || "") + '">' +
373
503
  '</div>';
374
- html += '</div>';
375
-
376
- html += '<div class="admin-smtp-row" style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border-color,#e5e7eb)">' +
504
+ html += '<div class="admin-smtp-row admin-smtp-row-otp">' +
377
505
  '<label>Email Login (OTP)</label>' +
378
506
  '<label class="admin-smtp-toggle"><input type="checkbox" id="smtp-email-login"' + (emailEnabled ? " checked" : "") + (hasConfig ? "" : " disabled") + '>' +
379
507
  '<span>' + (hasConfig ? "Require email for user registration and enable OTP login" : "Configure SMTP first to enable") + '</span></label>' +
380
508
  '</div>';
509
+ html += '</div>';
381
510
 
382
511
  html += '<div class="admin-smtp-actions">';
383
512
  html += '<button class="admin-action-btn" id="smtp-save">' + iconHtml("save") + ' Save</button>';
@@ -488,20 +617,23 @@ function loadProjectsTab(body) {
488
617
  return;
489
618
  }
490
619
 
491
- var promises = projectList.map(function (p) {
620
+ var accessPromises = projectList.map(function (p) {
492
621
  return apiGet("/api/admin/projects/" + p.slug + "/access").then(function (access) {
493
- return { slug: p.slug, title: p.title || p.project || p.slug, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [] };
622
+ return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [] };
494
623
  }).catch(function () {
495
- return { slug: p.slug, title: p.title || p.project || p.slug, visibility: "public", allowedUsers: [] };
624
+ return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: "public", allowedUsers: [] };
496
625
  });
497
626
  });
498
627
 
499
- Promise.all(promises).then(function (projectAccessList) {
500
- renderProjectsTab(body, projectAccessList);
628
+ Promise.all([
629
+ Promise.all(accessPromises),
630
+ apiGet("/api/admin/users").catch(function () { return { users: [] }; }),
631
+ ]).then(function (results) {
632
+ renderProjectsTab(body, results[0], results[1].users || []);
501
633
  });
502
634
  }
503
635
 
504
- function renderProjectsTab(body, projectAccessList) {
636
+ function renderProjectsTab(body, projectAccessList, allUsers) {
505
637
  var html = '<div class="admin-project-list">';
506
638
  for (var i = 0; i < projectAccessList.length; i++) {
507
639
  var p = projectAccessList[i];
@@ -512,6 +644,18 @@ function renderProjectsTab(body, projectAccessList) {
512
644
  html += '<div class="admin-project-slug">' + escapeHtml(p.slug) + '</div>';
513
645
  html += '</div>';
514
646
  html += '<div class="admin-project-controls">';
647
+ // Owner select
648
+ html += '<select class="admin-owner-select" data-slug="' + escapeHtml(p.slug) + '">';
649
+ html += '<option value=""' + (!p.projectOwnerId ? ' selected' : '') + '>No owner</option>';
650
+ for (var u = 0; u < allUsers.length; u++) {
651
+ var user = allUsers[u];
652
+ var sel = p.projectOwnerId === user.id ? ' selected' : '';
653
+ var label = escapeHtml(user.displayName || user.username);
654
+ if (user.linuxUser) label += ' (' + escapeHtml(user.linuxUser) + ')';
655
+ html += '<option value="' + escapeHtml(user.id) + '"' + sel + '>' + label + '</option>';
656
+ }
657
+ html += '</select>';
658
+ // Visibility select
515
659
  html += '<select class="admin-vis-select ' + visClass + '" data-slug="' + escapeHtml(p.slug) + '">';
516
660
  html += '<option value="public"' + (p.visibility === "public" ? " selected" : "") + '>Public</option>';
517
661
  html += '<option value="private"' + (p.visibility === "private" ? " selected" : "") + '>Private</option>';
@@ -527,6 +671,16 @@ function renderProjectsTab(body, projectAccessList) {
527
671
  body.innerHTML = html;
528
672
  refreshIcons(body);
529
673
 
674
+ // Bind owner selects
675
+ var ownerSelects = body.querySelectorAll(".admin-owner-select");
676
+ for (var oi = 0; oi < ownerSelects.length; oi++) {
677
+ ownerSelects[oi].addEventListener("change", function () {
678
+ var slug = this.dataset.slug;
679
+ var userId = this.value;
680
+ setProjectOwner(slug, userId, body);
681
+ });
682
+ }
683
+
530
684
  // Bind visibility selects
531
685
  var visSelects = body.querySelectorAll(".admin-vis-select");
532
686
  for (var j = 0; j < visSelects.length; j++) {
@@ -547,6 +701,19 @@ function renderProjectsTab(body, projectAccessList) {
547
701
  }
548
702
  }
549
703
 
704
+ function setProjectOwner(slug, userId, body) {
705
+ apiPut("/api/admin/projects/" + slug + "/owner", { userId: userId || null }).then(function (data) {
706
+ if (data.ok) {
707
+ showToast("Project owner updated");
708
+ loadProjectsTab(body);
709
+ } else {
710
+ showToast(data.error || "Failed to update owner", "error");
711
+ }
712
+ }).catch(function () {
713
+ showToast("Failed to update owner", "error");
714
+ });
715
+ }
716
+
550
717
  function setProjectVisibility(slug, visibility, body) {
551
718
  apiPut("/api/admin/projects/" + slug + "/visibility", { visibility: visibility }).then(function (data) {
552
719
  if (data.ok) {
@@ -23,6 +23,11 @@ export var builtinCommands = [
23
23
 
24
24
  // --- Send ---
25
25
  export function sendMessage() {
26
+ // DM mode intercept: if in DM mode, route to DM handler instead
27
+ if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
28
+ ctx.handleDmSend();
29
+ return;
30
+ }
26
31
  var text = ctx.inputEl.value.trim();
27
32
  var images = pendingImages.slice();
28
33
  if (!text && images.length === 0 && pendingPastes.length === 0 && pendingFiles.length === 0) return;
@@ -443,9 +448,15 @@ function updateSlashHighlight() {
443
448
  // --- Input sync across devices ---
444
449
  function sendInputSync() {
445
450
  if (isRemoteInput) return;
446
- if (ctx.ws && ctx.connected) {
447
- ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value }));
451
+ if (!ctx.ws || !ctx.connected) return;
452
+ // In DM mode, send typing indicator instead of input_sync
453
+ if (ctx.isDmMode && ctx.isDmMode()) {
454
+ var hasText = ctx.inputEl.value.length > 0;
455
+ var dk = ctx.getDmKey ? ctx.getDmKey() : null;
456
+ if (dk) ctx.ws.send(JSON.stringify({ type: "dm_typing", dmKey: dk, typing: hasText }));
457
+ return;
448
458
  }
459
+ ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value }));
449
460
  }
450
461
 
451
462
  export function handleInputSync(text) {
@@ -92,7 +92,9 @@ export function initNotifications(_ctx) {
92
92
  if (copyBtn) {
93
93
  copyBtn.addEventListener("click", function (e) {
94
94
  e.stopPropagation();
95
- copyToClipboard("npx clay-server@latest").then(function () {
95
+ var cmdEl = document.getElementById("update-manual-cmd");
96
+ var cmdText = cmdEl ? cmdEl.textContent : "npx clay-server@latest";
97
+ copyToClipboard(cmdText).then(function () {
96
98
  copyBtn.classList.add("copied");
97
99
  copyBtn.innerHTML = iconHtml("check");
98
100
  refreshIcons();