chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1 @@
export { SkillImport } from '@/app/workspace/[workspaceId]/skills/components/skill-import/skill-import'
@@ -0,0 +1,156 @@
'use client'
import { useCallback, useState } from 'react'
import { Chip, ChipInput, ChipModalField, Loader } from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { requestJson } from '@/lib/api/client/request'
import { importSkillContract } from '@/lib/api/contracts'
import {
extractSkillFromZip,
parseSkillMarkdown,
} from '@/app/workspace/[workspaceId]/skills/components/utils'
interface ImportedSkill {
name: string
description: string
content: string
}
interface SkillImportProps {
onImport: (data: ImportedSkill) => void
}
type ImportState = 'idle' | 'loading' | 'error'
const ACCEPTED_EXTENSIONS = ['.md', '.zip']
function isAcceptedFile(file: File): boolean {
const name = file.name.toLowerCase()
return ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext))
}
export function SkillImport({ onImport }: SkillImportProps) {
const [githubUrl, setGithubUrl] = useState('')
const [githubState, setGithubState] = useState<ImportState>('idle')
const [githubError, setGithubError] = useState('')
const [fileState, setFileState] = useState<ImportState>('idle')
const [fileError, setFileError] = useState('')
const handleGithubImport = useCallback(async () => {
const trimmed = githubUrl.trim()
if (!trimmed) {
setGithubError('Please enter a GitHub URL')
setGithubState('error')
return
}
setGithubState('loading')
setGithubError('')
try {
const data = await requestJson(importSkillContract, { body: { url: trimmed } })
const parsed = parseSkillMarkdown(data.content)
setGithubState('idle')
onImport(parsed)
} catch (err) {
const message = getErrorMessage(err, 'Failed to import from GitHub')
setGithubError(message)
setGithubState('error')
}
}, [githubUrl, onImport])
const processFile = useCallback(
async (file: File) => {
if (!isAcceptedFile(file)) {
setFileError('Unsupported file type. Use .md or .zip files.')
setFileState('error')
return
}
setFileState('loading')
setFileError('')
try {
let rawContent: string
if (file.name.toLowerCase().endsWith('.zip')) {
if (file.size > 5 * 1024 * 1024) {
setFileError('ZIP file is too large (max 5 MB)')
setFileState('error')
return
}
rawContent = await extractSkillFromZip(file)
} else {
rawContent = await file.text()
}
const parsed = parseSkillMarkdown(rawContent)
setFileState('idle')
onImport(parsed)
} catch (err) {
const message = getErrorMessage(err, 'Failed to process file')
setFileError(message)
setFileState('error')
}
},
[onImport]
)
const handleFiles = useCallback(
(files: File[]) => {
const file = files[0]
if (file) processFile(file)
},
[processFile]
)
return (
<div className='flex flex-col gap-4'>
<ChipModalField type='custom' title='Import from GitHub' error={githubError || undefined}>
<div className='flex gap-2'>
<ChipInput
placeholder='https://github.com/owner/repo/blob/main/SKILL.md'
value={githubUrl}
onChange={(e) => {
setGithubUrl(e.target.value)
if (githubError) setGithubError('')
}}
disabled={githubState === 'loading'}
className='min-w-0 flex-1'
/>
<Chip
flush
onClick={handleGithubImport}
disabled={githubState === 'loading' || !githubUrl.trim()}
>
{githubState === 'loading' ? <Loader className='size-[14px]' animate /> : 'Fetch'}
</Chip>
</div>
</ChipModalField>
<ImportDivider />
<ChipModalField
type='file'
title='Upload File'
accept='.md,.zip'
onChange={handleFiles}
loading={fileState === 'loading'}
label={fileState === 'loading' ? 'Importing…' : undefined}
description='.md file with YAML frontmatter, or .zip containing a SKILL.md'
error={fileError || undefined}
/>
</div>
)
}
function ImportDivider() {
return (
<div className='flex items-center gap-3 px-2'>
<div className='h-px flex-1 bg-[var(--border)]' />
<span className='text-[11px] text-[var(--text-muted)]'>or</span>
<div className='h-px flex-1 bg-[var(--border)]' />
</div>
)
}
@@ -0,0 +1 @@
export { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal/skill-modal'
@@ -0,0 +1,283 @@
'use client'
import { useState } from 'react'
import {
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
ChipModalTabs,
chipFieldSurfaceClass,
cn,
} from '@sim/emcn'
import dynamic from 'next/dynamic'
import { useParams } from 'next/navigation'
import { SkillImport } from '@/app/workspace/[workspaceId]/skills/components/skill-import'
import { parseSkillMarkdown } from '@/app/workspace/[workspaceId]/skills/components/utils'
import type { SkillDefinition } from '@/hooks/queries/skills'
import { useCreateSkill, useUpdateSkill } from '@/hooks/queries/skills'
const RichMarkdownField = dynamic(
() =>
import(
'@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field'
).then((m) => m.RichMarkdownField),
{
ssr: false,
loading: () => <div className={cn('min-h-[200px]', chipFieldSurfaceClass)} />,
}
)
interface SkillModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onSave: () => void
onDelete?: (skillId: string) => void
initialValues?: SkillDefinition
}
const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
interface FieldErrors {
name?: string
description?: string
content?: string
general?: string
}
type TabValue = 'create' | 'import'
export function SkillModal({
open,
onOpenChange,
onSave,
onDelete,
initialValues,
}: SkillModalProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const createSkill = useCreateSkill()
const updateSkill = useUpdateSkill()
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [content, setContent] = useState('')
/**
* Bumped to remount the seed-once rich Content editor whenever `content` is set programmatically — a
* reset from a changed `initialValues` or a destructured SKILL.md paste — so the editor re-seeds (an
* `initialValues` change for the same skill keeps the React key otherwise stable).
*/
const [contentSeed, setContentSeed] = useState(0)
const [errors, setErrors] = useState<FieldErrors>({})
const [saving, setSaving] = useState(false)
const [activeTab, setActiveTab] = useState<TabValue>('create')
const [prevOpen, setPrevOpen] = useState(false)
const [prevInitialValues, setPrevInitialValues] = useState(initialValues)
// Reset by skill id, not object identity — a background refetch for the same open skill must not clobber an in-progress edit.
if ((open && !prevOpen) || (open && initialValues?.id !== prevInitialValues?.id)) {
setName(initialValues?.name ?? '')
setDescription(initialValues?.description ?? '')
setContent(initialValues?.content ?? '')
setErrors({})
setActiveTab('create')
setContentSeed((seed) => seed + 1)
}
if (open !== prevOpen) setPrevOpen(open)
if (initialValues !== prevInitialValues) setPrevInitialValues(initialValues)
const hasChanges =
!initialValues ||
name !== initialValues.name ||
description !== initialValues.description ||
content !== initialValues.content
const handleSave = async () => {
const newErrors: FieldErrors = {}
if (!name.trim()) {
newErrors.name = 'Name is required'
} else if (name.length > 64) {
newErrors.name = 'Name must be 64 characters or less'
} else if (!KEBAB_CASE_REGEX.test(name)) {
newErrors.name = 'Name must be kebab-case (e.g. my-skill)'
}
if (!description.trim()) {
newErrors.description = 'Description is required'
}
if (!content.trim()) {
newErrors.content = 'Content is required'
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
setSaving(true)
try {
if (initialValues) {
await updateSkill.mutateAsync({
workspaceId,
skillId: initialValues.id,
updates: { name, description, content },
})
} else {
await createSkill.mutateAsync({
workspaceId,
skill: { name, description, content },
})
}
onSave()
} catch (error) {
const message =
error instanceof Error && error.message.includes('already exists')
? error.message
: 'Failed to save skill. Please try again.'
setErrors({ general: message })
} finally {
setSaving(false)
}
}
const applyImportedSkill = (data: { name: string; description: string; content: string }) => {
setName(data.name)
setDescription(data.description)
setContent(data.content)
setErrors({})
setContentSeed((seed) => seed + 1)
}
const handleImport = (data: { name: string; description: string; content: string }) => {
applyImportedSkill(data)
setActiveTab('create')
}
/**
* Pasting a full SKILL.md destructures it into the fields. Gated on a real YAML `name:` key — a
* stray `---` thematic break or a heading-only snippet pastes as ordinary content instead of
* silently overwriting all three fields.
*/
const handleContentPaste = (text: string): boolean => {
const parsed = parseSkillMarkdown(text)
if (!parsed.nameFromFrontmatter) return false
applyImportedSkill(parsed)
return true
}
const isEditing = !!initialValues
const readOnly = !!initialValues?.readOnly
const showFooter = activeTab === 'create' || isEditing
return (
<ChipModal
open={open}
onOpenChange={onOpenChange}
srTitle={isEditing ? 'Edit Skill' : 'Add Skill'}
size='lg'
>
<ChipModalHeader onClose={() => onOpenChange(false)}>
{isEditing ? 'Edit Skill' : 'Add Skill'}
</ChipModalHeader>
<ChipModalBody>
{!isEditing && (
<ChipModalTabs
tabs={[
{ value: 'create', label: 'Create' },
{ value: 'import', label: 'Import' },
]}
value={activeTab}
onChange={(value) => setActiveTab(value as TabValue)}
/>
)}
{activeTab === 'create' || isEditing ? (
<>
<ChipModalField
type='input'
title='Name'
value={name}
onChange={(value) => {
setName(value)
if (errors.name || errors.general)
setErrors((prev) => ({ ...prev, name: undefined, general: undefined }))
}}
placeholder='my-skill-name'
required
error={errors.name}
hint='Lowercase letters, numbers, and hyphens (e.g. my-skill)'
/>
<ChipModalField
type='input'
title='Description'
value={description}
onChange={(value) => {
setDescription(value)
if (errors.description || errors.general)
setErrors((prev) => ({ ...prev, description: undefined, general: undefined }))
}}
placeholder='What this skill does and when to use it...'
maxLength={1024}
required
error={errors.description}
/>
<ChipModalField type='custom' title='Content' required error={errors.content}>
<RichMarkdownField
key={`${initialValues?.id ?? 'new'}:${contentSeed}`}
value={content}
onChange={(value) => {
setContent(value)
if (errors.content || errors.general)
setErrors((prev) => ({ ...prev, content: undefined, general: undefined }))
}}
placeholder='Skill instructions in markdown...'
minHeight={200}
disabled={readOnly || saving}
error={!!errors.content}
workspaceId={workspaceId}
onPasteText={handleContentPaste}
/>
</ChipModalField>
<ChipModalError>{errors.general}</ChipModalError>
</>
) : (
<SkillImport onImport={handleImport} />
)}
</ChipModalBody>
{showFooter && (
<ChipModalFooter
onCancel={() => onOpenChange(false)}
cancelDisabled={readOnly}
secondaryActions={
isEditing && onDelete
? [
{
label: 'Delete',
onClick: () => onDelete(initialValues.id),
variant: 'destructive',
disabled: readOnly,
},
]
: undefined
}
primaryAction={{
label: saving ? 'Saving...' : isEditing ? 'Update' : 'Create',
onClick: handleSave,
disabled: readOnly || saving || !hasChanges,
}}
/>
)}
</ChipModal>
)
}
@@ -0,0 +1,207 @@
/**
* @vitest-environment node
*/
import JSZip from 'jszip'
import { describe, expect, it } from 'vitest'
import { extractSkillFromZip, parseSkillMarkdown } from './utils'
describe('parseSkillMarkdown', () => {
it('parses standard SKILL.md with name, description, and body', () => {
const input = [
'---',
'name: my-skill',
'description: Does something useful',
'---',
'',
'# Instructions',
'Use this skill to do things.',
].join('\n')
expect(parseSkillMarkdown(input)).toEqual({
name: 'my-skill',
description: 'Does something useful',
content: '# Instructions\nUse this skill to do things.',
nameFromFrontmatter: true,
})
})
it('strips single and double quotes from frontmatter values', () => {
const input = '---\nname: \'my-skill\'\ndescription: "A quoted description"\n---\nBody'
expect(parseSkillMarkdown(input)).toEqual({
name: 'my-skill',
description: 'A quoted description',
content: 'Body',
nameFromFrontmatter: true,
})
})
it('preserves colons inside description values', () => {
const input = '---\nname: api-tool\ndescription: API key: required for auth\n---\nBody'
expect(parseSkillMarkdown(input)).toEqual({
name: 'api-tool',
description: 'API key: required for auth',
content: 'Body',
nameFromFrontmatter: true,
})
})
it('ignores unknown frontmatter fields', () => {
const input = '---\nname: x\ndescription: y\nauthor: someone\nversion: 2\n---\nBody'
const result = parseSkillMarkdown(input)
expect(result.name).toBe('x')
expect(result.description).toBe('y')
expect(result.content).toBe('Body')
})
it('infers name from heading when frontmatter has no name field', () => {
const input =
'---\ndescription: A tool for blocks\nargument-hint: <name>\n---\n\n# Add Block Skill\n\nContent here.'
expect(parseSkillMarkdown(input)).toEqual({
name: 'add-block-skill',
description: 'A tool for blocks',
content: '# Add Block Skill\n\nContent here.',
nameFromFrontmatter: false,
})
})
it('infers name from heading when there is no frontmatter at all', () => {
const input = '# My Cool Tool\n\nSome instructions.'
expect(parseSkillMarkdown(input)).toEqual({
name: 'my-cool-tool',
description: '',
content: '# My Cool Tool\n\nSome instructions.',
nameFromFrontmatter: false,
})
})
it('returns empty name when there is no frontmatter and no heading', () => {
const input = 'Just some plain text without any structure.'
expect(parseSkillMarkdown(input)).toEqual({
name: '',
description: '',
content: 'Just some plain text without any structure.',
nameFromFrontmatter: false,
})
})
it('handles empty input', () => {
expect(parseSkillMarkdown('')).toEqual({
name: '',
description: '',
content: '',
nameFromFrontmatter: false,
})
})
it('handles frontmatter with empty name value', () => {
const input = '---\nname:\ndescription: Has a description\n---\n\n# Fallback Heading\nBody'
const result = parseSkillMarkdown(input)
expect(result.name).toBe('fallback-heading')
expect(result.description).toBe('Has a description')
})
it('handles frontmatter with no body', () => {
const input = '---\nname: solo\ndescription: Just frontmatter\n---'
expect(parseSkillMarkdown(input)).toEqual({
name: 'solo',
description: 'Just frontmatter',
content: '',
nameFromFrontmatter: true,
})
})
it('handles unclosed frontmatter as plain content', () => {
const input = '---\nname: broken\nno closing delimiter'
const result = parseSkillMarkdown(input)
expect(result.name).toBe('')
expect(result.content).toBe(input)
})
it('flags nameFromFrontmatter only for a real YAML name key (the paste-destructure gate)', () => {
expect(parseSkillMarkdown('---\nname: real\n---\nBody').nameFromFrontmatter).toBe(true)
// A `---` thematic break + heading must NOT count — it would wrongly destructure on paste.
expect(parseSkillMarkdown('---\n# Setup Guide\nnotes').nameFromFrontmatter).toBe(false)
// A changelog whose second `---` closes the regex but has no name key.
expect(parseSkillMarkdown('---\n\n## v2.0\n\n---\n\n## v1.0').nameFromFrontmatter).toBe(false)
})
it('trims whitespace from input', () => {
const input = '\n\n ---\nname: trimmed\ndescription: yes\n---\nBody \n\n'
const result = parseSkillMarkdown(input)
expect(result.name).toBe('trimmed')
expect(result.content).toBe('Body')
})
it('truncates inferred heading names to 64 characters', () => {
const longHeading = `# ${'A'.repeat(100)}`
const result = parseSkillMarkdown(longHeading)
expect(result.name.length).toBeLessThanOrEqual(64)
})
it('sanitizes special characters in inferred heading names', () => {
const input = '# Hello, World! (v2) — Updated'
const result = parseSkillMarkdown(input)
expect(result.name).toBe('hello-world-v2-updated')
})
it('handles h2 and h3 headings for name inference', () => {
expect(parseSkillMarkdown('## Sub Heading').name).toBe('sub-heading')
expect(parseSkillMarkdown('### Third Level').name).toBe('third-level')
})
it('does not match h4+ headings for name inference', () => {
expect(parseSkillMarkdown('#### Too Deep').name).toBe('')
})
it('uses first heading even when multiple exist', () => {
const input = '# First\n\n## Second\n\n### Third'
expect(parseSkillMarkdown(input).name).toBe('first')
})
})
describe('extractSkillFromZip', () => {
async function makeZipBuffer(files: Record<string, string>): Promise<Uint8Array> {
const zip = new JSZip()
for (const [path, content] of Object.entries(files)) {
zip.file(path, content)
}
return zip.generateAsync({ type: 'uint8array' })
}
it('extracts SKILL.md at root level', async () => {
const data = await makeZipBuffer({ 'SKILL.md': '---\nname: root\n---\nContent' })
const content = await extractSkillFromZip(data)
expect(content).toBe('---\nname: root\n---\nContent')
})
it('extracts SKILL.md from a nested directory', async () => {
const data = await makeZipBuffer({ 'my-skill/SKILL.md': '---\nname: nested\n---\nBody' })
const content = await extractSkillFromZip(data)
expect(content).toBe('---\nname: nested\n---\nBody')
})
it('prefers the shallowest SKILL.md when multiple exist', async () => {
const data = await makeZipBuffer({
'deep/nested/SKILL.md': 'deep',
'SKILL.md': 'root',
'other/SKILL.md': 'other',
})
const content = await extractSkillFromZip(data)
expect(content).toBe('root')
})
it('throws when no SKILL.md is found', async () => {
const data = await makeZipBuffer({ 'README.md': 'No skill here' })
await expect(extractSkillFromZip(data)).rejects.toThrow('No SKILL.md file found')
})
})
@@ -0,0 +1,115 @@
import JSZip from 'jszip'
interface ParsedSkill {
name: string
description: string
content: string
/** True only when `name` came from a real YAML `name:` key (not inferred from a heading). */
nameFromFrontmatter: boolean
}
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/
/**
* Parses a SKILL.md string with optional YAML frontmatter into structured fields.
*
* Expected format:
* ```
* ---
* name: my-skill
* description: What this skill does
* ---
* # Markdown content here...
* ```
*
* If no frontmatter is present, the entire text becomes the content field.
*/
export function parseSkillMarkdown(raw: string): ParsedSkill {
const trimmed = raw.replace(/\r\n/g, '\n').trim()
const match = trimmed.match(FRONTMATTER_REGEX)
if (!match) {
return {
name: inferNameFromHeading(trimmed),
description: '',
content: trimmed,
nameFromFrontmatter: false,
}
}
const frontmatter = match[1]
const body = (match[2] ?? '').trim()
let name = ''
let description = ''
for (const line of frontmatter.split('\n')) {
const colonIdx = line.indexOf(':')
if (colonIdx === -1) continue
const key = line.slice(0, colonIdx).trim().toLowerCase()
const value = line
.slice(colonIdx + 1)
.trim()
.replace(/^['"]|['"]$/g, '')
if (key === 'name') {
name = value
} else if (key === 'description') {
description = value
}
}
const nameFromFrontmatter = name !== ''
if (!name) {
name = inferNameFromHeading(body)
}
return { name, description, content: body, nameFromFrontmatter }
}
/**
* Derives a kebab-case name from the first markdown heading (e.g. `# Add Block Skill` -> `add-block-skill`).
*/
function inferNameFromHeading(markdown: string): string {
const headingMatch = markdown.match(/^#{1,3}\s+(.+)$/m)
if (!headingMatch) return ''
return headingMatch[1]
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 64)
}
/**
* Extracts the SKILL.md content from a ZIP archive.
* Searches for a file named SKILL.md at any depth within the archive.
* Accepts File, Blob, ArrayBuffer, or Uint8Array (anything JSZip supports).
*/
export async function extractSkillFromZip(
data: File | Blob | ArrayBuffer | Uint8Array
): Promise<string> {
const zip = await JSZip.loadAsync(data)
const candidates: string[] = []
zip.forEach((relativePath, entry) => {
if (!entry.dir && relativePath.endsWith('SKILL.md')) {
candidates.push(relativePath)
}
})
if (candidates.length === 0) {
throw new Error('No SKILL.md file found in the ZIP archive')
}
candidates.sort((a, b) => {
const depthA = a.split('/').length
const depthB = b.split('/').length
return depthA - depthB
})
const content = await zip.file(candidates[0])!.async('string')
return content
}