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.
- package/bin/cli.js +157 -1
- package/lib/daemon.js +341 -2
- package/lib/dm.js +135 -0
- package/lib/os-users.js +301 -0
- package/lib/pages.js +36 -0
- package/lib/project.js +386 -67
- package/lib/public/app.js +675 -17
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +22 -0
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/menus.css +23 -0
- package/lib/public/css/messages.css +245 -0
- package/lib/public/css/overlays.css +88 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +140 -66
- package/lib/public/modules/admin.js +179 -12
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +112 -6
- package/lib/public/modules/terminal.js +48 -10
- package/lib/public/modules/tools.js +214 -1
- package/lib/sdk-bridge.js +634 -6
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +335 -3
- package/lib/sessions.js +26 -0
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +20 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +79 -0
- package/package.json +2 -2
|
@@ -93,7 +93,11 @@ function loadUsersTab(body) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function renderUsersTab(body) {
|
|
96
|
-
var html = '<div class="admin-
|
|
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
|
|
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
|
|
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 += '
|
|
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
|
|
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(
|
|
500
|
-
|
|
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
|
|
447
|
-
|
|
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
|
-
|
|
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();
|