clay-server 4.9.0-beta.1 → 5.0.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.
- package/lib/mcp-browser-proxy.js +37 -0
- package/lib/mcp-connection-store.js +39 -0
- package/lib/mcp-network.js +46 -0
- package/lib/mcp-oauth.js +65 -0
- package/lib/mcp-proxy.js +32 -0
- package/lib/mcp-remote-connections.js +215 -0
- package/lib/mcp-session-access.js +15 -0
- package/lib/project-mcp.js +130 -357
- package/lib/project-session-document.js +9 -3
- package/lib/project-user-message.js +2 -0
- package/lib/project.js +28 -45
- package/lib/public/app.js +1 -0
- package/lib/public/css/mcp-connections.css +86 -0
- package/lib/public/css/skills-workbench.css +74 -0
- package/lib/public/index.html +16 -15
- package/lib/public/modules/app-messages.js +2 -1
- package/lib/public/modules/mcp-connection-view.js +136 -0
- package/lib/public/modules/mcp-permissions.js +122 -0
- package/lib/public/modules/mcp-ui.js +85 -461
- package/lib/public/modules/skills-actions.js +47 -79
- package/lib/public/modules/skills-detail.js +32 -145
- package/lib/public/modules/skills-list-view.js +29 -0
- package/lib/public/modules/skills-state.js +4 -2
- package/lib/public/modules/skills.js +104 -399
- package/lib/public/modules/vendor-login-browser.js +16 -5
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +24 -30
- package/lib/sdk-mcp-descriptors.js +2 -1
- package/lib/vendor-login-browser-worker.js +18 -8
- package/lib/vendor-login-browser.js +10 -3
- package/lib/yoke/adapters/codex.js +3 -20
- package/package.json +6 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
-
import { escapeHtml,
|
|
2
|
+
import { escapeHtml, showToast } from './utils.js';
|
|
3
3
|
import { store } from './store.js';
|
|
4
|
-
import { skillState, content, basePath, getInstalledInfo, scopeLabel } from './skills-state.js';
|
|
4
|
+
import { skillState, content, basePath, getInstalledInfo, scopeLabel, patchSkills } from './skills-state.js';
|
|
5
5
|
import { getWs } from './ws-ref.js';
|
|
6
6
|
function reloadActiveSessionSkills() { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'reload_skills' })); }
|
|
7
7
|
function installSkill(source, skill, scope) {
|
|
@@ -19,11 +19,13 @@ function installSkill(source, skill, scope) {
|
|
|
19
19
|
method: "POST",
|
|
20
20
|
headers: { "Content-Type": "application/json" },
|
|
21
21
|
body: JSON.stringify({ url: url, skill: skill, scope: scope }),
|
|
22
|
-
}).catch(function () {
|
|
22
|
+
}).then(function (res) { if (!res.ok) throw new Error('Request rejected'); }).catch(function () {
|
|
23
|
+
showToast('Could not start installation. Try again.', 'error');
|
|
23
24
|
// Re-enable buttons on fetch error
|
|
24
25
|
for (var j = 0; j < btns.length; j++) {
|
|
25
26
|
btns[j].disabled = false;
|
|
26
27
|
btns[j].classList.remove("installing");
|
|
28
|
+
btns[j].textContent = "Add to " + scopeLabel(btns[j].dataset.scope).toLowerCase();
|
|
27
29
|
}
|
|
28
30
|
});
|
|
29
31
|
}
|
|
@@ -45,10 +47,10 @@ export function handleSkillInstalled(msg) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// Invalidate skills data cache so list refreshes with updated badges
|
|
48
|
-
|
|
50
|
+
patchSkills({ skillsData: {}, searchCache: {}, inventoryLoaded: false });
|
|
49
51
|
|
|
50
52
|
// If we're on the detail view for this skill, re-render it
|
|
51
|
-
if (skillState().currentView === "detail") {
|
|
53
|
+
if (skillState().currentView === "detail" && skillState().selectedSkill === skill) {
|
|
52
54
|
var detailEl = content().querySelector(".skills-detail");
|
|
53
55
|
if (detailEl) {
|
|
54
56
|
// Re-render the install section in the sidebar
|
|
@@ -67,12 +69,13 @@ export function handleSkillInstalled(msg) {
|
|
|
67
69
|
// Show error toast
|
|
68
70
|
showToast("Failed to install " + skill + (msg.error ? ": " + msg.error : ""), "error");
|
|
69
71
|
// Re-enable buttons
|
|
72
|
+
if (skillState().selectedSkill !== skill) return;
|
|
70
73
|
var btns = content().querySelectorAll(".skills-install-btn.installing");
|
|
71
74
|
for (var i = 0; i < btns.length; i++) {
|
|
72
75
|
btns[i].disabled = false;
|
|
73
76
|
btns[i].classList.remove("installing");
|
|
74
77
|
// We can't easily restore the original text, so just set generic labels
|
|
75
|
-
btns[i].innerHTML = iconHtml("
|
|
78
|
+
btns[i].innerHTML = iconHtml("plus") + " Add to " + scopeLabel(btns[i].dataset.scope).toLowerCase();
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -90,11 +93,12 @@ export function uninstallSkill(skill, scope) {
|
|
|
90
93
|
method: "POST",
|
|
91
94
|
headers: { "Content-Type": "application/json" },
|
|
92
95
|
body: JSON.stringify({ skill: skill, scope: scope }),
|
|
93
|
-
}).catch(function () {
|
|
96
|
+
}).then(function (res) { if (!res.ok) throw new Error('Request rejected'); }).catch(function () {
|
|
97
|
+
showToast('Could not remove this skill. Try again.', 'error');
|
|
94
98
|
if (unBtn) {
|
|
95
99
|
unBtn.disabled = false;
|
|
96
100
|
unBtn.classList.remove("uninstalling");
|
|
97
|
-
unBtn.
|
|
101
|
+
unBtn.textContent = "Remove";
|
|
98
102
|
}
|
|
99
103
|
});
|
|
100
104
|
}
|
|
@@ -118,10 +122,10 @@ export function handleSkillUninstalled(msg) {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
// Invalidate skills data cache so list refreshes
|
|
121
|
-
|
|
125
|
+
patchSkills({ skillsData: {}, searchCache: {}, inventoryLoaded: false });
|
|
122
126
|
|
|
123
127
|
// If we're on the detail view, re-render buttons
|
|
124
|
-
if (skillState().currentView === "detail") {
|
|
128
|
+
if (skillState().currentView === "detail" && skillState().selectedSkill === skill) {
|
|
125
129
|
var detailEl = content().querySelector(".skills-detail");
|
|
126
130
|
if (detailEl) {
|
|
127
131
|
updateDetailInstallButtons(skill);
|
|
@@ -138,90 +142,45 @@ export function handleSkillUninstalled(msg) {
|
|
|
138
142
|
} else {
|
|
139
143
|
showToast("Failed to uninstall " + skill + (msg.error ? ": " + msg.error : ""), "error");
|
|
140
144
|
// Re-enable the uninstall button
|
|
145
|
+
if (skillState().selectedSkill !== skill) return;
|
|
141
146
|
var unBtn = content().querySelector('.skills-uninstall-btn[data-scope="' + scope + '"]');
|
|
142
147
|
if (unBtn) {
|
|
143
148
|
unBtn.disabled = false;
|
|
144
149
|
unBtn.classList.remove("uninstalling");
|
|
145
|
-
unBtn.
|
|
150
|
+
unBtn.textContent = "Remove";
|
|
146
151
|
refreshIcons(unBtn);
|
|
147
152
|
}
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
function updateDetailInstallButtons(skillId) {
|
|
157
|
+
if (skillState().selectedSkill !== skillId) return;
|
|
158
|
+
var data = skillState().detailData || {}, block = content().querySelector('[data-skill-actions]');
|
|
159
|
+
var note = content().querySelector('.skills-local-note');
|
|
160
|
+
if (note) note.hidden = !getInstalledInfo(skillId);
|
|
161
|
+
if (!block) return;
|
|
162
|
+
block.innerHTML = data._installed ? buildLocalActionsHtml(skillId) : buildInstallButtonsHtml(skillId, data._source || '', skillId);
|
|
163
|
+
refreshIcons(); attachInstallHandlers(block, data._source || '', skillId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildLocalActionsHtml(skillId) {
|
|
152
167
|
var info = getInstalledInfo(skillId);
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
168
|
+
if (!info) return '<p class="skills-removed-status">Removed from your library.</p>';
|
|
169
|
+
if (info.source === 'clay-builtin') return '<div class="skills-installed-status">' + iconHtml('circle-check') + ' Built in to Clay</div>';
|
|
170
|
+
var scopes = info.scope === 'both' ? ['project', 'global'] : [info.scope];
|
|
171
|
+
return scopes.map(function (scope) { return '<div class="skills-installed-row"><span class="skills-installed-status compact">' + iconHtml('circle-check') + ' ' + scopeLabel(scope) + '</span><button type="button" class="skills-uninstall-btn" data-scope="' + scope + '" aria-label="Remove from ' + scopeLabel(scope) + '">Remove</button></div>'; }).join('');
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
export function buildInstallButtonsHtml(skillId, source, skill) {
|
|
173
175
|
var info = getInstalledInfo(skillId);
|
|
174
|
-
if (info && info.source ===
|
|
175
|
-
|
|
176
|
+
if (info && info.source === 'clay-builtin') return buildLocalActionsHtml(skillId);
|
|
176
177
|
var html = '<div class="skills-install-actions" data-detail-source="' + escapeHtml(source) + '" data-detail-skill="' + escapeHtml(skill) + '">';
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
html += '<
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
178
|
+
if (info) html += buildLocalActionsHtml(skillId);
|
|
179
|
+
['project', 'global'].forEach(function (scope) {
|
|
180
|
+
if (info && (info.scope === scope || info.scope === 'both')) return;
|
|
181
|
+
html += '<button type="button" class="skills-install-btn' + (scope === 'global' ? ' secondary' : '') + '" data-scope="' + scope + '">' + iconHtml('plus') + ' Add to ' + scopeLabel(scope).toLowerCase() + '</button>';
|
|
182
|
+
});
|
|
183
|
+
return html + '</div>';
|
|
225
184
|
}
|
|
226
185
|
|
|
227
186
|
export function attachInstallHandlers(container, source, skill) {
|
|
@@ -239,7 +198,16 @@ export function attachInstallHandlers(container, source, skill) {
|
|
|
239
198
|
(function (btn) {
|
|
240
199
|
btn.addEventListener("click", function () {
|
|
241
200
|
var scope = btn.dataset.scope;
|
|
242
|
-
|
|
201
|
+
if (btn.dataset.confirmed === 'true') {
|
|
202
|
+
btn.dataset.confirmed = '';
|
|
203
|
+
var existingCancel = btn.parentElement.querySelector('.skills-cancel-remove');
|
|
204
|
+
if (existingCancel) existingCancel.remove();
|
|
205
|
+
uninstallSkill(skill, scope); return;
|
|
206
|
+
}
|
|
207
|
+
btn.dataset.confirmed = 'true'; btn.textContent = 'Confirm removal';
|
|
208
|
+
var cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'skills-cancel-remove'; cancel.textContent = 'Cancel';
|
|
209
|
+
cancel.onclick = function () { btn.dataset.confirmed = ''; btn.textContent = 'Remove'; cancel.remove(); };
|
|
210
|
+
btn.after(cancel);
|
|
243
211
|
});
|
|
244
212
|
})(unBtns[j]);
|
|
245
213
|
}
|
|
@@ -1,148 +1,35 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
-
import { escapeHtml, copyToClipboard
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
2
|
+
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
3
|
+
import { skillState, content, getInstalledInfo, scopeLabel, patchSkills } from './skills-state.js';
|
|
4
|
+
import { buildInstallButtonsHtml, buildLocalActionsHtml, attachInstallHandlers } from './skills-actions.js';
|
|
5
|
+
function text(value) { return escapeHtml(String(value || '')); }
|
|
6
6
|
export function renderDetail(data) {
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
html += '<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'</
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
7
|
+
patchSkills({ detailData: data });
|
|
8
|
+
var name = data.name || data._skill || 'Skill', skill = data._skill || name;
|
|
9
|
+
var info = getInstalledInfo(skill), builtin = info && info.source === 'clay-builtin';
|
|
10
|
+
var source = data._source || data.repository || '';
|
|
11
|
+
var cmd = data.command || 'npx skills add https://github.com/' + source + ' --skill ' + skill;
|
|
12
|
+
var html = '<article class="skills-detail" data-local="' + !!data._installed + '"><header class="skills-detail-heading"><span class="skills-detail-icon">' + iconHtml(builtin ? 'sparkles' : 'book-open') + '</span><h2 class="skills-detail-name" tabindex="-1">' + text(name) + '</h2>';
|
|
13
|
+
if (data.description) html += '<p class="skills-detail-desc">' + text(data.description) + '</p>';
|
|
14
|
+
html += '<div class="skills-detail-byline">' + (builtin ? 'Included with Clay' : data._installed ? text(scopeLabel(info && info.scope || data.scope)) : text(source)) + (data.weeklyInstalls ? '<span>' + text(data.weeklyInstalls) + ' weekly installs</span>' : '') + '</div></header>';
|
|
15
|
+
html += '<section class="skills-placement"><div class="skills-placement-heading"><h3>' + (builtin ? 'Ready to use' : data._installed ? 'Availability' : 'Add to your workflow') + '</h3><p>' + (builtin ? 'This skill comes with Clay and is maintained with the app.' : 'Project skills stay with this workspace. Global skills are available across your projects.') + '</p></div><div data-skill-actions>' + (data._installed ? buildLocalActionsHtml(skill) : buildInstallButtonsHtml(skill, source, skill)) + '</div></section>';
|
|
16
|
+
if (data.skillMd) html += '<section class="skills-detail-md-wrap"><h3 class="skills-detail-section-title">Instructions</h3><div class="skills-detail-md">' + (typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(data.skillMd) : text(data.skillMd)) + '</div></section>';
|
|
17
|
+
if (data._installed && !data.skillMd) html += '<div class="skills-local-note">' + iconHtml('circle-check') + '<p>Your agent can discover this skill when it is relevant to a task. You can also ask for it by name.</p></div>';
|
|
18
|
+
if (source || data.path || data.version || data.audits || data.githubStars || data.firstSeen || data.installedOn) {
|
|
19
|
+
html += '<details class="skills-metadata"><summary>Skill information</summary><dl>';
|
|
20
|
+
if (source) html += '<dt>Repository</dt><dd><a href="https://github.com/' + text(source) + '" target="_blank" rel="noopener noreferrer">' + text(source) + '</a></dd>';
|
|
21
|
+
if (data.version) html += '<dt>Version</dt><dd>' + text(data.version) + '</dd>';
|
|
22
|
+
if (data.path) html += '<dt>Location</dt><dd><code>' + text(data.path) + '</code></dd>';
|
|
23
|
+
if (data.githubStars) html += '<dt>GitHub stars</dt><dd>' + text(data.githubStars) + '</dd>';
|
|
24
|
+
if (data.firstSeen) html += '<dt>First seen</dt><dd>' + text(data.firstSeen) + '</dd>';
|
|
25
|
+
(data.audits || []).forEach(function (audit) { html += '<dt>' + text(audit.name) + '</dt><dd>' + text(audit.status) + '</dd>'; });
|
|
26
|
+
(data.installedOn || []).forEach(function (platform) { html += '<dt>' + text(platform.name) + '</dt><dd>' + text(platform.installs) + ' installs</dd>'; });
|
|
27
|
+
html += '</dl></details>';
|
|
28
|
+
}
|
|
29
|
+
if (!data._installed) html += '<details class="skills-manual"><summary>Install from your terminal</summary><div class="skills-detail-cmd"><code>' + text(cmd) + '</code><button type="button" class="skills-copy-btn" aria-label="Copy installation command">' + iconHtml('copy') + '</button></div></details>';
|
|
30
|
+
html += '</article>';
|
|
31
|
+
content().innerHTML = html; refreshIcons();
|
|
32
|
+
var copy = content().querySelector('.skills-copy-btn'); if (copy) copy.onclick = function () { copyToClipboard(cmd); };
|
|
33
|
+
attachInstallHandlers(content().querySelector('[data-skill-actions]'), source, skill);
|
|
34
|
+
content().querySelector('h2').focus({ preventScroll: true });
|
|
148
35
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
+
import { escapeHtml } from './utils.js';
|
|
3
|
+
import { content, skillState, getInstalledInfo, scopeLabel } from './skills-state.js';
|
|
4
|
+
|
|
5
|
+
function text(value) { return escapeHtml(String(value || '')); }
|
|
6
|
+
function count(value) { return typeof value === 'number' ? new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(value) : text(value || '0'); }
|
|
7
|
+
export function renderSkillList(skills, openSkill, discover) {
|
|
8
|
+
var state = skillState(), installed = state.activeTab === 'installed';
|
|
9
|
+
var query = state.searchQuery.toLowerCase();
|
|
10
|
+
if (installed) skills = Object.keys(state.installedSkills).sort().map(function (name) { return Object.assign({ name: name, skillId: name }, state.installedSkills[name]); }).filter(function (row) { return (row.name + ' ' + (row.description || '')).toLowerCase().includes(query); });
|
|
11
|
+
var html = '<div class="skills-list-intro"><span>' + (installed ? 'Available to your agent' : state.searchQuery ? 'Search results' : 'Explore community skills') + '</span><span>' + skills.length + (skills.length === 1 ? ' skill' : ' skills') + '</span></div>';
|
|
12
|
+
if (!skills.length) {
|
|
13
|
+
html += '<div class="skills-library-empty">' + iconHtml(query ? 'search' : 'book-open') + '<h3>' + (query ? 'No matching skills' : 'Build your skill library') + '</h3><p>' + (query ? 'Try a different name or a shorter search.' : 'Discover reusable workflows and add the ones that fit your work.') + '</p>' + (!query && installed ? '<button type="button" class="skills-browse-btn">Discover skills ' + iconHtml('arrow-right') + '</button>' : '') + '</div>';
|
|
14
|
+
} else html += '<div class="skills-library-list">' + skills.map(function (row, index) {
|
|
15
|
+
var info = installed ? row : getInstalledInfo(row.skillId || row.name);
|
|
16
|
+
var builtin = info && info.source === 'clay-builtin';
|
|
17
|
+
return '<button type="button" class="skills-library-row" data-skill-index="' + index + '"><span class="skills-library-icon">' + iconHtml(builtin ? 'sparkles' : 'book-open') + '</span><span class="skills-library-info"><span class="skills-library-name">' + text(row.name) + '</span>' + (row.description ? '<span class="skills-library-desc">' + text(row.description) + '</span>' : '') + '<span class="skills-library-meta">' + (installed ? (builtin ? 'Included with Clay' : text(scopeLabel(row.scope))) : text(row.source)) + '</span></span><span class="skills-library-end">' + (info ? '<span class="skills-library-badge">' + (builtin ? 'Built-in' : installed ? '' : 'Installed') + '</span>' : '<span class="skills-library-installs">' + iconHtml('download') + count(row.installs) + '</span>') + iconHtml('chevron-right') + '</span></button>';
|
|
18
|
+
}).join('') + '</div>';
|
|
19
|
+
if (!installed) html += '<p class="skills-catalog-credit">Community catalog from <a href="https://skills.sh" target="_blank" rel="noopener noreferrer">skills.sh</a></p>';
|
|
20
|
+
content().innerHTML = html;
|
|
21
|
+
content().querySelectorAll('[data-skill-index]').forEach(function (button) { button.onclick = function () { openSkill(skills[Number(button.dataset.skillIndex)], installed); }; });
|
|
22
|
+
var browse = content().querySelector('.skills-browse-btn'); if (browse) browse.onclick = discover;
|
|
23
|
+
refreshIcons();
|
|
24
|
+
}
|
|
25
|
+
export function renderSkillFeedback(message, retry) {
|
|
26
|
+
content().innerHTML = '<div class="skills-library-empty" role="status">' + iconHtml(retry ? 'circle-alert' : 'loader-circle') + '<p>' + text(message) + '</p>' + (retry ? '<button type="button" class="skills-browse-btn">Try again</button>' : '') + '</div>';
|
|
27
|
+
var button = content().querySelector('button'); if (button) button.onclick = retry;
|
|
28
|
+
refreshIcons();
|
|
29
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
export function skillState() {
|
|
3
3
|
var value = store.get('skillsUi');
|
|
4
|
-
if (!value) { value = { activeTab: 'installed', currentView: 'list', skillsData: {}, installedSkills: {}, searchQuery: '', searchTimer: null, searchCache: {} }; store.set({ skillsUi: value }); }
|
|
4
|
+
if (!value) { value = { activeTab: 'installed', discoverySort: 'all', requestVersion: 0, currentView: 'list', skillsData: {}, installedSkills: {}, searchQuery: '', searchTimer: null, searchCache: {} }; store.set({ skillsUi: value }); }
|
|
5
5
|
return value;
|
|
6
6
|
}
|
|
7
7
|
export function content() { return document.getElementById('skills-content'); }
|
|
8
8
|
export function basePath() { return store.get('basePath') || ''; }
|
|
9
9
|
export function getInstalledInfo(skillId) { return skillState().installedSkills[skillId] || null; }
|
|
10
|
-
export function scopeLabel(scope) { return scope === 'both' ? '
|
|
10
|
+
export function scopeLabel(scope) { return scope === 'both' ? 'This project + All projects' : scope === 'project' ? 'This project' : 'All projects'; }
|
|
11
|
+
|
|
12
|
+
export function patchSkills(values) { store.set({ skillsUi: Object.assign({}, skillState(), values) }); }
|