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
package/Claude/CLAUDE.md CHANGED
@@ -26,6 +26,7 @@ Cognova is a self-hosted Nuxt 4 web application for personal knowledge managemen
26
26
  | Task Management | `/task` | Create, list, update, complete tasks |
27
27
  | Project Management | `/project` | Organize tasks into projects |
28
28
  | Memory | `/memory` | Search past decisions, store insights, recall context |
29
+ | Secrets | `/secret` | Store, retrieve, list, delete encrypted API keys and credentials |
29
30
  | Environment | `/environment` | Check system status, troubleshoot issues |
30
31
  | Skill Creator | `/skill-creator` | Create new Claude Code skills |
31
32
 
@@ -103,11 +104,15 @@ Memory is your most important tool. You are stateless between sessions — witho
103
104
  **Rule: When in doubt, store it.** A redundant memory is harmless. A forgotten one wastes the user's time.
104
105
 
105
106
  ### Secrets & Sensitive Data
106
- - NEVER store passwords, tokens, API keys, or credentials in memory, notes, or conversation
107
- - NEVER write secrets to files use the Cognova settings UI or secrets API instead
108
- - If a user shares a credential in chat, warn them it should be stored as a secret
109
- - When you need a token for an integration, check the secrets API first before asking the user
107
+
108
+ **CRITICALZero tolerance for leaked secrets:**
109
+ - NEVER store passwords, tokens, API keys, or credentials in memory, notes, conversation, or any file
110
+ - NEVER write secrets to files use `/secret set KEY` or the Cognova settings UI instead
111
+ - NEVER embed API keys, tokens, or credentials in SKILL.md files or Python scripts when creating or modifying skills — always use `get_secret()` from `_lib/api.py`
112
+ - If a user shares a credential in chat, warn them and store it via `/secret set KEY` immediately
113
+ - When you need a token for an integration, check with `/secret list` and `/secret get KEY` before asking the user
110
114
  - Treat any string that looks like a key, token, or password as sensitive — do not echo it back
115
+ - When creating skills that need external API keys, declare them in `requires-secrets` frontmatter and use `get_secret()` in the script
111
116
 
112
117
  ### Troubleshooting
113
118
  - Use `/environment status` or `/environment health` to diagnose issues
@@ -2,6 +2,12 @@
2
2
  name: environment
3
3
  description: Check system status, view configuration, troubleshoot issues with the Cognova installation. Use when diagnosing problems, checking health, or understanding the current setup.
4
4
  allowed-tools: Bash, Read
5
+ metadata:
6
+ version: "1.0.0"
7
+ requires-secrets: []
8
+ author: Cognova
9
+ repository: ""
10
+ installed-from: ""
5
11
  ---
6
12
 
7
13
  # Environment Skill
@@ -2,6 +2,12 @@
2
2
  name: memory
3
3
  description: Access persistent memory across Claude sessions. Search past conversations, recall decisions, store key insights. Use when needing context from previous work or to save important information for future sessions.
4
4
  allowed-tools: Bash, Read
5
+ metadata:
6
+ version: "1.0.0"
7
+ requires-secrets: []
8
+ author: Cognova
9
+ repository: ""
10
+ installed-from: ""
5
11
  ---
6
12
 
7
13
  # Memory Skill
@@ -2,6 +2,12 @@
2
2
  name: project
3
3
  description: Manage Cognova projects - create, list, update projects. ALWAYS searches for existing projects before creating new ones. ALWAYS confirms with user before creating, showing potential matches.
4
4
  allowed-tools: Bash, Read
5
+ metadata:
6
+ version: "1.0.0"
7
+ requires-secrets: []
8
+ author: Cognova
9
+ repository: ""
10
+ installed-from: ""
5
11
  ---
6
12
 
7
13
  # Project Management Skill
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: secret
3
+ description: Manage encrypted secrets for API keys, tokens, and credentials. Use when the user needs to store, retrieve, list, or delete sensitive values.
4
+ allowed-tools: Bash, Read
5
+ metadata:
6
+ version: "1.0.0"
7
+ requires-secrets: []
8
+ author: Cognova
9
+ repository: ""
10
+ installed-from: ""
11
+ ---
12
+
13
+ # Secrets Management Skill
14
+
15
+ Manage encrypted secrets stored in Cognova. Use this for API keys, tokens, webhook URLs, and any sensitive credentials that skills or integrations need.
16
+
17
+ ## Commands
18
+
19
+ ### List all secrets
20
+
21
+ ```bash
22
+ python3 ~/.claude/skills/secret/secret.py list
23
+ ```
24
+
25
+ Shows all stored secret keys with descriptions and last updated timestamps. Values are NOT shown.
26
+
27
+ ### Get a secret value
28
+
29
+ ```bash
30
+ python3 ~/.claude/skills/secret/secret.py get <KEY>
31
+ ```
32
+
33
+ Retrieves the decrypted value of a secret. **Do NOT echo the value back to the user** — confirm it was retrieved successfully without displaying it.
34
+
35
+ Example:
36
+ ```bash
37
+ python3 ~/.claude/skills/secret/secret.py get OPENAI_API_KEY
38
+ ```
39
+
40
+ ### Store a secret
41
+
42
+ ```bash
43
+ python3 ~/.claude/skills/secret/secret.py set <KEY> --value <VALUE> [--description <DESC>]
44
+ ```
45
+
46
+ Creates or updates an encrypted secret. Key names should use `SCREAMING_SNAKE_CASE`.
47
+
48
+ Examples:
49
+ ```bash
50
+ python3 ~/.claude/skills/secret/secret.py set DISCORD_WEBHOOK_URL --value "https://discord.com/api/webhooks/..." --description "Discord notifications webhook"
51
+ python3 ~/.claude/skills/secret/secret.py set OPENAI_API_KEY --value "sk-..." --description "OpenAI API key for GPT integration"
52
+ ```
53
+
54
+ ### Delete a secret
55
+
56
+ ```bash
57
+ python3 ~/.claude/skills/secret/secret.py delete <KEY>
58
+ ```
59
+
60
+ Permanently removes an encrypted secret.
61
+
62
+ ## Natural Language Patterns
63
+
64
+ When users say things like:
65
+ - "Store this API key..." -> Use `set`
66
+ - "Save my token for..." -> Use `set`
67
+ - "What secrets do I have?" -> Use `list`
68
+ - "Get my Discord webhook" -> Use `get`
69
+ - "Remove the old API key" -> Use `delete`
70
+ - "I need to add a key for..." -> Use `set`
71
+
72
+ ## Key Naming Convention
73
+
74
+ Use `SCREAMING_SNAKE_CASE` for all secret keys:
75
+ - `GOOGLE_API_KEY`
76
+ - `DISCORD_WEBHOOK_URL`
77
+ - `OPENAI_API_KEY`
78
+ - `GITHUB_TOKEN`
79
+
80
+ ## Security Rules
81
+
82
+ 1. **Never echo secret values** — After `get`, confirm success without displaying the value
83
+ 2. **Never store secrets elsewhere** — No memory, no notes, no files, no conversation logs
84
+ 3. **Always use this skill** — When a user provides a credential, store it here immediately
85
+ 4. **Warn on exposure** — If a user pastes a key/token in chat, warn them and offer to store it
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """Manage encrypted secrets in Cognova."""
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Add parent directory for _lib imports
9
+ sys.path.insert(0, str(Path(__file__).parent.parent))
10
+ from _lib.api import get, post, put, delete, get_secret
11
+
12
+
13
+ def cmd_list(args):
14
+ """List all stored secrets."""
15
+ success, data = get("/secrets")
16
+ if not success:
17
+ print(f"Error: {data}")
18
+ sys.exit(1)
19
+
20
+ secrets = data if isinstance(data, list) else []
21
+ if not secrets:
22
+ print("No secrets stored.")
23
+ print("\nUse: python3 secret.py set KEY --value VALUE")
24
+ return
25
+
26
+ # Calculate column widths
27
+ key_width = max(len(s.get("key", "")) for s in secrets)
28
+ key_width = max(key_width, 3)
29
+
30
+ print(f"{'Key':<{key_width}} {'Description':<40} {'Updated'}")
31
+ print(f"{'-' * key_width} {'-' * 40} {'-' * 19}")
32
+
33
+ for s in secrets:
34
+ key = s.get("key", "")
35
+ desc = s.get("description", "") or ""
36
+ updated = s.get("updatedAt", s.get("createdAt", ""))[:19] if s.get("updatedAt") or s.get("createdAt") else ""
37
+ # Truncate description if too long
38
+ if len(desc) > 40:
39
+ desc = desc[:37] + "..."
40
+ print(f"{key:<{key_width}} {desc:<40} {updated}")
41
+
42
+ print(f"\n{len(secrets)} secret(s) stored.")
43
+
44
+
45
+ def cmd_get(args):
46
+ """Get a secret value."""
47
+ success, value = get_secret(args.key)
48
+ if not success:
49
+ print(f"Error: {value}")
50
+ print(f"\nSecret '{args.key}' not found. Use 'list' to see available secrets.")
51
+ sys.exit(1)
52
+
53
+ if args.raw:
54
+ # Raw output for piping to other commands
55
+ print(value, end="")
56
+ else:
57
+ print(f"Secret '{args.key}' retrieved successfully.")
58
+ print(f"Value: {value}")
59
+
60
+
61
+ def cmd_set(args):
62
+ """Create or update a secret."""
63
+ if not args.value:
64
+ print("Error: --value is required")
65
+ sys.exit(1)
66
+
67
+ data = {
68
+ "key": args.key,
69
+ "value": args.value,
70
+ }
71
+ if args.description:
72
+ data["description"] = args.description
73
+
74
+ # Try to check if it exists first
75
+ check_success, _ = get_secret(args.key)
76
+
77
+ if check_success:
78
+ # Update existing
79
+ success, result = put(f"/secrets/{args.key}", data)
80
+ action = "updated"
81
+ else:
82
+ # Create new
83
+ success, result = post("/secrets", data)
84
+ action = "created"
85
+
86
+ if not success:
87
+ print(f"Error: {result}")
88
+ sys.exit(1)
89
+
90
+ print(f"Secret '{args.key}' {action} successfully.")
91
+ if args.description:
92
+ print(f"Description: {args.description}")
93
+
94
+
95
+ def cmd_delete(args):
96
+ """Delete a secret."""
97
+ success, result = delete(f"/secrets/{args.key}")
98
+ if not success:
99
+ print(f"Error: {result}")
100
+ sys.exit(1)
101
+
102
+ print(f"Secret '{args.key}' deleted successfully.")
103
+
104
+
105
+ def main():
106
+ parser = argparse.ArgumentParser(
107
+ description="Manage encrypted secrets in Cognova"
108
+ )
109
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
110
+
111
+ # list
112
+ subparsers.add_parser("list", help="List all secrets")
113
+
114
+ # get
115
+ get_parser = subparsers.add_parser("get", help="Get a secret value")
116
+ get_parser.add_argument("key", help="Secret key name")
117
+ get_parser.add_argument("--raw", action="store_true", help="Output raw value only")
118
+
119
+ # set
120
+ set_parser = subparsers.add_parser("set", help="Create or update a secret")
121
+ set_parser.add_argument("key", help="Secret key name (SCREAMING_SNAKE_CASE)")
122
+ set_parser.add_argument("--value", required=True, help="Secret value")
123
+ set_parser.add_argument("--description", help="Description of what this secret is for")
124
+
125
+ # delete
126
+ del_parser = subparsers.add_parser("delete", help="Delete a secret")
127
+ del_parser.add_argument("key", help="Secret key name")
128
+
129
+ args = parser.parse_args()
130
+
131
+ if not args.command:
132
+ parser.print_help()
133
+ sys.exit(1)
134
+
135
+ commands = {
136
+ "list": cmd_list,
137
+ "get": cmd_get,
138
+ "set": cmd_set,
139
+ "delete": cmd_delete,
140
+ }
141
+
142
+ commands[args.command](args)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -3,6 +3,12 @@ name: skill-creator
3
3
  description: Assists users in creating new Claude Code skills. Uses web search to find latest conventions and suggests alternative approaches (existing MCPs, plugins) when appropriate.
4
4
  allowed-tools: Bash, Read, Write, WebSearch, WebFetch
5
5
  disable-model-invocation: true
6
+ metadata:
7
+ version: "1.0.0"
8
+ requires-secrets: []
9
+ author: Cognova
10
+ repository: ""
11
+ installed-from: ""
6
12
  ---
7
13
 
8
14
  # Skill Creator
@@ -11,6 +17,8 @@ Assists in creating new Claude Code skills for Cognova or personal use.
11
17
 
12
18
  ## Process
13
19
 
20
+ > **CRITICAL: Skills must NEVER contain hardcoded secrets.** Always use `get_secret()` from `_lib/api.py` for API keys, tokens, and credentials. If the user provides a key/token during skill creation, store it via `/secret set KEY` first, then reference it by key name in the script. Declare required secrets in `metadata.requires-secrets` frontmatter.
21
+
14
22
  When a user asks to create a new skill:
15
23
 
16
24
  ### 1. Understand the Requirement
@@ -193,6 +201,28 @@ my_var = os.environ.get('MY_CUSTOM_VAR', 'default')
193
201
  | `user-invocable` | false | Claude-only, hidden from user menu |
194
202
  | `context` | fork | Run in subagent |
195
203
  | `agent` | Explore, Plan, general-purpose | Agent type for subagent |
204
+ | `metadata` | object | Custom metadata (version, author, etc.) |
205
+
206
+ #### Metadata Fields
207
+
208
+ Nest under `metadata:` in frontmatter:
209
+
210
+ ```yaml
211
+ metadata:
212
+ version: "1.0.0"
213
+ requires-secrets: ["API_KEY_NAME"]
214
+ author: Your Name
215
+ repository: ""
216
+ installed-from: ""
217
+ ```
218
+
219
+ | Field | Type | Description |
220
+ |-------|------|-------------|
221
+ | `version` | string | Semantic version |
222
+ | `requires-secrets` | string[] | Secret keys the skill needs (fetched via `get_secret()`) |
223
+ | `author` | string | Skill author name |
224
+ | `repository` | string | Source repository URL |
225
+ | `installed-from` | string | Library name if installed from community |
196
226
 
197
227
  ### 7. Skill Locations
198
228
 
@@ -2,6 +2,12 @@
2
2
  name: task
3
3
  description: Manage Cognova tasks - create, list, update, complete tasks. Use when the user wants to track work items, todos, or action items. Can search for and associate tasks with projects.
4
4
  allowed-tools: Bash, Read
5
+ metadata:
6
+ version: "1.0.0"
7
+ requires-secrets: []
8
+ author: Cognova
9
+ repository: ""
10
+ installed-from: ""
5
11
  ---
6
12
 
7
13
  # Task Management Skill
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import type { SkillListItem } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ skill: SkillListItem
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ toggle: [name: string]
10
+ }>()
11
+
12
+ const toggling = ref(false)
13
+
14
+ async function handleToggle() {
15
+ toggling.value = true
16
+ emit('toggle', props.skill.name)
17
+ toggling.value = false
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <NuxtLink
23
+ :to="`/skills/${skill.name}`"
24
+ class="block p-4 rounded-lg border border-default bg-elevated/50 hover:bg-elevated transition-colors"
25
+ >
26
+ <div class="flex items-start justify-between gap-3">
27
+ <div class="flex-1 min-w-0">
28
+ <div class="flex items-center gap-2 mb-1">
29
+ <span class="font-medium text-sm truncate">{{ skill.name }}</span>
30
+ <UBadge
31
+ v-if="skill.core"
32
+ variant="subtle"
33
+ color="primary"
34
+ size="xs"
35
+ >
36
+ Core
37
+ </UBadge>
38
+ <UBadge
39
+ v-if="!skill.active"
40
+ variant="subtle"
41
+ color="neutral"
42
+ size="xs"
43
+ >
44
+ Disabled
45
+ </UBadge>
46
+ </div>
47
+ <p class="text-xs text-muted line-clamp-2">
48
+ {{ skill.description }}
49
+ </p>
50
+ </div>
51
+
52
+ <USwitch
53
+ v-if="!skill.core"
54
+ :model-value="skill.active"
55
+ size="sm"
56
+ :loading="toggling"
57
+ @update:model-value="handleToggle"
58
+ @click.prevent.stop
59
+ />
60
+ </div>
61
+
62
+ <div class="flex items-center gap-2 mt-3">
63
+ <UBadge
64
+ v-if="skill.version"
65
+ variant="soft"
66
+ color="neutral"
67
+ size="xs"
68
+ >
69
+ v{{ skill.version }}
70
+ </UBadge>
71
+ <UBadge
72
+ v-if="skill.author"
73
+ variant="soft"
74
+ color="neutral"
75
+ size="xs"
76
+ >
77
+ {{ skill.author }}
78
+ </UBadge>
79
+ <span class="text-xs text-dimmed ml-auto">{{ skill.fileCount }} file{{ skill.fileCount !== 1 ? 's' : '' }}</span>
80
+ </div>
81
+ </NuxtLink>
82
+ </template>
@@ -0,0 +1,156 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ open: boolean
4
+ }>()
5
+
6
+ const emit = defineEmits<{
7
+ 'update:open': [value: boolean]
8
+ 'created': [name: string]
9
+ }>()
10
+
11
+ const router = useRouter()
12
+ const toast = useToast()
13
+
14
+ const mode = ref<'blank' | 'generate'>('blank')
15
+ const name = ref('')
16
+ const description = ref('')
17
+ const loading = ref(false)
18
+
19
+ function resetForm() {
20
+ mode.value = 'blank'
21
+ name.value = ''
22
+ description.value = ''
23
+ loading.value = false
24
+ }
25
+
26
+ function close() {
27
+ emit('update:open', false)
28
+ resetForm()
29
+ }
30
+
31
+ async function handleCreate() {
32
+ if (!name.value.trim()) return
33
+
34
+ loading.value = true
35
+ try {
36
+ if (mode.value === 'blank') {
37
+ await $fetch('/api/skills/create', {
38
+ method: 'POST',
39
+ body: { name: name.value.trim(), description: description.value.trim() || undefined }
40
+ })
41
+ toast.add({ title: `Skill '${name.value}' created`, color: 'success' })
42
+ emit('created', name.value.trim())
43
+ close()
44
+ router.push(`/skills/${name.value.trim()}`)
45
+ } else {
46
+ if (!description.value.trim()) {
47
+ toast.add({ title: 'Description is required for AI generation', color: 'warning' })
48
+ loading.value = false
49
+ return
50
+ }
51
+ const res = await $fetch<{ data: { name: string, costUsd: number } }>('/api/skills/generate', {
52
+ method: 'POST',
53
+ body: { name: name.value.trim(), description: description.value.trim() }
54
+ })
55
+ toast.add({
56
+ title: `Skill '${name.value}' generated`,
57
+ description: `Cost: $${res.data.costUsd.toFixed(4)} — created in inactive state for review`,
58
+ color: 'success'
59
+ })
60
+ emit('created', name.value.trim())
61
+ close()
62
+ router.push(`/skills/${name.value.trim()}`)
63
+ }
64
+ } catch (e: unknown) {
65
+ const message = e instanceof Error ? e.message : 'Failed to create skill'
66
+ toast.add({ title: message, color: 'error' })
67
+ } finally {
68
+ loading.value = false
69
+ }
70
+ }
71
+ </script>
72
+
73
+ <template>
74
+ <UModal
75
+ :open="props.open"
76
+ @update:open="emit('update:open', $event)"
77
+ >
78
+ <template #header>
79
+ <span class="font-semibold">Create Skill</span>
80
+ </template>
81
+
82
+ <template #body>
83
+ <div class="space-y-4">
84
+ <div class="flex gap-2">
85
+ <UButton
86
+ :variant="mode === 'blank' ? 'solid' : 'outline'"
87
+ color="neutral"
88
+ size="sm"
89
+ icon="i-lucide-file-plus"
90
+ @click="mode = 'blank'"
91
+ >
92
+ Blank Skill
93
+ </UButton>
94
+ <UButton
95
+ :variant="mode === 'generate' ? 'solid' : 'outline'"
96
+ color="neutral"
97
+ size="sm"
98
+ icon="i-lucide-sparkles"
99
+ @click="mode = 'generate'"
100
+ >
101
+ AI Generate
102
+ </UButton>
103
+ </div>
104
+
105
+ <UFormField label="Name">
106
+ <UInput
107
+ v-model="name"
108
+ placeholder="my-skill"
109
+ :disabled="loading"
110
+ class="w-full"
111
+ />
112
+ </UFormField>
113
+
114
+ <UFormField
115
+ label="Description"
116
+ :required="mode === 'generate'"
117
+ >
118
+ <UTextarea
119
+ v-model="description"
120
+ :placeholder="mode === 'generate' ? 'Describe what this skill should do...' : 'Optional description'"
121
+ :rows="3"
122
+ :disabled="loading"
123
+ class="w-full"
124
+ />
125
+ </UFormField>
126
+
127
+ <p
128
+ v-if="mode === 'generate'"
129
+ class="text-xs text-muted"
130
+ >
131
+ AI-generated skills are created in an inactive state so you can review them before enabling.
132
+ </p>
133
+ </div>
134
+ </template>
135
+
136
+ <template #footer>
137
+ <div class="flex justify-end gap-2">
138
+ <UButton
139
+ variant="ghost"
140
+ color="neutral"
141
+ :disabled="loading"
142
+ @click="close"
143
+ >
144
+ Cancel
145
+ </UButton>
146
+ <UButton
147
+ :loading="loading"
148
+ :disabled="!name.trim()"
149
+ @click="handleCreate"
150
+ >
151
+ {{ mode === 'generate' ? 'Generate' : 'Create' }}
152
+ </UButton>
153
+ </div>
154
+ </template>
155
+ </UModal>
156
+ </template>