321 lines
20 KiB
JavaScript
321 lines
20 KiB
JavaScript
import { useSSRContext, defineComponent, ref, computed, watch, mergeProps } from 'vue';
|
|
import { marked } from 'marked';
|
|
import { ssrRenderAttrs, ssrRenderAttr, ssrInterpolate, ssrRenderList, ssrRenderClass, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual } from 'vue/server-renderer';
|
|
import { _ as _export_sfc } from './_plugin-vue_export-helper_B1lnwsE2.mjs';
|
|
|
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
__name: "SkillEditor",
|
|
props: {
|
|
mode: {},
|
|
slug: {},
|
|
initialName: {},
|
|
initialDescription: {},
|
|
initialAllowedTools: {},
|
|
initialArgumentHint: {},
|
|
initialModel: {},
|
|
initialUserInvocable: { type: Boolean },
|
|
initialDisableModelInvocation: { type: Boolean },
|
|
initialContext: {},
|
|
initialAgent: {},
|
|
initialHooks: {},
|
|
initialBody: {},
|
|
availableTools: {},
|
|
availableModels: {}
|
|
},
|
|
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 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 body = ref(props.initialBody || "");
|
|
const saving = ref(false);
|
|
const error = ref("");
|
|
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 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";
|
|
});
|
|
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 (argumentHint.value) lines.push(`argument-hint: ${argumentHint.value}`);
|
|
if (tools.length > 0) lines.push(`allowed-tools: ${tools.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();
|
|
if (props.mode === "create") {
|
|
const res = await fetch("/api/skills", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
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: { "Content-Type": "application/json" },
|
|
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, name, description, argumentHint, model, userInvocable, disableModelInvocation, context, agent, hooksJson, body, saving, error, selectedTools, toggleTool, computedSlug, 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))}><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 placeholder="My Awesome Skill" 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="mt-1.5 text-xs text-gray-600"> Slug: <code class="text-gray-500 font-mono">${ssrInterpolate($setup.computedSlug)}</code></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" 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"></div></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="block text-xs font-medium uppercase tracking-wider text-gray-500 mb-1.5">Skill Body</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) ? " disabled" : ""} 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..." : $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 };
|