Files
skills-here-run-place/dist/server/chunks/models_BK7lP4G3.mjs
Alejandro Martinez aa477a553b Add author auth, forking, tags, and stats tracking
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.
2026-02-12 14:37:40 +01:00

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&#39;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. &lt;file-path&gt;" 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-[&#39;&#39;] 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-[&#39;&#39;] 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="{ &quot;preToolExecution&quot;: [{ &quot;matcher&quot;: &quot;Bash&quot;, &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;echo pre&quot; }] }] }" 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 };