Introduce token-based author authentication (register/verify API), skill forking with EditGate protection, tag metadata on skills, and download/push stats. Enhanced push scripts with token auth and per-skill filtering. Updated UI with stats, tags, and author info on skill cards.
442 lines
28 KiB
JavaScript
442 lines
28 KiB
JavaScript
import { useSSRContext, defineComponent, computed, ref, watch, mergeProps } from 'vue';
|
|
import { marked } from 'marked';
|
|
import { ssrRenderAttrs, ssrRenderAttr, ssrRenderClass, ssrInterpolate, ssrRenderList, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual } from 'vue/server-renderer';
|
|
import { _ as _export_sfc } from './_plugin-vue_export-helper_CEgY73aA.mjs';
|
|
|
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
__name: "SkillEditor",
|
|
props: {
|
|
mode: {},
|
|
slug: {},
|
|
forkOf: {},
|
|
initialName: {},
|
|
initialDescription: {},
|
|
initialAllowedTools: {},
|
|
initialArgumentHint: {},
|
|
initialModel: {},
|
|
initialUserInvocable: { type: Boolean },
|
|
initialDisableModelInvocation: { type: Boolean },
|
|
initialContext: {},
|
|
initialAgent: {},
|
|
initialHooks: {},
|
|
initialBody: {},
|
|
initialAuthor: {},
|
|
initialAuthorEmail: {},
|
|
initialTags: {},
|
|
availableTools: {},
|
|
availableModels: {},
|
|
availableTags: {}
|
|
},
|
|
setup(__props, { expose: __expose }) {
|
|
__expose();
|
|
const props = __props;
|
|
const AVAILABLE_TOOLS = props.availableTools ?? [
|
|
"Bash",
|
|
"Read",
|
|
"Write",
|
|
"Edit",
|
|
"Glob",
|
|
"Grep",
|
|
"WebFetch",
|
|
"WebSearch",
|
|
"Task",
|
|
"NotebookEdit"
|
|
];
|
|
const AVAILABLE_MODELS = props.availableModels ?? [
|
|
{ id: "claude-opus-4-6", display_name: "Claude Opus 4.6" },
|
|
{ id: "claude-sonnet-4-5-20250929", display_name: "Claude Sonnet 4.5" },
|
|
{ id: "claude-haiku-4-5-20251001", display_name: "Claude Haiku 4.5" }
|
|
];
|
|
const isFork = computed(() => Boolean(props.forkOf));
|
|
const name = ref(props.initialName || "");
|
|
const description = ref(props.initialDescription || "");
|
|
const argumentHint = ref(props.initialArgumentHint || "");
|
|
const model = ref(props.initialModel || "");
|
|
const userInvocable = ref(props.initialUserInvocable ?? true);
|
|
const disableModelInvocation = ref(props.initialDisableModelInvocation ?? false);
|
|
const context = ref(props.initialContext || "");
|
|
const agent = ref(props.initialAgent || "");
|
|
const hooksJson = ref(props.initialHooks || "");
|
|
const tags = ref(
|
|
props.initialTags ? props.initialTags.split(",").map((t) => t.trim()).filter(Boolean) : []
|
|
);
|
|
const tagQuery = ref("");
|
|
const tagSuggestionsOpen = ref(false);
|
|
const tagInputEl = ref();
|
|
const knownTags = props.availableTags ? props.availableTags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
const body = ref(props.initialBody || "");
|
|
const saving = ref(false);
|
|
const error = ref("");
|
|
const forkAuthorName = ref("");
|
|
const forkAuthorEmail = ref("");
|
|
const authorToken = ref(
|
|
typeof localStorage !== "undefined" ? localStorage.getItem("skillshere-token") || "" : ""
|
|
);
|
|
const selectedTools = ref(new Set(
|
|
props.initialAllowedTools ? props.initialAllowedTools.split(",").map((t) => t.trim()).filter(Boolean) : []
|
|
));
|
|
function toggleTool(tool) {
|
|
if (selectedTools.value.has(tool)) {
|
|
selectedTools.value.delete(tool);
|
|
} else {
|
|
selectedTools.value.add(tool);
|
|
}
|
|
selectedTools.value = new Set(selectedTools.value);
|
|
}
|
|
const tagSuggestions = computed(() => {
|
|
const q = tagQuery.value.toLowerCase().trim();
|
|
const current = new Set(tags.value.map((t) => t.toLowerCase()));
|
|
const matches = knownTags.filter((t) => !current.has(t.toLowerCase()) && (!q || t.toLowerCase().includes(q)));
|
|
if (q && !current.has(q) && !matches.some((m) => m.toLowerCase() === q)) {
|
|
matches.push(q);
|
|
}
|
|
return matches;
|
|
});
|
|
const isNewTag = (tag) => !knownTags.some((t) => t.toLowerCase() === tag.toLowerCase());
|
|
function addTag(tag) {
|
|
const normalized = tag.trim().toLowerCase();
|
|
if (normalized && !tags.value.some((t) => t.toLowerCase() === normalized)) {
|
|
tags.value.push(normalized);
|
|
}
|
|
tagQuery.value = "";
|
|
tagInputEl.value?.focus();
|
|
}
|
|
function removeTag(idx) {
|
|
tags.value.splice(idx, 1);
|
|
}
|
|
function onTagKeydown(e) {
|
|
if (e.key === "Enter" || e.key === ",") {
|
|
e.preventDefault();
|
|
if (tagQuery.value.trim()) {
|
|
addTag(tagQuery.value);
|
|
}
|
|
} else if (e.key === "Backspace" && !tagQuery.value && tags.value.length) {
|
|
tags.value.pop();
|
|
}
|
|
}
|
|
let tagBlurTimer;
|
|
function onTagBlur() {
|
|
tagBlurTimer = setTimeout(() => {
|
|
tagSuggestionsOpen.value = false;
|
|
}, 200);
|
|
}
|
|
function onTagSuggestionClick(tag) {
|
|
clearTimeout(tagBlurTimer);
|
|
addTag(tag);
|
|
}
|
|
const computedSlug = computed(() => {
|
|
if (props.mode === "edit" && props.slug) return props.slug;
|
|
return name.value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "my-skill";
|
|
});
|
|
const slugMatchesOriginal = computed(() => {
|
|
if (!props.forkOf) return false;
|
|
return computedSlug.value === props.forkOf;
|
|
});
|
|
const bodyLines = computed(() => body.value.split("\n").length);
|
|
let previewHtml = ref("");
|
|
let debounceTimer;
|
|
watch(body, (val) => {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(async () => {
|
|
previewHtml.value = await marked(val || "");
|
|
}, 300);
|
|
}, { immediate: true });
|
|
function buildContent() {
|
|
const tools = [...selectedTools.value];
|
|
const lines = ["---"];
|
|
lines.push(`name: ${name.value}`);
|
|
if (description.value) lines.push(`description: ${description.value}`);
|
|
if (isFork.value) {
|
|
if (forkAuthorName.value) lines.push(`author: ${forkAuthorName.value}`);
|
|
if (forkAuthorEmail.value) lines.push(`author-email: ${forkAuthorEmail.value}`);
|
|
lines.push(`fork-of: ${props.forkOf}`);
|
|
} else {
|
|
if (props.initialAuthor) lines.push(`author: ${props.initialAuthor}`);
|
|
if (props.initialAuthorEmail) lines.push(`author-email: ${props.initialAuthorEmail}`);
|
|
}
|
|
if (argumentHint.value) lines.push(`argument-hint: ${argumentHint.value}`);
|
|
if (tools.length > 0) lines.push(`allowed-tools: ${tools.join(", ")}`);
|
|
if (tags.value.length > 0) lines.push(`tags: ${tags.value.join(", ")}`);
|
|
if (model.value) lines.push(`model: ${model.value}`);
|
|
if (userInvocable.value === false) lines.push("user-invocable: false");
|
|
if (disableModelInvocation.value) lines.push("disable-model-invocation: true");
|
|
if (context.value) lines.push(`context: ${context.value}`);
|
|
if (agent.value) lines.push(`agent: ${agent.value}`);
|
|
if (hooksJson.value.trim()) {
|
|
try {
|
|
const parsed = JSON.parse(hooksJson.value.trim());
|
|
lines.push(`hooks: ${JSON.stringify(parsed)}`);
|
|
} catch {
|
|
}
|
|
}
|
|
lines.push("---");
|
|
return lines.join("\n") + "\n\n" + body.value.trim() + "\n";
|
|
}
|
|
async function save() {
|
|
saving.value = true;
|
|
error.value = "";
|
|
try {
|
|
const content = buildContent();
|
|
const headers = { "Content-Type": "application/json" };
|
|
if (!isFork.value && authorToken.value) {
|
|
headers["Authorization"] = `Bearer ${authorToken.value}`;
|
|
}
|
|
if (props.mode === "create") {
|
|
const res = await fetch("/api/skills", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ slug: computedSlug.value, content })
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || "Failed to create skill");
|
|
}
|
|
window.location.href = `/${computedSlug.value}`;
|
|
} else {
|
|
const res = await fetch(`/api/skills/${props.slug}`, {
|
|
method: "PUT",
|
|
headers,
|
|
body: JSON.stringify({ content })
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || "Failed to update skill");
|
|
}
|
|
window.location.href = `/${props.slug}`;
|
|
}
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : "Something went wrong";
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
const __returned__ = { props, AVAILABLE_TOOLS, AVAILABLE_MODELS, isFork, name, description, argumentHint, model, userInvocable, disableModelInvocation, context, agent, hooksJson, tags, tagQuery, tagSuggestionsOpen, tagInputEl, knownTags, body, saving, error, forkAuthorName, forkAuthorEmail, authorToken, selectedTools, toggleTool, tagSuggestions, isNewTag, addTag, removeTag, onTagKeydown, get tagBlurTimer() {
|
|
return tagBlurTimer;
|
|
}, set tagBlurTimer(v) {
|
|
tagBlurTimer = v;
|
|
}, onTagBlur, onTagSuggestionClick, computedSlug, slugMatchesOriginal, bodyLines, get previewHtml() {
|
|
return previewHtml;
|
|
}, set previewHtml(v) {
|
|
previewHtml = v;
|
|
}, get debounceTimer() {
|
|
return debounceTimer;
|
|
}, set debounceTimer(v) {
|
|
debounceTimer = v;
|
|
}, buildContent, save };
|
|
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
|
|
return __returned__;
|
|
}
|
|
});
|
|
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
|
|
_push(`<form${ssrRenderAttrs(mergeProps({ class: "space-y-6" }, _attrs))}>`);
|
|
if ($setup.isFork) {
|
|
_push(`<div class="rounded-xl border border-[var(--color-accent-500)]/20 bg-[var(--color-accent-500)]/5 p-4 space-y-3"><p class="text-sm text-[var(--color-accent-400)]">Claim this fork as yours. It will stay open for editing until you push from CLI, which registers a token and locks it to you.</p><div class="grid gap-3 sm:grid-cols-2"><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Your Name</label><input${ssrRenderAttr("value", $setup.forkAuthorName)} type="text" placeholder="Jane Doe" class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Your Email</label><input${ssrRenderAttr("value", $setup.forkAuthorEmail)} type="email" placeholder="jane@example.com" class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"></div></div></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
if ($setup.isFork && $setup.slugMatchesOriginal) {
|
|
_push(`<div class="rounded-xl border border-amber-500/20 bg-amber-500/5 p-4"><p class="text-sm text-amber-400">Change the <strong>name</strong> to generate a different slug. You can't save a fork with the same slug as the original.</p></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`<div class="grid gap-4 sm:grid-cols-2"><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Name</label><input${ssrRenderAttr("value", $setup.name)} type="text" required maxlength="64" placeholder="My Awesome Skill" class="${ssrRenderClass([
|
|
"w-full rounded-xl border px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:outline-none focus:ring-1 transition-all",
|
|
$setup.isFork && $setup.slugMatchesOriginal ? "border-amber-500/30 bg-[var(--color-surface-100)] focus:border-amber-500/50 focus:ring-amber-500/20" : "border-white/[0.06] bg-[var(--color-surface-100)] focus:border-[var(--color-accent-500)]/50 focus:ring-[var(--color-accent-500)]/20"
|
|
])}"><p class="mt-1.5 text-xs text-gray-600 flex justify-between"><span>Slug: <code class="${ssrRenderClass(["font-mono", $setup.isFork && $setup.slugMatchesOriginal ? "text-amber-500" : "text-gray-500"])}">${ssrInterpolate($setup.computedSlug)}</code></span><span class="${ssrRenderClass($setup.name.length > 58 ? "text-amber-500" : "")}">${ssrInterpolate($setup.name.length)}/64</span></p></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Description</label><input${ssrRenderAttr("value", $setup.description)} type="text" maxlength="200" placeholder="Brief description of what this skill does" class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"><p class="${ssrRenderClass([$setup.description.length > 180 ? "text-amber-500" : "", "mt-1.5 text-xs text-gray-600 text-right"])}">${ssrInterpolate($setup.description.length)}/200</p></div></div><div class="relative"><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Tags</label><div class="flex flex-wrap items-center gap-1.5 rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-3 py-2 min-h-[42px] cursor-text focus-within:border-[var(--color-accent-500)]/50 focus-within:ring-1 focus-within:ring-[var(--color-accent-500)]/20 transition-all"><!--[-->`);
|
|
ssrRenderList($setup.tags, (tag, i) => {
|
|
_push(`<span class="inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-500)]/15 border border-[var(--color-accent-500)]/25 pl-2.5 pr-1.5 py-0.5 text-xs font-medium text-[var(--color-accent-400)]">${ssrInterpolate(tag)} <button type="button" class="rounded-full p-0.5 hover:bg-[var(--color-accent-500)]/30 transition-colors"><svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg></button></span>`);
|
|
});
|
|
_push(`<!--]--><input${ssrRenderAttr("value", $setup.tagQuery)} type="text" placeholder="Add tag..." class="flex-1 min-w-[80px] bg-transparent text-sm text-white placeholder-gray-600 outline-none"></div>`);
|
|
if ($setup.tagSuggestionsOpen && $setup.tagSuggestions.length > 0) {
|
|
_push(`<div class="absolute z-10 mt-1 w-full max-h-40 overflow-auto rounded-xl border border-white/[0.08] bg-[var(--color-surface-200)] shadow-xl"><!--[-->`);
|
|
ssrRenderList($setup.tagSuggestions, (s) => {
|
|
_push(`<button type="button" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-300 hover:bg-white/[0.06] hover:text-white transition-colors text-left">`);
|
|
if ($setup.isNewTag(s)) {
|
|
_push(`<span class="text-[var(--color-accent-500)] text-xs">+</span>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(` ${ssrInterpolate(s)} `);
|
|
if ($setup.isNewTag(s)) {
|
|
_push(`<span class="text-xs text-gray-600">(new)</span>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</button>`);
|
|
});
|
|
_push(`<!--]--></div>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`<p class="mt-1.5 text-xs text-gray-600">Type and press Enter or comma. Click suggestions to add.</p></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Allowed Tools</label><div class="flex flex-wrap gap-1.5 rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-3 py-2.5 min-h-[42px]"><!--[-->`);
|
|
ssrRenderList($setup.AVAILABLE_TOOLS, (tool) => {
|
|
_push(`<button type="button" class="${ssrRenderClass([
|
|
"rounded-md px-2.5 py-1 text-xs font-medium transition-all",
|
|
$setup.selectedTools.has(tool) ? "bg-[var(--color-accent-500)] text-white shadow-sm" : "bg-white/[0.04] border border-white/[0.06] text-gray-500 hover:text-gray-300 hover:bg-white/[0.08]"
|
|
])}">${ssrInterpolate(tool)}</button>`);
|
|
});
|
|
_push(`<!--]--></div></div><div class="grid gap-4 sm:grid-cols-3"><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Model</label><select class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"><option value=""${ssrIncludeBooleanAttr(Array.isArray($setup.model) ? ssrLooseContain($setup.model, "") : ssrLooseEqual($setup.model, "")) ? " selected" : ""}>Default</option><!--[-->`);
|
|
ssrRenderList($setup.AVAILABLE_MODELS, (m) => {
|
|
_push(`<option${ssrRenderAttr("value", m.id)}${ssrIncludeBooleanAttr(Array.isArray($setup.model) ? ssrLooseContain($setup.model, m.id) : ssrLooseEqual($setup.model, m.id)) ? " selected" : ""}>${ssrInterpolate(m.display_name)}</option>`);
|
|
});
|
|
_push(`<!--]--></select></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Argument Hint</label><input${ssrRenderAttr("value", $setup.argumentHint)} type="text" placeholder="e.g. <file-path>" class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Agent</label><select class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"><option value=""${ssrIncludeBooleanAttr(Array.isArray($setup.agent) ? ssrLooseContain($setup.agent, "") : ssrLooseEqual($setup.agent, "")) ? " selected" : ""}>general-purpose (default)</option><option value="Explore"${ssrIncludeBooleanAttr(Array.isArray($setup.agent) ? ssrLooseContain($setup.agent, "Explore") : ssrLooseEqual($setup.agent, "Explore")) ? " selected" : ""}>Explore</option><option value="Plan"${ssrIncludeBooleanAttr(Array.isArray($setup.agent) ? ssrLooseContain($setup.agent, "Plan") : ssrLooseEqual($setup.agent, "Plan")) ? " selected" : ""}>Plan</option></select></div></div><div class="flex flex-wrap gap-6"><label class="flex items-center gap-2.5 cursor-pointer group"><input type="checkbox"${ssrIncludeBooleanAttr(Array.isArray($setup.userInvocable) ? ssrLooseContain($setup.userInvocable, null) : $setup.userInvocable) ? " checked" : ""} class="sr-only peer"><div class="h-5 w-9 rounded-full bg-white/[0.06] border border-white/[0.06] peer-checked:bg-[var(--color-accent-500)] peer-checked:border-[var(--color-accent-500)] relative transition-all after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:h-4 after:w-4 after:rounded-full after:bg-gray-400 after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div><span class="text-xs text-gray-500 group-hover:text-gray-300 transition-colors">User Invocable <span class="text-gray-600">(show in /menu)</span></span></label><label class="flex items-center gap-2.5 cursor-pointer group"><input type="checkbox"${ssrIncludeBooleanAttr(Array.isArray($setup.disableModelInvocation) ? ssrLooseContain($setup.disableModelInvocation, null) : $setup.disableModelInvocation) ? " checked" : ""} class="sr-only peer"><div class="h-5 w-9 rounded-full bg-white/[0.06] border border-white/[0.06] peer-checked:bg-[var(--color-accent-500)] peer-checked:border-[var(--color-accent-500)] relative transition-all after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:h-4 after:w-4 after:rounded-full after:bg-gray-400 after:transition-all peer-checked:after:translate-x-4 peer-checked:after:bg-white"></div><span class="text-xs text-gray-500 group-hover:text-gray-300 transition-colors">Disable Model Invocation <span class="text-gray-600">(manual only)</span></span></label></div><div><label class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Context</label><select class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"><option value=""${ssrIncludeBooleanAttr(Array.isArray($setup.context) ? ssrLooseContain($setup.context, "") : ssrLooseEqual($setup.context, "")) ? " selected" : ""}>Inline (default)</option><option value="fork"${ssrIncludeBooleanAttr(Array.isArray($setup.context) ? ssrLooseContain($setup.context, "fork") : ssrLooseEqual($setup.context, "fork")) ? " selected" : ""}>Fork (run in subagent)</option></select><p class="mt-1.5 text-xs text-gray-600">Fork runs the skill in an isolated subagent context</p></div><details class="group"><summary class="text-xs font-medium uppercase tracking-wider text-gray-500 cursor-pointer hover:text-gray-400 transition-colors">Hooks (advanced)</summary><div class="mt-3"><textarea rows="4" placeholder="{ "preToolExecution": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "echo pre" }] }] }" class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-3 font-mono text-xs text-white placeholder-gray-700 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all resize-y">${ssrInterpolate($setup.hooksJson)}</textarea><p class="mt-1.5 text-xs text-gray-600">JSON object. Leave empty to omit.</p></div></details><div class="grid gap-4 lg:grid-cols-2"><div><label class="flex justify-between text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5"><span>Skill Body</span><span class="${ssrRenderClass($setup.bodyLines > 400 ? "text-amber-500" : "")}">${ssrInterpolate($setup.bodyLines)}/500 lines</span></label><textarea rows="20" placeholder="# My Skill
|
|
|
|
Instructions for Claude..." class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-3 font-mono text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all resize-y leading-relaxed">${ssrInterpolate($setup.body)}</textarea></div><div><p class="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Preview</p><div class="skill-prose rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] p-5 min-h-[20rem] overflow-auto">${$setup.previewHtml ?? ""}</div></div></div><div class="flex items-center gap-4 pt-2"><button type="submit"${ssrIncludeBooleanAttr($setup.saving || $setup.isFork && $setup.slugMatchesOriginal) ? " disabled" : ""}${ssrRenderAttr("title", $setup.isFork && $setup.slugMatchesOriginal ? "Change the name to generate a different slug" : "")} class="inline-flex items-center gap-2 rounded-xl bg-[var(--color-accent-500)] px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-[var(--color-accent-500)]/20 hover:bg-[var(--color-accent-600)] hover:shadow-[var(--color-accent-500)]/30 disabled:opacity-50 active:scale-[0.97] transition-all">`);
|
|
if ($setup.saving) {
|
|
_push(`<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(` ${ssrInterpolate($setup.saving ? "Saving..." : $setup.isFork ? "Create Fork" : $props.mode === "create" ? "Create Skill" : "Save Changes")}</button><a href="/" class="text-sm text-gray-600 hover:text-gray-300 transition-colors">Cancel</a>`);
|
|
if ($setup.error) {
|
|
_push(`<p class="text-sm text-red-400">${ssrInterpolate($setup.error)}</p>`);
|
|
} else {
|
|
_push(`<!---->`);
|
|
}
|
|
_push(`</div></form>`);
|
|
}
|
|
const _sfc_setup = _sfc_main.setup;
|
|
_sfc_main.setup = (props, ctx) => {
|
|
const ssrContext = useSSRContext();
|
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/SkillEditor.vue");
|
|
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
};
|
|
const SkillEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]);
|
|
|
|
const FALLBACK_TOOLS = [
|
|
"Bash",
|
|
"Read",
|
|
"Write",
|
|
"Edit",
|
|
"MultiEdit",
|
|
"Glob",
|
|
"Grep",
|
|
"WebFetch",
|
|
"WebSearch",
|
|
"Task",
|
|
"NotebookEdit",
|
|
"NotebookRead",
|
|
"TodoRead",
|
|
"TodoWrite"
|
|
];
|
|
const GIST_URL = "https://gist.githubusercontent.com/wong2/e0f34aac66caf890a332f7b6f9e2ba8f/raw";
|
|
const IGNORED = /* @__PURE__ */ new Set(["LS", "exit_plan_mode", "Agent", "BashOutput", "KillShell", "SlashCommand", "ExitPlanMode"]);
|
|
let cached$1 = null;
|
|
let lastFetch$1 = 0;
|
|
const CACHE_TTL$1 = 1e3 * 60 * 60;
|
|
async function getAvailableTools() {
|
|
if (cached$1 && Date.now() - lastFetch$1 < CACHE_TTL$1) {
|
|
return cached$1;
|
|
}
|
|
try {
|
|
const res = await fetch(GIST_URL, { signal: AbortSignal.timeout(5e3) });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const text = await res.text();
|
|
const matches = text.matchAll(/"name":\s*"([A-Z][a-zA-Z]+)"/g);
|
|
const tools = /* @__PURE__ */ new Set();
|
|
for (const m of matches) {
|
|
if (!IGNORED.has(m[1])) {
|
|
tools.add(m[1]);
|
|
}
|
|
}
|
|
if (tools.size >= 5) {
|
|
cached$1 = [...tools].sort();
|
|
lastFetch$1 = Date.now();
|
|
return cached$1;
|
|
}
|
|
} catch {
|
|
}
|
|
cached$1 = FALLBACK_TOOLS;
|
|
lastFetch$1 = Date.now();
|
|
return cached$1;
|
|
}
|
|
|
|
const FALLBACK_MODELS = [
|
|
{ id: "claude-opus-4-6", display_name: "Claude Opus 4.6" },
|
|
{ id: "claude-sonnet-4-5-20250929", display_name: "Claude Sonnet 4.5" },
|
|
{ id: "claude-haiku-4-5-20251001", display_name: "Claude Haiku 4.5" }
|
|
];
|
|
const DOCS_URL = "https://platform.claude.com/docs/en/about-claude/models/overview";
|
|
const MODEL_ID_RE = /claude-(?:opus|sonnet|haiku|3-\d+-(?:opus|sonnet|haiku)|3-(?:opus|sonnet|haiku))-[\w-]*\d+(?:-\d{8})?/g;
|
|
let cached = null;
|
|
let lastFetch = 0;
|
|
const CACHE_TTL = 1e3 * 60 * 60 * 24;
|
|
function displayName(id) {
|
|
const clean = id.replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
const parts = clean.split("-");
|
|
const words = parts.map((p, i) => {
|
|
if (/^\d+$/.test(p) && i === parts.length - 1 && parts.length > 1) {
|
|
const prev = parts[i - 1];
|
|
if (/^\d+$/.test(prev)) return null;
|
|
return p;
|
|
}
|
|
return p.charAt(0).toUpperCase() + p.slice(1);
|
|
}).filter(Boolean);
|
|
const result = [];
|
|
for (const w of words) {
|
|
if (/^\d+$/.test(w) && result.length > 0 && /^\d+$/.test(result[result.length - 1])) {
|
|
result[result.length - 1] += "." + w;
|
|
} else {
|
|
result.push(w);
|
|
}
|
|
}
|
|
return "Claude " + result.join(" ");
|
|
}
|
|
async function getAvailableModels() {
|
|
if (cached && Date.now() - lastFetch < CACHE_TTL) {
|
|
return cached;
|
|
}
|
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
if (apiKey) {
|
|
try {
|
|
const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
signal: AbortSignal.timeout(5e3)
|
|
});
|
|
if (res.ok) {
|
|
const body = await res.json();
|
|
const models = body.data.filter((m) => m.id.startsWith("claude-")).map((m) => ({ id: m.id, display_name: m.display_name }));
|
|
if (models.length > 0) {
|
|
cached = models;
|
|
lastFetch = Date.now();
|
|
return cached;
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
try {
|
|
const res = await fetch(DOCS_URL, { signal: AbortSignal.timeout(5e3) });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const html = await res.text();
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const models = [];
|
|
for (const match of html.matchAll(MODEL_ID_RE)) {
|
|
const id = match[0];
|
|
if (id.endsWith("-v1") || id.includes("-v1:")) continue;
|
|
if (seen.has(id)) continue;
|
|
seen.add(id);
|
|
models.push({ id, display_name: displayName(id) });
|
|
}
|
|
const deduped = /* @__PURE__ */ new Map();
|
|
for (const m of models) {
|
|
const base = m.id.replace(/-\d{8}$/, "").replace(/-0$/, "");
|
|
if (!deduped.has(base) || m.id.length < deduped.get(base).id.length) {
|
|
deduped.set(base, m);
|
|
}
|
|
}
|
|
const result = [...deduped.values()];
|
|
if (result.length > 0) {
|
|
cached = result;
|
|
lastFetch = Date.now();
|
|
return cached;
|
|
}
|
|
} catch {
|
|
}
|
|
cached = FALLBACK_MODELS;
|
|
lastFetch = Date.now();
|
|
return cached;
|
|
}
|
|
|
|
export { SkillEditor as S, getAvailableModels as a, getAvailableTools as g };
|