feat(skills): import SKILL.md bundles from public GitHub URLs (#2576)

* feat(skills): import SKILL.md bundles from public GitHub URLs

Supports GitHub tree/blob/raw links and skills.sh pages that resolve to GitHub.
Installs SKILL.md plus sibling text assets under data/skills/imported/.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): admin-gate URL import and validate redirect hosts

- require_admin on POST /api/skills/import-from-url (matches other skill admin routes)
- reject cross-host redirects after httpx follow_redirects
- test for redirect host validation

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): match Brain Add panel import/submit button styles

- Skill URL Import: theme-io-btn + download icon (same as memory Import)
- Add Skill submit: confirm-btn confirm-btn-primary

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): allow api.github.com during directory import

Real imports hit the GitHub contents API after redirects; whitelist
api.github.com and add regression tests. Shrink Import button with flex:none.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): align skill Import button with URL input row

Match memory-add-input height (28px) in memory-add-row and center the
download icon with flexbox instead of vertical-align hacks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): cancel modal-body margin on skill Import button

The skill Import button sits in .memory-add-row beside an input; the
global .modal-body button { margin-top: 6px } rule only affected buttons,
pushing Import down and misaligning the download icon. Reset margin-top
and match Memory Import SVG markup at 28px row height.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skills): surface GitHub API errors on URL import

Pass through GitHub response messages (especially 403 rate limits) as
SkillImportError instead of a generic download failure.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Giulio Zelante
2026-06-05 19:48:23 +02:00
committed by GitHub
parent 977daf0643
commit b448119919
7 changed files with 597 additions and 2 deletions
+33
View File
@@ -1818,6 +1818,35 @@ async function _showSkillSource(name) {
});
}
async function importSkillFromUrl() {
const input = document.getElementById('skill-import-url');
const url = (input?.value || '').trim();
if (!url) {
uiModule.showError('Paste a GitHub or skills.sh URL first');
return;
}
const btn = document.getElementById('skill-import-url-btn');
if (btn) btn.disabled = true;
try {
const res = await fetch(`${API}/api/skills/import-from-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);
if (input) input.value = '';
await loadSkills();
const name = data.skill?.name || 'skill';
uiModule.showToast(`Imported ${name} (${data.files || 1} file(s))`);
if (name) openSkill(name);
} catch (err) {
uiModule.showError('Import failed: ' + err.message);
} finally {
if (btn) btn.disabled = false;
}
}
async function addSkill() {
const name = document.getElementById('new-skill-name')?.value.trim()
|| document.getElementById('new-skill-title')?.value.trim();
@@ -1866,6 +1895,10 @@ async function addSkill() {
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('skill-import-url-btn')?.addEventListener('click', importSkillFromUrl);
document.getElementById('skill-import-url')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') importSkillFromUrl();
});
document.getElementById('add-skill-btn')?.addEventListener('click', addSkill);
document.getElementById('skills-search')?.addEventListener('input', renderSkillsList);
document.getElementById('skills-sort')?.addEventListener('change', (e) => {