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.
- package/bin/cli.js +31 -17
- package/lib/config.js +7 -4
- package/lib/project.js +343 -15
- package/lib/public/app.js +1039 -134
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +18 -1
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +141 -23
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1305 -0
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +336 -44
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/markdown.js +18 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +1 -9
- package/lib/public/modules/scheduler.js +2826 -0
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +376 -32
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +35 -2
- 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">×</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
|
+
}
|