clay-server 2.7.2 → 2.8.2

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 (55) hide show
  1. package/bin/cli.js +31 -17
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1039 -134
  5. package/lib/public/apple-touch-icon-dark.png +0 -0
  6. package/lib/public/apple-touch-icon.png +0 -0
  7. package/lib/public/clay-logo.png +0 -0
  8. package/lib/public/css/base.css +18 -1
  9. package/lib/public/css/filebrowser.css +1 -0
  10. package/lib/public/css/home-hub.css +455 -0
  11. package/lib/public/css/icon-strip.css +6 -5
  12. package/lib/public/css/loop.css +141 -23
  13. package/lib/public/css/messages.css +2 -0
  14. package/lib/public/css/mobile-nav.css +38 -12
  15. package/lib/public/css/overlays.css +205 -169
  16. package/lib/public/css/playbook.css +264 -0
  17. package/lib/public/css/profile.css +268 -0
  18. package/lib/public/css/scheduler-modal.css +1429 -0
  19. package/lib/public/css/scheduler.css +1305 -0
  20. package/lib/public/css/sidebar.css +305 -11
  21. package/lib/public/css/sticky-notes.css +23 -19
  22. package/lib/public/css/stt.css +155 -0
  23. package/lib/public/css/title-bar.css +14 -6
  24. package/lib/public/favicon-banded-32.png +0 -0
  25. package/lib/public/favicon-banded.png +0 -0
  26. package/lib/public/icon-192-dark.png +0 -0
  27. package/lib/public/icon-192.png +0 -0
  28. package/lib/public/icon-512-dark.png +0 -0
  29. package/lib/public/icon-512.png +0 -0
  30. package/lib/public/icon-banded-76.png +0 -0
  31. package/lib/public/icon-banded-96.png +0 -0
  32. package/lib/public/index.html +336 -44
  33. package/lib/public/modules/ascii-logo.js +442 -0
  34. package/lib/public/modules/markdown.js +18 -0
  35. package/lib/public/modules/notifications.js +50 -63
  36. package/lib/public/modules/playbook.js +578 -0
  37. package/lib/public/modules/profile.js +357 -0
  38. package/lib/public/modules/project-settings.js +1 -9
  39. package/lib/public/modules/scheduler.js +2826 -0
  40. package/lib/public/modules/server-settings.js +1 -1
  41. package/lib/public/modules/sidebar.js +376 -32
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/style.css +6 -0
  46. package/lib/public/sw.js +82 -3
  47. package/lib/public/wordmark-banded-20.png +0 -0
  48. package/lib/public/wordmark-banded-32.png +0 -0
  49. package/lib/public/wordmark-banded-64.png +0 -0
  50. package/lib/public/wordmark-banded-80.png +0 -0
  51. package/lib/scheduler.js +402 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server.js +124 -3
  54. package/lib/sessions.js +35 -2
  55. package/package.json +1 -1
@@ -0,0 +1,272 @@
1
+ // Speech-to-Text module using Web Speech API
2
+ // Uses browser's built-in speech recognition (Chrome/Edge/Safari → Google servers)
3
+
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+ import { autoResize } from './input.js';
6
+
7
+ var ctx;
8
+
9
+ // --- State ---
10
+ var recording = false;
11
+ var recognition = null;
12
+ var selectedLang = null;
13
+ var textBeforeSTT = '';
14
+ var interimText = '';
15
+
16
+ // DOM refs
17
+ var sttBtn = null;
18
+ var langPopover = null;
19
+
20
+ // --- Language options ---
21
+ // Web Speech API uses BCP-47 language tags
22
+ var LANGUAGES = [
23
+ { code: 'en-US', name: 'English' },
24
+ { code: 'ko-KR', name: 'Korean' },
25
+ { code: 'ja-JP', name: 'Japanese' },
26
+ { code: 'zh-CN', name: 'Chinese' },
27
+ { code: 'es-ES', name: 'Spanish' },
28
+ { code: 'fr-FR', name: 'French' },
29
+ { code: 'de-DE', name: 'German' },
30
+ ];
31
+
32
+ // --- Persist language choice ---
33
+ function saveLang(code) {
34
+ try { localStorage.setItem('stt-lang', code); } catch (e) { /* ignore */ }
35
+ }
36
+
37
+ function loadLang() {
38
+ try { return localStorage.getItem('stt-lang'); } catch (e) { return null; }
39
+ }
40
+
41
+ // --- Check browser support ---
42
+ function getSpeechRecognition() {
43
+ return window.SpeechRecognition || window.webkitSpeechRecognition || null;
44
+ }
45
+
46
+ // --- Init ---
47
+ export function initSTT(_ctx) {
48
+ ctx = _ctx;
49
+
50
+ sttBtn = document.getElementById('stt-btn');
51
+ if (!sttBtn) return;
52
+
53
+ if (!getSpeechRecognition()) {
54
+ sttBtn.style.display = 'none';
55
+ console.warn('[STT] Web Speech API not supported in this browser');
56
+ return;
57
+ }
58
+
59
+ // Restore saved language
60
+ selectedLang = loadLang();
61
+
62
+ sttBtn.addEventListener('click', function(e) {
63
+ e.stopPropagation();
64
+
65
+ if (recording) {
66
+ stopRecording();
67
+ return;
68
+ }
69
+
70
+ if (!selectedLang) {
71
+ showLangPopover();
72
+ } else {
73
+ startRecording();
74
+ }
75
+ });
76
+
77
+ // Right-click to change language
78
+ sttBtn.addEventListener('contextmenu', function(e) {
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ if (recording) stopRecording();
82
+ showLangPopover();
83
+ });
84
+ }
85
+
86
+ // --- Language popover ---
87
+ function showLangPopover() {
88
+ if (langPopover) {
89
+ hideLangPopover();
90
+ return;
91
+ }
92
+
93
+ langPopover = document.createElement('div');
94
+ langPopover.className = 'stt-lang-popover';
95
+
96
+ var html = '<div class="stt-lang-title">Voice Input Language</div>';
97
+ for (var i = 0; i < LANGUAGES.length; i++) {
98
+ var l = LANGUAGES[i];
99
+ var activeClass = (selectedLang === l.code) ? ' stt-lang-active' : '';
100
+ html += '<button class="stt-lang-option' + activeClass + '" data-lang="' + l.code + '">' +
101
+ '<span class="stt-lang-name">' + l.name + '</span>' +
102
+ '</button>';
103
+ }
104
+ langPopover.innerHTML = html;
105
+
106
+ langPopover.querySelectorAll('.stt-lang-option').forEach(function(btn) {
107
+ btn.addEventListener('click', function() {
108
+ onLangSelected(btn.dataset.lang);
109
+ });
110
+ });
111
+
112
+ var wrapper = document.getElementById('input-wrapper');
113
+ wrapper.appendChild(langPopover);
114
+
115
+ setTimeout(function() {
116
+ document.addEventListener('click', closeLangOnOutside);
117
+ }, 0);
118
+ }
119
+
120
+ function closeLangOnOutside(e) {
121
+ if (langPopover && !langPopover.contains(e.target) && e.target !== sttBtn && !sttBtn.contains(e.target)) {
122
+ hideLangPopover();
123
+ }
124
+ }
125
+
126
+ function hideLangPopover() {
127
+ if (langPopover) {
128
+ langPopover.remove();
129
+ langPopover = null;
130
+ }
131
+ document.removeEventListener('click', closeLangOnOutside);
132
+ }
133
+
134
+ function onLangSelected(code) {
135
+ selectedLang = code;
136
+ saveLang(code);
137
+ hideLangPopover();
138
+ startRecording();
139
+ }
140
+
141
+ // --- Recording ---
142
+ function startRecording() {
143
+ if (recording) return;
144
+
145
+ var SpeechRecognition = getSpeechRecognition();
146
+ if (!SpeechRecognition) return;
147
+
148
+ recognition = new SpeechRecognition();
149
+ recognition.lang = selectedLang || 'en-US';
150
+ recognition.continuous = true;
151
+ recognition.interimResults = true;
152
+
153
+ textBeforeSTT = ctx.inputEl.value;
154
+ interimText = '';
155
+
156
+ recognition.onresult = function(e) {
157
+ var final = '';
158
+ var interim = '';
159
+
160
+ for (var i = 0; i < e.results.length; i++) {
161
+ var result = e.results[i];
162
+ if (result.isFinal) {
163
+ final += result[0].transcript;
164
+ } else {
165
+ interim += result[0].transcript;
166
+ }
167
+ }
168
+
169
+ var text = textBeforeSTT;
170
+ if (final) {
171
+ if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
172
+ text += ' ';
173
+ }
174
+ text += final;
175
+ }
176
+ if (interim) {
177
+ if (text && text.length > 0 && text[text.length - 1] !== ' ' && text[text.length - 1] !== '\n') {
178
+ text += ' ';
179
+ }
180
+ text += interim;
181
+ }
182
+
183
+ ctx.inputEl.value = text;
184
+ autoResize();
185
+ };
186
+
187
+ recognition.onerror = function(e) {
188
+ console.error('[STT] Recognition error:', e.error);
189
+ if (e.error === 'not-allowed') {
190
+ if (ctx.addSystemMessage) {
191
+ ctx.addSystemMessage('Microphone access denied.\n\nTo fix: click the lock icon in the address bar → Site settings → Microphone → Allow, then reload.', true);
192
+ }
193
+ stopRecording();
194
+ } else if (e.error === 'no-speech') {
195
+ // Silence — just keep listening
196
+ } else if (e.error === 'network') {
197
+ if (ctx.addSystemMessage) {
198
+ ctx.addSystemMessage('Speech recognition unavailable.\n\nWeb Speech API sends audio to Google servers for recognition. Some Chromium forks (Arc, Brave) block this connection.\n\nSupported: Chrome, Edge, Safari 14.1+, Samsung Internet\nNot supported: Arc, Brave, Firefox', true);
199
+ }
200
+ stopRecording();
201
+ }
202
+ };
203
+
204
+ recognition.onend = function() {
205
+ // Auto-restart if still recording (browser may stop after silence)
206
+ if (recording) {
207
+ // Save confirmed text so far
208
+ textBeforeSTT = ctx.inputEl.value;
209
+ try {
210
+ recognition.start();
211
+ } catch (e) {
212
+ // Already started or other error
213
+ stopRecording();
214
+ }
215
+ }
216
+ };
217
+
218
+ try {
219
+ recognition.start();
220
+ recording = true;
221
+ sttBtn.classList.add('stt-active');
222
+ sttBtn.innerHTML =
223
+ '<span class="stt-wave">' +
224
+ '<span class="stt-wave-bar"></span>' +
225
+ '<span class="stt-wave-bar"></span>' +
226
+ '<span class="stt-wave-bar"></span>' +
227
+ '<span class="stt-wave-bar"></span>' +
228
+ '<span class="stt-wave-bar"></span>' +
229
+ '</span>' +
230
+ '<span class="stt-stop-label">Stop</span>';
231
+ ctx.inputEl.setAttribute('placeholder', 'Listening...');
232
+ } catch (err) {
233
+ console.error('[STT] Failed to start:', err);
234
+ if (ctx.addSystemMessage) {
235
+ ctx.addSystemMessage('Failed to start voice input: ' + err.message, true);
236
+ }
237
+ }
238
+ }
239
+
240
+ function stopRecording() {
241
+ if (!recording) return;
242
+ recording = false;
243
+
244
+ if (recognition) {
245
+ try { recognition.stop(); } catch (e) { /* ignore */ }
246
+ recognition = null;
247
+ }
248
+
249
+ sttBtn.classList.remove('stt-active');
250
+ sttBtn.innerHTML = iconHtml('mic');
251
+ refreshIcons();
252
+ ctx.inputEl.setAttribute('placeholder', 'Message Claude Code...');
253
+ }
254
+
255
+ // --- External lang setter (used by profile module) ---
256
+ export function setSTTLang(code) {
257
+ selectedLang = code;
258
+ saveLang(code);
259
+ }
260
+
261
+ export function getSTTLang() {
262
+ return selectedLang;
263
+ }
264
+
265
+ // --- Exports ---
266
+ export function isSTTRecording() {
267
+ return recording;
268
+ }
269
+
270
+ export function isSTTInitializing() {
271
+ return false;
272
+ }
@@ -239,6 +239,20 @@ function createXtermForTab(tab) {
239
239
  }
240
240
  });
241
241
 
242
+ // Ctrl+V paste for Firefox (Firefox blocks xterm.js clipboard access)
243
+ bodyEl.addEventListener("keydown", function (e) {
244
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
245
+ e.preventDefault();
246
+ if (navigator.clipboard && navigator.clipboard.readText) {
247
+ navigator.clipboard.readText().then(function (text) {
248
+ if (text && ctx.ws && ctx.connected) {
249
+ ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
250
+ }
251
+ }).catch(function () { /* permission denied or not available */ });
252
+ }
253
+ }
254
+ });
255
+
242
256
  // Right-click context menu
243
257
  bodyEl.addEventListener("contextmenu", function (e) {
244
258
  showTermCtxMenu(e, tab);
@@ -599,6 +613,24 @@ function showTermCtxMenu(e, tab) {
599
613
  });
600
614
  menu.appendChild(copyItem);
601
615
 
616
+ // Paste
617
+ var pasteItem = document.createElement("button");
618
+ pasteItem.className = "term-ctx-item";
619
+ pasteItem.innerHTML = iconHtml("clipboard-paste") + " <span>Paste</span>";
620
+ pasteItem.addEventListener("click", function (ev) {
621
+ ev.stopPropagation();
622
+ closeTermCtxMenu();
623
+ if (!tab.xterm) return;
624
+ if (navigator.clipboard && navigator.clipboard.readText) {
625
+ navigator.clipboard.readText().then(function (text) {
626
+ if (text && ctx.ws && ctx.connected) {
627
+ ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: text }));
628
+ }
629
+ }).catch(function () { /* permission denied or not available */ });
630
+ }
631
+ });
632
+ menu.appendChild(pasteItem);
633
+
602
634
  // Clear
603
635
  var clearItem = document.createElement("button");
604
636
  clearItem.className = "term-ctx-item";
@@ -370,17 +370,10 @@ export function applyTheme(themeId, fromPicker) {
370
370
  }
371
371
  }
372
372
 
373
- // --- Mascot icon update (swap light/dark src) ---
373
+ // --- Favicon update on theme change ---
374
374
  function updateMascotSvgs(vars, isLight) {
375
- var lightSrc = "favicon.svg";
376
- var darkSrc = "favicon-dark.svg";
377
- var src = isLight ? lightSrc : darkSrc;
378
- var mascots = document.querySelectorAll("img.footer-mascot");
379
- for (var i = 0; i < mascots.length; i++) {
380
- mascots[i].setAttribute("src", src);
381
- }
382
- var faviconEl = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
383
- if (faviconEl) faviconEl.setAttribute("href", src);
375
+ var faviconEl = document.querySelector('link[rel="icon"][type="image/png"]');
376
+ if (faviconEl) faviconEl.setAttribute("href", "favicon-banded.png");
384
377
  }
385
378
 
386
379
  // --- Theme loading from server ---
@@ -15,3 +15,9 @@
15
15
  @import url("css/skills.css");
16
16
  @import url("css/mobile-nav.css");
17
17
  @import url("css/loop.css");
18
+ @import url("css/scheduler.css");
19
+ @import url("css/scheduler-modal.css");
20
+ @import url("css/home-hub.css");
21
+ @import url("css/playbook.css");
22
+ @import url("css/stt.css");
23
+ @import url("css/profile.css");
package/lib/public/sw.js CHANGED
@@ -1,11 +1,88 @@
1
- self.addEventListener("install", function () {
2
- self.skipWaiting();
1
+ var CACHE_NAME = "clay-offline-v1";
2
+
3
+ self.addEventListener("install", function (event) {
4
+ event.waitUntil(self.skipWaiting());
3
5
  });
4
6
 
5
7
  self.addEventListener("activate", function (event) {
6
- event.waitUntil(self.clients.claim());
8
+ // Clean up old cache versions
9
+ event.waitUntil(
10
+ caches.keys().then(function (names) {
11
+ return Promise.all(
12
+ names.filter(function (n) { return n !== CACHE_NAME; })
13
+ .map(function (n) { return caches.delete(n); })
14
+ );
15
+ }).then(function () {
16
+ return self.clients.claim();
17
+ })
18
+ );
19
+ });
20
+
21
+ // --- Offline cache: network-first, cache-fallback ---
22
+
23
+ function shouldCache(request, response) {
24
+ if (request.method !== "GET") return false;
25
+ if (!response || !response.ok) return false;
26
+ // Cache same-origin static assets and CDN resources (jsdelivr, fonts)
27
+ var url = new URL(request.url);
28
+ if (url.origin === self.location.origin) return true;
29
+ if (url.hostname === "cdn.jsdelivr.net") return true;
30
+ if (url.hostname === "fonts.googleapis.com") return true;
31
+ if (url.hostname === "fonts.gstatic.com") return true;
32
+ return false;
33
+ }
34
+
35
+ self.addEventListener("fetch", function (event) {
36
+ var request = event.request;
37
+
38
+ // Only handle GET requests
39
+ if (request.method !== "GET") return;
40
+
41
+ // Skip WebSocket upgrade requests and API/data endpoints
42
+ var url = new URL(request.url);
43
+ if (url.pathname.indexOf("/ws") !== -1) return;
44
+ if (url.pathname.indexOf("/api/") !== -1) return;
45
+
46
+ event.respondWith(
47
+ fetch(request).then(function (response) {
48
+ // Network succeeded: cache a clone for offline use
49
+ if (shouldCache(request, response)) {
50
+ var clone = response.clone();
51
+ caches.open(CACHE_NAME).then(function (cache) {
52
+ cache.put(request, clone);
53
+ });
54
+ }
55
+ return response;
56
+ }).catch(function () {
57
+ // Network failed: serve from cache
58
+ return caches.match(request).then(function (cached) {
59
+ if (cached) return cached;
60
+
61
+ // For navigation requests, serve cached index.html as fallback
62
+ // (handles /p/slug/ routes that all serve the same SPA shell)
63
+ if (request.mode === "navigate") {
64
+ return caches.match("/index.html").then(function (indexCached) {
65
+ // If even the index page is not cached, show a minimal offline page
66
+ if (indexCached) return indexCached;
67
+ return new Response(
68
+ "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Clay</title>" +
69
+ "<style>body{margin:0;background:#000;color:#888;display:flex;" +
70
+ "align-items:center;justify-content:center;height:100vh;" +
71
+ "font-family:monospace;font-size:1.2em}</style></head>" +
72
+ "<body><p>Waiting for server&hellip;</p></body></html>",
73
+ { headers: { "Content-Type": "text/html" } }
74
+ );
75
+ });
76
+ }
77
+ // Non-navigation request with no cache: return network error
78
+ return new Response("", { status: 503, statusText: "Offline" });
79
+ });
80
+ })
81
+ );
7
82
  });
8
83
 
84
+ // --- Push notifications ---
85
+
9
86
  self.addEventListener("push", function (event) {
10
87
  var data = {};
11
88
  try { data = event.data.json(); } catch (e) { return; }
@@ -46,6 +123,8 @@ self.addEventListener("push", function (event) {
46
123
  );
47
124
  });
48
125
 
126
+ // --- Notification click ---
127
+
49
128
  self.addEventListener("notificationclick", function (event) {
50
129
  var data = event.notification.data || {};
51
130
  event.notification.close();