clay-server 2.5.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,56 @@
1
+ export function showToast(message, level, detail) {
2
+ var el = document.createElement("div");
3
+ el.className = "toast";
4
+ if (level) el.classList.add("toast-" + level);
5
+ el.textContent = message;
6
+ if (detail) {
7
+ var detailEl = document.createElement("div");
8
+ detailEl.style.cssText = "font-size:11px;opacity:0.7;margin-top:4px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
9
+ detailEl.textContent = detail.split("\n")[0];
10
+ el.appendChild(detailEl);
11
+ }
12
+ document.body.appendChild(el);
13
+ requestAnimationFrame(function () { el.classList.add("visible"); });
14
+ var duration = level === "warn" ? 5000 : 1500;
15
+ setTimeout(function () {
16
+ el.classList.remove("visible");
17
+ setTimeout(function () { el.remove(); }, 300);
18
+ }, duration);
19
+ }
20
+
21
+ var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
22
+ (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
23
+
24
+ export function copyToClipboard(text) {
25
+ var p;
26
+ if (isIOS) {
27
+ // iOS Safari URL-encodes clipboard text that contains colons via the
28
+ // Clipboard API. Use textarea + execCommand to copy raw text instead.
29
+ var ta = document.createElement("textarea");
30
+ ta.value = text;
31
+ ta.style.cssText = "position:fixed;left:-9999px;opacity:0";
32
+ document.body.appendChild(ta);
33
+ ta.focus();
34
+ ta.setSelectionRange(0, ta.value.length);
35
+ document.execCommand("copy");
36
+ document.body.removeChild(ta);
37
+ p = Promise.resolve();
38
+ } else if (navigator.clipboard && navigator.clipboard.writeText) {
39
+ p = navigator.clipboard.writeText(text);
40
+ } else {
41
+ var ta2 = document.createElement("textarea");
42
+ ta2.value = text;
43
+ ta2.style.cssText = "position:fixed;left:-9999px;opacity:0";
44
+ document.body.appendChild(ta2);
45
+ ta2.focus();
46
+ ta2.setSelectionRange(0, ta2.value.length);
47
+ document.execCommand("copy");
48
+ document.body.removeChild(ta2);
49
+ p = Promise.resolve();
50
+ }
51
+ return p.then(function () { showToast("Copied to clipboard"); });
52
+ }
53
+
54
+ export function escapeHtml(s) {
55
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
56
+ }
@@ -0,0 +1,15 @@
1
+ @import url("css/base.css");
2
+ @import url("css/icon-strip.css");
3
+ @import url("css/title-bar.css");
4
+ @import url("css/sidebar.css");
5
+ @import url("css/overlays.css");
6
+ @import url("css/menus.css");
7
+ @import url("css/messages.css");
8
+ @import url("css/rewind.css");
9
+ @import url("css/input.css");
10
+ @import url("css/filebrowser.css");
11
+ @import url("css/diff.css");
12
+ @import url("css/highlight.css");
13
+ @import url("css/server-settings.css");
14
+ @import url("css/sticky-notes.css");
15
+ @import url("css/mobile-nav.css");
@@ -0,0 +1,75 @@
1
+ self.addEventListener("install", function () {
2
+ self.skipWaiting();
3
+ });
4
+
5
+ self.addEventListener("activate", function (event) {
6
+ event.waitUntil(self.clients.claim());
7
+ });
8
+
9
+ self.addEventListener("push", function (event) {
10
+ var data = {};
11
+ try { data = event.data.json(); } catch (e) { return; }
12
+
13
+ // Silent validation push, do not show notification
14
+ if (data.type === "test") return;
15
+
16
+ var options = {
17
+ body: data.body || "",
18
+ tag: data.tag || "clay",
19
+ data: data,
20
+ };
21
+
22
+ if (data.type === "permission_request") {
23
+ options.requireInteraction = true;
24
+ options.tag = "perm-" + data.requestId;
25
+ } else if (data.type === "done") {
26
+ options.tag = data.tag || "claude-done";
27
+ } else if (data.type === "ask_user") {
28
+ options.requireInteraction = true;
29
+ options.tag = "claude-ask";
30
+ } else if (data.type === "error") {
31
+ options.requireInteraction = true;
32
+ options.tag = "claude-error";
33
+ }
34
+
35
+ event.waitUntil(
36
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
37
+ // Always show permission requests, questions, and errors
38
+ // Only suppress "done" notifications when app is in foreground
39
+ if (data.type !== "permission_request" && data.type !== "ask_user" && data.type !== "error") {
40
+ for (var i = 0; i < clientList.length; i++) {
41
+ if (clientList[i].focused || clientList[i].visibilityState === "visible") return;
42
+ }
43
+ }
44
+ return self.registration.showNotification(data.title || "Clay", options);
45
+ }).catch(function () {})
46
+ );
47
+ });
48
+
49
+ self.addEventListener("notificationclick", function (event) {
50
+ var data = event.notification.data || {};
51
+ event.notification.close();
52
+
53
+ // Build target URL from slug so we open the correct project
54
+ var baseUrl = self.registration.scope || "/";
55
+ var targetUrl = data.slug ? baseUrl + "p/" + data.slug + "/" : baseUrl;
56
+
57
+ event.waitUntil(
58
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
59
+ // Prefer a client already on the correct project
60
+ for (var i = 0; i < clientList.length; i++) {
61
+ if (clientList[i].url.indexOf(targetUrl) !== -1) {
62
+ return clientList[i].focus();
63
+ }
64
+ }
65
+ // Fall back to any visible client
66
+ for (var i = 0; i < clientList.length; i++) {
67
+ if (clientList[i].visibilityState !== "hidden") {
68
+ return clientList[i].focus();
69
+ }
70
+ }
71
+ if (clientList.length > 0) return clientList[0].focus();
72
+ return self.clients.openWindow(targetUrl);
73
+ })
74
+ );
75
+ });
package/lib/push.js ADDED
@@ -0,0 +1,124 @@
1
+ var webpush = require("web-push");
2
+ var fs = require("fs");
3
+ var path = require("path");
4
+ var config = require("./config");
5
+
6
+ function loadOrCreateVapidKeys() {
7
+ var dir = config.CONFIG_DIR;
8
+ var keyFile = path.join(dir, "vapid.json");
9
+
10
+ try {
11
+ var data = fs.readFileSync(keyFile, "utf8");
12
+ return JSON.parse(data);
13
+ } catch (e) {
14
+ // Generate new keys
15
+ }
16
+
17
+ var keys = webpush.generateVAPIDKeys();
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ fs.writeFileSync(keyFile, JSON.stringify(keys, null, 2));
20
+ return keys;
21
+ }
22
+
23
+ function initPush() {
24
+ var keys = loadOrCreateVapidKeys();
25
+
26
+ var vapidDetails = {
27
+ subject: "mailto:push@clay.dev",
28
+ publicKey: keys.publicKey,
29
+ privateKey: keys.privateKey,
30
+ };
31
+
32
+ var dir = config.CONFIG_DIR;
33
+ var subFile = path.join(dir, "push-subs.json");
34
+ var subscriptions = new Map();
35
+
36
+ // Load persisted subscriptions, but clear if VAPID key changed
37
+ try {
38
+ var saved = JSON.parse(fs.readFileSync(subFile, "utf8"));
39
+ if (saved.vapidKey && saved.vapidKey !== keys.publicKey) {
40
+ saved.subs = [];
41
+ }
42
+ var subs = saved.subs || saved;
43
+ if (Array.isArray(subs)) {
44
+ for (var i = 0; i < subs.length; i++) {
45
+ if (subs[i] && subs[i].endpoint) subscriptions.set(subs[i].endpoint, subs[i]);
46
+ }
47
+ }
48
+ } catch (e) {}
49
+
50
+ function save() {
51
+ try {
52
+ fs.writeFileSync(subFile, JSON.stringify({
53
+ vapidKey: keys.publicKey,
54
+ subs: [...subscriptions.values()],
55
+ }));
56
+ } catch (e) {}
57
+ }
58
+
59
+ save();
60
+
61
+ // Purge stale subscriptions on startup
62
+ var startupEndpoints = Array.from(subscriptions.keys());
63
+ for (var si = 0; si < startupEndpoints.length; si++) {
64
+ (function (ep) {
65
+ var sub = subscriptions.get(ep);
66
+ webpush.sendNotification(sub, JSON.stringify({ type: "test" }), { TTL: 0, vapidDetails: vapidDetails })
67
+ .then(function () {})
68
+ .catch(function (err) {
69
+ if (err.statusCode === 403 || err.statusCode === 410 || err.statusCode === 404) {
70
+ subscriptions.delete(ep);
71
+ save();
72
+ }
73
+ });
74
+ })(startupEndpoints[si]);
75
+ }
76
+
77
+ function addSubscription(sub, replaceEndpoint) {
78
+ if (!sub || !sub.endpoint) return;
79
+ // Remove previous subscription from the same client if endpoint changed
80
+ if (replaceEndpoint && replaceEndpoint !== sub.endpoint) {
81
+ subscriptions.delete(replaceEndpoint);
82
+ }
83
+ // Store immediately, then validate async. Invalid subs get cleaned on first sendPush.
84
+ subscriptions.set(sub.endpoint, sub);
85
+ save();
86
+ // Validate with a silent push (TTL 0 = don't actually deliver if device offline)
87
+ webpush.sendNotification(sub, JSON.stringify({ type: "test" }), { TTL: 0, vapidDetails: vapidDetails })
88
+ .then(function () {})
89
+ .catch(function (err) {
90
+ if (err.statusCode === 403 || err.statusCode === 410 || err.statusCode === 404) {
91
+ subscriptions.delete(sub.endpoint);
92
+ save();
93
+ }
94
+ });
95
+ }
96
+
97
+ function removeSubscription(endpoint) {
98
+ subscriptions.delete(endpoint);
99
+ save();
100
+ }
101
+
102
+ function sendPush(payload) {
103
+ var json = JSON.stringify(payload);
104
+ subscriptions.forEach(function (sub, endpoint) {
105
+ webpush.sendNotification(sub, json, { vapidDetails: vapidDetails })
106
+ .then(function () {})
107
+ .catch(function (err) {
108
+ if (err.statusCode === 410 || err.statusCode === 404 || err.statusCode === 403) {
109
+ subscriptions.delete(endpoint);
110
+ save();
111
+ }
112
+ });
113
+ });
114
+ }
115
+
116
+ return {
117
+ publicKey: keys.publicKey,
118
+ addSubscription: addSubscription,
119
+ removeSubscription: removeSubscription,
120
+ sendPush: sendPush,
121
+ };
122
+ }
123
+
124
+ module.exports = { initPush };