cognova 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Claude/CLAUDE.md +9 -4
- package/Claude/skills/environment/SKILL.md +6 -0
- package/Claude/skills/memory/SKILL.md +6 -0
- package/Claude/skills/project/SKILL.md +6 -0
- package/Claude/skills/secret/SKILL.md +85 -0
- package/Claude/skills/secret/secret.py +146 -0
- package/Claude/skills/skill-creator/SKILL.md +30 -0
- package/Claude/skills/task/SKILL.md +6 -0
- package/app/components/skills/Card.vue +82 -0
- package/app/components/skills/CreateModal.vue +156 -0
- package/app/components/skills/Editor.vue +135 -0
- package/app/components/skills/FileTree.vue +336 -0
- package/app/components/skills/LibraryCard.vue +122 -0
- package/app/components/skills/RenameModal.vue +84 -0
- package/app/layouts/dashboard.vue +7 -0
- package/app/pages/skills/[name].vue +198 -0
- package/app/pages/skills/index.vue +157 -0
- package/app/pages/skills/library.vue +209 -0
- package/dist/cli/index.js +23 -23
- package/nuxt.config.ts +9 -0
- package/package.json +1 -1
- package/server/api/skills/[name]/files/create.post.ts +45 -0
- package/server/api/skills/[name]/files/delete.post.ts +45 -0
- package/server/api/skills/[name]/files/index.get.ts +28 -0
- package/server/api/skills/[name]/files/read.post.ts +41 -0
- package/server/api/skills/[name]/files/write.post.ts +42 -0
- package/server/api/skills/[name]/index.get.ts +54 -0
- package/server/api/skills/[name]/rename.post.ts +64 -0
- package/server/api/skills/[name]/toggle.post.ts +32 -0
- package/server/api/skills/create.post.ts +51 -0
- package/server/api/skills/generate.post.ts +126 -0
- package/server/api/skills/index.get.ts +57 -0
- package/server/api/skills/library/check-updates.get.ts +46 -0
- package/server/api/skills/library/index.get.ts +56 -0
- package/server/api/skills/library/install.post.ts +73 -0
- package/server/db/schema.ts +17 -0
- package/server/drizzle/migrations/0012_good_deadpool.sql +12 -0
- package/server/drizzle/migrations/0013_swift_snowbird.sql +1 -0
- package/server/drizzle/migrations/meta/0012_snapshot.json +1713 -0
- package/server/drizzle/migrations/meta/0013_snapshot.json +1720 -0
- package/server/drizzle/migrations/meta/_journal.json +14 -0
- package/server/middleware/auth.ts +0 -1
- package/server/plugins/05.skills-catalog.ts +105 -0
- package/server/utils/skills-path.ts +197 -0
- package/shared/types/index.ts +63 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, writeFile, stat } from 'fs/promises'
|
|
2
|
+
import { join, normalize } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
const body = await readBody<{ path: string, type: 'file' | 'directory' }>(event)
|
|
11
|
+
if (!body?.path)
|
|
12
|
+
throw createError({ statusCode: 400, message: 'Path is required' })
|
|
13
|
+
|
|
14
|
+
const skillDir = await findSkillDir(name)
|
|
15
|
+
if (!skillDir)
|
|
16
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
17
|
+
|
|
18
|
+
const targetPath = normalize(join(skillDir, body.path))
|
|
19
|
+
if (!targetPath.startsWith(skillDir))
|
|
20
|
+
throw createError({ statusCode: 403, message: 'Path traversal not allowed' })
|
|
21
|
+
|
|
22
|
+
const existing = await stat(targetPath).catch(() => null)
|
|
23
|
+
if (existing)
|
|
24
|
+
throw createError({ statusCode: 409, message: 'Already exists' })
|
|
25
|
+
|
|
26
|
+
if (body.type === 'directory') {
|
|
27
|
+
await mkdir(targetPath, { recursive: true })
|
|
28
|
+
} else {
|
|
29
|
+
await writeFile(targetPath, '', 'utf-8')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { data: { path: body.path, type: body.type || 'file' } }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
async function findSkillDir(name: string): Promise<string | null> {
|
|
36
|
+
const activeDir = join(getSkillsDir(), name)
|
|
37
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
38
|
+
if (activeStat?.isDirectory()) return activeDir
|
|
39
|
+
|
|
40
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
41
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
42
|
+
if (inactiveStat?.isDirectory()) return inactiveDir
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { rm, stat } from 'fs/promises'
|
|
2
|
+
import { join, normalize } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
const body = await readBody<{ path: string }>(event)
|
|
11
|
+
if (!body?.path)
|
|
12
|
+
throw createError({ statusCode: 400, message: 'Path is required' })
|
|
13
|
+
|
|
14
|
+
const skillDir = await findSkillDir(name)
|
|
15
|
+
if (!skillDir)
|
|
16
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
17
|
+
|
|
18
|
+
const targetPath = normalize(join(skillDir, body.path))
|
|
19
|
+
if (!targetPath.startsWith(skillDir))
|
|
20
|
+
throw createError({ statusCode: 403, message: 'Path traversal not allowed' })
|
|
21
|
+
|
|
22
|
+
// Prevent deleting SKILL.md
|
|
23
|
+
if (body.path === 'SKILL.md')
|
|
24
|
+
throw createError({ statusCode: 403, message: 'Cannot delete SKILL.md' })
|
|
25
|
+
|
|
26
|
+
const targetStat = await stat(targetPath).catch(() => null)
|
|
27
|
+
if (!targetStat)
|
|
28
|
+
throw createError({ statusCode: 404, message: 'File not found' })
|
|
29
|
+
|
|
30
|
+
await rm(targetPath, { recursive: true })
|
|
31
|
+
|
|
32
|
+
return { data: { path: body.path, deleted: true } }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
async function findSkillDir(name: string): Promise<string | null> {
|
|
36
|
+
const activeDir = join(getSkillsDir(), name)
|
|
37
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
38
|
+
if (activeStat?.isDirectory()) return activeDir
|
|
39
|
+
|
|
40
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
41
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
42
|
+
if (inactiveStat?.isDirectory()) return inactiveDir
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir, buildSkillFileTree } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
const skillDir = await findSkillDir(name)
|
|
11
|
+
if (!skillDir)
|
|
12
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
13
|
+
|
|
14
|
+
const files = await buildSkillFileTree(skillDir)
|
|
15
|
+
return { data: files }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
async function findSkillDir(name: string): Promise<string | null> {
|
|
19
|
+
const activeDir = join(getSkillsDir(), name)
|
|
20
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
21
|
+
if (activeStat?.isDirectory()) return activeDir
|
|
22
|
+
|
|
23
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
24
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
25
|
+
if (inactiveStat?.isDirectory()) return inactiveDir
|
|
26
|
+
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile, stat } from 'fs/promises'
|
|
2
|
+
import { join, normalize } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
const body = await readBody<{ path: string }>(event)
|
|
11
|
+
if (!body?.path)
|
|
12
|
+
throw createError({ statusCode: 400, message: 'File path is required' })
|
|
13
|
+
|
|
14
|
+
const skillDir = await findSkillDir(name)
|
|
15
|
+
if (!skillDir)
|
|
16
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
17
|
+
|
|
18
|
+
// Prevent path traversal
|
|
19
|
+
const filePath = normalize(join(skillDir, body.path))
|
|
20
|
+
if (!filePath.startsWith(skillDir))
|
|
21
|
+
throw createError({ statusCode: 403, message: 'Path traversal not allowed' })
|
|
22
|
+
|
|
23
|
+
const fileStat = await stat(filePath).catch(() => null)
|
|
24
|
+
if (!fileStat?.isFile())
|
|
25
|
+
throw createError({ statusCode: 404, message: 'File not found' })
|
|
26
|
+
|
|
27
|
+
const content = await readFile(filePath, 'utf-8')
|
|
28
|
+
return { data: { path: body.path, content } }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
async function findSkillDir(name: string): Promise<string | null> {
|
|
32
|
+
const activeDir = join(getSkillsDir(), name)
|
|
33
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
34
|
+
if (activeStat?.isDirectory()) return activeDir
|
|
35
|
+
|
|
36
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
37
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
38
|
+
if (inactiveStat?.isDirectory()) return inactiveDir
|
|
39
|
+
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { writeFile, mkdir, stat } from 'fs/promises'
|
|
2
|
+
import { join, normalize, dirname } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
const body = await readBody<{ path: string, content: string }>(event)
|
|
11
|
+
if (!body?.path)
|
|
12
|
+
throw createError({ statusCode: 400, message: 'File path is required' })
|
|
13
|
+
if (typeof body.content !== 'string')
|
|
14
|
+
throw createError({ statusCode: 400, message: 'Content is required' })
|
|
15
|
+
|
|
16
|
+
const skillDir = await findSkillDir(name)
|
|
17
|
+
if (!skillDir)
|
|
18
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
19
|
+
|
|
20
|
+
// Prevent path traversal
|
|
21
|
+
const filePath = normalize(join(skillDir, body.path))
|
|
22
|
+
if (!filePath.startsWith(skillDir))
|
|
23
|
+
throw createError({ statusCode: 403, message: 'Path traversal not allowed' })
|
|
24
|
+
|
|
25
|
+
// Ensure parent directory exists
|
|
26
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
27
|
+
await writeFile(filePath, body.content, 'utf-8')
|
|
28
|
+
|
|
29
|
+
return { data: { path: body.path, saved: true } }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async function findSkillDir(name: string): Promise<string | null> {
|
|
33
|
+
const activeDir = join(getSkillsDir(), name)
|
|
34
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
35
|
+
if (activeStat?.isDirectory()) return activeDir
|
|
36
|
+
|
|
37
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
38
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
39
|
+
if (inactiveStat?.isDirectory()) return inactiveDir
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile, stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir, parseSkillFrontmatter, buildSkillFileTree, isCoreSkill } from '~~/server/utils/skills-path'
|
|
4
|
+
import type { SkillDetail } from '~~/shared/types'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
const name = getRouterParam(event, 'name')
|
|
8
|
+
if (!name)
|
|
9
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
10
|
+
|
|
11
|
+
// Check active first, then inactive
|
|
12
|
+
const activeDir = join(getSkillsDir(), name)
|
|
13
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
14
|
+
|
|
15
|
+
let skillDir = ''
|
|
16
|
+
let active = false
|
|
17
|
+
|
|
18
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
19
|
+
if (activeStat?.isDirectory()) {
|
|
20
|
+
skillDir = activeDir
|
|
21
|
+
active = true
|
|
22
|
+
} else {
|
|
23
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
24
|
+
if (inactiveStat?.isDirectory()) {
|
|
25
|
+
skillDir = inactiveDir
|
|
26
|
+
active = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!skillDir)
|
|
31
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
32
|
+
|
|
33
|
+
const skillMdPath = join(skillDir, 'SKILL.md')
|
|
34
|
+
const content = await readFile(skillMdPath, 'utf-8').catch(() => '')
|
|
35
|
+
const meta = parseSkillFrontmatter(content)
|
|
36
|
+
const files = await buildSkillFileTree(skillDir)
|
|
37
|
+
|
|
38
|
+
const detail: SkillDetail = {
|
|
39
|
+
name: meta.name || name,
|
|
40
|
+
description: meta.description || '',
|
|
41
|
+
version: meta.version || '',
|
|
42
|
+
author: meta.author || '',
|
|
43
|
+
active,
|
|
44
|
+
core: isCoreSkill(name),
|
|
45
|
+
allowedTools: meta.allowedTools,
|
|
46
|
+
requiresSecrets: meta.requiresSecrets,
|
|
47
|
+
installedFrom: meta.installedFrom || '',
|
|
48
|
+
fileCount: files.filter(f => f.type === 'file').length,
|
|
49
|
+
meta,
|
|
50
|
+
files
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { data: detail }
|
|
54
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { stat, rename, readFile, writeFile } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir, isCoreSkill, validateSkillName } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
if (isCoreSkill(name))
|
|
11
|
+
throw createError({ statusCode: 403, message: `'${name}' is a core skill and cannot be renamed` })
|
|
12
|
+
|
|
13
|
+
const body = await readBody<{ newName: string }>(event)
|
|
14
|
+
if (!body?.newName)
|
|
15
|
+
throw createError({ statusCode: 400, message: 'newName is required' })
|
|
16
|
+
|
|
17
|
+
const nameError = validateSkillName(body.newName)
|
|
18
|
+
if (nameError)
|
|
19
|
+
throw createError({ statusCode: 400, message: nameError })
|
|
20
|
+
|
|
21
|
+
// Find where the skill currently lives
|
|
22
|
+
const activeDir = join(getSkillsDir(), name)
|
|
23
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
24
|
+
|
|
25
|
+
let currentDir = ''
|
|
26
|
+
let baseDir = ''
|
|
27
|
+
|
|
28
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
29
|
+
if (activeStat?.isDirectory()) {
|
|
30
|
+
currentDir = activeDir
|
|
31
|
+
baseDir = getSkillsDir()
|
|
32
|
+
} else {
|
|
33
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
34
|
+
if (inactiveStat?.isDirectory()) {
|
|
35
|
+
currentDir = inactiveDir
|
|
36
|
+
baseDir = getInactiveSkillsDir()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!currentDir)
|
|
41
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
42
|
+
|
|
43
|
+
const newDir = join(baseDir, body.newName)
|
|
44
|
+
const newDirStat = await stat(newDir).catch(() => null)
|
|
45
|
+
if (newDirStat)
|
|
46
|
+
throw createError({ statusCode: 409, message: `Skill '${body.newName}' already exists` })
|
|
47
|
+
|
|
48
|
+
// Rename directory
|
|
49
|
+
await rename(currentDir, newDir)
|
|
50
|
+
|
|
51
|
+
// Update name in SKILL.md frontmatter if it exists
|
|
52
|
+
const skillMdPath = join(newDir, 'SKILL.md')
|
|
53
|
+
const content = await readFile(skillMdPath, 'utf-8').catch(() => '')
|
|
54
|
+
if (content) {
|
|
55
|
+
const updated = content.replace(
|
|
56
|
+
/^(name:\s*).+$/m,
|
|
57
|
+
`$1${body.newName}`
|
|
58
|
+
)
|
|
59
|
+
if (updated !== content)
|
|
60
|
+
await writeFile(skillMdPath, updated, 'utf-8')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { data: { name: body.newName } }
|
|
64
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { stat, mkdir, rename } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir, isCoreSkill } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const name = getRouterParam(event, 'name')
|
|
7
|
+
if (!name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name required' })
|
|
9
|
+
|
|
10
|
+
if (isCoreSkill(name))
|
|
11
|
+
throw createError({ statusCode: 403, message: `'${name}' is a core skill and cannot be disabled` })
|
|
12
|
+
|
|
13
|
+
const activeDir = join(getSkillsDir(), name)
|
|
14
|
+
const inactiveDir = join(getInactiveSkillsDir(), name)
|
|
15
|
+
|
|
16
|
+
const activeStat = await stat(activeDir).catch(() => null)
|
|
17
|
+
if (activeStat?.isDirectory()) {
|
|
18
|
+
// Move active -> inactive
|
|
19
|
+
await mkdir(getInactiveSkillsDir(), { recursive: true })
|
|
20
|
+
await rename(activeDir, inactiveDir)
|
|
21
|
+
return { data: { name, active: false } }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const inactiveStat = await stat(inactiveDir).catch(() => null)
|
|
25
|
+
if (inactiveStat?.isDirectory()) {
|
|
26
|
+
// Move inactive -> active
|
|
27
|
+
await rename(inactiveDir, activeDir)
|
|
28
|
+
return { data: { name, active: true } }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw createError({ statusCode: 404, message: `Skill '${name}' not found` })
|
|
32
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, writeFile, stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, validateSkillName } from '~~/server/utils/skills-path'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const body = await readBody<{ name: string, description?: string }>(event)
|
|
7
|
+
if (!body?.name)
|
|
8
|
+
throw createError({ statusCode: 400, message: 'Skill name is required' })
|
|
9
|
+
|
|
10
|
+
const nameError = validateSkillName(body.name)
|
|
11
|
+
if (nameError)
|
|
12
|
+
throw createError({ statusCode: 400, message: nameError })
|
|
13
|
+
|
|
14
|
+
const skillDir = join(getSkillsDir(), body.name)
|
|
15
|
+
|
|
16
|
+
// Check if already exists
|
|
17
|
+
const existing = await stat(skillDir).catch(() => null)
|
|
18
|
+
if (existing)
|
|
19
|
+
throw createError({ statusCode: 409, message: `Skill '${body.name}' already exists` })
|
|
20
|
+
|
|
21
|
+
await mkdir(skillDir, { recursive: true })
|
|
22
|
+
|
|
23
|
+
const description = body.description || `Description for ${body.name}`
|
|
24
|
+
|
|
25
|
+
const skillMd = `---
|
|
26
|
+
name: ${body.name}
|
|
27
|
+
description: ${description}
|
|
28
|
+
allowed-tools: Bash, Read
|
|
29
|
+
metadata:
|
|
30
|
+
version: "1.0.0"
|
|
31
|
+
requires-secrets: []
|
|
32
|
+
author: ""
|
|
33
|
+
repository: ""
|
|
34
|
+
installed-from: ""
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# ${body.name}
|
|
38
|
+
|
|
39
|
+
${description}
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
\`\`\`bash
|
|
44
|
+
python3 ~/.claude/skills/${body.name}/${body.name}.py <command>
|
|
45
|
+
\`\`\`
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
await writeFile(join(skillDir, 'SKILL.md'), skillMd, 'utf-8')
|
|
49
|
+
|
|
50
|
+
return { data: { name: body.name, path: skillDir } }
|
|
51
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdir, readdir, rm, stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { query } from '@anthropic-ai/claude-agent-sdk'
|
|
4
|
+
import { getInactiveSkillsDir, validateSkillName } from '~~/server/utils/skills-path'
|
|
5
|
+
import { logTokenUsage } from '~~/server/utils/log-token-usage'
|
|
6
|
+
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
const body = await readBody<{ name: string, description: string }>(event)
|
|
9
|
+
if (!body?.name || !body?.description)
|
|
10
|
+
throw createError({ statusCode: 400, message: 'name and description are required' })
|
|
11
|
+
|
|
12
|
+
const nameError = validateSkillName(body.name)
|
|
13
|
+
if (nameError)
|
|
14
|
+
throw createError({ statusCode: 400, message: nameError })
|
|
15
|
+
|
|
16
|
+
// Create in inactive-skills so user can review before enabling
|
|
17
|
+
const skillDir = join(getInactiveSkillsDir(), body.name)
|
|
18
|
+
|
|
19
|
+
const existing = await stat(skillDir).catch(() => null)
|
|
20
|
+
if (existing)
|
|
21
|
+
throw createError({ statusCode: 409, message: `Skill '${body.name}' already exists` })
|
|
22
|
+
|
|
23
|
+
await mkdir(skillDir, { recursive: true })
|
|
24
|
+
|
|
25
|
+
const appendInstructions = `You are creating a Claude Code skill called "${body.name}".
|
|
26
|
+
Description: ${body.description}
|
|
27
|
+
|
|
28
|
+
Use the Write tool to create files in the current directory. You MUST create:
|
|
29
|
+
1. SKILL.md with proper frontmatter (name, description, allowed-tools, metadata with version/requires-secrets/author)
|
|
30
|
+
2. A Python script if the skill needs to call APIs or run logic
|
|
31
|
+
|
|
32
|
+
CRITICAL RULES:
|
|
33
|
+
- Use the Write tool to create each file
|
|
34
|
+
- NEVER hardcode API keys, tokens, or secrets. Use get_secret() from the shared library.
|
|
35
|
+
- If the skill needs external API keys, list them in metadata.requires-secrets
|
|
36
|
+
- Import the shared client: sys.path.insert(0, str(Path(__file__).parent.parent)); from _lib.api import get, post, get_secret
|
|
37
|
+
- Use argparse for CLI interface
|
|
38
|
+
- Follow existing skill patterns (task.py, memory.py, etc.)
|
|
39
|
+
|
|
40
|
+
SKILL.md frontmatter format:
|
|
41
|
+
---
|
|
42
|
+
name: ${body.name}
|
|
43
|
+
description: ${body.description}
|
|
44
|
+
allowed-tools: Bash, Read
|
|
45
|
+
metadata:
|
|
46
|
+
version: "1.0.0"
|
|
47
|
+
requires-secrets: []
|
|
48
|
+
author: ""
|
|
49
|
+
repository: ""
|
|
50
|
+
installed-from: ""
|
|
51
|
+
---`
|
|
52
|
+
|
|
53
|
+
const startTime = Date.now()
|
|
54
|
+
let totalInput = 0
|
|
55
|
+
let totalOutput = 0
|
|
56
|
+
let totalCost = 0
|
|
57
|
+
let turns = 0
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const conversation = query({
|
|
61
|
+
prompt: `Create the "${body.name}" skill: ${body.description}`,
|
|
62
|
+
options: {
|
|
63
|
+
systemPrompt: {
|
|
64
|
+
type: 'preset',
|
|
65
|
+
preset: 'claude_code',
|
|
66
|
+
append: appendInstructions
|
|
67
|
+
},
|
|
68
|
+
cwd: skillDir,
|
|
69
|
+
settingSources: ['user'],
|
|
70
|
+
permissionMode: 'bypassPermissions',
|
|
71
|
+
allowDangerouslySkipPermissions: true,
|
|
72
|
+
maxTurns: 30
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
for await (const event of conversation) {
|
|
77
|
+
if (event.type === 'result') {
|
|
78
|
+
// Cast to access usage fields the SDK provides but aren't in the TS types
|
|
79
|
+
const msg = event as unknown as {
|
|
80
|
+
total_cost_usd: number
|
|
81
|
+
num_turns: number
|
|
82
|
+
usage: { input_tokens: number, output_tokens: number }
|
|
83
|
+
}
|
|
84
|
+
totalInput = msg.usage?.input_tokens || 0
|
|
85
|
+
totalOutput = msg.usage?.output_tokens || 0
|
|
86
|
+
totalCost = msg.total_cost_usd || 0
|
|
87
|
+
turns = msg.num_turns || 0
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error('[skills/generate] SDK error:', e)
|
|
92
|
+
// Clean up empty directory on failure
|
|
93
|
+
await rm(skillDir, { recursive: true }).catch(() => {})
|
|
94
|
+
throw createError({ statusCode: 500, message: 'Skill generation failed' })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify files were actually created
|
|
98
|
+
const files = await readdir(skillDir).catch(() => [])
|
|
99
|
+
if (files.length === 0) {
|
|
100
|
+
await rm(skillDir, { recursive: true }).catch(() => {})
|
|
101
|
+
throw createError({ statusCode: 500, message: 'Skill generation produced no files' })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const durationMs = Date.now() - startTime
|
|
105
|
+
|
|
106
|
+
// Log token usage
|
|
107
|
+
await logTokenUsage({
|
|
108
|
+
source: 'agent',
|
|
109
|
+
sourceName: 'Skill Generator',
|
|
110
|
+
inputTokens: totalInput,
|
|
111
|
+
outputTokens: totalOutput,
|
|
112
|
+
costUsd: totalCost,
|
|
113
|
+
durationMs,
|
|
114
|
+
numTurns: turns
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
data: {
|
|
119
|
+
name: body.name,
|
|
120
|
+
path: skillDir,
|
|
121
|
+
active: false,
|
|
122
|
+
costUsd: totalCost,
|
|
123
|
+
durationMs
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getSkillsDir, getInactiveSkillsDir, parseSkillFrontmatter, isCoreSkill } from '~~/server/utils/skills-path'
|
|
4
|
+
import type { SkillListItem } from '~~/shared/types'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async () => {
|
|
7
|
+
const skills: SkillListItem[] = []
|
|
8
|
+
|
|
9
|
+
// Scan active skills
|
|
10
|
+
await scanDir(getSkillsDir(), true, skills)
|
|
11
|
+
|
|
12
|
+
// Scan inactive skills
|
|
13
|
+
await scanDir(getInactiveSkillsDir(), false, skills)
|
|
14
|
+
|
|
15
|
+
// Sort: core first, then alphabetical
|
|
16
|
+
skills.sort((a, b) => {
|
|
17
|
+
if (a.core !== b.core) return a.core ? -1 : 1
|
|
18
|
+
return a.name.localeCompare(b.name)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return { data: skills }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
async function scanDir(dir: string, active: boolean, skills: SkillListItem[]): Promise<void> {
|
|
25
|
+
const entries = await readdir(dir).catch(() => [])
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
// Skip _lib and hidden dirs
|
|
29
|
+
if (entry.startsWith('_') || entry.startsWith('.'))
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
const skillDir = join(dir, entry)
|
|
33
|
+
const stats = await stat(skillDir).catch(() => null)
|
|
34
|
+
if (!stats?.isDirectory()) continue
|
|
35
|
+
|
|
36
|
+
const skillMdPath = join(skillDir, 'SKILL.md')
|
|
37
|
+
const content = await readFile(skillMdPath, 'utf-8').catch(() => '')
|
|
38
|
+
const meta = parseSkillFrontmatter(content)
|
|
39
|
+
|
|
40
|
+
// Count files (excluding __pycache__)
|
|
41
|
+
const files = await readdir(skillDir).catch(() => [])
|
|
42
|
+
const fileCount = files.filter(f => f !== '__pycache__' && !f.endsWith('.pyc')).length
|
|
43
|
+
|
|
44
|
+
skills.push({
|
|
45
|
+
name: meta.name || entry,
|
|
46
|
+
description: meta.description || '',
|
|
47
|
+
version: meta.version || '',
|
|
48
|
+
author: meta.author || '',
|
|
49
|
+
active,
|
|
50
|
+
core: isCoreSkill(entry),
|
|
51
|
+
allowedTools: meta.allowedTools,
|
|
52
|
+
requiresSecrets: meta.requiresSecrets,
|
|
53
|
+
installedFrom: meta.installedFrom || '',
|
|
54
|
+
fileCount
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { getDb } from '~~/server/db'
|
|
4
|
+
import { getSkillsDir, getInactiveSkillsDir, parseSkillFrontmatter } from '~~/server/utils/skills-path'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async () => {
|
|
7
|
+
const db = getDb()
|
|
8
|
+
const catalogItems = await db.query.skillsCatalog.findMany()
|
|
9
|
+
|
|
10
|
+
if (catalogItems.length === 0)
|
|
11
|
+
return { data: { updates: [] } }
|
|
12
|
+
|
|
13
|
+
const catalogByName = new Map(catalogItems.map(c => [c.name, c]))
|
|
14
|
+
const updates: { name: string, installed: string, latest: string }[] = []
|
|
15
|
+
|
|
16
|
+
// Scan both active and inactive skill dirs
|
|
17
|
+
for (const dir of [getSkillsDir(), getInactiveSkillsDir()]) {
|
|
18
|
+
const dirStat = await stat(dir).catch(() => null)
|
|
19
|
+
if (!dirStat) continue
|
|
20
|
+
|
|
21
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory()) continue
|
|
24
|
+
|
|
25
|
+
const skillMdPath = join(dir, entry.name, 'SKILL.md')
|
|
26
|
+
const content = await readFile(skillMdPath, 'utf-8').catch(() => '')
|
|
27
|
+
if (!content) continue
|
|
28
|
+
|
|
29
|
+
const meta = parseSkillFrontmatter(content)
|
|
30
|
+
if (!meta.installedFrom) continue
|
|
31
|
+
|
|
32
|
+
const catalogEntry = catalogByName.get(entry.name)
|
|
33
|
+
if (!catalogEntry) continue
|
|
34
|
+
|
|
35
|
+
if (meta.version && meta.version !== catalogEntry.version) {
|
|
36
|
+
updates.push({
|
|
37
|
+
name: entry.name,
|
|
38
|
+
installed: meta.version,
|
|
39
|
+
latest: catalogEntry.version
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { data: { updates } }
|
|
46
|
+
})
|