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
@@ -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
  }
@@ -33,7 +33,6 @@ async function checkApiToken(event: H3Event): Promise<boolean> {
33
33
 
34
34
  export default defineEventHandler(async (event) => {
35
35
  const path = getRequestURL(event).pathname
36
-
37
36
  // Skip auth for root path (public home page)
38
37
  if (path === '/') return
39
38
 
@@ -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
+ }
@@ -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