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,135 @@
1
+ <script setup lang="ts">
2
+ import type { CodeLanguage } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ skillName: string
6
+ filePath: string
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ saved: []
11
+ }>()
12
+
13
+ const toast = useToast()
14
+ const content = ref('')
15
+ const originalContent = ref('')
16
+ const loading = ref(true)
17
+ const saving = ref(false)
18
+
19
+ const isDirty = computed(() => content.value !== originalContent.value)
20
+
21
+ const language = computed<CodeLanguage>(() => {
22
+ const ext = props.filePath.split('.').pop()?.toLowerCase()
23
+ switch (ext) {
24
+ case 'py': return 'python'
25
+ case 'md': return 'markdown'
26
+ case 'json': return 'json'
27
+ case 'ts': return 'typescript'
28
+ case 'js': return 'javascript'
29
+ case 'yaml':
30
+ case 'yml': return 'yaml'
31
+ case 'sh':
32
+ case 'bash': return 'bash'
33
+ default: return 'plaintext'
34
+ }
35
+ })
36
+
37
+ async function loadFile() {
38
+ loading.value = true
39
+ try {
40
+ const res = await $fetch<{ data: { content: string } }>(`/api/skills/${props.skillName}/files/read`, {
41
+ method: 'POST',
42
+ body: { path: props.filePath }
43
+ })
44
+ content.value = res.data.content
45
+ originalContent.value = res.data.content
46
+ } catch {
47
+ toast.add({ title: 'Failed to load file', color: 'error' })
48
+ } finally {
49
+ loading.value = false
50
+ }
51
+ }
52
+
53
+ async function save() {
54
+ saving.value = true
55
+ try {
56
+ await $fetch(`/api/skills/${props.skillName}/files/write`, {
57
+ method: 'POST',
58
+ body: { path: props.filePath, content: content.value }
59
+ })
60
+ originalContent.value = content.value
61
+ toast.add({ title: 'File saved', color: 'success' })
62
+ emit('saved')
63
+ } catch {
64
+ toast.add({ title: 'Failed to save file', color: 'error' })
65
+ } finally {
66
+ saving.value = false
67
+ }
68
+ }
69
+
70
+ // Ctrl+S / Cmd+S keyboard shortcut
71
+ function handleKeydown(e: KeyboardEvent) {
72
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
73
+ e.preventDefault()
74
+ if (isDirty.value) save()
75
+ }
76
+ }
77
+
78
+ onMounted(() => {
79
+ loadFile()
80
+ window.addEventListener('keydown', handleKeydown)
81
+ })
82
+
83
+ onUnmounted(() => {
84
+ window.removeEventListener('keydown', handleKeydown)
85
+ })
86
+
87
+ watch(() => props.filePath, () => loadFile())
88
+ </script>
89
+
90
+ <template>
91
+ <div class="flex flex-col h-full">
92
+ <div class="flex items-center justify-between px-4 py-2 border-b border-default">
93
+ <div class="flex items-center gap-2 text-sm text-muted">
94
+ <UIcon
95
+ name="i-lucide-file-code"
96
+ class="size-4"
97
+ />
98
+ <span>{{ filePath }}</span>
99
+ <UBadge
100
+ v-if="isDirty"
101
+ variant="soft"
102
+ color="warning"
103
+ size="xs"
104
+ >
105
+ Modified
106
+ </UBadge>
107
+ </div>
108
+ <UButton
109
+ size="xs"
110
+ :loading="saving"
111
+ :disabled="!isDirty"
112
+ @click="save"
113
+ >
114
+ Save
115
+ </UButton>
116
+ </div>
117
+
118
+ <div
119
+ v-if="loading"
120
+ class="flex-1 flex items-center justify-center"
121
+ >
122
+ <UIcon
123
+ name="i-lucide-loader-2"
124
+ class="size-6 animate-spin text-dimmed"
125
+ />
126
+ </div>
127
+
128
+ <EditorCodeEditor
129
+ v-else
130
+ v-model="content"
131
+ :language="language"
132
+ class="flex-1"
133
+ />
134
+ </div>
135
+ </template>
@@ -0,0 +1,336 @@
1
+ <script setup lang="ts">
2
+ import type { TreeItem } from '~/composables/useFileTree'
3
+ import type { SkillFile } from '~~/shared/types'
4
+
5
+ const props = defineProps<{
6
+ skillName: string
7
+ files: SkillFile[]
8
+ selectedPath?: string
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ select: [path: string]
13
+ refresh: []
14
+ }>()
15
+
16
+ const toast = useToast()
17
+
18
+ // Convert SkillFile[] to TreeItem[] for UTree
19
+ function toTreeItems(files: SkillFile[], parentPath = ''): TreeItem[] {
20
+ return files.map((f) => {
21
+ const fullPath = parentPath ? `${parentPath}/${f.path}` : f.path
22
+ const item: TreeItem = {
23
+ id: fullPath,
24
+ label: f.name,
25
+ path: fullPath,
26
+ type: f.type,
27
+ defaultExpanded: true
28
+ }
29
+ if (f.type === 'directory') {
30
+ item.children = f.children ? toTreeItems(f.children, fullPath) : []
31
+ } else {
32
+ if (f.name.endsWith('.py')) item.icon = 'i-lucide-file-code'
33
+ else if (f.name.endsWith('.md')) item.icon = 'i-lucide-file-text'
34
+ else if (f.name.endsWith('.json')) item.icon = 'i-lucide-file-json'
35
+ else item.icon = 'i-lucide-file'
36
+ }
37
+ return item
38
+ })
39
+ }
40
+
41
+ const treeItems = computed(() => toTreeItems(props.files))
42
+
43
+ const contextMenuTarget = ref<TreeItem | null>(null)
44
+ const showNewFileModal = ref(false)
45
+ const showNewFolderModal = ref(false)
46
+ const showDeleteConfirm = ref(false)
47
+ const newItemName = ref('')
48
+
49
+ const contextMenuItems = computed(() => [
50
+ [{
51
+ label: 'New File',
52
+ icon: 'i-lucide-file-plus',
53
+ onSelect: () => {
54
+ newItemName.value = ''
55
+ showNewFileModal.value = true
56
+ }
57
+ }, {
58
+ label: 'New Folder',
59
+ icon: 'i-lucide-folder-plus',
60
+ onSelect: () => {
61
+ newItemName.value = ''
62
+ showNewFolderModal.value = true
63
+ }
64
+ }],
65
+ [{
66
+ label: 'Delete',
67
+ icon: 'i-lucide-trash-2',
68
+ color: 'error' as const,
69
+ onSelect: () => {
70
+ if (contextMenuTarget.value && contextMenuTarget.value.label !== 'SKILL.md')
71
+ showDeleteConfirm.value = true
72
+ }
73
+ }]
74
+ ])
75
+
76
+ function handleSelect(_e: unknown, item: TreeItem) {
77
+ if (item.type === 'file') {
78
+ emit('select', item.path)
79
+ }
80
+ }
81
+
82
+ async function handleCreateFile() {
83
+ if (!newItemName.value.trim()) return
84
+
85
+ let filename = newItemName.value.trim()
86
+ if (!filename.includes('.')) filename += '.py'
87
+
88
+ const parentPath = contextMenuTarget.value?.type === 'directory'
89
+ ? contextMenuTarget.value.path
90
+ : ''
91
+
92
+ const filePath = parentPath ? `${parentPath}/${filename}` : filename
93
+
94
+ try {
95
+ await $fetch(`/api/skills/${props.skillName}/files/create`, {
96
+ method: 'POST',
97
+ body: { path: filePath, type: 'file' }
98
+ })
99
+ emit('refresh')
100
+ emit('select', filePath)
101
+ toast.add({ title: `Created ${filename}`, color: 'success' })
102
+ } catch {
103
+ toast.add({ title: 'Failed to create file', color: 'error' })
104
+ }
105
+
106
+ showNewFileModal.value = false
107
+ newItemName.value = ''
108
+ }
109
+
110
+ async function handleCreateFolder() {
111
+ if (!newItemName.value.trim()) return
112
+
113
+ const folderName = newItemName.value.trim()
114
+ const parentPath = contextMenuTarget.value?.type === 'directory'
115
+ ? contextMenuTarget.value.path
116
+ : ''
117
+
118
+ const folderPath = parentPath ? `${parentPath}/${folderName}` : folderName
119
+
120
+ try {
121
+ await $fetch(`/api/skills/${props.skillName}/files/create`, {
122
+ method: 'POST',
123
+ body: { path: folderPath, type: 'directory' }
124
+ })
125
+ emit('refresh')
126
+ toast.add({ title: `Created ${folderName}/`, color: 'success' })
127
+ } catch {
128
+ toast.add({ title: 'Failed to create folder', color: 'error' })
129
+ }
130
+
131
+ showNewFolderModal.value = false
132
+ newItemName.value = ''
133
+ }
134
+
135
+ async function handleDelete() {
136
+ if (!contextMenuTarget.value) return
137
+
138
+ try {
139
+ await $fetch(`/api/skills/${props.skillName}/files/delete`, {
140
+ method: 'POST',
141
+ body: { path: contextMenuTarget.value.path }
142
+ })
143
+ emit('refresh')
144
+ toast.add({ title: `Deleted ${contextMenuTarget.value.label}`, color: 'success' })
145
+ } catch {
146
+ toast.add({ title: 'Failed to delete', color: 'error' })
147
+ }
148
+
149
+ showDeleteConfirm.value = false
150
+ }
151
+ </script>
152
+
153
+ <template>
154
+ <div class="h-full flex flex-col">
155
+ <UContextMenu
156
+ :items="contextMenuItems"
157
+ >
158
+ <div
159
+ class="flex-1 h-full overflow-auto p-2"
160
+ @contextmenu.self="contextMenuTarget = null"
161
+ >
162
+ <div
163
+ v-if="files.length === 0"
164
+ class="h-full min-h-32 flex flex-col items-center justify-center text-dimmed text-sm"
165
+ >
166
+ <UIcon
167
+ name="i-lucide-folder-open"
168
+ class="size-8 mx-auto mb-2 opacity-50"
169
+ />
170
+ <p>No files found</p>
171
+ <p class="text-xs mt-1">
172
+ Right-click to create a file
173
+ </p>
174
+ </div>
175
+
176
+ <UTree
177
+ v-else
178
+ :items="treeItems"
179
+ :get-key="(item: TreeItem) => item.id"
180
+ color="primary"
181
+ expanded-icon="i-lucide-folder-open"
182
+ collapsed-icon="i-lucide-folder"
183
+ @select="handleSelect"
184
+ >
185
+ <template #item="{ item, expanded }">
186
+ <div
187
+ class="flex items-center gap-2 w-full"
188
+ @contextmenu="contextMenuTarget = item"
189
+ >
190
+ <UIcon
191
+ v-if="item.children?.length"
192
+ :name="expanded ? 'i-lucide-folder-open' : 'i-lucide-folder'"
193
+ class="size-4 shrink-0 text-dimmed"
194
+ />
195
+ <UIcon
196
+ v-else-if="item.icon"
197
+ :name="item.icon"
198
+ class="size-4 shrink-0 text-dimmed"
199
+ />
200
+ <UIcon
201
+ v-else
202
+ name="i-lucide-file"
203
+ class="size-4 shrink-0 text-dimmed"
204
+ />
205
+ <span class="truncate">{{ item.label }}</span>
206
+ </div>
207
+ </template>
208
+ </UTree>
209
+ </div>
210
+ </UContextMenu>
211
+
212
+ <!-- New File Modal -->
213
+ <UModal v-model:open="showNewFileModal">
214
+ <template #content>
215
+ <UCard>
216
+ <template #header>
217
+ <div class="flex items-center gap-2">
218
+ <UIcon
219
+ name="i-lucide-file-plus"
220
+ class="size-5"
221
+ />
222
+ <span class="font-semibold">New File</span>
223
+ </div>
224
+ </template>
225
+
226
+ <UInput
227
+ v-model="newItemName"
228
+ placeholder="filename.py"
229
+ autofocus
230
+ @keyup.enter="handleCreateFile"
231
+ />
232
+
233
+ <template #footer>
234
+ <div class="flex justify-end gap-2">
235
+ <UButton
236
+ color="neutral"
237
+ variant="ghost"
238
+ @click="showNewFileModal = false"
239
+ >
240
+ Cancel
241
+ </UButton>
242
+ <UButton @click="handleCreateFile">
243
+ Create
244
+ </UButton>
245
+ </div>
246
+ </template>
247
+ </UCard>
248
+ </template>
249
+ </UModal>
250
+
251
+ <!-- New Folder Modal -->
252
+ <UModal v-model:open="showNewFolderModal">
253
+ <template #content>
254
+ <UCard>
255
+ <template #header>
256
+ <div class="flex items-center gap-2">
257
+ <UIcon
258
+ name="i-lucide-folder-plus"
259
+ class="size-5"
260
+ />
261
+ <span class="font-semibold">New Folder</span>
262
+ </div>
263
+ </template>
264
+
265
+ <UInput
266
+ v-model="newItemName"
267
+ placeholder="folder-name"
268
+ autofocus
269
+ @keyup.enter="handleCreateFolder"
270
+ />
271
+
272
+ <template #footer>
273
+ <div class="flex justify-end gap-2">
274
+ <UButton
275
+ color="neutral"
276
+ variant="ghost"
277
+ @click="showNewFolderModal = false"
278
+ >
279
+ Cancel
280
+ </UButton>
281
+ <UButton @click="handleCreateFolder">
282
+ Create
283
+ </UButton>
284
+ </div>
285
+ </template>
286
+ </UCard>
287
+ </template>
288
+ </UModal>
289
+
290
+ <!-- Delete Confirmation -->
291
+ <UModal v-model:open="showDeleteConfirm">
292
+ <template #content>
293
+ <UCard>
294
+ <template #header>
295
+ <div class="flex items-center gap-2 text-error">
296
+ <UIcon
297
+ name="i-lucide-trash-2"
298
+ class="size-5"
299
+ />
300
+ <span class="font-semibold">Delete</span>
301
+ </div>
302
+ </template>
303
+
304
+ <p>
305
+ Are you sure you want to delete
306
+ <strong>{{ contextMenuTarget?.label }}</strong>?
307
+ </p>
308
+ <p
309
+ v-if="contextMenuTarget?.type === 'directory'"
310
+ class="text-sm text-dimmed mt-1"
311
+ >
312
+ This will delete all files and folders inside.
313
+ </p>
314
+
315
+ <template #footer>
316
+ <div class="flex justify-end gap-2">
317
+ <UButton
318
+ color="neutral"
319
+ variant="ghost"
320
+ @click="showDeleteConfirm = false"
321
+ >
322
+ Cancel
323
+ </UButton>
324
+ <UButton
325
+ color="error"
326
+ @click="handleDelete"
327
+ >
328
+ Delete
329
+ </UButton>
330
+ </div>
331
+ </template>
332
+ </UCard>
333
+ </template>
334
+ </UModal>
335
+ </div>
336
+ </template>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ import type { SkillCatalogItem } from '~~/shared/types'
3
+
4
+ defineProps<{
5
+ skill: SkillCatalogItem
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ install: [name: string]
10
+ update: [name: string]
11
+ }>()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="p-4 rounded-lg border border-default bg-elevated/50 hover:bg-elevated transition-colors">
16
+ <div class="flex items-start justify-between gap-2 mb-2">
17
+ <div class="flex items-center gap-2 min-w-0">
18
+ <UIcon
19
+ name="i-lucide-puzzle"
20
+ class="size-4 shrink-0 text-primary"
21
+ />
22
+ <span class="font-medium truncate">{{ skill.name }}</span>
23
+ </div>
24
+ <div class="flex items-center gap-1.5 shrink-0">
25
+ <UBadge
26
+ v-if="skill.installed && !skill.hasUpdate"
27
+ variant="subtle"
28
+ color="success"
29
+ size="xs"
30
+ >
31
+ Installed
32
+ </UBadge>
33
+ <UBadge
34
+ v-if="skill.hasUpdate"
35
+ variant="subtle"
36
+ color="warning"
37
+ size="xs"
38
+ >
39
+ Update available
40
+ </UBadge>
41
+ <UBadge
42
+ variant="subtle"
43
+ color="neutral"
44
+ size="xs"
45
+ >
46
+ v{{ skill.version }}
47
+ </UBadge>
48
+ </div>
49
+ </div>
50
+
51
+ <p class="text-sm text-muted line-clamp-2 mb-3">
52
+ {{ skill.description }}
53
+ </p>
54
+
55
+ <div
56
+ v-if="skill.tags.length > 0"
57
+ class="flex flex-wrap gap-1 mb-3"
58
+ >
59
+ <UBadge
60
+ v-for="tag in skill.tags"
61
+ :key="tag"
62
+ variant="subtle"
63
+ :color="tag === 'official' ? 'primary' : 'neutral'"
64
+ size="xs"
65
+ >
66
+ {{ tag }}
67
+ </UBadge>
68
+ </div>
69
+
70
+ <div
71
+ v-if="skill.requiresSecrets.length > 0"
72
+ class="flex items-center gap-1 mb-3 text-xs text-dimmed"
73
+ >
74
+ <UIcon
75
+ name="i-lucide-key-round"
76
+ class="size-3 shrink-0"
77
+ />
78
+ <span>Requires: {{ skill.requiresSecrets.join(', ') }}</span>
79
+ </div>
80
+
81
+ <div class="flex items-center justify-between">
82
+ <span
83
+ v-if="skill.author"
84
+ class="text-xs text-dimmed"
85
+ >
86
+ by {{ skill.author }}
87
+ </span>
88
+ <span v-else />
89
+
90
+ <div class="flex items-center gap-1.5">
91
+ <UButton
92
+ size="xs"
93
+ variant="ghost"
94
+ color="neutral"
95
+ icon="i-lucide-external-link"
96
+ :to="`https://github.com/Patrity/cognova-skills/tree/main/${skill.name}`"
97
+ target="_blank"
98
+ >
99
+ View
100
+ </UButton>
101
+ <UButton
102
+ v-if="!skill.installed"
103
+ size="xs"
104
+ icon="i-lucide-download"
105
+ @click="emit('install', skill.name)"
106
+ >
107
+ Install
108
+ </UButton>
109
+ <UButton
110
+ v-else-if="skill.hasUpdate"
111
+ size="xs"
112
+ color="warning"
113
+ variant="soft"
114
+ icon="i-lucide-refresh-cw"
115
+ @click="emit('update', skill.name)"
116
+ >
117
+ Update
118
+ </UButton>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ open: boolean
4
+ currentName: string
5
+ }>()
6
+
7
+ const emit = defineEmits<{
8
+ 'update:open': [value: boolean]
9
+ 'renamed': [newName: string]
10
+ }>()
11
+
12
+ const toast = useToast()
13
+ const newName = ref(props.currentName)
14
+ const loading = ref(false)
15
+
16
+ watch(() => props.currentName, (val) => {
17
+ newName.value = val
18
+ })
19
+
20
+ function close() {
21
+ emit('update:open', false)
22
+ }
23
+
24
+ async function handleRename() {
25
+ if (!newName.value.trim() || newName.value.trim() === props.currentName) return
26
+
27
+ loading.value = true
28
+ try {
29
+ await $fetch(`/api/skills/${props.currentName}/rename`, {
30
+ method: 'POST',
31
+ body: { newName: newName.value.trim() }
32
+ })
33
+ toast.add({ title: `Skill renamed to '${newName.value.trim()}'`, color: 'success' })
34
+ emit('renamed', newName.value.trim())
35
+ close()
36
+ } catch (e: unknown) {
37
+ const message = e instanceof Error ? e.message : 'Failed to rename skill'
38
+ toast.add({ title: message, color: 'error' })
39
+ } finally {
40
+ loading.value = false
41
+ }
42
+ }
43
+ </script>
44
+
45
+ <template>
46
+ <UModal
47
+ :open="props.open"
48
+ @update:open="emit('update:open', $event)"
49
+ >
50
+ <template #header>
51
+ <span class="font-semibold">Rename Skill</span>
52
+ </template>
53
+
54
+ <template #body>
55
+ <UFormField label="New Name">
56
+ <UInput
57
+ v-model="newName"
58
+ placeholder="my-skill"
59
+ :disabled="loading"
60
+ />
61
+ </UFormField>
62
+ </template>
63
+
64
+ <template #footer>
65
+ <div class="flex justify-end gap-2">
66
+ <UButton
67
+ variant="ghost"
68
+ color="neutral"
69
+ :disabled="loading"
70
+ @click="close"
71
+ >
72
+ Cancel
73
+ </UButton>
74
+ <UButton
75
+ :loading="loading"
76
+ :disabled="!newName.trim() || newName.trim() === currentName"
77
+ @click="handleRename"
78
+ >
79
+ Rename
80
+ </UButton>
81
+ </div>
82
+ </template>
83
+ </UModal>
84
+ </template>
@@ -49,6 +49,13 @@ const links = [[{
49
49
  onSelect: () => {
50
50
  open.value = false
51
51
  }
52
+ }, {
53
+ label: 'Skills',
54
+ icon: 'i-lucide-puzzle',
55
+ to: '/skills',
56
+ onSelect: () => {
57
+ open.value = false
58
+ }
52
59
  }, {
53
60
  label: 'Memories',
54
61
  icon: 'i-lucide-brain',