@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.
@@ -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
+ }