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
|
@@ -85,6 +85,20 @@
|
|
|
85
85
|
"when": 1771384095170,
|
|
86
86
|
"tag": "0011_tearful_johnny_storm",
|
|
87
87
|
"breakpoints": true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"idx": 12,
|
|
91
|
+
"version": "7",
|
|
92
|
+
"when": 1771472797273,
|
|
93
|
+
"tag": "0012_good_deadpool",
|
|
94
|
+
"breakpoints": true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"idx": 13,
|
|
98
|
+
"version": "7",
|
|
99
|
+
"when": 1771518918468,
|
|
100
|
+
"tag": "0013_swift_snowbird",
|
|
101
|
+
"breakpoints": true
|
|
88
102
|
}
|
|
89
103
|
]
|
|
90
104
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { waitForDb } from '~~/server/utils/db-state'
|
|
3
|
+
import { getDb } from '~~/server/db'
|
|
4
|
+
import * as schema from '~~/server/db/schema'
|
|
5
|
+
|
|
6
|
+
const REGISTRY_URL = 'https://raw.githubusercontent.com/Patrity/cognova-skills/main/registry.json'
|
|
7
|
+
const SYNC_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes
|
|
8
|
+
|
|
9
|
+
interface RegistryEntry {
|
|
10
|
+
name: string
|
|
11
|
+
description: string
|
|
12
|
+
version: string
|
|
13
|
+
author: string
|
|
14
|
+
tags: string[]
|
|
15
|
+
requiresSecrets: string[]
|
|
16
|
+
files: string[]
|
|
17
|
+
updatedAt: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function syncRegistry() {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(REGISTRY_URL)
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
console.warn(`[skills-catalog] Failed to fetch registry: ${response.status}`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries: RegistryEntry[] = await response.json()
|
|
29
|
+
if (!Array.isArray(entries)) {
|
|
30
|
+
console.warn('[skills-catalog] Invalid registry format')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const db = getDb()
|
|
35
|
+
const now = new Date()
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.name || !entry.description || !entry.version) continue
|
|
39
|
+
|
|
40
|
+
await db.insert(schema.skillsCatalog)
|
|
41
|
+
.values({
|
|
42
|
+
name: entry.name,
|
|
43
|
+
description: entry.description,
|
|
44
|
+
version: entry.version,
|
|
45
|
+
author: entry.author || '',
|
|
46
|
+
tags: entry.tags || [],
|
|
47
|
+
requiresSecrets: entry.requiresSecrets || [],
|
|
48
|
+
files: entry.files || [],
|
|
49
|
+
updatedAt: new Date(entry.updatedAt || now),
|
|
50
|
+
syncedAt: now
|
|
51
|
+
})
|
|
52
|
+
.onConflictDoUpdate({
|
|
53
|
+
target: schema.skillsCatalog.name,
|
|
54
|
+
set: {
|
|
55
|
+
description: entry.description,
|
|
56
|
+
version: entry.version,
|
|
57
|
+
author: entry.author || '',
|
|
58
|
+
tags: entry.tags || [],
|
|
59
|
+
requiresSecrets: entry.requiresSecrets || [],
|
|
60
|
+
files: entry.files || [],
|
|
61
|
+
updatedAt: new Date(entry.updatedAt || now),
|
|
62
|
+
syncedAt: now
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Remove skills no longer in registry
|
|
68
|
+
const catalogItems = await db.query.skillsCatalog.findMany()
|
|
69
|
+
const registryNames = new Set(entries.map(e => e.name))
|
|
70
|
+
for (const item of catalogItems) {
|
|
71
|
+
if (!registryNames.has(item.name)) {
|
|
72
|
+
await db.delete(schema.skillsCatalog)
|
|
73
|
+
.where(eq(schema.skillsCatalog.id, item.id))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[skills-catalog] Synced ${entries.length} skills from registry`)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn('[skills-catalog] Registry sync failed:', error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let syncTimer: ReturnType<typeof setInterval> | null = null
|
|
84
|
+
|
|
85
|
+
export default defineNitroPlugin(async (nitroApp) => {
|
|
86
|
+
const dbAvailable = await waitForDb(10000)
|
|
87
|
+
if (!dbAvailable) {
|
|
88
|
+
console.log('[skills-catalog] Database not available, skipping')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initial sync after a short delay
|
|
93
|
+
setTimeout(() => syncRegistry(), 10000)
|
|
94
|
+
|
|
95
|
+
// Periodic sync
|
|
96
|
+
syncTimer = setInterval(() => syncRegistry(), SYNC_INTERVAL_MS)
|
|
97
|
+
|
|
98
|
+
nitroApp.hooks.hook('close', () => {
|
|
99
|
+
if (syncTimer) {
|
|
100
|
+
clearInterval(syncTimer)
|
|
101
|
+
syncTimer = null
|
|
102
|
+
}
|
|
103
|
+
console.log('[skills-catalog] Cleanup complete')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { homedir } from 'os'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import type { SkillMeta, SkillFile } from '~~/shared/types'
|
|
4
|
+
|
|
5
|
+
const CORE_SKILLS = ['memory', 'task', 'project', 'secret', 'environment']
|
|
6
|
+
|
|
7
|
+
export function getSkillsDir(): string {
|
|
8
|
+
return join(homedir(), '.claude', 'skills')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getInactiveSkillsDir(): string {
|
|
12
|
+
return join(homedir(), '.claude', 'inactive-skills')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getSkillPath(name: string, active: boolean = true): string {
|
|
16
|
+
const base = active ? getSkillsDir() : getInactiveSkillsDir()
|
|
17
|
+
return join(base, name)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isCoreSkill(name: string): boolean {
|
|
21
|
+
return CORE_SKILLS.includes(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse SKILL.md YAML frontmatter into SkillMeta.
|
|
26
|
+
* Handles both top-level and metadata-nested custom fields.
|
|
27
|
+
*/
|
|
28
|
+
export function parseSkillFrontmatter(content: string): SkillMeta {
|
|
29
|
+
const defaults: SkillMeta = {
|
|
30
|
+
name: '',
|
|
31
|
+
description: '',
|
|
32
|
+
version: '',
|
|
33
|
+
author: '',
|
|
34
|
+
tags: [],
|
|
35
|
+
allowedTools: [],
|
|
36
|
+
requiresSecrets: [],
|
|
37
|
+
repository: '',
|
|
38
|
+
installedFrom: '',
|
|
39
|
+
disableModelInvocation: false,
|
|
40
|
+
userInvocable: true,
|
|
41
|
+
context: '',
|
|
42
|
+
agent: ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/)
|
|
46
|
+
if (!match?.[1]) return defaults
|
|
47
|
+
|
|
48
|
+
const yaml = match[1]
|
|
49
|
+
const result = { ...defaults }
|
|
50
|
+
|
|
51
|
+
// Simple YAML parser for flat + one-level nested keys
|
|
52
|
+
let inMetadata = false
|
|
53
|
+
for (const line of yaml.split('\n')) {
|
|
54
|
+
// Check for metadata block
|
|
55
|
+
if (line === 'metadata:') {
|
|
56
|
+
inMetadata = true
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Nested under metadata (indented)
|
|
61
|
+
if (inMetadata && /^\s{2}\S/.test(line)) {
|
|
62
|
+
const nested = line.trim()
|
|
63
|
+
const colonIdx = nested.indexOf(':')
|
|
64
|
+
if (colonIdx === -1) continue
|
|
65
|
+
const key = nested.slice(0, colonIdx).trim()
|
|
66
|
+
const val = nested.slice(colonIdx + 1).trim()
|
|
67
|
+
assignMetaField(result, key, val)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// End of metadata block if we hit a non-indented line
|
|
72
|
+
if (inMetadata && /^\S/.test(line))
|
|
73
|
+
inMetadata = false
|
|
74
|
+
|
|
75
|
+
// Top-level key
|
|
76
|
+
const colonIdx = line.indexOf(':')
|
|
77
|
+
if (colonIdx === -1) continue
|
|
78
|
+
const key = line.slice(0, colonIdx).trim()
|
|
79
|
+
const val = line.slice(colonIdx + 1).trim()
|
|
80
|
+
assignTopLevelField(result, key, val)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assignTopLevelField(result: SkillMeta, key: string, val: string): void {
|
|
87
|
+
switch (key) {
|
|
88
|
+
case 'name':
|
|
89
|
+
result.name = unquote(val)
|
|
90
|
+
break
|
|
91
|
+
case 'description':
|
|
92
|
+
result.description = unquote(val)
|
|
93
|
+
break
|
|
94
|
+
case 'allowed-tools':
|
|
95
|
+
result.allowedTools = val.split(',').map(s => s.trim()).filter(Boolean)
|
|
96
|
+
break
|
|
97
|
+
case 'disable-model-invocation':
|
|
98
|
+
result.disableModelInvocation = val === 'true'
|
|
99
|
+
break
|
|
100
|
+
case 'user-invocable':
|
|
101
|
+
result.userInvocable = val !== 'false'
|
|
102
|
+
break
|
|
103
|
+
case 'context':
|
|
104
|
+
result.context = unquote(val)
|
|
105
|
+
break
|
|
106
|
+
case 'agent':
|
|
107
|
+
result.agent = unquote(val)
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function assignMetaField(result: SkillMeta, key: string, val: string): void {
|
|
113
|
+
switch (key) {
|
|
114
|
+
case 'version':
|
|
115
|
+
result.version = unquote(val)
|
|
116
|
+
break
|
|
117
|
+
case 'author':
|
|
118
|
+
result.author = unquote(val)
|
|
119
|
+
break
|
|
120
|
+
case 'tags':
|
|
121
|
+
result.tags = parseYamlArray(val)
|
|
122
|
+
break
|
|
123
|
+
case 'requires-secrets':
|
|
124
|
+
result.requiresSecrets = parseYamlArray(val)
|
|
125
|
+
break
|
|
126
|
+
case 'repository':
|
|
127
|
+
result.repository = unquote(val)
|
|
128
|
+
break
|
|
129
|
+
case 'installed-from':
|
|
130
|
+
result.installedFrom = unquote(val)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function unquote(s: string): string {
|
|
136
|
+
if ((s.startsWith('"') && s.endsWith('"'))
|
|
137
|
+
|| (s.startsWith('\'') && s.endsWith('\'')))
|
|
138
|
+
return s.slice(1, -1)
|
|
139
|
+
return s
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseYamlArray(val: string): string[] {
|
|
143
|
+
// Handle inline array: ["key1", "key2"] or [key1, key2]
|
|
144
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
145
|
+
const inner = val.slice(1, -1).trim()
|
|
146
|
+
if (!inner) return []
|
|
147
|
+
return inner.split(',').map(s => unquote(s.trim())).filter(Boolean)
|
|
148
|
+
}
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build a file tree for a skill directory.
|
|
154
|
+
* Excludes __pycache__ and .pyc files.
|
|
155
|
+
*/
|
|
156
|
+
export async function buildSkillFileTree(dirPath: string): Promise<SkillFile[]> {
|
|
157
|
+
const { readdir, stat } = await import('fs/promises')
|
|
158
|
+
const entries = await readdir(dirPath).catch(() => [])
|
|
159
|
+
const files: SkillFile[] = []
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry === '__pycache__' || entry.endsWith('.pyc'))
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
const fullPath = join(dirPath, entry)
|
|
166
|
+
const stats = await stat(fullPath).catch(() => null)
|
|
167
|
+
if (!stats) continue
|
|
168
|
+
|
|
169
|
+
if (stats.isDirectory()) {
|
|
170
|
+
const children = await buildSkillFileTree(fullPath)
|
|
171
|
+
files.push({ name: entry, path: entry, type: 'directory', children })
|
|
172
|
+
} else {
|
|
173
|
+
files.push({ name: entry, path: entry, type: 'file' })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort: directories first, then alphabetical
|
|
178
|
+
files.sort((a, b) => {
|
|
179
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
|
|
180
|
+
return a.name.localeCompare(b.name)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return files
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validate a skill name for filesystem safety.
|
|
188
|
+
*/
|
|
189
|
+
export function validateSkillName(name: string): string | null {
|
|
190
|
+
if (!name || name.length > 64)
|
|
191
|
+
return 'Name must be 1-64 characters'
|
|
192
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(name))
|
|
193
|
+
return 'Name must start with a letter/digit and contain only lowercase letters, digits, hyphens, and underscores'
|
|
194
|
+
if (name === '_lib')
|
|
195
|
+
return '_lib is reserved'
|
|
196
|
+
return null
|
|
197
|
+
}
|
package/shared/types/index.ts
CHANGED
|
@@ -454,6 +454,69 @@ export interface DashboardOverview {
|
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// === Skills ===
|
|
458
|
+
|
|
459
|
+
export interface SkillMeta {
|
|
460
|
+
name: string
|
|
461
|
+
description: string
|
|
462
|
+
version: string
|
|
463
|
+
author: string
|
|
464
|
+
tags: string[]
|
|
465
|
+
allowedTools: string[]
|
|
466
|
+
requiresSecrets: string[]
|
|
467
|
+
repository: string
|
|
468
|
+
installedFrom: string
|
|
469
|
+
disableModelInvocation: boolean
|
|
470
|
+
userInvocable: boolean
|
|
471
|
+
context: string
|
|
472
|
+
agent: string
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface SkillListItem {
|
|
476
|
+
name: string
|
|
477
|
+
description: string
|
|
478
|
+
version: string
|
|
479
|
+
author: string
|
|
480
|
+
active: boolean
|
|
481
|
+
core: boolean
|
|
482
|
+
allowedTools: string[]
|
|
483
|
+
requiresSecrets: string[]
|
|
484
|
+
installedFrom: string
|
|
485
|
+
fileCount: number
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface SkillDetail extends SkillListItem {
|
|
489
|
+
meta: SkillMeta
|
|
490
|
+
files: SkillFile[]
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface SkillFile {
|
|
494
|
+
name: string
|
|
495
|
+
path: string
|
|
496
|
+
type: 'file' | 'directory'
|
|
497
|
+
children?: SkillFile[]
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// === Skills Library ===
|
|
501
|
+
|
|
502
|
+
// First tag must be 'community' or 'official'. Up to 4 additional free-form tags.
|
|
503
|
+
export type SkillSourceTag = 'community' | 'official'
|
|
504
|
+
|
|
505
|
+
export interface SkillCatalogItem {
|
|
506
|
+
id: string
|
|
507
|
+
name: string
|
|
508
|
+
description: string
|
|
509
|
+
version: string
|
|
510
|
+
author: string
|
|
511
|
+
tags: string[]
|
|
512
|
+
requiresSecrets: string[]
|
|
513
|
+
files: string[]
|
|
514
|
+
updatedAt: string
|
|
515
|
+
installed?: boolean
|
|
516
|
+
installedVersion?: string
|
|
517
|
+
hasUpdate?: boolean
|
|
518
|
+
}
|
|
519
|
+
|
|
457
520
|
// === Hook Events ===
|
|
458
521
|
|
|
459
522
|
export type HookEventType
|