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,578 @@
1
+ /**
2
+ * Playbook Engine — interactive step-by-step tutorials with branching.
3
+ *
4
+ * Each playbook is a state machine: steps with actions, conditions, and branches.
5
+ * Define playbooks declaratively, register them, then open by id.
6
+ *
7
+ * Playbook shape:
8
+ * { id, title, icon, description, steps: [ { id, title, body, actions?, condition?, branches? } ] }
9
+ *
10
+ * Step shape:
11
+ * id: unique step identifier
12
+ * title: heading text
13
+ * body: description (supports simple HTML)
14
+ * actions: [ { label, action?, next } ] — buttons. action = string key → resolved by actionHandlers
15
+ * condition: function returning a string key
16
+ * branches: { [key]: stepId } — auto-navigate based on condition result
17
+ * note: optional small footnote text
18
+ */
19
+
20
+ import { iconHtml, refreshIcons } from './icons.js';
21
+
22
+ var registry = {}; // id → playbook definition
23
+ var actionHandlers = {}; // action key → function(cb) — cb(result) when done
24
+ var modal = null; // DOM element
25
+ var overlay = null;
26
+ var currentPlaybook = null;
27
+ var currentStepIdx = 0;
28
+ var stepHistory = [];
29
+ var completedPlaybooks = {};
30
+ var onCloseCallback = null;
31
+
32
+ // --- Registry ---
33
+
34
+ export function registerPlaybook(pb) {
35
+ registry[pb.id] = pb;
36
+ }
37
+
38
+ export function registerAction(key, handler) {
39
+ actionHandlers[key] = handler;
40
+ }
41
+
42
+ export function getPlaybooks() {
43
+ var list = [];
44
+ for (var id in registry) {
45
+ if (registry.hasOwnProperty(id)) {
46
+ var pb = registry[id];
47
+ list.push({
48
+ id: pb.id,
49
+ title: pb.title,
50
+ icon: pb.icon || "📖",
51
+ description: pb.description || "",
52
+ completed: !!completedPlaybooks[pb.id],
53
+ steps: pb.steps.length,
54
+ });
55
+ }
56
+ }
57
+ return list;
58
+ }
59
+
60
+ export function isCompleted(id) {
61
+ return !!completedPlaybooks[id];
62
+ }
63
+
64
+ // --- Open / Close ---
65
+
66
+ export function openPlaybook(id, onClose) {
67
+ var pb = registry[id];
68
+ if (!pb) return;
69
+ currentPlaybook = pb;
70
+ currentStepIdx = 0;
71
+ stepHistory = [];
72
+ onCloseCallback = onClose || null;
73
+ ensureModal();
74
+ navigateToStep(pb.steps[0].id);
75
+ overlay.classList.remove("hidden");
76
+ modal.classList.remove("hidden");
77
+ // Focus trap
78
+ setTimeout(function () { modal.focus(); }, 50);
79
+ }
80
+
81
+ export function closePlaybook() {
82
+ if (overlay) overlay.classList.add("hidden");
83
+ if (modal) modal.classList.add("hidden");
84
+ var cb = onCloseCallback;
85
+ currentPlaybook = null;
86
+ onCloseCallback = null;
87
+ if (cb) cb();
88
+ }
89
+
90
+ // --- Init (call once from app.js) ---
91
+
92
+ export function initPlaybook() {
93
+ loadCompleted();
94
+ registerBuiltinPlaybooks();
95
+ }
96
+
97
+ // --- DOM ---
98
+
99
+ function ensureModal() {
100
+ if (modal) return;
101
+ overlay = document.createElement("div");
102
+ overlay.className = "playbook-overlay hidden";
103
+ overlay.addEventListener("click", function (e) {
104
+ if (e.target === overlay) closePlaybook();
105
+ });
106
+
107
+ modal = document.createElement("div");
108
+ modal.className = "playbook-modal hidden";
109
+ modal.setAttribute("tabindex", "-1");
110
+ modal.addEventListener("keydown", function (e) {
111
+ if (e.key === "Escape") closePlaybook();
112
+ });
113
+
114
+ overlay.appendChild(modal);
115
+ document.body.appendChild(overlay);
116
+ }
117
+
118
+ function navigateToStep(stepId) {
119
+ if (!currentPlaybook) return;
120
+ var step = null;
121
+ var idx = 0;
122
+ for (var i = 0; i < currentPlaybook.steps.length; i++) {
123
+ if (currentPlaybook.steps[i].id === stepId) {
124
+ step = currentPlaybook.steps[i];
125
+ idx = i;
126
+ break;
127
+ }
128
+ }
129
+ if (!step) return;
130
+ currentStepIdx = idx;
131
+ stepHistory.push(stepId);
132
+
133
+ // Auto-branch if condition exists
134
+ if (step.condition && step.branches) {
135
+ var key = step.condition();
136
+ var nextId = step.branches[key] || step.branches["default"];
137
+ if (nextId) {
138
+ navigateToStep(nextId);
139
+ return;
140
+ }
141
+ }
142
+
143
+ renderStep(step);
144
+ }
145
+
146
+ function renderStep(step) {
147
+ var pb = currentPlaybook;
148
+ var totalSteps = pb.steps.filter(function (s) { return !s.condition; }).length;
149
+ var visibleIdx = 0;
150
+ var count = 0;
151
+ for (var i = 0; i < pb.steps.length; i++) {
152
+ if (!pb.steps[i].condition) {
153
+ count++;
154
+ if (pb.steps[i].id === step.id) visibleIdx = count;
155
+ }
156
+ }
157
+
158
+ var html = '';
159
+ // Header
160
+ html += '<div class="playbook-header">';
161
+ html += '<div class="playbook-header-left">';
162
+ html += '<span class="playbook-icon">' + (pb.icon || "📖") + '</span>';
163
+ html += '<span class="playbook-title">' + escHtml(pb.title) + '</span>';
164
+ html += '</div>';
165
+ html += '<button class="playbook-close" title="Close">&times;</button>';
166
+ html += '</div>';
167
+
168
+ // Progress
169
+ html += '<div class="playbook-progress">';
170
+ for (var p = 0; p < totalSteps; p++) {
171
+ var cls = "playbook-progress-dot";
172
+ if (p + 1 < visibleIdx) cls += " done";
173
+ else if (p + 1 === visibleIdx) cls += " active";
174
+ html += '<span class="' + cls + '"></span>';
175
+ }
176
+ html += '</div>';
177
+
178
+ // Body
179
+ html += '<div class="playbook-body">';
180
+ html += '<h2 class="playbook-step-title">' + escHtml(step.title) + '</h2>';
181
+ var bodyContent = typeof step.body === "function" ? step.body() : (step.body || "");
182
+ html += '<div class="playbook-step-body">' + bodyContent + '</div>';
183
+ if (step.note) {
184
+ html += '<div class="playbook-step-note">' + escHtml(step.note) + '</div>';
185
+ }
186
+ html += '</div>';
187
+
188
+ // Actions
189
+ if (step.actions && step.actions.length > 0) {
190
+ html += '<div class="playbook-actions">';
191
+ for (var a = 0; a < step.actions.length; a++) {
192
+ var act = step.actions[a];
193
+ var btnClass = "playbook-btn";
194
+ if (a === 0) btnClass += " playbook-btn-primary";
195
+ else btnClass += " playbook-btn-secondary";
196
+ html += '<button class="' + btnClass + '" data-action="' + (act.action || "") + '" data-next="' + (act.next || "") + '">' + escHtml(act.label) + '</button>';
197
+ }
198
+ html += '</div>';
199
+ }
200
+
201
+ modal.innerHTML = html;
202
+ refreshIcons();
203
+
204
+ // Wire close button
205
+ var closeBtn = modal.querySelector(".playbook-close");
206
+ if (closeBtn) closeBtn.addEventListener("click", closePlaybook);
207
+
208
+ // Wire action buttons
209
+ var btns = modal.querySelectorAll(".playbook-btn");
210
+ for (var b = 0; b < btns.length; b++) {
211
+ (function (btn) {
212
+ btn.addEventListener("click", function () {
213
+ var actionKey = btn.dataset.action;
214
+ var nextStep = btn.dataset.next;
215
+ if (actionKey && actionHandlers[actionKey]) {
216
+ btn.disabled = true;
217
+ btn.textContent += "...";
218
+ actionHandlers[actionKey](function () {
219
+ if (nextStep) navigateToStep(nextStep);
220
+ else {
221
+ markCompleted(currentPlaybook.id);
222
+ closePlaybook();
223
+ }
224
+ });
225
+ } else if (nextStep) {
226
+ navigateToStep(nextStep);
227
+ } else {
228
+ markCompleted(currentPlaybook.id);
229
+ closePlaybook();
230
+ }
231
+ });
232
+ })(btns[b]);
233
+ }
234
+
235
+ // Wire copy-url buttons (for denied step)
236
+ var copyBtns = modal.querySelectorAll("[data-copy-url]");
237
+ for (var c = 0; c < copyBtns.length; c++) {
238
+ (function (btn) {
239
+ btn.addEventListener("click", function (e) {
240
+ e.stopPropagation();
241
+ var url = btn.dataset.copyUrl;
242
+ navigator.clipboard.writeText(url).then(function () {
243
+ btn.textContent = "Copied!";
244
+ setTimeout(function () { btn.textContent = "Copy"; }, 1500);
245
+ });
246
+ });
247
+ })(copyBtns[c]);
248
+ }
249
+ }
250
+
251
+ // --- Completion tracking ---
252
+
253
+ function markCompleted(id) {
254
+ completedPlaybooks[id] = true;
255
+ try { localStorage.setItem("clay-playbooks-done", JSON.stringify(completedPlaybooks)); } catch (e) {}
256
+ }
257
+
258
+ function loadCompleted() {
259
+ try {
260
+ var data = JSON.parse(localStorage.getItem("clay-playbooks-done") || "{}");
261
+ completedPlaybooks = data || {};
262
+ } catch (e) { completedPlaybooks = {}; }
263
+ }
264
+
265
+ // --- Tip ↔ Playbook linking ---
266
+
267
+ export function getPlaybookForTip(tipText) {
268
+ for (var id in registry) {
269
+ if (!registry.hasOwnProperty(id)) continue;
270
+ var pb = registry[id];
271
+ if (pb.tipMatch && pb.tipMatch(tipText)) return pb.id;
272
+ }
273
+ return null;
274
+ }
275
+
276
+ // --- Built-in Playbooks ---
277
+
278
+ function registerBuiltinPlaybooks() {
279
+ // === Push Notifications ===
280
+ registerPlaybook({
281
+ id: "push-notifications",
282
+ title: "Push Notifications",
283
+ icon: "🔔",
284
+ description: "Get notified when Claude finishes a long task",
285
+ tipMatch: function (t) { return t.indexOf("Push notification") !== -1; },
286
+ steps: [
287
+ {
288
+ id: "intro",
289
+ title: "Never miss a response",
290
+ body: "When Claude is working on a long task, you can walk away and get a push notification the moment it's done. Works even when Clay is in the background.",
291
+ actions: [
292
+ { label: "Enable notifications", action: "notif_request", next: "check_perm" },
293
+ { label: "Not now", next: "" },
294
+ ],
295
+ },
296
+ {
297
+ id: "check_perm",
298
+ condition: function () {
299
+ if (typeof Notification === "undefined") return "unsupported";
300
+ return Notification.permission; // "granted" | "denied" | "default"
301
+ },
302
+ branches: {
303
+ "granted": "test",
304
+ "denied": "denied",
305
+ "default": "denied",
306
+ "unsupported": "unsupported",
307
+ },
308
+ },
309
+ {
310
+ id: "test",
311
+ title: "Let's test it!",
312
+ body: "Click below to send a test notification. You should see it pop up from your system.",
313
+ actions: [
314
+ { label: "Send test notification", action: "notif_test", next: "check_test" },
315
+ { label: "Skip", next: "done" },
316
+ ],
317
+ },
318
+ {
319
+ id: "check_test",
320
+ title: "Did you see it?",
321
+ body: "A notification should have appeared from your system just now.",
322
+ actions: [
323
+ { label: "Yes, it worked!", next: "done" },
324
+ { label: "No, nothing appeared", next: "troubleshoot" },
325
+ ],
326
+ },
327
+ {
328
+ id: "troubleshoot",
329
+ title: "Troubleshooting",
330
+ body: function () {
331
+ var isMac = navigator.platform && navigator.platform.indexOf("Mac") !== -1;
332
+ var browser = "your browser";
333
+ var ua = navigator.userAgent || "";
334
+ if (ua.indexOf("Arc") !== -1) browser = "Arc";
335
+ else if (ua.indexOf("Edg/") !== -1) browser = "Edge";
336
+ else if (ua.indexOf("Firefox") !== -1) browser = "Firefox";
337
+ else if (ua.indexOf("Chrome") !== -1) browser = "Chrome";
338
+ else if (ua.indexOf("Safari") !== -1) browser = "Safari";
339
+
340
+ var hasUntrustedCert = location.protocol === "https:" &&
341
+ location.hostname !== "localhost" &&
342
+ location.hostname !== "127.0.0.1";
343
+
344
+ var html = "The browser says permission is granted, but the notification didn't show.<br><br>";
345
+
346
+ if (hasUntrustedCert) {
347
+ html += "🔒 <strong>Untrusted certificate?</strong><br>" +
348
+ "If you're using a self-signed or untrusted TLS certificate, " +
349
+ "some browsers silently block notifications even on HTTPS.<br>" +
350
+ "Run <strong>mkcert -install</strong> on the server to trust the root CA, " +
351
+ "or add the certificate to your system keychain as trusted.<br><br>";
352
+ }
353
+
354
+ html += "<strong>Also check your OS notification settings:</strong><br>";
355
+ if (isMac) {
356
+ html += "<strong>1.</strong> Open <strong>System Settings → Notifications</strong><br>" +
357
+ "<strong>2.</strong> Find <strong>" + browser + "</strong> in the list<br>" +
358
+ "<strong>3.</strong> Make sure <strong>Allow Notifications</strong> is turned on<br>" +
359
+ "<strong>4.</strong> Check that <strong>Do Not Disturb</strong> / Focus mode is off<br>";
360
+ } else {
361
+ html += "<strong>1.</strong> Make sure notifications are enabled for <strong>" + browser + "</strong><br>" +
362
+ "<strong>2.</strong> Check that Do Not Disturb / Focus mode is off<br>";
363
+ }
364
+ html += "<br>After adjusting, come back and try again.";
365
+ return html;
366
+ },
367
+ actions: [
368
+ { label: "Try again", action: "notif_test", next: "check_test" },
369
+ { label: "I'll fix it later", next: "done" },
370
+ ],
371
+ },
372
+ {
373
+ id: "denied",
374
+ title: "Permission needed",
375
+ body: function () {
376
+ var isInsecure = !window.isSecureContext ||
377
+ (location.protocol === "http:" &&
378
+ location.hostname !== "localhost" &&
379
+ location.hostname !== "127.0.0.1" &&
380
+ location.hostname !== "[::1]");
381
+
382
+ if (isInsecure) {
383
+ return "⚠️ <strong>Insecure context detected.</strong><br><br>" +
384
+ "Notifications require a <strong>secure context</strong> (HTTPS or localhost). " +
385
+ "You're accessing Clay over <strong>" + location.protocol + "//" + location.hostname + "</strong>, so the browser automatically blocks notifications.<br><br>" +
386
+ "<strong>How to fix:</strong><br>" +
387
+ "<strong>1.</strong> Access Clay via <strong>https://</strong> (set up a TLS certificate)<br>" +
388
+ "<strong>2.</strong> Or access via <strong>localhost:" + location.port + "</strong> instead of an IP address<br>";
389
+ }
390
+
391
+ var url = "chrome://settings/content/notifications";
392
+ var ua = navigator.userAgent || "";
393
+ if (ua.indexOf("Firefox") !== -1) url = "about:preferences#privacy";
394
+ else if (ua.indexOf("Edg/") !== -1) url = "edge://settings/content/notifications";
395
+ else if (ua.indexOf("Arc") !== -1) url = "arc://settings/content/notifications";
396
+ return "Your browser blocked the notification permission.<br><br>" +
397
+ "<strong>1.</strong> Click the lock icon in the address bar<br>" +
398
+ "<strong>2.</strong> Find \"Notifications\" and set it to \"Allow\"<br>" +
399
+ "<strong>3.</strong> Reload the page<br><br>" +
400
+ "Or paste this into your address bar:<br>" +
401
+ "<div style=\"display:flex;align-items:center;gap:6px;margin-top:6px\">" +
402
+ "<code style=\"font-size:12px;color:var(--accent);flex:1;user-select:all\">" + url + "</code>" +
403
+ "<button class=\"playbook-btn-secondary\" style=\"padding:3px 8px;font-size:11px\" data-copy-url=\"" + url + "\">Copy</button>" +
404
+ "</div>";
405
+ },
406
+ actions: [
407
+ { label: "Got it", next: "" },
408
+ ],
409
+ },
410
+ {
411
+ id: "unsupported",
412
+ title: "Not supported",
413
+ body: "Your browser doesn't support notifications. Try Chrome, Edge, or Firefox for the best experience.",
414
+ actions: [
415
+ { label: "Got it", next: "" },
416
+ ],
417
+ },
418
+ {
419
+ id: "done",
420
+ title: "All set! 🎉",
421
+ body: "You'll now get a notification whenever Claude finishes processing. This works even when Clay is minimized or in another tab.",
422
+ note: "You can toggle notifications anytime from the header bar.",
423
+ actions: [
424
+ { label: "Done", next: "" },
425
+ ],
426
+ },
427
+ ],
428
+ });
429
+
430
+ // === Action handlers for Push Notifications ===
431
+ registerAction("notif_request", function (cb) {
432
+ if (typeof Notification === "undefined") { cb(); return; }
433
+ Notification.requestPermission().then(function (perm) {
434
+ // Sync with app's notification toggle state
435
+ if (perm === "granted") {
436
+ try {
437
+ localStorage.setItem("notif-alert", "1");
438
+ // Update the toggle if visible
439
+ var toggle = document.getElementById("notif-toggle-alert");
440
+ if (toggle) toggle.checked = true;
441
+ } catch (e) {}
442
+ }
443
+ cb();
444
+ }).catch(function () { cb(); });
445
+ });
446
+
447
+ registerAction("notif_test", function (cb) {
448
+ if (typeof Notification === "undefined") { cb(); return; }
449
+ // Ensure permission is granted before trying
450
+ if (Notification.permission !== "granted") {
451
+ Notification.requestPermission().then(function (perm) {
452
+ if (perm === "granted") fireTestNotification(cb);
453
+ else cb();
454
+ }).catch(function () { cb(); });
455
+ return;
456
+ }
457
+ fireTestNotification(cb);
458
+ });
459
+
460
+ // === Certificate Trust (HTTPS only) ===
461
+ if (location.protocol === "https:") {
462
+ function detectOS() {
463
+ var ua = navigator.userAgent || "";
464
+ var platform = navigator.platform || "";
465
+ if (platform.indexOf("Mac") !== -1 || ua.indexOf("Mac") !== -1) return "mac";
466
+ if (platform.indexOf("Win") !== -1 || ua.indexOf("Windows") !== -1) return "windows";
467
+ // Linux, ChromeOS, etc.
468
+ return "linux";
469
+ }
470
+
471
+ function certInstallBody() {
472
+ var os = detectOS();
473
+ var html = "The certificate has been downloaded as <strong>clay-ca.pem</strong>.<br><br>";
474
+
475
+ if (os === "mac") {
476
+ html += "<strong>macOS — run in Terminal:</strong><br>";
477
+ html += certCodeBlock(
478
+ "sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/Downloads/clay-ca.pem"
479
+ );
480
+ } else if (os === "windows") {
481
+ html += "<strong>Windows — run in PowerShell (Admin):</strong><br>";
482
+ html += certCodeBlock(
483
+ "certutil -addstore -f \"Root\" %USERPROFILE%\\Downloads\\clay-ca.pem"
484
+ );
485
+ } else {
486
+ html += "<strong>Linux — run in terminal:</strong><br>";
487
+ html += certCodeBlock(
488
+ "sudo cp ~/Downloads/clay-ca.pem /usr/local/share/ca-certificates/clay-ca.crt && sudo update-ca-certificates"
489
+ );
490
+ }
491
+
492
+ html += "<br><div style=\"padding:8px 10px;background:var(--bg-deeper);border-radius:6px;border-left:3px solid var(--accent)\">" +
493
+ "<strong style=\"font-size:11px\">Or ask Claude Code:</strong><br>" +
494
+ "<code style=\"font-family:monospace;font-size:12px;line-height:1.5;color:var(--text-secondary)\">" +
495
+ "Install ~/Downloads/clay-ca.pem as a trusted root certificate</code></div>";
496
+
497
+ html += "<br>Then <strong>restart your browser</strong> to apply the change.";
498
+ return html;
499
+ }
500
+
501
+ function certCodeBlock(cmd) {
502
+ return "<div style=\"margin-top:8px;position:relative;background:var(--bg-deeper);border-radius:6px;padding:8px 10px\">" +
503
+ "<code style=\"display:block;font-family:monospace;font-size:12px;word-break:break-all;line-height:1.5;padding-right:50px\">" + escHtml(cmd) + "</code>" +
504
+ "<button class=\"playbook-btn-secondary\" style=\"position:absolute;top:8px;right:8px;padding:2px 8px;font-size:11px;white-space:nowrap\" data-copy-url=\"" + escHtml(cmd) + "\">Copy</button>" +
505
+ "</div>";
506
+ }
507
+
508
+ registerPlaybook({
509
+ id: "trust-certificate",
510
+ title: "Trust Certificate",
511
+ icon: "🔒",
512
+ description: "Getting certificate warnings? Fix them here",
513
+ tipMatch: function (t) { return t.indexOf("certificate") !== -1 || t.indexOf("Certificate") !== -1; },
514
+ steps: [
515
+ {
516
+ id: "intro",
517
+ title: "Getting certificate warnings?",
518
+ body: "Clay generates a local CA certificate for secure HTTPS connections. " +
519
+ "Your browser may show warnings until you install and trust this certificate on your device.",
520
+ actions: [
521
+ { label: "Download certificate", action: "cert_download", next: "install" },
522
+ { label: "Not now", next: "" },
523
+ ],
524
+ },
525
+ {
526
+ id: "install",
527
+ title: "Install the certificate",
528
+ body: certInstallBody,
529
+ actions: [
530
+ { label: "Done, I installed it", next: "done" },
531
+ { label: "I'll do it later", next: "" },
532
+ ],
533
+ },
534
+ {
535
+ id: "done",
536
+ title: "All set! 🎉",
537
+ body: "After restarting your browser, certificate warnings should disappear. " +
538
+ "If you still see them, make sure you ran the terminal command with admin privileges.",
539
+ actions: [
540
+ { label: "Done", next: "" },
541
+ ],
542
+ },
543
+ ],
544
+ });
545
+
546
+ registerAction("cert_download", function (cb) {
547
+ var a = document.createElement("a");
548
+ a.href = "/ca/download";
549
+ a.download = "clay-ca.pem";
550
+ document.body.appendChild(a);
551
+ a.click();
552
+ document.body.removeChild(a);
553
+ setTimeout(cb, 500);
554
+ });
555
+ }
556
+
557
+ function fireTestNotification(cb) {
558
+ try {
559
+ var n = new Notification("Clay", {
560
+ body: "Notifications are working! 🎉",
561
+ tag: "clay-test-" + Date.now(),
562
+ });
563
+ n.onclick = function () { window.focus(); n.close(); };
564
+ setTimeout(function () { try { n.close(); } catch (e) {} }, 5000);
565
+ } catch (e) {
566
+ console.warn("[Playbook] Notification failed:", e);
567
+ }
568
+ setTimeout(cb, 800);
569
+ }
570
+ }
571
+
572
+ // --- Utils ---
573
+
574
+ function escHtml(str) {
575
+ var div = document.createElement("div");
576
+ div.textContent = str || "";
577
+ return div.innerHTML;
578
+ }