clay-server 4.8.0-beta.1 → 4.8.0-beta.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 (44) hide show
  1. package/lib/bundled-skills/clay-agent-browser/SKILL.md +36 -0
  2. package/lib/clay-skill-install.js +33 -0
  3. package/lib/clay-skill-storage.js +49 -0
  4. package/lib/project-http.js +14 -5
  5. package/lib/project-shared-browser.js +256 -0
  6. package/lib/project.js +35 -1
  7. package/lib/public/app.js +7 -0
  8. package/lib/public/css/filebrowser.css +2 -25
  9. package/lib/public/css/issues.css +1 -14
  10. package/lib/public/css/mcp-skills-workbench.css +51 -0
  11. package/lib/public/css/project-logs.css +0 -11
  12. package/lib/public/css/right-workbench.css +28 -0
  13. package/lib/public/css/scheduled-tasks.css +1 -1
  14. package/lib/public/css/shared-browser-skins.css +28 -0
  15. package/lib/public/css/shared-browser.css +91 -0
  16. package/lib/public/css/sticky-notes.css +0 -11
  17. package/lib/public/index.html +30 -22
  18. package/lib/public/modules/app-messages.js +2 -0
  19. package/lib/public/modules/extension-settings.js +110 -0
  20. package/lib/public/modules/mcp-skills-workbench.js +67 -0
  21. package/lib/public/modules/mcp-ui.js +9 -12
  22. package/lib/public/modules/notifications.js +2 -107
  23. package/lib/public/modules/shared-browser-activity.js +66 -0
  24. package/lib/public/modules/shared-browser-input.js +73 -0
  25. package/lib/public/modules/shared-browser-tabs.js +33 -0
  26. package/lib/public/modules/shared-browser-viewport-menu.js +76 -0
  27. package/lib/public/modules/shared-browser.js +214 -0
  28. package/lib/public/modules/sidebar-mobile.js +3 -3
  29. package/lib/public/modules/skills-actions.js +247 -0
  30. package/lib/public/modules/skills-detail.js +148 -0
  31. package/lib/public/modules/skills-state.js +10 -0
  32. package/lib/public/modules/skills.js +64 -459
  33. package/lib/public/modules/tool-palette-order.js +11 -13
  34. package/lib/public/style.css +6 -0
  35. package/lib/sdk-bridge.js +14 -1
  36. package/lib/sessions.js +2 -0
  37. package/lib/shared-browser-activity-history.js +17 -0
  38. package/lib/shared-browser-policy.js +26 -0
  39. package/lib/shared-browser-runtime.js +82 -0
  40. package/lib/shared-browser-tools.js +21 -0
  41. package/lib/shared-browser-worker.js +160 -0
  42. package/lib/ws-schema.js +10 -0
  43. package/lib/yoke/skill-discovery.js +22 -2
  44. package/package.json +1 -1
@@ -0,0 +1,247 @@
1
+ import { iconHtml, refreshIcons } from './icons.js';
2
+ import { escapeHtml, copyToClipboard, showToast } from './utils.js';
3
+ import { store } from './store.js';
4
+ import { skillState, content, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
5
+ import { getWs } from './ws-ref.js';
6
+ function reloadActiveSessionSkills() { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'reload_skills' })); }
7
+ function installSkill(source, skill, scope) {
8
+ var url = "https://github.com/" + source;
9
+
10
+ // Set all install buttons in the detail view to installing state
11
+ var btns = content().querySelectorAll(".skills-install-btn:not(.installed-state)");
12
+ for (var i = 0; i < btns.length; i++) {
13
+ btns[i].disabled = true;
14
+ btns[i].classList.add("installing");
15
+ btns[i].innerHTML = '<div class="skills-btn-spinner"></div> Installing...';
16
+ }
17
+
18
+ fetch(basePath() + "api/install-skill", {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({ url: url, skill: skill, scope: scope }),
22
+ }).catch(function () {
23
+ // Re-enable buttons on fetch error
24
+ for (var j = 0; j < btns.length; j++) {
25
+ btns[j].disabled = false;
26
+ btns[j].classList.remove("installing");
27
+ }
28
+ });
29
+ }
30
+
31
+ export function handleSkillInstalled(msg) {
32
+ var skill = msg.skill;
33
+ var scope = msg.scope;
34
+ var success = msg.success;
35
+
36
+ if (success) {
37
+ // Update skillState().installedSkills cache
38
+ var existing = skillState().installedSkills[skill];
39
+ if (existing) {
40
+ if (existing.scope !== scope && existing.scope !== "both") {
41
+ existing.scope = "both";
42
+ }
43
+ } else {
44
+ skillState().installedSkills[skill] = { scope: scope };
45
+ }
46
+
47
+ // Invalidate skills data cache so list refreshes with updated badges
48
+ skillState().skillsData = {};
49
+
50
+ // If we're on the detail view for this skill, re-render it
51
+ if (skillState().currentView === "detail") {
52
+ var detailEl = content().querySelector(".skills-detail");
53
+ if (detailEl) {
54
+ // Re-render the install section in the sidebar
55
+ updateDetailInstallButtons(skill);
56
+ }
57
+ }
58
+
59
+ // If we're on the installed tab, refresh it
60
+ if (skillState().activeTab === "installed" && skillState().currentView === "list") {
61
+ store.set({ skillsRefreshVersion: (store.get('skillsRefreshVersion') || 0) + 1 });
62
+ }
63
+
64
+ // Hot-reload skills in the active session so it's usable immediately
65
+ reloadActiveSessionSkills();
66
+ } else {
67
+ // Show error toast
68
+ showToast("Failed to install " + skill + (msg.error ? ": " + msg.error : ""), "error");
69
+ // Re-enable buttons
70
+ var btns = content().querySelectorAll(".skills-install-btn.installing");
71
+ for (var i = 0; i < btns.length; i++) {
72
+ btns[i].disabled = false;
73
+ btns[i].classList.remove("installing");
74
+ // We can't easily restore the original text, so just set generic labels
75
+ btns[i].innerHTML = iconHtml("download") + " Install";
76
+ }
77
+ }
78
+ }
79
+
80
+ export function uninstallSkill(skill, scope) {
81
+ // Set the uninstall button to loading state
82
+ var unBtn = content().querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
83
+ if (unBtn) {
84
+ unBtn.disabled = true;
85
+ unBtn.classList.add("uninstalling");
86
+ unBtn.innerHTML = '<div class="skills-btn-spinner small"></div>';
87
+ }
88
+
89
+ fetch(basePath() + "api/uninstall-skill", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ skill: skill, scope: scope }),
93
+ }).catch(function () {
94
+ if (unBtn) {
95
+ unBtn.disabled = false;
96
+ unBtn.classList.remove("uninstalling");
97
+ unBtn.innerHTML = iconHtml("x");
98
+ }
99
+ });
100
+ }
101
+
102
+ export function handleSkillUninstalled(msg) {
103
+ var skill = msg.skill;
104
+ var scope = msg.scope;
105
+ var success = msg.success;
106
+
107
+ if (success) {
108
+ // Update skillState().installedSkills cache
109
+ var existing = skillState().installedSkills[skill];
110
+ if (existing) {
111
+ if (existing.scope === "both") {
112
+ // Downgrade from "both" to whichever scope remains
113
+ existing.scope = scope === "global" ? "project" : "global";
114
+ } else {
115
+ // Fully uninstalled
116
+ delete skillState().installedSkills[skill];
117
+ }
118
+ }
119
+
120
+ // Invalidate skills data cache so list refreshes
121
+ skillState().skillsData = {};
122
+
123
+ // If we're on the detail view, re-render buttons
124
+ if (skillState().currentView === "detail") {
125
+ var detailEl = content().querySelector(".skills-detail");
126
+ if (detailEl) {
127
+ updateDetailInstallButtons(skill);
128
+ }
129
+ }
130
+
131
+ // If we're on the installed tab, refresh it
132
+ if (skillState().activeTab === "installed" && skillState().currentView === "list") {
133
+ store.set({ skillsRefreshVersion: (store.get('skillsRefreshVersion') || 0) + 1 });
134
+ }
135
+
136
+ // Hot-reload skills in the active session so the change applies immediately
137
+ reloadActiveSessionSkills();
138
+ } else {
139
+ showToast("Failed to uninstall " + skill + (msg.error ? ": " + msg.error : ""), "error");
140
+ // Re-enable the uninstall button
141
+ var unBtn = content().querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
142
+ if (unBtn) {
143
+ unBtn.disabled = false;
144
+ unBtn.classList.remove("uninstalling");
145
+ unBtn.innerHTML = iconHtml("x");
146
+ refreshIcons(unBtn);
147
+ }
148
+ }
149
+ }
150
+
151
+ function updateDetailInstallButtons(skillId) {
152
+ var info = getInstalledInfo(skillId);
153
+ var sidebar = content().querySelector(".skills-detail-sidebar");
154
+ if (!sidebar) return;
155
+
156
+ // Find the install actions or installed status block (last .skills-meta-block)
157
+ var blocks = sidebar.querySelectorAll(".skills-meta-block");
158
+ var lastBlock = blocks[blocks.length - 1];
159
+ if (!lastBlock) return;
160
+
161
+ // Get source/skill from the current detail view data attributes
162
+ var sourceEl = sidebar.querySelector("[data-detail-source]");
163
+ if (!sourceEl) sourceEl = content().querySelector("[data-detail-source]");
164
+ var source = sourceEl ? sourceEl.dataset.detailSource : "";
165
+ var skill = sourceEl ? sourceEl.dataset.detailSkill : skillId;
166
+
167
+ lastBlock.innerHTML = buildInstallButtonsHtml(skillId, source, skill);
168
+ refreshIcons(lastBlock);
169
+ attachInstallHandlers(lastBlock, source, skill);
170
+ }
171
+
172
+ export function buildInstallButtonsHtml(skillId, source, skill) {
173
+ var info = getInstalledInfo(skillId);
174
+ if (info && info.source === "clay-builtin") return '<div class="skills-installed-status">' + iconHtml("circle-check") + ' Built in to Clay</div>';
175
+
176
+ var html = '<div class="skills-install-actions" data-detail-source="' + escapeHtml(source) + '" data-detail-skill="' + escapeHtml(skill) + '">';
177
+
178
+ if (info && info.scope === "both") {
179
+ // Both scopes installed — show status + uninstall for each
180
+ html += '<div class="skills-installed-row">' +
181
+ '<div class="skills-installed-status compact">' +
182
+ iconHtml("circle-check") + ' Installed (Project)' +
183
+ '</div>' +
184
+ '<button class="skills-uninstall-btn" data-scope="project" title="Uninstall (Project)">' +
185
+ iconHtml("x") +
186
+ '</button>' +
187
+ '</div>';
188
+ html += '<div class="skills-installed-row">' +
189
+ '<div class="skills-installed-status compact">' +
190
+ iconHtml("circle-check") + ' Installed (Global)' +
191
+ '</div>' +
192
+ '<button class="skills-uninstall-btn" data-scope="global" title="Uninstall (Global)">' +
193
+ iconHtml("x") +
194
+ '</button>' +
195
+ '</div>';
196
+ } else if (info) {
197
+ // One scope installed — show status with uninstall + install button for other scope
198
+ html += '<div class="skills-installed-row">' +
199
+ '<div class="skills-installed-status compact">' +
200
+ iconHtml("circle-check") + ' Installed (' + scopeLabel(info.scope) + ')' +
201
+ '</div>' +
202
+ '<button class="skills-uninstall-btn" data-scope="' + escapeHtml(info.scope) + '" title="Uninstall (' + scopeLabel(info.scope) + ')">' +
203
+ iconHtml("x") +
204
+ '</button>' +
205
+ '</div>';
206
+
207
+ // Show install button for the other scope
208
+ var otherScope = info.scope === "global" ? "project" : "global";
209
+ var otherLabel = info.scope === "global" ? "Project" : "Global";
210
+ html += '<button class="skills-install-btn secondary" data-scope="' + otherScope + '">' +
211
+ iconHtml("download") + ' Install (' + otherLabel + ')' +
212
+ '</button>';
213
+ } else {
214
+ // Not installed — show both install buttons
215
+ html += '<button class="skills-install-btn" data-scope="project">' +
216
+ iconHtml("download") + ' Install (Project)' +
217
+ '</button>';
218
+ html += '<button class="skills-install-btn secondary" data-scope="global">' +
219
+ iconHtml("download") + ' Install (Global)' +
220
+ '</button>';
221
+ }
222
+
223
+ html += '</div>';
224
+ return html;
225
+ }
226
+
227
+ export function attachInstallHandlers(container, source, skill) {
228
+ var btns = container.querySelectorAll(".skills-install-btn");
229
+ for (var i = 0; i < btns.length; i++) {
230
+ (function (btn) {
231
+ btn.addEventListener("click", function () {
232
+ var scope = btn.dataset.scope;
233
+ installSkill(source, skill, scope);
234
+ });
235
+ })(btns[i]);
236
+ }
237
+ var unBtns = container.querySelectorAll(".skills-uninstall-btn");
238
+ for (var j = 0; j < unBtns.length; j++) {
239
+ (function (btn) {
240
+ btn.addEventListener("click", function () {
241
+ var scope = btn.dataset.scope;
242
+ uninstallSkill(skill, scope);
243
+ });
244
+ })(unBtns[j]);
245
+ }
246
+ }
247
+
@@ -0,0 +1,148 @@
1
+ import { iconHtml, refreshIcons } from './icons.js';
2
+ import { escapeHtml, copyToClipboard, showToast } from './utils.js';
3
+ import { store } from './store.js';
4
+ import { skillState, content, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
5
+ import { buildInstallButtonsHtml, attachInstallHandlers } from './skills-actions.js';
6
+ export function renderDetail(data) {
7
+ var name = data.name || data._skill || "Unknown";
8
+ var desc = data.description || "";
9
+ var cmd = data.command || "npx skills add https://github.com/" + data._source + " --skill " + data._skill;
10
+ var skillId = data._skill || name;
11
+ var info = getInstalledInfo(skillId);
12
+
13
+ // Show back button in header
14
+ var backBtn = document.getElementById("skills-back-btn");
15
+ if (backBtn) backBtn.classList.remove("hidden");
16
+
17
+ var html = '<div class="skills-detail">';
18
+
19
+ // --- Main content (left) ---
20
+ html += '<div class="skills-detail-main">';
21
+ html += '<div class="skills-detail-name">' + escapeHtml(name) + '</div>';
22
+
23
+ if (desc) {
24
+ html += '<div class="skills-detail-desc">' + escapeHtml(desc) + '</div>';
25
+ }
26
+
27
+ html += '<div class="skills-detail-cmd">' +
28
+ '<code>' + escapeHtml(cmd) + '</code>' +
29
+ '<button class="skills-copy-btn" data-cmd="' + escapeHtml(cmd) + '">' + iconHtml("copy") + '</button>' +
30
+ '</div>';
31
+
32
+ // SKILL.md content (already rendered HTML from skills.sh)
33
+ if (data.skillMd) {
34
+ html += '<div class="skills-detail-md-wrap">' +
35
+ '<div class="skills-detail-section-title">SKILL.md</div>' +
36
+ '<div class="skills-detail-md">' + sanitizeSkillHtml(data.skillMd) + '</div>' +
37
+ '</div>';
38
+ }
39
+
40
+ html += '</div>'; // end main
41
+
42
+ // --- Sidebar (right) ---
43
+ html += '<div class="skills-detail-sidebar">';
44
+
45
+ // Weekly installs
46
+ if (data.weeklyInstalls) {
47
+ html += '<div class="skills-meta-block">' +
48
+ '<div class="skills-meta-label">Weekly Installs</div>' +
49
+ '<div class="skills-meta-value">' + escapeHtml(data.weeklyInstalls) + '</div>' +
50
+ '</div>';
51
+ }
52
+
53
+ // Repository
54
+ if (data.repository || data._source) {
55
+ var repo = data.repository || data._source;
56
+ html += '<div class="skills-meta-block">' +
57
+ '<div class="skills-meta-label">Repository</div>' +
58
+ '<div class="skills-meta-value small">' +
59
+ '<div class="skills-meta-repo">' +
60
+ iconHtml("external-link") +
61
+ '<a href="https://github.com/' + escapeHtml(repo) + '" target="_blank" rel="noopener">' + escapeHtml(repo) + '</a>' +
62
+ '</div>' +
63
+ '</div>' +
64
+ '</div>';
65
+ }
66
+
67
+ // GitHub stars
68
+ if (data.githubStars) {
69
+ html += '<div class="skills-meta-block">' +
70
+ '<div class="skills-meta-label">GitHub Stars</div>' +
71
+ '<div class="skills-meta-value">' + escapeHtml(data.githubStars) + '</div>' +
72
+ '</div>';
73
+ }
74
+
75
+ // First seen
76
+ if (data.firstSeen) {
77
+ html += '<div class="skills-meta-block">' +
78
+ '<div class="skills-meta-label">First Seen</div>' +
79
+ '<div class="skills-meta-value small">' + escapeHtml(data.firstSeen) + '</div>' +
80
+ '</div>';
81
+ }
82
+
83
+ // Security audits
84
+ if (data.audits && data.audits.length) {
85
+ html += '<div class="skills-meta-block">' +
86
+ '<div class="skills-meta-label">Security Audits</div>' +
87
+ '<div class="skills-audit-list">';
88
+ for (var a = 0; a < data.audits.length; a++) {
89
+ var audit = data.audits[a];
90
+ html += '<div class="skills-audit-item">' +
91
+ '<span>' + escapeHtml(audit.name) + '</span>' +
92
+ '<span class="skills-audit-badge ' + escapeHtml(audit.status) + '">' + escapeHtml(audit.status.toUpperCase()) + '</span>' +
93
+ '</div>';
94
+ }
95
+ html += '</div></div>';
96
+ }
97
+
98
+ // Installed on
99
+ if (data.installedOn && data.installedOn.length) {
100
+ html += '<div class="skills-meta-block">' +
101
+ '<div class="skills-meta-label">Installed On</div>' +
102
+ '<div class="skills-platform-list">';
103
+ for (var p = 0; p < data.installedOn.length; p++) {
104
+ var plat = data.installedOn[p];
105
+ html += '<div class="skills-platform-item">' +
106
+ '<span>' + escapeHtml(plat.name) + '</span>' +
107
+ '<span class="skills-platform-count">' + escapeHtml(plat.installs) + '</span>' +
108
+ '</div>';
109
+ }
110
+ html += '</div></div>';
111
+ }
112
+
113
+ // Install buttons or installed status
114
+ html += '<div class="skills-meta-block">' +
115
+ buildInstallButtonsHtml(skillId, data._source, data._skill) +
116
+ '</div>';
117
+
118
+ html += '</div>'; // end sidebar
119
+ html += '</div>'; // end detail
120
+
121
+ content().innerHTML = html;
122
+ refreshIcons(content());
123
+
124
+ // Copy button handler
125
+ var copyBtn = content().querySelector(".skills-copy-btn");
126
+ if (copyBtn) {
127
+ copyBtn.addEventListener("click", function (e) {
128
+ e.stopPropagation();
129
+ var cmdText = copyBtn.dataset.cmd;
130
+ copyToClipboard(cmdText);
131
+ copyBtn.classList.add("copied");
132
+ setTimeout(function () { copyBtn.classList.remove("copied"); }, 1500);
133
+ });
134
+ }
135
+
136
+ // Install button handlers
137
+ var installActions = content().querySelector(".skills-install-actions");
138
+ if (installActions) {
139
+ attachInstallHandlers(installActions, data._source, data._skill);
140
+ }
141
+ }
142
+
143
+ function sanitizeSkillHtml(rawHtml) {
144
+ if (typeof DOMPurify !== "undefined") {
145
+ return DOMPurify.sanitize(rawHtml);
146
+ }
147
+ return rawHtml;
148
+ }
@@ -0,0 +1,10 @@
1
+ import { store } from './store.js';
2
+ export function skillState() {
3
+ var value = store.get('skillsUi');
4
+ if (!value) { value = { activeTab: 'installed', currentView: 'list', skillsData: {}, installedSkills: {}, searchQuery: '', searchTimer: null, searchCache: {} }; store.set({ skillsUi: value }); }
5
+ return value;
6
+ }
7
+ export function content() { return document.getElementById('skills-content'); }
8
+ export function basePath() { return store.get('basePath') || ''; }
9
+ export function getInstalledInfo(skillId) { return skillState().installedSkills[skillId] || null; }
10
+ export function scopeLabel(scope) { return scope === 'both' ? 'Global + Project' : scope === 'project' ? 'Project' : 'Global'; }