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.
Files changed (45) hide show
  1. package/Claude/CLAUDE.md +9 -4
  2. package/Claude/skills/environment/SKILL.md +6 -0
  3. package/Claude/skills/memory/SKILL.md +6 -0
  4. package/Claude/skills/project/SKILL.md +6 -0
  5. package/Claude/skills/secret/SKILL.md +85 -0
  6. package/Claude/skills/secret/secret.py +146 -0
  7. package/Claude/skills/skill-creator/SKILL.md +30 -0
  8. package/Claude/skills/task/SKILL.md +6 -0
  9. package/app/components/skills/Card.vue +82 -0
  10. package/app/components/skills/CreateModal.vue +156 -0
  11. package/app/components/skills/Editor.vue +135 -0
  12. package/app/components/skills/FileTree.vue +336 -0
  13. package/app/components/skills/LibraryCard.vue +122 -0
  14. package/app/components/skills/RenameModal.vue +84 -0
  15. package/app/layouts/dashboard.vue +7 -0
  16. package/app/pages/skills/[name].vue +198 -0
  17. package/app/pages/skills/index.vue +157 -0
  18. package/app/pages/skills/library.vue +209 -0
  19. package/dist/cli/index.js +23 -23
  20. package/nuxt.config.ts +9 -0
  21. package/package.json +1 -1
  22. package/server/api/skills/[name]/files/create.post.ts +45 -0
  23. package/server/api/skills/[name]/files/delete.post.ts +45 -0
  24. package/server/api/skills/[name]/files/index.get.ts +28 -0
  25. package/server/api/skills/[name]/files/read.post.ts +41 -0
  26. package/server/api/skills/[name]/files/write.post.ts +42 -0
  27. package/server/api/skills/[name]/index.get.ts +54 -0
  28. package/server/api/skills/[name]/rename.post.ts +64 -0
  29. package/server/api/skills/[name]/toggle.post.ts +32 -0
  30. package/server/api/skills/create.post.ts +51 -0
  31. package/server/api/skills/generate.post.ts +126 -0
  32. package/server/api/skills/index.get.ts +57 -0
  33. package/server/api/skills/library/check-updates.get.ts +46 -0
  34. package/server/api/skills/library/index.get.ts +56 -0
  35. package/server/api/skills/library/install.post.ts +73 -0
  36. package/server/db/schema.ts +17 -0
  37. package/server/drizzle/migrations/0012_good_deadpool.sql +12 -0
  38. package/server/drizzle/migrations/0013_swift_snowbird.sql +1 -0
  39. package/server/drizzle/migrations/meta/0012_snapshot.json +1713 -0
  40. package/server/drizzle/migrations/meta/0013_snapshot.json +1720 -0
  41. package/server/drizzle/migrations/meta/_journal.json +14 -0
  42. package/server/middleware/auth.ts +0 -1
  43. package/server/plugins/05.skills-catalog.ts +105 -0
  44. package/server/utils/skills-path.ts +197 -0
  45. 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
+ })