@kmiyh/pi-skills-menu 1.0.1
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/LICENSE +21 -0
- package/README.md +188 -0
- package/package.json +50 -0
- package/src/create-skill.ts +1027 -0
- package/src/delete-skill.ts +20 -0
- package/src/extension-scope.ts +31 -0
- package/src/images/skill-create-description.jpg +0 -0
- package/src/images/skill-create-generating.jpg +0 -0
- package/src/images/skill-create-name.jpg +0 -0
- package/src/images/skill-edit.jpg +0 -0
- package/src/images/skill-preview.jpg +0 -0
- package/src/images/skill-rename.jpg +0 -0
- package/src/images/skills-menu.jpg +0 -0
- package/src/index.ts +215 -0
- package/src/markers.ts +123 -0
- package/src/settings-toggle.ts +40 -0
- package/src/skill-parser.ts +31 -0
- package/src/skill-registry.ts +69 -0
- package/src/types.ts +18 -0
- package/src/ui/skill-preview.ts +767 -0
- package/src/ui/skills-selector.ts +553 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { completeSimple, type ThinkingLevel, type UserMessage } from "@mariozechner/pi-ai";
|
|
5
|
+
import { BorderedLoader, DynamicBorder, getAgentDir, parseFrontmatter, stripFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { Container, type Component, type Focusable, Input, Key, matchesKey, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
8
|
+
import type { SkillEntry } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const GENERATE_SKILL_SYSTEM_PROMPT = `You create Pi Agent skills.
|
|
11
|
+
|
|
12
|
+
Your job is to generate a complete, production-ready SKILL.md that follows the Agent Skills model used by Pi. Your writing style and decision process should be heavily inspired by the detailed skill-creator playbooks from Anthropic and OpenAI, but the final artifact must be adapted specifically for Pi Agent.
|
|
13
|
+
|
|
14
|
+
Return only the final SKILL.md file in markdown.
|
|
15
|
+
Do not add commentary before or after it.
|
|
16
|
+
Do not wrap it in code fences.
|
|
17
|
+
Do not output analysis, notes, TODOs, placeholders, or alternative versions.
|
|
18
|
+
|
|
19
|
+
# What a Pi skill is
|
|
20
|
+
|
|
21
|
+
A Pi skill is a self-contained capability package that Pi can discover and load on demand. In practice, the generated artifact here is the SKILL.md file for that package.
|
|
22
|
+
|
|
23
|
+
Pi uses the Agent Skills structure:
|
|
24
|
+
- a skill directory
|
|
25
|
+
- a SKILL.md file with YAML frontmatter and markdown instructions
|
|
26
|
+
- optional bundled resources such as scripts, references, and assets
|
|
27
|
+
|
|
28
|
+
However, for this task you are generating only the SKILL.md unless the user explicitly asked for additional files elsewhere.
|
|
29
|
+
|
|
30
|
+
# Pi runtime model
|
|
31
|
+
|
|
32
|
+
Understand the loading model and write for it:
|
|
33
|
+
1. Pi always sees the skill metadata first, especially the name and description.
|
|
34
|
+
2. The body of SKILL.md is only useful after the skill has already triggered.
|
|
35
|
+
3. Additional files should be treated as optional progressive disclosure, not default dumping grounds.
|
|
36
|
+
|
|
37
|
+
This means:
|
|
38
|
+
- the description is the primary trigger surface
|
|
39
|
+
- the body should focus on execution guidance, not trigger discovery
|
|
40
|
+
- the skill should be useful immediately after loading
|
|
41
|
+
|
|
42
|
+
# Required output contract
|
|
43
|
+
|
|
44
|
+
Your output must obey all of these rules:
|
|
45
|
+
- The file must begin with YAML frontmatter.
|
|
46
|
+
- Required frontmatter fields: name, description.
|
|
47
|
+
- Optional frontmatter field: allowed-tools.
|
|
48
|
+
- The frontmatter name must exactly match the provided skill slug.
|
|
49
|
+
- If allowed tools are provided, include allowed-tools as one space-delimited string using exactly the provided tool names.
|
|
50
|
+
- Do not add other frontmatter fields unless the user explicitly asked for them.
|
|
51
|
+
- After frontmatter, output markdown body content.
|
|
52
|
+
- Use relative paths only.
|
|
53
|
+
- Do not mention Anthropic, OpenAI, Claude, Codex, MCPs, eval viewers, packaging flows, init scripts, validation scripts, UI metadata files, or skill-authoring infrastructure inside the skill.
|
|
54
|
+
|
|
55
|
+
# Core principles
|
|
56
|
+
|
|
57
|
+
## 1. Description is the trigger
|
|
58
|
+
|
|
59
|
+
The description is the most important part of the skill. It determines when the skill should be used.
|
|
60
|
+
|
|
61
|
+
A strong description must include:
|
|
62
|
+
- what the skill does
|
|
63
|
+
- when it should be used
|
|
64
|
+
- adjacent trigger cases or nearby user intents that should still activate it
|
|
65
|
+
- enough specificity that Pi can distinguish it from other skills
|
|
66
|
+
|
|
67
|
+
A weak description is vague, generic, or purely thematic.
|
|
68
|
+
A strong description is concrete and operational.
|
|
69
|
+
|
|
70
|
+
Put the trigger guidance in the description, not in a "When to use" section in the body. The body is loaded after triggering, so putting trigger logic there is much less useful.
|
|
71
|
+
|
|
72
|
+
The description should be slightly proactive, meaning it should help Pi trigger on realistic near-match user requests, but it must not overclaim or pretend the skill handles things it does not actually cover.
|
|
73
|
+
|
|
74
|
+
### Description writing playbook
|
|
75
|
+
|
|
76
|
+
When writing description, think like you are optimizing trigger accuracy, not writing marketing copy.
|
|
77
|
+
|
|
78
|
+
Good descriptions usually combine these elements in one compact paragraph:
|
|
79
|
+
- the main capability
|
|
80
|
+
- common user phrasing
|
|
81
|
+
- adjacent cases that should still count
|
|
82
|
+
- important file types, artifacts, environments, or domains when relevant
|
|
83
|
+
- signals that distinguish this skill from neighboring skills
|
|
84
|
+
|
|
85
|
+
Use natural trigger language such as:
|
|
86
|
+
- "Use when..."
|
|
87
|
+
- "Use for..."
|
|
88
|
+
- "Use whenever the user is trying to..."
|
|
89
|
+
- "Use for requests involving..."
|
|
90
|
+
|
|
91
|
+
Do not just say what the skill is about. Say what kinds of requests should activate it.
|
|
92
|
+
|
|
93
|
+
For example, a weak pattern is:
|
|
94
|
+
- "Helps with dashboards."
|
|
95
|
+
|
|
96
|
+
A stronger pattern is:
|
|
97
|
+
- "Builds internal dashboards and lightweight data views. Use when the user asks for dashboards, KPI views, metric explorers, quick admin panels, or simple data visualizations, even if they do not explicitly say 'dashboard'."
|
|
98
|
+
|
|
99
|
+
### Trigger boundary thinking
|
|
100
|
+
|
|
101
|
+
Before finalizing the description, reason about both sides:
|
|
102
|
+
- should-trigger requests
|
|
103
|
+
- should-not-trigger nearby requests
|
|
104
|
+
|
|
105
|
+
Ask internally:
|
|
106
|
+
- What real user requests should definitely activate this skill?
|
|
107
|
+
- What similar requests should probably use a different skill or no skill at all?
|
|
108
|
+
- What nouns, verbs, deliverables, file types, or workflows best separate this skill from adjacent ones?
|
|
109
|
+
|
|
110
|
+
Use that distinction to make the description sharper.
|
|
111
|
+
|
|
112
|
+
### Description anti-patterns
|
|
113
|
+
|
|
114
|
+
Avoid descriptions that are:
|
|
115
|
+
- too short to convey trigger conditions
|
|
116
|
+
- broad enough to steal many unrelated tasks
|
|
117
|
+
- narrow enough to only match one provided example
|
|
118
|
+
- phrased as internal implementation details instead of user-facing intent
|
|
119
|
+
- redundant with the body while still failing to specify activation conditions
|
|
120
|
+
|
|
121
|
+
## 2. Concise is critical
|
|
122
|
+
|
|
123
|
+
Context is a shared resource. Assume Pi is already highly capable.
|
|
124
|
+
|
|
125
|
+
Do not explain generic concepts the model already knows.
|
|
126
|
+
Only include information that materially improves execution quality.
|
|
127
|
+
Every section should earn its place.
|
|
128
|
+
Prefer a lean, high-signal skill over a long but repetitive one.
|
|
129
|
+
|
|
130
|
+
## 3. Include procedural knowledge, not generic prose
|
|
131
|
+
|
|
132
|
+
A good skill gives the model things it would not reliably infer from first principles every time:
|
|
133
|
+
- decision rules
|
|
134
|
+
- task-specific workflows
|
|
135
|
+
- output structure requirements
|
|
136
|
+
- edge cases
|
|
137
|
+
- failure modes
|
|
138
|
+
- ordering constraints
|
|
139
|
+
- domain-specific heuristics
|
|
140
|
+
- practical tradeoffs
|
|
141
|
+
|
|
142
|
+
Avoid generic motivational prose or textbook explanations.
|
|
143
|
+
|
|
144
|
+
## 4. Set the right degree of freedom
|
|
145
|
+
|
|
146
|
+
Choose the right instruction style for the task:
|
|
147
|
+
- For fragile, error-prone, deterministic, or compliance-sensitive tasks, give tighter instructions and clearer guardrails.
|
|
148
|
+
- For open-ended creative or investigative tasks, give higher-level heuristics and decision criteria.
|
|
149
|
+
- Do not over-constrain flexible tasks with brittle rigid templates unless reliability truly depends on them.
|
|
150
|
+
|
|
151
|
+
## 5. Use imperative, execution-oriented writing
|
|
152
|
+
|
|
153
|
+
Write instructions in an action-oriented style.
|
|
154
|
+
Prefer:
|
|
155
|
+
- "Check..."
|
|
156
|
+
- "Use..."
|
|
157
|
+
- "Prefer..."
|
|
158
|
+
- "If X, then Y..."
|
|
159
|
+
- "Produce..."
|
|
160
|
+
|
|
161
|
+
Avoid rambling explanatory style unless a short explanation is necessary to clarify why a rule matters.
|
|
162
|
+
|
|
163
|
+
## 6. Explain important constraints and failure modes
|
|
164
|
+
|
|
165
|
+
If a task tends to fail in predictable ways, the skill should warn about those failure modes.
|
|
166
|
+
If output quality depends on certain checks, make those checks explicit.
|
|
167
|
+
If there are common edge cases, say how to handle them.
|
|
168
|
+
|
|
169
|
+
## 7. Reusable over overfit
|
|
170
|
+
|
|
171
|
+
Use example requests and domain context to infer the general workflow, not to overfit the skill to a tiny set of examples.
|
|
172
|
+
The resulting skill should generalize across many similar requests.
|
|
173
|
+
Do not bake in unnecessary specifics from one example unless they represent a real recurring constraint.
|
|
174
|
+
|
|
175
|
+
# Anatomy of a strong Pi skill
|
|
176
|
+
|
|
177
|
+
A strong SKILL.md usually contains:
|
|
178
|
+
- frontmatter
|
|
179
|
+
- a clear title
|
|
180
|
+
- a compact set of sections with practical instructions
|
|
181
|
+
|
|
182
|
+
Common useful section types include:
|
|
183
|
+
- Core workflow
|
|
184
|
+
- Decision rules
|
|
185
|
+
- Output expectations
|
|
186
|
+
- Constraints or guardrails
|
|
187
|
+
- Edge cases
|
|
188
|
+
- Examples
|
|
189
|
+
- Reference usage notes
|
|
190
|
+
|
|
191
|
+
You do not need to use all of these sections.
|
|
192
|
+
Choose only the sections that materially improve execution.
|
|
193
|
+
|
|
194
|
+
Avoid filler sections such as:
|
|
195
|
+
- "Overview" that merely restates the description
|
|
196
|
+
- "When to use" repeating trigger logic already covered by description
|
|
197
|
+
- generic setup or authoring notes
|
|
198
|
+
- changelogs, installation guides, or meta documentation
|
|
199
|
+
|
|
200
|
+
# Progressive disclosure
|
|
201
|
+
|
|
202
|
+
Follow progressive disclosure principles.
|
|
203
|
+
Keep SKILL.md focused on the minimum high-value guidance Pi needs after the skill triggers.
|
|
204
|
+
|
|
205
|
+
If additional files are explicitly provided or clearly requested by the user, you may reference them from SKILL.md, but only when helpful.
|
|
206
|
+
When referencing other files:
|
|
207
|
+
- use relative paths only
|
|
208
|
+
- say when Pi should consult them
|
|
209
|
+
- keep the reference one step away from SKILL.md, not deeply nested chains of instructions
|
|
210
|
+
|
|
211
|
+
If no extra resources were provided, do not invent scripts, references, assets, templates, repositories, APIs, or file paths just to make the skill look more sophisticated.
|
|
212
|
+
|
|
213
|
+
# Bundled resources guidance
|
|
214
|
+
|
|
215
|
+
These principles should shape the skill body even if you are only generating SKILL.md.
|
|
216
|
+
|
|
217
|
+
Think deliberately about resource planning. A strong skill is not "fancier" because it mentions more files. It is better only when each bundled resource removes repeated work, reduces errors, or keeps the core SKILL.md lean.
|
|
218
|
+
|
|
219
|
+
Before referencing any resource, ask:
|
|
220
|
+
- Would Pi repeatedly benefit from having this outside the main SKILL.md?
|
|
221
|
+
- Does this remove deterministic busywork or repeated explanation?
|
|
222
|
+
- Is the resource clearly grounded in the user request or existing project context?
|
|
223
|
+
- Would omitting the resource actually produce a cleaner and more truthful skill?
|
|
224
|
+
|
|
225
|
+
If the answer is unclear, do not invent the resource.
|
|
226
|
+
|
|
227
|
+
## Scripts
|
|
228
|
+
|
|
229
|
+
Scripts are appropriate when work is deterministic, repetitive, or easy to get wrong manually.
|
|
230
|
+
Examples include:
|
|
231
|
+
- file format transformations
|
|
232
|
+
- repetitive validation
|
|
233
|
+
- structured extraction
|
|
234
|
+
- conversions
|
|
235
|
+
- fixed data processing
|
|
236
|
+
|
|
237
|
+
A script is a good fit when the same code would otherwise be rewritten repeatedly, when exact output matters, or when a repeatable command is more reliable than freeform reasoning.
|
|
238
|
+
|
|
239
|
+
But do not invent scripts unless the user explicitly asked for them or clearly established that such a file exists or should exist.
|
|
240
|
+
If no script is available, keep the workflow inline in SKILL.md.
|
|
241
|
+
If you reference a script, make the reference practical: explain when Pi should use it and what problem it solves.
|
|
242
|
+
|
|
243
|
+
## References
|
|
244
|
+
|
|
245
|
+
Reference files are appropriate for large, domain-specific material that should not live inline in SKILL.md.
|
|
246
|
+
Examples include:
|
|
247
|
+
- schemas
|
|
248
|
+
- API details
|
|
249
|
+
- policies
|
|
250
|
+
- large style guides
|
|
251
|
+
- framework-specific notes
|
|
252
|
+
|
|
253
|
+
A reference is a good fit when the information is important but too bulky, too domain-specific, or too conditional to keep inside the core workflow.
|
|
254
|
+
|
|
255
|
+
But do not invent reference documents.
|
|
256
|
+
If a detail is essential and no reference file exists, keep the essential guidance in SKILL.md.
|
|
257
|
+
If you reference a document, say what Pi should read there and in what situations.
|
|
258
|
+
|
|
259
|
+
## Assets
|
|
260
|
+
|
|
261
|
+
Assets are output resources such as templates, images, or boilerplate files.
|
|
262
|
+
Assets are appropriate when Pi is expected to copy from or build on concrete materials supplied by the user or project.
|
|
263
|
+
Do not invent or reference them unless the user explicitly provided or requested them.
|
|
264
|
+
|
|
265
|
+
## Resource planning heuristic
|
|
266
|
+
|
|
267
|
+
When deciding whether a skill should mention scripts, references, or assets, use this heuristic:
|
|
268
|
+
- Put core reusable workflow rules in SKILL.md.
|
|
269
|
+
- Put bulky but occasionally-needed knowledge in references.
|
|
270
|
+
- Put deterministic repeatable execution in scripts.
|
|
271
|
+
- Put output materials in assets.
|
|
272
|
+
- If none of those are clearly justified, keep the skill self-contained and do not mention extra files.
|
|
273
|
+
|
|
274
|
+
# How to infer the skill from inputs
|
|
275
|
+
|
|
276
|
+
You will receive:
|
|
277
|
+
- a skill slug
|
|
278
|
+
- a requested description from the user
|
|
279
|
+
- optional allowed tools
|
|
280
|
+
- optional example requests
|
|
281
|
+
- optional domain context
|
|
282
|
+
- the chosen save location
|
|
283
|
+
|
|
284
|
+
Use these inputs to infer the real skill.
|
|
285
|
+
|
|
286
|
+
## Step 1: Infer intent
|
|
287
|
+
|
|
288
|
+
Determine:
|
|
289
|
+
- what capability the skill should provide
|
|
290
|
+
- what kinds of user requests should trigger it
|
|
291
|
+
- what outputs Pi is likely expected to produce
|
|
292
|
+
- what constraints, conventions, or quality bars matter
|
|
293
|
+
|
|
294
|
+
If example requests are present, mine them for:
|
|
295
|
+
- realistic trigger phrasing
|
|
296
|
+
- the sequence of work Pi should perform
|
|
297
|
+
- repeated expectations
|
|
298
|
+
- output shape or deliverables
|
|
299
|
+
- edge cases or pitfalls
|
|
300
|
+
|
|
301
|
+
## Step 2: Infer reusable workflow
|
|
302
|
+
|
|
303
|
+
Ask internally:
|
|
304
|
+
- What sequence of steps would a strong Pi agent repeatedly follow for this task?
|
|
305
|
+
- What decision points need to be made explicit?
|
|
306
|
+
- What mistakes would a generic agent be likely to make?
|
|
307
|
+
- What should the final answer or deliverable look like?
|
|
308
|
+
- Which parts belong in core instructions versus optional resources?
|
|
309
|
+
|
|
310
|
+
The skill should encode those reusable instructions.
|
|
311
|
+
|
|
312
|
+
### Resource planning during workflow design
|
|
313
|
+
|
|
314
|
+
As you infer the workflow, also decide whether the workflow implies reusable resources.
|
|
315
|
+
Think in terms of repeated future use, not one-off elegance.
|
|
316
|
+
|
|
317
|
+
For each likely subtask, ask:
|
|
318
|
+
- Is this best expressed as a short instruction in SKILL.md?
|
|
319
|
+
- Does it imply a deterministic helper script?
|
|
320
|
+
- Does it imply a large body of reference knowledge?
|
|
321
|
+
- Does it imply a template or asset the user explicitly expects?
|
|
322
|
+
|
|
323
|
+
In this generator, default to a self-contained SKILL.md unless the input clearly grounds extra resources.
|
|
324
|
+
|
|
325
|
+
## Step 3: Decide how specific to be
|
|
326
|
+
|
|
327
|
+
Tighten the workflow when:
|
|
328
|
+
- correctness depends on order
|
|
329
|
+
- the task is brittle
|
|
330
|
+
- outputs must match a format
|
|
331
|
+
- common errors are costly
|
|
332
|
+
|
|
333
|
+
Keep it more flexible when:
|
|
334
|
+
- there are several valid approaches
|
|
335
|
+
- the task depends heavily on user context
|
|
336
|
+
- creativity or adaptation is important
|
|
337
|
+
|
|
338
|
+
## Step 4: Sanity-check against the provided description
|
|
339
|
+
|
|
340
|
+
The generated skill must remain faithful to the user request.
|
|
341
|
+
Sharpen and operationalize the requested description, but do not drift into a different skill.
|
|
342
|
+
|
|
343
|
+
# Frontmatter guidance
|
|
344
|
+
|
|
345
|
+
## name
|
|
346
|
+
- Must exactly equal the provided slug.
|
|
347
|
+
- Treat the slug as authoritative.
|
|
348
|
+
|
|
349
|
+
## description
|
|
350
|
+
Write a strong operational description.
|
|
351
|
+
It should:
|
|
352
|
+
- state the skill's capability clearly
|
|
353
|
+
- include trigger contexts in realistic language
|
|
354
|
+
- mention adjacent cases Pi should still treat as matches
|
|
355
|
+
- be more specific than the user's raw one-line request when possible
|
|
356
|
+
- remain truthful to the actual body instructions
|
|
357
|
+
|
|
358
|
+
If the user gave example requests, use them to enrich the description's trigger cues.
|
|
359
|
+
If the user gave domain context, incorporate the parts that help define when and how the skill should be used.
|
|
360
|
+
|
|
361
|
+
## allowed-tools
|
|
362
|
+
Only include this field if tool names were provided.
|
|
363
|
+
Use exactly the provided names as a single space-delimited string.
|
|
364
|
+
Do not add tools on your own.
|
|
365
|
+
|
|
366
|
+
# Body writing guide
|
|
367
|
+
|
|
368
|
+
The body should help another Pi agent instance execute the task well immediately after loading.
|
|
369
|
+
|
|
370
|
+
## Good body characteristics
|
|
371
|
+
- short sections
|
|
372
|
+
- high signal density
|
|
373
|
+
- direct instructions
|
|
374
|
+
- practical decision rules
|
|
375
|
+
- strong defaults
|
|
376
|
+
- clear output expectations where relevant
|
|
377
|
+
- realistic examples only when they reduce ambiguity
|
|
378
|
+
|
|
379
|
+
## Body section design playbook
|
|
380
|
+
|
|
381
|
+
Design the body like an execution manual, not a brochure.
|
|
382
|
+
The best structure depends on the task, but in most cases the body should move from:
|
|
383
|
+
- what Pi should do first
|
|
384
|
+
- how Pi should proceed
|
|
385
|
+
- how Pi should choose between options
|
|
386
|
+
- what good output looks like
|
|
387
|
+
- what mistakes or edge cases to watch for
|
|
388
|
+
|
|
389
|
+
In most skills, the first actionable section should appear early.
|
|
390
|
+
Do not waste the opening body sections on repeating the title or description.
|
|
391
|
+
|
|
392
|
+
### Common high-value section patterns
|
|
393
|
+
|
|
394
|
+
Use only the patterns that help this skill.
|
|
395
|
+
|
|
396
|
+
**Pattern A: Workflow-first**
|
|
397
|
+
Use when the task is procedural.
|
|
398
|
+
Typical sections:
|
|
399
|
+
- Core workflow
|
|
400
|
+
- Decision rules
|
|
401
|
+
- Output expectations
|
|
402
|
+
- Edge cases
|
|
403
|
+
|
|
404
|
+
**Pattern B: Decision-first**
|
|
405
|
+
Use when the main challenge is choosing the right approach.
|
|
406
|
+
Typical sections:
|
|
407
|
+
- Decision rules
|
|
408
|
+
- Recommended workflow by case
|
|
409
|
+
- Constraints
|
|
410
|
+
- Final checks
|
|
411
|
+
|
|
412
|
+
**Pattern C: Output-first**
|
|
413
|
+
Use when the quality bar depends on a specific deliverable format.
|
|
414
|
+
Typical sections:
|
|
415
|
+
- Output requirements
|
|
416
|
+
- Workflow
|
|
417
|
+
- Quality checks
|
|
418
|
+
- Examples
|
|
419
|
+
|
|
420
|
+
**Pattern D: Reference-aware**
|
|
421
|
+
Use when the skill depends on optional large supporting material.
|
|
422
|
+
Typical sections:
|
|
423
|
+
- Core workflow
|
|
424
|
+
- When to read specific references
|
|
425
|
+
- Variant-specific notes
|
|
426
|
+
- Final checks
|
|
427
|
+
|
|
428
|
+
### Section ordering heuristics
|
|
429
|
+
|
|
430
|
+
Prefer section order that mirrors real execution.
|
|
431
|
+
For example:
|
|
432
|
+
- If Pi must inspect inputs before acting, put that early.
|
|
433
|
+
- If output shape determines all later choices, put output expectations before the workflow.
|
|
434
|
+
- If the main source of failure is choosing the wrong path, put decision rules before step-by-step instructions.
|
|
435
|
+
- If a final review step matters, end with explicit quality checks.
|
|
436
|
+
|
|
437
|
+
### Section content guidance
|
|
438
|
+
|
|
439
|
+
For each section, prefer:
|
|
440
|
+
- terse bullets over long paragraphs when procedural clarity matters
|
|
441
|
+
- numbered steps when order matters
|
|
442
|
+
- conditional rules when behavior depends on context
|
|
443
|
+
- compact examples when format is easier to show than describe
|
|
444
|
+
|
|
445
|
+
A section should answer one practical question clearly, such as:
|
|
446
|
+
- What should Pi do first?
|
|
447
|
+
- How should Pi choose an approach?
|
|
448
|
+
- What must the result contain?
|
|
449
|
+
- What should Pi avoid?
|
|
450
|
+
- What should Pi verify before finishing?
|
|
451
|
+
|
|
452
|
+
## What to include in the body
|
|
453
|
+
Include whichever of these are truly useful:
|
|
454
|
+
- the main workflow Pi should follow
|
|
455
|
+
- how to choose between multiple approaches
|
|
456
|
+
- what to inspect first
|
|
457
|
+
- what information to preserve
|
|
458
|
+
- what the output should contain
|
|
459
|
+
- what to avoid
|
|
460
|
+
- how to handle common edge cases
|
|
461
|
+
- quality checks before finishing
|
|
462
|
+
|
|
463
|
+
## What not to include in the body
|
|
464
|
+
Do not include:
|
|
465
|
+
- trigger guidance that belongs in description
|
|
466
|
+
- irrelevant background theory
|
|
467
|
+
- skill-authoring notes
|
|
468
|
+
- TODO markers
|
|
469
|
+
- placeholder text
|
|
470
|
+
- fake file references
|
|
471
|
+
- setup, packaging, benchmarking, or maintenance instructions unless explicitly requested by the user
|
|
472
|
+
|
|
473
|
+
## Body anti-patterns
|
|
474
|
+
|
|
475
|
+
Avoid body designs that:
|
|
476
|
+
- repeat the same point in multiple sections
|
|
477
|
+
- bury the real workflow under generic context-setting text
|
|
478
|
+
- use rigid templates for tasks that need judgment
|
|
479
|
+
- stay so abstract that Pi still has to reinvent the workflow from scratch
|
|
480
|
+
- mention optional references without saying when to consult them
|
|
481
|
+
- include examples that accidentally narrow the skill too much
|
|
482
|
+
|
|
483
|
+
## Minimality heuristic
|
|
484
|
+
|
|
485
|
+
If two sections could be merged without losing clarity, merge them.
|
|
486
|
+
If a section only restates something Pi already knows, delete it.
|
|
487
|
+
If one strong checklist can replace three weak prose sections, prefer the checklist.
|
|
488
|
+
|
|
489
|
+
# Examples guidance
|
|
490
|
+
|
|
491
|
+
Examples are optional.
|
|
492
|
+
Use them only when they materially improve reliability.
|
|
493
|
+
If examples are included:
|
|
494
|
+
- keep them realistic
|
|
495
|
+
- make them representative rather than overly narrow
|
|
496
|
+
- use them to clarify format, decisions, or expected outputs
|
|
497
|
+
- do not bloat the skill with many repetitive examples
|
|
498
|
+
|
|
499
|
+
# Quality bar
|
|
500
|
+
|
|
501
|
+
Before finalizing, ensure the SKILL.md would feel like a real Pi skill that another Pi agent instance can load and use immediately.
|
|
502
|
+
|
|
503
|
+
A good final result should be:
|
|
504
|
+
- specific
|
|
505
|
+
- concise
|
|
506
|
+
- reusable
|
|
507
|
+
- operational
|
|
508
|
+
- faithful to the user's goal
|
|
509
|
+
- adapted to Pi's trigger model
|
|
510
|
+
- free of authoring-process noise
|
|
511
|
+
|
|
512
|
+
# Final output checklist
|
|
513
|
+
|
|
514
|
+
Make sure the result:
|
|
515
|
+
- starts with valid YAML frontmatter
|
|
516
|
+
- includes name and description
|
|
517
|
+
- includes allowed-tools only if provided
|
|
518
|
+
- uses the exact provided slug as name
|
|
519
|
+
- contains a polished markdown body
|
|
520
|
+
- contains no code fences around the whole file
|
|
521
|
+
- contains no meta commentary
|
|
522
|
+
- contains no placeholders or TODOs
|
|
523
|
+
- does not invent external resources unless explicitly grounded in user input
|
|
524
|
+
- is ready to save directly as SKILL.md`;
|
|
525
|
+
|
|
526
|
+
export type SkillLocation = "project" | "global";
|
|
527
|
+
|
|
528
|
+
export interface SkillCreationAnswers {
|
|
529
|
+
name: string;
|
|
530
|
+
description: string;
|
|
531
|
+
exampleRequests?: string;
|
|
532
|
+
domainContext?: string;
|
|
533
|
+
allowedTools: string[];
|
|
534
|
+
location: SkillLocation;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
interface ParsedSkillDraft {
|
|
538
|
+
name: string;
|
|
539
|
+
description: string;
|
|
540
|
+
frontmatter: Record<string, unknown>;
|
|
541
|
+
content: string;
|
|
542
|
+
raw: string;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export type SkillCreationThinkingLevel = ThinkingLevel | "off";
|
|
546
|
+
|
|
547
|
+
export interface SkillGenerationOptions {
|
|
548
|
+
thinkingLevel?: SkillCreationThinkingLevel;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
class SingleLineText implements Component {
|
|
552
|
+
constructor(
|
|
553
|
+
private readonly text: string,
|
|
554
|
+
private readonly ellipsis = "...",
|
|
555
|
+
) {}
|
|
556
|
+
|
|
557
|
+
render(width: number): string[] {
|
|
558
|
+
return [truncateToWidth(this.text, width, this.ellipsis)];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
invalidate(): void {}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
type WizardTextStepId = "name" | "description";
|
|
565
|
+
|
|
566
|
+
type WizardStep = {
|
|
567
|
+
id: WizardTextStepId;
|
|
568
|
+
title: string;
|
|
569
|
+
hint: string;
|
|
570
|
+
optional: boolean;
|
|
571
|
+
kind: "text";
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const WIZARD_STEPS: WizardStep[] = [
|
|
575
|
+
{
|
|
576
|
+
id: "name",
|
|
577
|
+
title: "Name",
|
|
578
|
+
hint: "Use lowercase letters, numbers, and hyphens, for example react-review.",
|
|
579
|
+
optional: false,
|
|
580
|
+
kind: "text",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
id: "description",
|
|
584
|
+
title: "Description",
|
|
585
|
+
hint: "Describe what the skill does and when it should be used in one clear sentence.",
|
|
586
|
+
optional: false,
|
|
587
|
+
kind: "text",
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
class SkillCreationWizard extends Container implements Focusable {
|
|
592
|
+
private readonly input = new Input();
|
|
593
|
+
private readonly values: Record<WizardTextStepId, string> = {
|
|
594
|
+
name: "",
|
|
595
|
+
description: "",
|
|
596
|
+
};
|
|
597
|
+
private stepIndex = 0;
|
|
598
|
+
private errorMessage: string | undefined;
|
|
599
|
+
private _focused = false;
|
|
600
|
+
|
|
601
|
+
get focused(): boolean {
|
|
602
|
+
return this._focused;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
set focused(value: boolean) {
|
|
606
|
+
this._focused = value;
|
|
607
|
+
this.input.focused = value;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
constructor(
|
|
611
|
+
private readonly theme: ExtensionContext["ui"]["theme"],
|
|
612
|
+
private readonly done: (value: SkillCreationAnswers | null) => void,
|
|
613
|
+
) {
|
|
614
|
+
super();
|
|
615
|
+
this.syncInputFromState();
|
|
616
|
+
this.renderContent();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private get currentStep(): WizardStep {
|
|
620
|
+
return WIZARD_STEPS[this.stepIndex]!;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private syncInputFromState(): void {
|
|
624
|
+
this.input.setValue(this.values[this.currentStep.id]);
|
|
625
|
+
this.input.focused = this._focused;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private persistInputToState(): void {
|
|
629
|
+
this.values[this.currentStep.id] = this.input.getValue();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private setError(message: string | undefined): void {
|
|
633
|
+
this.errorMessage = message;
|
|
634
|
+
this.renderContent();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private validateCurrentStep(): boolean {
|
|
638
|
+
this.persistInputToState();
|
|
639
|
+
const step = this.currentStep;
|
|
640
|
+
if (step.kind === "text" && !step.optional) {
|
|
641
|
+
const value = this.values[step.id as WizardTextStepId].trim();
|
|
642
|
+
if (!value) {
|
|
643
|
+
this.setError(`${step.title} is required.`);
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
if (step.id === "name" && !normalizeSkillName(value)) {
|
|
647
|
+
this.setError("Name must contain letters, numbers, or hyphens.");
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
this.errorMessage = undefined;
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private goToPreviousStep(): void {
|
|
656
|
+
this.persistInputToState();
|
|
657
|
+
if (this.stepIndex === 0) return;
|
|
658
|
+
this.errorMessage = undefined;
|
|
659
|
+
this.stepIndex -= 1;
|
|
660
|
+
this.syncInputFromState();
|
|
661
|
+
this.renderContent();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private goToNextStep(): void {
|
|
665
|
+
if (!this.validateCurrentStep()) return;
|
|
666
|
+
if (this.stepIndex >= WIZARD_STEPS.length - 1) {
|
|
667
|
+
this.finish();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
this.stepIndex += 1;
|
|
671
|
+
this.syncInputFromState();
|
|
672
|
+
this.renderContent();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private finish(): void {
|
|
676
|
+
this.persistInputToState();
|
|
677
|
+
const normalizedName = normalizeSkillName(this.values.name);
|
|
678
|
+
if (!normalizedName) {
|
|
679
|
+
this.stepIndex = 0;
|
|
680
|
+
this.syncInputFromState();
|
|
681
|
+
this.setError("Name must contain letters, numbers, or hyphens.");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (!this.values.description.trim()) {
|
|
685
|
+
this.stepIndex = 1;
|
|
686
|
+
this.syncInputFromState();
|
|
687
|
+
this.setError("Description is required.");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
this.done({
|
|
692
|
+
name: normalizedName,
|
|
693
|
+
description: this.values.description.trim(),
|
|
694
|
+
allowedTools: [],
|
|
695
|
+
location: "project",
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private getTitle(): string {
|
|
700
|
+
const step = this.currentStep;
|
|
701
|
+
return `${step.title} (${step.optional ? "optional" : "required"})`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private getHint(): string {
|
|
705
|
+
return this.currentStep.hint;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private renderTextStep(): void {
|
|
709
|
+
if (this.currentStep.id === "name") {
|
|
710
|
+
const raw = this.input.getValue().trim();
|
|
711
|
+
const normalized = normalizeSkillName(raw);
|
|
712
|
+
if (raw && normalized && normalized !== raw) {
|
|
713
|
+
this.addChild(new Spacer(1));
|
|
714
|
+
this.addChild(new Text(this.theme.fg("muted", `Will be saved as: ${normalized}`), 1, 0));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private renderControls(): void {
|
|
720
|
+
const controls = this.stepIndex >= WIZARD_STEPS.length - 1
|
|
721
|
+
? "enter create • alt+← back • alt+→ next • esc cancel"
|
|
722
|
+
: "enter next • alt+← back • alt+→ next • esc cancel";
|
|
723
|
+
this.addChild(new Text(this.theme.fg("dim", controls), 1, 0));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private renderContent(): void {
|
|
727
|
+
this.clear();
|
|
728
|
+
this.addChild(new DynamicBorder((s) => this.theme.fg("accent", s)));
|
|
729
|
+
this.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.getTitle())), 1, 0));
|
|
730
|
+
this.addChild(new Text(this.theme.fg("dim", this.getHint()), 1, 0));
|
|
731
|
+
this.addChild(new Spacer(1));
|
|
732
|
+
this.addChild(this.input);
|
|
733
|
+
this.addChild(new Spacer(1));
|
|
734
|
+
this.renderTextStep();
|
|
735
|
+
|
|
736
|
+
if (this.errorMessage) {
|
|
737
|
+
this.addChild(new Spacer(1));
|
|
738
|
+
this.addChild(new Text(this.theme.fg("error", this.errorMessage), 1, 0));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
this.addChild(new Spacer(1));
|
|
742
|
+
this.renderControls();
|
|
743
|
+
this.addChild(new DynamicBorder((s) => this.theme.fg("accent", s)));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
handleInput(data: string): void {
|
|
747
|
+
if (matchesKey(data, Key.escape)) {
|
|
748
|
+
this.done(null);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (matchesKey(data, Key.alt("left"))) {
|
|
752
|
+
this.goToPreviousStep();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (matchesKey(data, Key.alt("right"))) {
|
|
756
|
+
this.goToNextStep();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (matchesKey(data, Key.enter)) {
|
|
760
|
+
this.goToNextStep();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.errorMessage = undefined;
|
|
765
|
+
this.input.handleInput(data);
|
|
766
|
+
this.renderContent();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function normalizeSkillName(name: string): string {
|
|
771
|
+
return name
|
|
772
|
+
.toLowerCase()
|
|
773
|
+
.trim()
|
|
774
|
+
.replace(/[^a-z0-9-\s]/g, "")
|
|
775
|
+
.replace(/\s+/g, "-")
|
|
776
|
+
.replace(/-+/g, "-")
|
|
777
|
+
.replace(/^-|-$/g, "");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function getTargetDir(ctx: ExtensionContext, location: SkillLocation, skillName: string): string {
|
|
781
|
+
if (location === "global") {
|
|
782
|
+
return join(getAgentDir(), "skills", skillName);
|
|
783
|
+
}
|
|
784
|
+
return resolve(ctx.cwd, ".pi", "skills", skillName);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function buildFallbackSkill(answers: SkillCreationAnswers): string {
|
|
788
|
+
const frontmatterLines = [
|
|
789
|
+
"---",
|
|
790
|
+
`name: ${answers.name}`,
|
|
791
|
+
`description: ${answers.description}`,
|
|
792
|
+
];
|
|
793
|
+
if (answers.allowedTools.length > 0) {
|
|
794
|
+
frontmatterLines.push(`allowed-tools: ${answers.allowedTools.join(" ")}`);
|
|
795
|
+
}
|
|
796
|
+
frontmatterLines.push("---");
|
|
797
|
+
|
|
798
|
+
const sections = [
|
|
799
|
+
frontmatterLines.join("\n"),
|
|
800
|
+
`# ${answers.name}`,
|
|
801
|
+
"## Core workflow",
|
|
802
|
+
"- Confirm the request matches the skill description and intended trigger conditions.",
|
|
803
|
+
"- Apply the most direct workflow for the task instead of giving generic advice.",
|
|
804
|
+
"- Keep outputs concrete, reusable, and adapted to the current request.",
|
|
805
|
+
];
|
|
806
|
+
|
|
807
|
+
if (answers.exampleRequests?.trim()) {
|
|
808
|
+
sections.push("## Example requests", answers.exampleRequests.trim());
|
|
809
|
+
}
|
|
810
|
+
if (answers.domainContext?.trim()) {
|
|
811
|
+
sections.push("## Domain context", answers.domainContext.trim());
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
sections.push(
|
|
815
|
+
"## Guidance",
|
|
816
|
+
"- Prefer concrete steps, checks, and deliverables over abstract explanation.",
|
|
817
|
+
"- Reuse provided context and examples, but do not overfit to them.",
|
|
818
|
+
"- Call out important edge cases, constraints, and failure modes when relevant.",
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
return sections.join("\n\n").trim() + "\n";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function parseSkillDraft(raw: string, expectedName: string): ParsedSkillDraft {
|
|
825
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(raw);
|
|
826
|
+
const name = typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name.trim() : "";
|
|
827
|
+
const description = typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description.trim() : "";
|
|
828
|
+
|
|
829
|
+
if (!name || !description) {
|
|
830
|
+
throw new Error("Skill must include frontmatter fields 'name' and 'description'");
|
|
831
|
+
}
|
|
832
|
+
if (name !== expectedName) {
|
|
833
|
+
throw new Error(`Frontmatter name must be '${expectedName}'`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
name,
|
|
838
|
+
description,
|
|
839
|
+
frontmatter: Object.fromEntries(Object.entries(parsed.frontmatter).filter(([, value]) => value !== undefined)),
|
|
840
|
+
content: stripFrontmatter(raw).trim(),
|
|
841
|
+
raw: raw.trim() + "\n",
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function getEffectiveReasoningLevel(
|
|
846
|
+
ctx: ExtensionContext,
|
|
847
|
+
thinkingLevel?: SkillCreationThinkingLevel,
|
|
848
|
+
): ThinkingLevel | undefined {
|
|
849
|
+
if (!ctx.model?.reasoning || !thinkingLevel || thinkingLevel === "off") {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
return thinkingLevel;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function getGenerationStatusLabel(
|
|
856
|
+
ctx: ExtensionContext,
|
|
857
|
+
thinkingLevel?: SkillCreationThinkingLevel,
|
|
858
|
+
): string {
|
|
859
|
+
const modelLabel = ctx.model?.id ?? "template";
|
|
860
|
+
const reasoning = getEffectiveReasoningLevel(ctx, thinkingLevel);
|
|
861
|
+
return reasoning
|
|
862
|
+
? `Generating skill draft using ${modelLabel} • ${reasoning}...`
|
|
863
|
+
: `Generating skill draft using ${modelLabel}...`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function generateSkillDraft(
|
|
867
|
+
ctx: ExtensionContext,
|
|
868
|
+
answers: SkillCreationAnswers,
|
|
869
|
+
options?: SkillGenerationOptions,
|
|
870
|
+
): Promise<string> {
|
|
871
|
+
if (!ctx.model) {
|
|
872
|
+
return buildFallbackSkill(answers);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
876
|
+
if (!auth.ok || !auth.apiKey) {
|
|
877
|
+
return buildFallbackSkill(answers);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const userMessage: UserMessage = {
|
|
881
|
+
role: "user",
|
|
882
|
+
content: [{
|
|
883
|
+
type: "text",
|
|
884
|
+
text: [
|
|
885
|
+
"Create a production-ready Pi skill draft.",
|
|
886
|
+
"",
|
|
887
|
+
"Inputs",
|
|
888
|
+
`- skill_slug: ${answers.name}`,
|
|
889
|
+
`- requested_description: ${answers.description}`,
|
|
890
|
+
`- save_location: ${answers.location}`,
|
|
891
|
+
answers.allowedTools.length > 0
|
|
892
|
+
? `- allowed_tools: ${answers.allowedTools.join(" ")}`
|
|
893
|
+
: "- allowed_tools: (omit allowed-tools frontmatter field)",
|
|
894
|
+
`- example_requests: ${answers.exampleRequests?.trim() || "(none provided)"}`,
|
|
895
|
+
`- domain_context: ${answers.domainContext?.trim() || "(none provided)"}`,
|
|
896
|
+
"",
|
|
897
|
+
"Instructions",
|
|
898
|
+
"- Infer the real trigger situations from the requested description and example requests.",
|
|
899
|
+
"- Write a concise, high-signal SKILL.md for Pi.",
|
|
900
|
+
"- Make the description specific enough to help activation.",
|
|
901
|
+
"- Keep the body focused on execution guidance, decision rules, output expectations, and important edge cases.",
|
|
902
|
+
"- Do not invent extra files or capabilities unless explicitly requested.",
|
|
903
|
+
"- If information is missing, make conservative, reusable choices instead of adding placeholders or TODOs.",
|
|
904
|
+
].join("\n"),
|
|
905
|
+
}],
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const reasoning = getEffectiveReasoningLevel(ctx, options?.thinkingLevel);
|
|
910
|
+
const response = await completeSimple(
|
|
911
|
+
ctx.model,
|
|
912
|
+
{ systemPrompt: GENERATE_SKILL_SYSTEM_PROMPT, messages: [userMessage] },
|
|
913
|
+
{ apiKey: auth.apiKey, headers: auth.headers, ...(reasoning ? { reasoning } : {}) },
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const generated = response.content
|
|
917
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
918
|
+
.map((c) => c.text)
|
|
919
|
+
.join("\n")
|
|
920
|
+
.trim();
|
|
921
|
+
|
|
922
|
+
if (!generated) {
|
|
923
|
+
return buildFallbackSkill(answers);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
parseSkillDraft(generated, answers.name);
|
|
928
|
+
return generated;
|
|
929
|
+
} catch {
|
|
930
|
+
return buildFallbackSkill(answers);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function runDraftGeneration(
|
|
935
|
+
ctx: ExtensionContext,
|
|
936
|
+
answers: SkillCreationAnswers,
|
|
937
|
+
options?: SkillGenerationOptions,
|
|
938
|
+
): Promise<string | null> {
|
|
939
|
+
return await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
940
|
+
const loader = new BorderedLoader(tui, theme, getGenerationStatusLabel(ctx, options?.thinkingLevel));
|
|
941
|
+
loader.onAbort = () => done(null);
|
|
942
|
+
|
|
943
|
+
generateSkillDraft(ctx, answers, options)
|
|
944
|
+
.then(done)
|
|
945
|
+
.catch(() => done(buildFallbackSkill(answers)));
|
|
946
|
+
|
|
947
|
+
return loader;
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function collectAnswers(ctx: ExtensionContext): Promise<SkillCreationAnswers | null> {
|
|
952
|
+
const answers = await ctx.ui.custom<SkillCreationAnswers | null>((tui, _theme, _kb, done) => {
|
|
953
|
+
const component = new SkillCreationWizard(ctx.ui.theme, done);
|
|
954
|
+
return {
|
|
955
|
+
get focused() {
|
|
956
|
+
return component.focused;
|
|
957
|
+
},
|
|
958
|
+
set focused(value: boolean) {
|
|
959
|
+
component.focused = value;
|
|
960
|
+
},
|
|
961
|
+
render(width: number) {
|
|
962
|
+
return component.render(width);
|
|
963
|
+
},
|
|
964
|
+
invalidate() {
|
|
965
|
+
component.invalidate();
|
|
966
|
+
},
|
|
967
|
+
handleInput(data: string) {
|
|
968
|
+
component.handleInput(data);
|
|
969
|
+
tui.requestRender();
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
}, { overlay: true, overlayOptions: { width: "70%", maxHeight: "80%", anchor: "center" } });
|
|
973
|
+
|
|
974
|
+
return answers;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export async function createSkillFromAnswers(
|
|
978
|
+
ctx: ExtensionContext,
|
|
979
|
+
answers: SkillCreationAnswers,
|
|
980
|
+
options?: SkillGenerationOptions,
|
|
981
|
+
): Promise<SkillEntry | null> {
|
|
982
|
+
const targetDir = getTargetDir(ctx, answers.location, answers.name);
|
|
983
|
+
const targetPath = join(targetDir, "SKILL.md");
|
|
984
|
+
if (existsSync(targetPath)) {
|
|
985
|
+
ctx.ui.notify(`Skill already exists: ${targetPath}`, "error");
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const draft = await runDraftGeneration(ctx, answers, options);
|
|
990
|
+
if (draft === null) {
|
|
991
|
+
ctx.ui.notify("Cancelled", "info");
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let parsedSkill: ParsedSkillDraft;
|
|
996
|
+
try {
|
|
997
|
+
parsedSkill = parseSkillDraft(draft, answers.name);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Invalid generated SKILL.md", "error");
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
await mkdir(targetDir, { recursive: true });
|
|
1004
|
+
await writeFile(targetPath, parsedSkill.raw, "utf8");
|
|
1005
|
+
|
|
1006
|
+
ctx.ui.notify(`Created skill: ${targetPath}`, "info");
|
|
1007
|
+
return {
|
|
1008
|
+
name: parsedSkill.name,
|
|
1009
|
+
description: parsedSkill.description,
|
|
1010
|
+
path: targetPath,
|
|
1011
|
+
content: parsedSkill.content,
|
|
1012
|
+
frontmatter: parsedSkill.frontmatter,
|
|
1013
|
+
scope: answers.location === "global" ? "user" : "project",
|
|
1014
|
+
origin: "top-level",
|
|
1015
|
+
source: "auto",
|
|
1016
|
+
baseDir: targetDir,
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export async function createNewSkill(
|
|
1021
|
+
ctx: ExtensionContext,
|
|
1022
|
+
options?: SkillGenerationOptions,
|
|
1023
|
+
): Promise<SkillEntry | null> {
|
|
1024
|
+
const answers = await collectAnswers(ctx);
|
|
1025
|
+
if (!answers) return null;
|
|
1026
|
+
return await createSkillFromAnswers(ctx, answers, options);
|
|
1027
|
+
}
|