diff --git a/Dockerfile b/Dockerfile index 6ddc2de..1d3d637 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,12 +9,15 @@ FROM node:22-alpine AS runtime WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/data/skills ./data/skills +COPY --from=build /app/data ./data ENV HOST=0.0.0.0 ENV PORT=4321 ENV SKILLS_DIR=/app/data/skills -ENV SITE_URL=https://skills.here.run.place +ENV AGENTS_DIR=/app/data/agents +ENV OUTPUT_STYLES_DIR=/app/data/output-styles +ENV RULES_DIR=/app/data/rules +ENV SITE_URL=https://grimoi.red EXPOSE 4321 CMD ["node", "./dist/server/entry.mjs"] diff --git a/astro.config.mjs b/astro.config.mjs index b9212a3..57799db 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,7 +4,7 @@ import vue from '@astrojs/vue'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ - site: process.env.SITE_URL || 'https://skills.here.run.place', + site: process.env.SITE_URL || 'https://grimoi.red', output: 'server', adapter: node({ mode: 'standalone' }), integrations: [vue()], diff --git a/grimoired.svg b/grimoired.svg new file mode 100644 index 0000000..bcfd27b --- /dev/null +++ b/grimoired.svg @@ -0,0 +1,18 @@ + + + + diff --git a/public/grimoired.svg b/public/grimoired.svg new file mode 100644 index 0000000..bcfd27b --- /dev/null +++ b/public/grimoired.svg @@ -0,0 +1,18 @@ + + + + diff --git a/src/components/DeleteButton.vue b/src/components/DeleteButton.vue index 45652fa..cf7d819 100644 --- a/src/components/DeleteButton.vue +++ b/src/components/DeleteButton.vue @@ -11,13 +11,13 @@ {{ deleting ? 'Deleting...' : 'Delete' }} - + - Delete Skill + Delete Resource - This skill is owned by {{ authorName || authorEmail }}. Enter your token to delete it. + This resource is owned by {{ authorName || authorEmail }}. Enter your token to delete it. @@ -65,8 +65,11 @@ const props = defineProps<{ authorEmail?: string; authorName?: string; authorHasToken?: boolean; + resourceType?: string; }>(); +const type = props.resourceType || 'skills'; + const deleting = ref(false); const showModal = ref(false); const token = ref(''); @@ -75,8 +78,7 @@ const tokenInput = ref(); async function handleClick() { if (props.authorEmail && props.authorHasToken) { - // Try saved token first - const saved = localStorage.getItem('skillshere-token') || ''; + const saved = localStorage.getItem('grimoired-token') || ''; if (saved) { try { const res = await fetch('/api/auth/verify', { @@ -100,7 +102,6 @@ async function handleClick() { } async function verifyAndDelete() { - // Verify token first error.value = ''; deleting.value = true; @@ -123,7 +124,7 @@ async function verifyAndDelete() { return; } - localStorage.setItem('skillshere-token', token.value); + localStorage.setItem('grimoired-token', token.value); doDelete(token.value); } @@ -142,7 +143,7 @@ async function doDelete(authToken: string) { headers['Authorization'] = `Bearer ${authToken}`; } - const res = await fetch(`/api/skills/${props.slug}`, { method: 'DELETE', headers }); + const res = await fetch(`/api/resources/${type}/${props.slug}`, { method: 'DELETE', headers }); if (res.status === 403) { const data = await res.json(); error.value = data.error || 'Permission denied'; @@ -156,7 +157,7 @@ async function doDelete(authToken: string) { } window.location.href = '/'; } catch (err) { - error.value = err instanceof Error ? err.message : 'Failed to delete skill.'; + error.value = err instanceof Error ? err.message : 'Failed to delete.'; deleting.value = false; } } diff --git a/src/components/EditGate.vue b/src/components/EditGate.vue index 642f09a..a9ec8fb 100644 --- a/src/components/EditGate.vue +++ b/src/components/EditGate.vue @@ -16,7 +16,7 @@ Author Verification - This skill is owned by {{ authorName || authorEmail }}. Enter your token to edit. + This resource is owned by {{ authorName || authorEmail }}. Enter your token to edit. @@ -44,7 +44,7 @@ Fork instead @@ -72,8 +72,11 @@ const props = defineProps<{ authorEmail?: string; authorName?: string; authorHasToken?: boolean; + resourceType?: string; }>(); +const type = props.resourceType || 'skills'; + const showModal = ref(false); const token = ref(''); const error = ref(''); @@ -82,12 +85,11 @@ const tokenInput = ref(); async function handleClick() { if (!props.authorEmail || !props.authorHasToken) { - window.location.href = `/${props.slug}/edit`; + window.location.href = `/${type}/${props.slug}/edit`; return; } - // Try saved token first - const saved = localStorage.getItem('skillshere-token') || ''; + const saved = localStorage.getItem('grimoired-token') || ''; if (saved) { try { const res = await fetch('/api/auth/verify', { @@ -96,8 +98,8 @@ async function handleClick() { body: JSON.stringify({ email: props.authorEmail, token: saved }), }); if (res.ok) { - localStorage.setItem('skillshere-token', saved); - window.location.href = `/${props.slug}/edit`; + localStorage.setItem('grimoired-token', saved); + window.location.href = `/${type}/${props.slug}/edit`; return; } } catch { /* fall through to modal */ } @@ -126,9 +128,8 @@ async function verify() { return; } - // Store token for the editor to use - localStorage.setItem('skillshere-token', token.value); - window.location.href = `/${props.slug}/edit`; + localStorage.setItem('grimoired-token', token.value); + window.location.href = `/${type}/${props.slug}/edit`; } catch { error.value = 'Could not verify token'; } finally { @@ -136,8 +137,8 @@ async function verify() { } } -function forkSkill() { +function forkResource() { showModal.value = false; - window.location.href = `/new?from=${encodeURIComponent(props.slug)}`; + window.location.href = `/${type}/new?from=${encodeURIComponent(props.slug)}`; } diff --git a/src/components/FieldRenderer.vue b/src/components/FieldRenderer.vue new file mode 100644 index 0000000..b661539 --- /dev/null +++ b/src/components/FieldRenderer.vue @@ -0,0 +1,267 @@ + + + + {{ field.label }} + + {{ field.hint }} + + + + + {{ field.label }} + + {{ field.hint }} + + + + + {{ field.label }} + + Default + {{ opt.label }} + + {{ field.hint }} + + + + + + + + + {{ field.label }} + ({{ field.hint }}) + + + + + + + {{ field.label }} + + + {{ opt }} + + + {{ field.hint }} + + + + + + {{ field.label }} (advanced) + + + {{ field.hint }} + + + + + + + {{ field.label }} + + + {{ tag }} + + + + + + + + + + + {{ s }} + + + {{ field.hint }} + + + + diff --git a/src/components/FileManager.vue b/src/components/FileManager.vue new file mode 100644 index 0000000..1b79db7 --- /dev/null +++ b/src/components/FileManager.vue @@ -0,0 +1,357 @@ + + + + Files + + + + + + Create + + + + + + Upload + + + + + + + + + Directory + + scripts/ — executable code + references/ — docs & context + assets/ — templates & files + + + + File name + + + Create + + scripts/ run via hooks or tool calls · references/ Claude reads as context · assets/ copied into the project + {{ createError }} + + + + + + + Directory + + scripts/ — executable code + references/ — docs & context + assets/ — templates & files + + + + File + + + {{ saving ? 'Uploading...' : 'Upload' }} + + {{ uploadError }} + + + + + + + + + + + + + + + + {{ f.relativePath }} + {{ formatSize(f.size) }} + + + + + + + + + Loading... + + + + {{ savingFile === f.relativePath ? 'Saving...' : 'Save' }} + + {{ fileSaveStatus[f.relativePath] === 'saved' ? 'Saved' : fileSaveStatus[f.relativePath] }} + + + + + + + No files yet. Create scripts, docs, or upload assets. + + + + diff --git a/src/components/FolderTree.astro b/src/components/FolderTree.astro new file mode 100644 index 0000000..c8a5824 --- /dev/null +++ b/src/components/FolderTree.astro @@ -0,0 +1,98 @@ +--- +interface FileEntry { + relativePath: string; + size: number; +} + +interface Props { + files: FileEntry[]; + slug: string; + type: string; + mainFileName: string; + mainFileSize: number; +} + +const { files, slug, type, mainFileName, mainFileSize } = Astro.props; + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +const DIR_HINTS: Record = { + scripts: 'executable code', + references: 'docs & context', + assets: 'templates & files', +}; + +// Group files by top-level directory +const groups: Record = {}; +for (const f of files) { + const dir = f.relativePath.split('/')[0]; + if (!groups[dir]) groups[dir] = []; + groups[dir].push(f); +} +--- + + + + + + + + {mainFileName} + {formatSize(mainFileSize)} + main + + + + {Object.entries(groups).map(([dir, entries]) => ( + + + + + + + + + {dir}/ + {DIR_HINTS[dir] && — {DIR_HINTS[dir]}} + {entries.length} file{entries.length !== 1 ? 's' : ''} + + + {entries.map((f) => { + const fileName = f.relativePath.split('/').slice(1).join('/'); + const downloadUrl = `/api/resources/${type}/${slug}/files/${f.relativePath}`; + return ( + + + + + + {fileName} + {formatSize(f.size)} + + + ); + })} + + + ))} + + + diff --git a/src/components/ResourceCard.astro b/src/components/ResourceCard.astro new file mode 100644 index 0000000..a9fc15a --- /dev/null +++ b/src/components/ResourceCard.astro @@ -0,0 +1,106 @@ +--- +interface Props { + resourceType: string; + slug: string; + name: string; + description: string; + tags?: string[]; + author?: string; + forkCount?: number; + downloads?: number; + pushes?: number; + lastPushedAt?: string | null; + typeLabel: string; + typeColor: string; + /** For skills: allowed tools badges */ + tools?: string[]; +} + +const { + resourceType, + slug, + name, + description, + tags = [], + author, + forkCount = 0, + downloads = 0, + pushes = 0, + lastPushedAt, + typeLabel, + typeColor, + tools = [], +} = Astro.props; + +const updatedLabel = lastPushedAt ? new Date(lastPushedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : null; +const truncated = description.length > 120 ? description.slice(0, 120) + '...' : description; +--- + + + + + + + + {typeLabel} + + {name} + + + + + + {truncated && {truncated}} + {tools.length > 0 && ( + + {tools.map((tool) => ( + + {tool} + + ))} + + )} + {tags.length > 0 && ( + + {tags.map((tag) => ( + + {tag} + + ))} + + )} + + {author && by {author}} + {forkCount > 0 && ( + + + + + {forkCount} + + )} + {downloads > 0 && ( + + + + + {downloads} + + )} + {pushes > 0 && ( + + + + + {pushes} + + )} + {updatedLabel && ( + {updatedLabel} + )} + + + diff --git a/src/components/ResourceEditor.vue b/src/components/ResourceEditor.vue new file mode 100644 index 0000000..e790168 --- /dev/null +++ b/src/components/ResourceEditor.vue @@ -0,0 +1,732 @@ + + + + + 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. + + + Your Name + + + + Your Email + + + + + + + + Change the name to generate a different slug. You can't save a fork with the same slug as the original. + + + + + + Name + + + Slug: {{ computedSlug }} + {{ name.length }}/64 + + + + Description + + {{ description.length }}/200 + + + + + + Tags + + + {{ tag }} + + + + + + + + + + + + + {{ s }} + (new) + + + Type and press Enter or comma. Click suggestions to add. + + + + + Format + + Simple (.md) + Folder + + + {{ format === 'file' ? 'Single markdown file.' : 'Directory with scripts/, references/, and assets/ subdirectories.' }} + + + + + + + + Folder files + {{ computedSlug }}/{{ mainFileName }} is generated from the body. Add scripts, docs, or assets here. + + + + + + + Create + + + + + + Upload + + + + + + + + + Directory + + scripts/ — executable code + references/ — docs & context + assets/ — templates & files + + + + File name + + + Add + + scripts/ run via hooks or tool calls · references/ Claude reads as context · assets/ copied into the project + + + + + + + Directory + + scripts/ — executable code + references/ — docs & context + assets/ — templates & files + + + + File + + + + Text files (.sh, .md, .py...) become editable inline. Binary files (images, fonts) are stored as-is. + + + + + + + + + + + + + + + + {{ f.path }} + {{ f.content.split('\n').length }} lines + {{ formatSize(f.size) }} + {{ f.kind === 'inline' ? 'text' : 'binary' }} + + + + + + + + + + + + + No extra files yet. Create scripts, docs, or upload assets. + + + + + folder + Folder resource. Manage sub-files below. + + + + + + + + + + + + + + + + + + + + + Body + {{ bodyLines }}/500 lines + + + + + Preview + + + + + + + + + + + + + + {{ saving ? 'Saving...' : (isFork ? 'Create Fork' : (mode === 'create' ? `Create ${typeSingular}` : 'Save Changes')) }} + + + Cancel + {{ error }} + + + + + diff --git a/src/components/ResourceSearch.vue b/src/components/ResourceSearch.vue new file mode 100644 index 0000000..0d5b949 --- /dev/null +++ b/src/components/ResourceSearch.vue @@ -0,0 +1,402 @@ + + + + + + {{ tab.label }} + {{ tab.count }} + + + + + + + + Search + + + + + + + + + + + Authors + + + {{ a }} + × + + + + + + {{ a }} + + + + + + + Tags + + + {{ t }} + × + + + + + + {{ t }} + + + + + + + Clear + + + + + + + + + + + + + + + + + + + + + Showing {{ rangeStart }}–{{ rangeEnd }} of {{ filteredCount }} + + + + Prev + + + {{ p }} + + + Next + + + + + + + diff --git a/src/components/SkillEditor.vue b/src/components/SkillEditor.vue index c63078f..c5765f6 100644 --- a/src/components/SkillEditor.vue +++ b/src/components/SkillEditor.vue @@ -327,7 +327,7 @@ const forkAuthorEmail = ref(''); // Load token from localStorage (set by EditGate modal) const authorToken = ref( typeof localStorage !== 'undefined' - ? localStorage.getItem('skillshere-token') || '' + ? localStorage.getItem('grimoired-token') || '' : '' ); diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 4e8e143..61177f8 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -5,7 +5,7 @@ interface Props { title?: string; } -const { title = 'Skills Here' } = Astro.props; +const { title = 'Grimoired' } = Astro.props; --- @@ -28,23 +28,58 @@ const { title = 'Skills Here' } = Astro.props; - - Skills Here - - - - - - New Skill + + Grimoired + + + + + + + New + + + + + + + + New Skill + + + + New Agent + + + + New Output Style + + + + New Rule + + + + +
- This skill is owned by {{ authorName || authorEmail }}. Enter your token to delete it. + This resource is owned by {{ authorName || authorEmail }}. Enter your token to delete it.
- This skill is owned by {{ authorName || authorEmail }}. Enter your token to edit. + This resource is owned by {{ authorName || authorEmail }}. Enter your token to edit.
{{ field.hint }}
scripts/ run via hooks or tool calls · references/ Claude reads as context · assets/ copied into the project
No files yet. Create scripts, docs, or upload assets.
{truncated}
by {author}
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.
Change the name to generate a different slug. You can't save a fork with the same slug as the original.
+ Slug: {{ computedSlug }} + {{ name.length }}/64 +
{{ computedSlug }}
{{ description.length }}/200
Type and press Enter or comma. Click suggestions to add.
+ {{ format === 'file' ? 'Single markdown file.' : 'Directory with scripts/, references/, and assets/ subdirectories.' }} +
Folder files
{{ computedSlug }}/{{ mainFileName }} is generated from the body. Add scripts, docs, or assets here.
{{ computedSlug }}/{{ mainFileName }}
Text files (.sh, .md, .py...) become editable inline. Binary files (images, fonts) are stored as-is.
No extra files yet. Create scripts, docs, or upload assets.
Preview
{{ error }}