llms-py 3.0.10__py3-none-any.whl → 3.0.18__py3-none-any.whl

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 (41) hide show
  1. llms/extensions/app/__init__.py +0 -1
  2. llms/extensions/app/db.py +7 -3
  3. llms/extensions/app/ui/threadStore.mjs +10 -3
  4. llms/extensions/computer/README.md +96 -0
  5. llms/extensions/computer/__init__.py +59 -0
  6. llms/extensions/computer/base.py +80 -0
  7. llms/extensions/computer/bash.py +185 -0
  8. llms/extensions/computer/computer.py +523 -0
  9. llms/extensions/computer/edit.py +299 -0
  10. llms/extensions/computer/filesystem.py +542 -0
  11. llms/extensions/computer/platform.py +461 -0
  12. llms/extensions/computer/run.py +37 -0
  13. llms/extensions/core_tools/__init__.py +0 -38
  14. llms/extensions/providers/anthropic.py +28 -1
  15. llms/extensions/providers/cerebras.py +0 -1
  16. llms/extensions/providers/google.py +112 -34
  17. llms/extensions/skills/LICENSE +202 -0
  18. llms/extensions/skills/__init__.py +130 -0
  19. llms/extensions/skills/errors.py +25 -0
  20. llms/extensions/skills/models.py +39 -0
  21. llms/extensions/skills/parser.py +178 -0
  22. llms/extensions/skills/ui/index.mjs +376 -0
  23. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  24. llms/extensions/skills/validator.py +177 -0
  25. llms/extensions/system_prompts/ui/index.mjs +6 -10
  26. llms/extensions/tools/__init__.py +5 -82
  27. llms/extensions/tools/ui/index.mjs +194 -63
  28. llms/main.py +502 -146
  29. llms/ui/ai.mjs +1 -1
  30. llms/ui/app.css +530 -0
  31. llms/ui/ctx.mjs +53 -6
  32. llms/ui/modules/chat/ChatBody.mjs +200 -20
  33. llms/ui/modules/chat/index.mjs +108 -104
  34. llms/ui/tailwind.input.css +10 -0
  35. llms/ui/utils.mjs +25 -1
  36. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
  37. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
  38. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  39. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  40. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
  41. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,178 @@
1
+ """YAML frontmatter parsing for SKILL.md files."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from .errors import ParseError, ValidationError
7
+ from .models import SkillProperties
8
+
9
+
10
+ def load_yaml(content: str) -> dict:
11
+ """Simple YAML parser for skill frontmatter.
12
+
13
+ Supports:
14
+ - Key-value pairs: key: "value"
15
+ - Comments: # comment
16
+ - Simple nesting (indentation-based)
17
+ """
18
+ result = {}
19
+ stack = [result]
20
+ indents = [-1]
21
+ last_key = None
22
+
23
+ for line in content.splitlines():
24
+ # Skip empty lines or full comments
25
+ stripped = line.strip()
26
+ if not stripped or stripped.startswith("#"):
27
+ continue
28
+
29
+ indent = len(line) - len(line.lstrip())
30
+
31
+ # Handle indent levels
32
+ while indent <= indents[-1]:
33
+ indents.pop()
34
+ stack.pop()
35
+
36
+ # If we have a nested block under last key
37
+ if indent > indents[-1] and last_key and isinstance(stack[-1], dict) and stack[-1].get(last_key) is None:
38
+ # This branch is tricky with the simple look-behind.
39
+ # Better approach: check if line is a key-value or array item
40
+ pass
41
+
42
+ # Parse key: value
43
+ if ":" in stripped:
44
+ key, val = stripped.split(":", 1)
45
+ key = key.strip()
46
+ val = val.strip()
47
+
48
+ # Handle quotes
49
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
50
+ val = val[1:-1]
51
+ elif val.lower() == "true":
52
+ val = True
53
+ elif val.lower() == "false":
54
+ val = False
55
+ elif val == "":
56
+ val = None # Could be start of nested object
57
+
58
+ current_dict = stack[-1]
59
+
60
+ if val is None:
61
+ # Prepare for nested object
62
+ new_dict = {}
63
+ current_dict[key] = new_dict
64
+ stack.append(new_dict)
65
+ indents.append(indent)
66
+ else:
67
+ current_dict[key] = val
68
+
69
+ last_key = key
70
+ else:
71
+ # Handle continuation lines or unknown format if needed,
72
+ # but for our simple use case we might error or ignore.
73
+ pass
74
+
75
+ return result
76
+
77
+
78
+ def find_skill_md(skill_dir: Path) -> Optional[Path]:
79
+ """Find the SKILL.md file in a skill directory.
80
+
81
+ Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
82
+
83
+ Args:
84
+ skill_dir: Path to the skill directory
85
+
86
+ Returns:
87
+ Path to the SKILL.md file, or None if not found
88
+ """
89
+ for name in ("SKILL.md", "skill.md"):
90
+ path = skill_dir / name
91
+ if path.exists():
92
+ return path
93
+ return None
94
+
95
+
96
+ def parse_frontmatter(content: str) -> tuple[dict, str]:
97
+ """Parse YAML frontmatter from SKILL.md content.
98
+
99
+ Args:
100
+ content: Raw content of SKILL.md file
101
+
102
+ Returns:
103
+ Tuple of (metadata dict, markdown body)
104
+
105
+ Raises:
106
+ ParseError: If frontmatter is missing or invalid
107
+ """
108
+ if not content.startswith("---"):
109
+ raise ParseError("SKILL.md must start with YAML frontmatter (---)")
110
+
111
+ parts = content.split("---", 2)
112
+ if len(parts) < 3:
113
+ raise ParseError("SKILL.md frontmatter not properly closed with ---")
114
+
115
+ frontmatter_str = parts[1]
116
+ body = parts[2].strip()
117
+
118
+ try:
119
+ metadata = load_yaml(frontmatter_str)
120
+ except Exception as e:
121
+ raise ParseError(f"Invalid YAML in frontmatter: {e}") from e
122
+
123
+ if not isinstance(metadata, dict):
124
+ raise ParseError("SKILL.md frontmatter must be a YAML mapping")
125
+
126
+ # Clean up metadata values if necessary (simple parser already handles basics)
127
+ if "metadata" in metadata and isinstance(metadata["metadata"], dict):
128
+ metadata["metadata"] = {str(k): str(v) for k, v in metadata["metadata"].items()}
129
+
130
+ return metadata, body
131
+
132
+
133
+ def read_properties(skill_dir: Path) -> SkillProperties:
134
+ """Read skill properties from SKILL.md frontmatter.
135
+
136
+ This function parses the frontmatter and returns properties.
137
+ It does NOT perform full validation. Use validate() for that.
138
+
139
+ Args:
140
+ skill_dir: Path to the skill directory
141
+
142
+ Returns:
143
+ SkillProperties with parsed metadata
144
+
145
+ Raises:
146
+ ParseError: If SKILL.md is missing or has invalid YAML
147
+ ValidationError: If required fields (name, description) are missing
148
+ """
149
+ skill_dir = Path(skill_dir)
150
+ skill_md = find_skill_md(skill_dir)
151
+
152
+ if skill_md is None:
153
+ raise ParseError(f"SKILL.md not found in {skill_dir}")
154
+
155
+ content = skill_md.read_text()
156
+ metadata, _ = parse_frontmatter(content)
157
+
158
+ if "name" not in metadata:
159
+ raise ValidationError("Missing required field in frontmatter: name")
160
+ if "description" not in metadata:
161
+ raise ValidationError("Missing required field in frontmatter: description")
162
+
163
+ name = metadata["name"]
164
+ description = metadata["description"]
165
+
166
+ if not isinstance(name, str) or not name.strip():
167
+ raise ValidationError("Field 'name' must be a non-empty string")
168
+ if not isinstance(description, str) or not description.strip():
169
+ raise ValidationError("Field 'description' must be a non-empty string")
170
+
171
+ return SkillProperties(
172
+ name=name.strip(),
173
+ description=description.strip(),
174
+ license=metadata.get("license"),
175
+ compatibility=metadata.get("compatibility"),
176
+ allowed_tools=metadata.get("allowed-tools"),
177
+ metadata=metadata.get("metadata"),
178
+ )
@@ -0,0 +1,376 @@
1
+ import { ref, inject, computed } from "vue"
2
+ import { leftPart } from "@servicestack/client"
3
+
4
+ let ext
5
+
6
+ const SkillSelector = {
7
+ template: `
8
+ <div class="px-4 py-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 max-h-[80vh] overflow-y-auto">
9
+
10
+ <!-- Global Controls -->
11
+ <div class="flex items-center justify-between mb-4">
12
+ <span class="text-xs font-bold uppercase text-gray-500 tracking-wider">Include Skills</span>
13
+ <div class="flex items-center gap-2">
14
+ <button type="button" v-if="!$ctx.tools?.isToolEnabled('skill')"
15
+ class="px-3 py-1 rounded-md text-xs font-medium border transition-colors select-none cursor-pointer bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
16
+ @click="$ctx.tools?.enableTool('skill')"
17
+ title="'skill' tool needs to be enabled to use Skills"
18
+ >
19
+ <span class="text-xs font-semibold text-red-700 dark:text-red-300">⚠️ Enable skill tool</span>
20
+ </button>
21
+ <button type="button" @click="$ctx.setPrefs({ onlySkills: null })"
22
+ class="px-3 py-1 rounded-md text-xs font-medium border transition-colors select-none"
23
+ :class="$prefs.onlySkills == null
24
+ ? 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 border-green-300 dark:border-green-800'
25
+ : 'cursor-pointer bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
26
+ All Skills
27
+ </button>
28
+ <button type="button" @click="$ctx.setPrefs({ onlySkills:[] })"
29
+ class="px-3 py-1 rounded-md text-xs font-medium border transition-colors select-none"
30
+ :class="$prefs.onlySkills?.length === 0
31
+ ? 'bg-fuchsia-100 dark:bg-fuchsia-900/40 text-fuchsia-800 dark:text-fuchsia-300 border-fuchsia-200 dark:border-fuchsia-800'
32
+ : 'cursor-pointer bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
33
+ No Skills
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Groups -->
39
+ <div class="space-y-3">
40
+ <div v-for="group in skillGroups" :key="group.name"
41
+ class="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
42
+
43
+ <!-- Group Header -->
44
+ <div class="flex items-center justify-between px-3 py-2 bg-gray-50/50 dark:bg-gray-800/50 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
45
+ @click="toggleCollapse(group.name)">
46
+
47
+ <div class="flex items-center gap-2 min-w-0">
48
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4 text-gray-400 transition-transform duration-200" :class="{ '-rotate-90': isCollapsed(group.name) }">
49
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
50
+ </svg>
51
+ <span class="font-semibold text-sm text-gray-700 dark:text-gray-200 truncate">
52
+ {{ group.name || 'Other Skills' }}
53
+ </span>
54
+ <span class="text-xs text-gray-400 font-mono">
55
+ {{ getActiveCount(group) }}/{{ group.skills.length }}
56
+ </span>
57
+ </div>
58
+
59
+ <div class="flex items-center gap-2" @click.stop>
60
+ <button @click="setGroupSkills(group, true)" type="button"
61
+ title="Include All in Group"
62
+ class="px-2 py-0.5 rounded text-xs font-medium border transition-colors select-none"
63
+ :class="getActiveCount(group) === group.skills.length
64
+ ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-300 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/40'
65
+ : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
66
+ all
67
+ </button>
68
+ <button @click="setGroupSkills(group, false)" type="button"
69
+ title="Include None in Group"
70
+ class="px-2 py-0.5 rounded text-xs font-medium border transition-colors select-none"
71
+ :class="getActiveCount(group) === 0
72
+ ? 'bg-fuchsia-50 dark:bg-fuchsia-900/20 text-fuchsia-700 dark:text-fuchsia-300 border-fuchsia-200 dark:border-fuchsia-800 hover:bg-fuchsia-100 dark:hover:bg-fuchsia-900/40'
73
+ : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
74
+ none
75
+ </button>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Group Body -->
80
+ <div v-show="!isCollapsed(group.name)" class="p-3 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800">
81
+ <div class="flex flex-wrap gap-2">
82
+ <button v-for="skill in group.skills" :key="skill.name" type="button"
83
+ @click="toggleSkill(skill.name)"
84
+ :title="skill.description"
85
+ class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors select-none text-left truncate max-w-[200px]"
86
+ :class="isSkillActive(skill.name)
87
+ ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800'
88
+ : 'bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'">
89
+ {{ skill.name }}
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ `,
97
+ setup() {
98
+ const ctx = inject('ctx')
99
+ const collapsedState = ref({})
100
+
101
+ const availableSkills = computed(() => Object.values(ctx.state.skills || {}))
102
+
103
+ const skillGroups = computed(() => {
104
+ const skills = availableSkills.value
105
+ const groupsMap = {}
106
+ const otherSkills = []
107
+
108
+ skills.forEach(skill => {
109
+ if (skill.group) {
110
+ if (!groupsMap[skill.group]) groupsMap[skill.group] = []
111
+ groupsMap[skill.group].push(skill)
112
+ } else {
113
+ otherSkills.push(skill)
114
+ }
115
+ })
116
+
117
+ const definedGroups = Object.entries(groupsMap).map(([name, skills]) => ({
118
+ name,
119
+ skills
120
+ }))
121
+
122
+ // Sort groups by name if needed, but for now rely on insertion order or backend order
123
+ definedGroups.sort((a, b) => a.name.localeCompare(b.name))
124
+
125
+ if (otherSkills.length > 0) {
126
+ definedGroups.push({ name: '', skills: otherSkills })
127
+ }
128
+
129
+ return definedGroups
130
+ })
131
+
132
+ function isSkillActive(name) {
133
+ const only = ctx.prefs.onlySkills
134
+ if (only == null) return true
135
+ if (Array.isArray(only)) {
136
+ return only.includes(name)
137
+ }
138
+ return false
139
+ }
140
+
141
+ function toggleSkill(name) {
142
+ let onlySkills = ctx.prefs.onlySkills
143
+
144
+ if (onlySkills == null) {
145
+ // If currently 'All', clicking a skill means we enter custom mode with all OTHER skills selected (deselecting clicked)
146
+ // Wait, logic in ToolSelector:
147
+ // if (onlyTools == null) { onlyTools = availableTools.value.map(t => t.function.name).filter(t => t !== name) }
148
+ // This means deselecting one tool switches to "custom" with all but that one.
149
+
150
+ onlySkills = availableSkills.value.map(s => s.name).filter(s => s !== name)
151
+ } else {
152
+ if (onlySkills.includes(name)) {
153
+ onlySkills = onlySkills.filter(s => s !== name)
154
+ } else {
155
+ onlySkills = [...onlySkills, name]
156
+ }
157
+ }
158
+
159
+ ctx.setPrefs({ onlySkills })
160
+ }
161
+
162
+ function toggleCollapse(groupName) {
163
+ const key = groupName || '_other_'
164
+ collapsedState.value[key] = !collapsedState.value[key]
165
+ }
166
+
167
+ function isCollapsed(groupName) {
168
+ const key = groupName || '_other_'
169
+ return !!collapsedState.value[key]
170
+ }
171
+
172
+ function setGroupSkills(group, enable) {
173
+ const groupSkillNames = group.skills.map(s => s.name)
174
+ let onlySkills = ctx.prefs.onlySkills
175
+
176
+ if (enable) {
177
+ if (onlySkills == null) return
178
+ const newSet = new Set(onlySkills)
179
+ groupSkillNames.forEach(n => newSet.add(n))
180
+ onlySkills = Array.from(newSet)
181
+ if (onlySkills.length === availableSkills.value.length) {
182
+ onlySkills = null
183
+ }
184
+ } else {
185
+ if (onlySkills == null) {
186
+ onlySkills = availableSkills.value
187
+ .map(s => s.name)
188
+ .filter(n => !groupSkillNames.includes(n))
189
+ } else {
190
+ onlySkills = onlySkills.filter(n => !groupSkillNames.includes(n))
191
+ }
192
+ }
193
+
194
+ ctx.setPrefs({ onlySkills })
195
+ }
196
+
197
+ function getActiveCount(group) {
198
+ const onlySkills = ctx.prefs.onlySkills
199
+ if (onlySkills == null) return group.skills.length
200
+ return group.skills.filter(s => onlySkills.includes(s.name)).length
201
+ }
202
+
203
+ return {
204
+ availableSkills,
205
+ skillGroups,
206
+ isSkillActive,
207
+ toggleSkill,
208
+ toggleCollapse,
209
+ isCollapsed,
210
+ setGroupSkills,
211
+ getActiveCount
212
+ }
213
+ }
214
+ }
215
+
216
+ function codeFragment(s) {
217
+ return "`" + s + "`"
218
+ }
219
+ function codeBlock(s) {
220
+ return "```\n" + s + "\n```\n"
221
+ }
222
+
223
+ const SkillInstructions = `
224
+ You have access to specialized skills that extend your capabilities with domain-specific knowledge, workflows, and tools.
225
+ Skills are modular packages containing instructions, scripts, references, and assets for particular tasks.
226
+
227
+ ## Using Skills
228
+
229
+ Use the skill tool to read a skill's main instructions and guidance, e.g:
230
+ ${codeBlock("skill({ name: \"skill-name\" })")}
231
+
232
+ To read a specific file within a skill (scripts, references, assets):
233
+ ${codeBlock("skill({ name: \"skill-name\", file: \"relative/path/to/file\" })")}
234
+
235
+ Examples:
236
+ - ${codeFragment("skill({ name: \"create-plan\" })")} - Read the create-plan skill's SKILL.md instructions
237
+ - ${codeFragment("skill({ name: \"web-artifacts-builder\", file: \"scripts/init-artifact.sh\" })")} - Read a specific script
238
+
239
+ ## When to Use Skills
240
+
241
+ You should read the appropriate skill BEFORE starting work on relevant tasks. Skills contain best practices, scripts, and reference materials that significantly improve output quality.
242
+
243
+ **Skill Selection Guidelines:**
244
+ - Match the task to available skill descriptions
245
+ - Multiple skills may be relevant - read all that apply
246
+ - Read the skill first, then follow its instructions
247
+
248
+ ## Available Skills
249
+ $$AVAILABLE_SKILLS$$
250
+
251
+ ## Important Notes
252
+
253
+ - Always read the skill BEFORE starting implementation
254
+ - Skills may contain scripts that can be executed directly without loading into context
255
+ - Multiple skills can and should be combined when tasks span multiple domains
256
+ - If a skill references additional files (references/, scripts/, assets/), read those as needed during execution
257
+ `
258
+
259
+ export default {
260
+ order: 15 - 100,
261
+
262
+ install(ctx) {
263
+ ext = ctx.scope("skills")
264
+
265
+ ctx.components({ SkillSelector })
266
+
267
+ const svg = (attrs, title) => `<svg ${attrs} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">${title ? "<title>" + title + "</title>" : ''}<path fill="currentColor" d="M20 17a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H9.46c.35.61.54 1.3.54 2h10v11h-9v2m4-10v2H9v13H7v-6H5v6H3v-8H1.5V9a2 2 0 0 1 2-2zM8 4a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2"/></svg>`
268
+
269
+ ctx.setTopIcons({
270
+ skills: {
271
+ component: {
272
+ template: svg([
273
+ `@click="$ctx.toggleTop('SkillSelector')"`,
274
+ `:class="!$tools?.isToolEnabled('skill') ? '' : $prefs.onlySkills == null ? 'text-green-600 dark:text-green-300' : $prefs.onlySkills.length ? 'text-blue-600! dark:text-blue-300!' : ''"`
275
+ ].join(' ')),
276
+ },
277
+ isActive({ top }) {
278
+ return top === 'SkillSelector'
279
+ },
280
+ get title() {
281
+ return !ctx.tools?.isToolEnabled('skill')
282
+ ? `skill tool not enabled`
283
+ : ctx.prefs.onlySkills == null
284
+ ? `All Skills Included`
285
+ : ctx.prefs.onlySkills.length
286
+ ? `${ctx.prefs.onlySkills.length} ${ctx.utils.pluralize('Skill', ctx.prefs.onlySkills.length)} Included`
287
+ : 'No Skills Included'
288
+ }
289
+ }
290
+ })
291
+
292
+ ctx.chatRequestFilters.push(({ request, thread, context }) => {
293
+
294
+ if (!ctx.tools?.isToolEnabled('skill')) {
295
+ console.log(`skills.chatRequestFilters: 'skill' tool is not enabled`)
296
+ return
297
+ }
298
+
299
+ const prefs = ctx.prefs
300
+ if (prefs.onlySkills != null) {
301
+ if (Array.isArray(prefs.onlySkills)) {
302
+ request.metadata.skills = prefs.onlySkills.length > 0
303
+ ? prefs.onlySkills.join(',')
304
+ : 'none'
305
+ }
306
+ } else {
307
+ request.metadata.skills = 'all'
308
+ }
309
+
310
+ console.log('skills.chatRequestFilters', prefs.onlySkills, Object.keys(ctx.state.skills || {}))
311
+ const skills = ctx.state.skills
312
+ if (!skills) return
313
+
314
+ const includeSkills = []
315
+ for (const skill of Object.values(skills)) {
316
+ if (prefs.onlySkills == null || prefs.onlySkills.includes(skill.name)) {
317
+ includeSkills.push(skill)
318
+ }
319
+ }
320
+ if (!includeSkills.length) return
321
+
322
+ const sb = []
323
+ sb.push("<available_skills>")
324
+ for (const skill of includeSkills) {
325
+ sb.push(" <skill>")
326
+ sb.push(" <name>" + ctx.utils.encodeHtml(skill.name) + "</name>")
327
+ sb.push(" <description>" + ctx.utils.encodeHtml(skill.description) + "</description>")
328
+ sb.push(" <location>" + ctx.utils.encodeHtml(skill.location) + "</location>")
329
+ sb.push(" </skill>")
330
+ }
331
+ sb.push("</available_skills>")
332
+
333
+ const skillsPrompt = SkillInstructions.replace('$$AVAILABLE_SKILLS$$', sb.join('\n')).trim()
334
+ context.requiredSystemPrompts.push(skillsPrompt)
335
+ })
336
+
337
+ ctx.setThreadFooters({
338
+ skills: {
339
+ component: {
340
+ template: `
341
+ <div class="mt-2 w-full flex justify-center">
342
+ <button type="button" @click="$ctx.chat.sendUserMessage('proceed')"
343
+ class="px-3 py-1 rounded-md text-xs font-medium border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors select-none">
344
+ proceed
345
+ </button>
346
+ </div>
347
+ `
348
+ },
349
+ show({ thread }) {
350
+ if (thread.messages.length < 2) return false
351
+ const msgRoles = thread.messages.map(m => m.role)
352
+ if (msgRoles[msgRoles.length - 1] != "assistant") return false
353
+ const hasSkillToolCall = thread.messages.some(m =>
354
+ m.tool_calls?.some(tc => tc.type == "function" && tc.function.name == "skill"))
355
+ const systemPrompt = thread.messages.find(m => m.role == "system")?.content.toLowerCase() || ''
356
+ const line1 = leftPart(systemPrompt.trim(), "\n")
357
+ const hasPlanSystemPrompt = line1.includes("plan") || systemPrompt.includes("# plan")
358
+ return hasSkillToolCall || hasPlanSystemPrompt
359
+ }
360
+ }
361
+ })
362
+
363
+ ctx.setState({
364
+ skills: {}
365
+ })
366
+ },
367
+
368
+ async load(ctx) {
369
+ const api = await ext.getJson('/')
370
+ if (api.response) {
371
+ ctx.setState({ skills: api.response })
372
+ } else {
373
+ ctx.setError(api.error)
374
+ }
375
+ }
376
+ }
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: create-plan
3
+ description: Create a concise plan. Use when a user explicitly asks for a plan related to a coding task.
4
+ metadata:
5
+ short-description: Create a plan
6
+ ---
7
+
8
+ # Create Plan
9
+
10
+ ## Goal
11
+
12
+ Turn a user prompt into a **single, actionable plan** delivered in the final assistant message.
13
+
14
+ ## Minimal workflow
15
+
16
+ Throughout the entire workflow, operate in read-only mode. Do not write or update files.
17
+
18
+ 1. **Scan context quickly**
19
+ - Read `README.md` and any obvious docs (`docs/`, `CONTRIBUTING.md`, `ARCHITECTURE.md`).
20
+ - Skim relevant files (the ones most likely touched).
21
+ - Identify constraints (language, frameworks, CI/test commands, deployment shape).
22
+
23
+ 2. **Ask follow-ups only if blocking**
24
+ - Ask **at most 1–2 questions**.
25
+ - Only ask if you cannot responsibly plan without the answer; prefer multiple-choice.
26
+ - If unsure but not blocked, make a reasonable assumption and proceed.
27
+
28
+ 3. **Create a plan using the template below**
29
+ - Start with **1 short paragraph** describing the intent and approach.
30
+ - Clearly call out what is **in scope** and what is **not in scope** in short.
31
+ - Then provide a **small checklist** of action items (default 6–10 items).
32
+ - Each checklist item should be a concrete action and, when helpful, mention files/commands.
33
+ - **Make items atomic and ordered**: discovery → changes → tests → rollout.
34
+ - **Verb-first**: “Add…”, “Refactor…”, “Verify…”, “Ship…”.
35
+ - Include at least one item for **tests/validation** and one for **edge cases/risk** when applicable.
36
+ - If there are unknowns, include a tiny **Open questions** section (max 3).
37
+
38
+ 4. **Do not preface the plan with meta explanations; output only the plan as per template**
39
+
40
+ ## Plan template (follow exactly)
41
+
42
+ ```markdown
43
+ # Plan
44
+
45
+ <1–3 sentences: what we’re doing, why, and the high-level approach.>
46
+
47
+ ## Scope
48
+ - In:
49
+ - Out:
50
+
51
+ ## Action items
52
+ [ ] <Step 1>
53
+ [ ] <Step 2>
54
+ [ ] <Step 3>
55
+ [ ] <Step 4>
56
+ [ ] <Step 5>
57
+ [ ] <Step 6>
58
+
59
+ ## Open questions
60
+ - <Question 1>
61
+ - <Question 2>
62
+ - <Question 3>
63
+ ```
64
+
65
+ ## Checklist item guidance
66
+ Good checklist items:
67
+ - Point to likely files/modules: src/..., app/..., services/...
68
+ - Name concrete validation: “Run npm test”, “Add unit tests for X”
69
+ - Include safe rollout when relevant: feature flag, migration plan, rollback note
70
+
71
+ Avoid:
72
+ - Vague steps (“handle backend”, “do auth”)
73
+ - Too many micro-steps
74
+ - Writing code snippets (keep the plan implementation-agnostic)