@t3lnet/sceneforge 1.0.9 → 1.0.11

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 (35) hide show
  1. package/README.md +57 -0
  2. package/cli/cli.js +6 -0
  3. package/cli/commands/add-audio-to-steps.js +9 -3
  4. package/cli/commands/concat-final-videos.js +6 -2
  5. package/cli/commands/context.js +791 -0
  6. package/cli/commands/split-video.js +3 -1
  7. package/context/context-builder.ts +318 -0
  8. package/context/index.ts +52 -0
  9. package/context/template-loader.ts +161 -0
  10. package/context/templates/base/actions-reference.md +299 -0
  11. package/context/templates/base/cli-reference.md +236 -0
  12. package/context/templates/base/project-overview.md +58 -0
  13. package/context/templates/base/selectors-guide.md +233 -0
  14. package/context/templates/base/yaml-schema.md +210 -0
  15. package/context/templates/skills/balance-timing.md +136 -0
  16. package/context/templates/skills/debug-selector.md +193 -0
  17. package/context/templates/skills/generate-actions.md +94 -0
  18. package/context/templates/skills/optimize-demo.md +218 -0
  19. package/context/templates/skills/review-demo-yaml.md +164 -0
  20. package/context/templates/skills/write-step-script.md +136 -0
  21. package/context/templates/stages/stage1-actions.md +236 -0
  22. package/context/templates/stages/stage2-scripts.md +197 -0
  23. package/context/templates/stages/stage3-balancing.md +229 -0
  24. package/context/templates/stages/stage4-rebalancing.md +228 -0
  25. package/context/tests/context-builder.test.ts +237 -0
  26. package/context/tests/template-loader.test.ts +181 -0
  27. package/context/tests/tool-formatter.test.ts +198 -0
  28. package/context/tool-formatter.ts +189 -0
  29. package/dist/index.cjs +416 -11
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +182 -1
  32. package/dist/index.d.ts +182 -1
  33. package/dist/index.js +391 -11
  34. package/dist/index.js.map +1 -1
  35. package/package.json +2 -1
@@ -0,0 +1,229 @@
1
+ # Stage 3: Step Balancing
2
+
3
+ This stage focuses on analyzing timing data and adjusting scripts/actions to achieve optimal synchronization between voiceover and video.
4
+
5
+ ## Objectives
6
+
7
+ 1. Analyze script JSON output for timing data
8
+ 2. Compare estimated vs. actual durations
9
+ 3. Adjust scripts or add wait actions
10
+ 4. Achieve smooth synchronization
11
+
12
+ ## Understanding Timing Data
13
+
14
+ After running `sceneforge record`, timing data is saved to:
15
+ ```
16
+ output/<demo-name>/scripts/<demo-name>-scripts.json
17
+ ```
18
+
19
+ ### Script JSON Format
20
+
21
+ ```json
22
+ {
23
+ "demoName": "my-demo",
24
+ "generatedAt": "2024-01-15T10:30:00Z",
25
+ "steps": [
26
+ {
27
+ "stepId": "intro",
28
+ "script": "Welcome to this demo...",
29
+ "wordCount": 12,
30
+ "estimatedDurationMs": 4800,
31
+ "stepTimingMs": 3000,
32
+ "actions": [
33
+ {
34
+ "action": "wait",
35
+ "duration": 3000
36
+ }
37
+ ]
38
+ }
39
+ ],
40
+ "totalEstimatedDurationMs": 45000,
41
+ "totalWordCount": 112
42
+ }
43
+ ```
44
+
45
+ ### Key Metrics
46
+
47
+ | Metric | Description |
48
+ |--------|-------------|
49
+ | `estimatedDurationMs` | Expected voiceover duration (wordCount × 400ms) |
50
+ | `stepTimingMs` | Actual recorded step duration |
51
+ | `wordCount` | Number of words in script |
52
+
53
+ ## Balancing Strategy
54
+
55
+ ### Step Duration Analysis
56
+
57
+ For each step, compare:
58
+ ```
59
+ Difference = stepTimingMs - estimatedDurationMs
60
+ ```
61
+
62
+ **If positive (actions longer than script):**
63
+ - Video has "dead air"
64
+ - Options: Expand script OR add wait action at start
65
+
66
+ **If negative (script longer than actions):**
67
+ - Voiceover will overlap next step
68
+ - Options: Shorten script OR add wait action at end
69
+
70
+ ### Adjustment Options
71
+
72
+ #### 1. Expand Script (actions > script)
73
+
74
+ **Before:**
75
+ ```yaml
76
+ - id: create-project
77
+ script: "Click Create to make a new project."
78
+ actions:
79
+ - action: click
80
+ target: { type: button, text: "Create" }
81
+ - action: wait
82
+ waitFor: { type: selector, value: ".modal" }
83
+ - action: type
84
+ target: { type: input, name: "name" }
85
+ text: "My Project"
86
+ ```
87
+
88
+ **After (expanded script):**
89
+ ```yaml
90
+ - id: create-project
91
+ script: "Click the Create button to start a new project. In the dialog that appears, enter a descriptive name for your project."
92
+ actions:
93
+ - action: click
94
+ target: { type: button, text: "Create" }
95
+ - action: wait
96
+ waitFor: { type: selector, value: ".modal" }
97
+ - action: type
98
+ target: { type: input, name: "name" }
99
+ text: "My Project"
100
+ ```
101
+
102
+ #### 2. Add Wait Action (actions < script)
103
+
104
+ **Before:**
105
+ ```yaml
106
+ - id: quick-action
107
+ script: "This powerful feature allows you to quickly process multiple items at once, saving significant time in your workflow."
108
+ actions:
109
+ - action: click
110
+ target: { type: button, text: "Process All" }
111
+ ```
112
+
113
+ **After (with wait):**
114
+ ```yaml
115
+ - id: quick-action
116
+ script: "This powerful feature allows you to quickly process multiple items at once, saving significant time in your workflow."
117
+ actions:
118
+ - action: click
119
+ target: { type: button, text: "Process All" }
120
+ - action: wait
121
+ duration: 3000 # Allow voiceover to complete
122
+ ```
123
+
124
+ #### 3. Shorten Script (script too long)
125
+
126
+ **Before:**
127
+ ```yaml
128
+ - id: save-settings
129
+ script: "Now we need to click the Save button located at the bottom of the form to save all of our configuration changes to the system."
130
+ actions:
131
+ - action: click
132
+ target: { type: button, text: "Save" }
133
+ ```
134
+
135
+ **After (shortened):**
136
+ ```yaml
137
+ - id: save-settings
138
+ script: "Click Save to apply your changes."
139
+ actions:
140
+ - action: click
141
+ target: { type: button, text: "Save" }
142
+ ```
143
+
144
+ ## Timing Formulas
145
+
146
+ ### Estimate Script Duration
147
+ ```
148
+ estimatedMs = wordCount × 400
149
+ ```
150
+
151
+ ### Calculate Required Wait
152
+ ```
153
+ waitMs = estimatedDurationMs - stepTimingMs
154
+ ```
155
+ If `waitMs > 0`, add wait action with that duration.
156
+
157
+ ### Words Per Second
158
+ ```
159
+ wordsPerSecond = 2.5 (150 WPM)
160
+ ```
161
+
162
+ ## Balancing Workflow
163
+
164
+ 1. **Record demo:**
165
+ ```bash
166
+ sceneforge record -d demo.yaml -b http://localhost:3000
167
+ ```
168
+
169
+ 2. **Review timing data:**
170
+ ```bash
171
+ cat output/my-demo/scripts/my-demo-scripts.json
172
+ ```
173
+
174
+ 3. **Identify imbalances:**
175
+ - Steps where `stepTimingMs << estimatedDurationMs`
176
+ - Steps where `stepTimingMs >> estimatedDurationMs`
177
+
178
+ 4. **Make adjustments:**
179
+ - Edit YAML to modify scripts
180
+ - Add/remove wait actions
181
+
182
+ 5. **Re-record and verify:**
183
+ ```bash
184
+ sceneforge record -d demo.yaml -b http://localhost:3000
185
+ ```
186
+
187
+ ## Common Adjustments
188
+
189
+ ### Add Intro Padding
190
+ ```yaml
191
+ - id: intro
192
+ script: "Welcome to the Product Tour. In this demo, we'll explore the key features."
193
+ actions:
194
+ - action: wait
195
+ duration: 4000 # Match script duration
196
+ ```
197
+
198
+ ### Extend Transition
199
+ ```yaml
200
+ - id: navigate-dashboard
201
+ script: "Navigate to the dashboard to see your analytics overview."
202
+ actions:
203
+ - action: click
204
+ target: { type: link, text: "Dashboard" }
205
+ - action: wait
206
+ waitFor: { type: idle }
207
+ - action: wait
208
+ duration: 1500 # Extra time for voiceover
209
+ ```
210
+
211
+ ### Quick Action Padding
212
+ ```yaml
213
+ - id: single-click
214
+ script: "Enable notifications to stay updated on project changes."
215
+ actions:
216
+ - action: click
217
+ target: { type: selector, selector: "[data-testid='notifications-toggle']" }
218
+ - action: wait
219
+ duration: 2500 # Voiceover needs time
220
+ ```
221
+
222
+ ## Checklist Before Stage 4
223
+
224
+ - [ ] All steps reviewed for timing balance
225
+ - [ ] Scripts match action durations (within ~20%)
226
+ - [ ] Wait actions added where needed
227
+ - [ ] No rapid-fire short steps
228
+ - [ ] Intro/outro have adequate duration
229
+ - [ ] Demo flows naturally when watched
@@ -0,0 +1,228 @@
1
+ # Stage 4: Rebalancing
2
+
3
+ This stage focuses on the iteration cycle after audio generation, using actual audio durations to fine-tune synchronization.
4
+
5
+ ## Objectives
6
+
7
+ 1. Analyze audio manifest with actual durations
8
+ 2. Compare actual audio vs. video timing
9
+ 3. Make final timing adjustments
10
+ 4. Achieve polished final output
11
+
12
+ ## Audio Manifest Format
13
+
14
+ After running `sceneforge voiceover`, the audio manifest is saved to:
15
+ ```
16
+ output/<demo-name>/audio/manifest.json
17
+ ```
18
+
19
+ ### Manifest Structure
20
+
21
+ ```json
22
+ {
23
+ "demoName": "my-demo",
24
+ "generatedAt": "2024-01-15T11:00:00Z",
25
+ "voiceId": "21m00Tcm4TlvDq8ikWAM",
26
+ "segments": [
27
+ {
28
+ "stepId": "intro",
29
+ "script": "Welcome to this demo...",
30
+ "audioFile": "intro.mp3",
31
+ "durationMs": 4850,
32
+ "estimatedDurationMs": 4800,
33
+ "variance": 50
34
+ }
35
+ ],
36
+ "totalDurationMs": 47200,
37
+ "totalEstimatedMs": 45000
38
+ }
39
+ ```
40
+
41
+ ### Key Fields
42
+
43
+ | Field | Description |
44
+ |-------|-------------|
45
+ | `durationMs` | Actual audio duration from ElevenLabs |
46
+ | `estimatedDurationMs` | Our word count estimate |
47
+ | `variance` | Difference (actual - estimated) |
48
+ | `audioFile` | Generated audio file path |
49
+
50
+ ## Rebalancing Workflow
51
+
52
+ ### 1. Generate Voiceover
53
+ ```bash
54
+ sceneforge voiceover --demo my-demo
55
+ ```
56
+
57
+ ### 2. Analyze Manifest
58
+ ```bash
59
+ cat output/my-demo/audio/manifest.json | jq '.segments[] | {stepId, durationMs, variance}'
60
+ ```
61
+
62
+ ### 3. Identify Problem Steps
63
+
64
+ **Audio longer than video:**
65
+ ```
66
+ stepTimingMs: 3000
67
+ audioDurationMs: 4500
68
+ Problem: Voiceover will be cut off or overlap
69
+ ```
70
+
71
+ **Audio shorter than video:**
72
+ ```
73
+ stepTimingMs: 5000
74
+ audioDurationMs: 2000
75
+ Problem: Silent gap in demo
76
+ ```
77
+
78
+ ### 4. Make Adjustments
79
+
80
+ Adjust the YAML based on actual audio durations.
81
+
82
+ ### 5. Re-run Pipeline
83
+ ```bash
84
+ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume
85
+ ```
86
+
87
+ The `--resume` flag skips completed steps and re-processes from where changes are needed.
88
+
89
+ ## Adjustment Strategies
90
+
91
+ ### Audio Too Long
92
+
93
+ **Option A: Add wait action to extend step**
94
+ ```yaml
95
+ - id: explain-feature
96
+ script: "This feature provides comprehensive analytics..."
97
+ actions:
98
+ - action: hover
99
+ target: { type: selector, selector: ".analytics-panel" }
100
+ - action: wait
101
+ duration: 2000 # Added to match audio duration
102
+ ```
103
+
104
+ **Option B: Shorten script**
105
+ ```yaml
106
+ # Before (estimated 6s, actual 7.2s)
107
+ script: "This feature provides comprehensive analytics including detailed breakdowns of user engagement metrics."
108
+
109
+ # After (shorter to match available time)
110
+ script: "This feature provides comprehensive analytics on user engagement."
111
+ ```
112
+
113
+ ### Audio Too Short
114
+
115
+ **Option A: Expand script**
116
+ ```yaml
117
+ # Before (estimated 3s, actual 2.5s, but step is 5s)
118
+ script: "Click Save to finish."
119
+
120
+ # After (expanded to fill time)
121
+ script: "Click the Save button to store your changes. The system will confirm once saved."
122
+ ```
123
+
124
+ **Option B: Split into multiple steps**
125
+ ```yaml
126
+ # Before: One step with long video and short script
127
+ - id: complex-workflow
128
+ script: "Complete the workflow."
129
+ actions:
130
+ # ... many actions
131
+
132
+ # After: Split into logical parts
133
+ - id: workflow-part-1
134
+ script: "First, configure the basic settings."
135
+ actions:
136
+ # ... first set of actions
137
+
138
+ - id: workflow-part-2
139
+ script: "Next, set up the advanced options."
140
+ actions:
141
+ # ... second set of actions
142
+ ```
143
+
144
+ ## Iteration Cycle
145
+
146
+ ```
147
+ ┌─────────────────────────────────┐
148
+ │ 1. Edit YAML (scripts/waits) │
149
+ └───────────────┬─────────────────┘
150
+
151
+
152
+ ┌─────────────────────────────────┐
153
+ │ 2. Run pipeline with --resume │
154
+ │ (re-records if needed) │
155
+ └───────────────┬─────────────────┘
156
+
157
+
158
+ ┌─────────────────────────────────┐
159
+ │ 3. Generate new voiceover │
160
+ │ (or use cache if unchanged) │
161
+ └───────────────┬─────────────────┘
162
+
163
+
164
+ ┌─────────────────────────────────┐
165
+ │ 4. Check manifest for timing │
166
+ └───────────────┬─────────────────┘
167
+
168
+
169
+ ┌───────┴───────┐
170
+ │ Acceptable? │
171
+ └───────┬───────┘
172
+ No ──┼── Yes
173
+ │ │
174
+ │ ▼
175
+ Loop │ Done!
176
+ back ──┘
177
+ ```
178
+
179
+ ## Using --resume Flag
180
+
181
+ The `--resume` flag is essential for efficient iteration:
182
+
183
+ ```bash
184
+ # First run: full pipeline
185
+ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --clean
186
+
187
+ # After editing scripts only (no action changes)
188
+ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume --skip-record
189
+
190
+ # After editing actions
191
+ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --resume
192
+ ```
193
+
194
+ **Resume behavior:**
195
+ - Detects which steps have changes
196
+ - Re-uses cached voiceover for unchanged scripts
197
+ - Only re-records steps with action changes
198
+ - Regenerates final video
199
+
200
+ ## Quality Checks
201
+
202
+ ### Visual Sync Check
203
+ Watch the final video and note:
204
+ - [ ] Voiceover starts match action starts
205
+ - [ ] No awkward silences
206
+ - [ ] No cut-off narration
207
+ - [ ] Transitions feel natural
208
+
209
+ ### Timing Tolerance
210
+ Aim for variance within ±500ms per step:
211
+ - Under 500ms: Usually acceptable
212
+ - 500-1000ms: Consider adjustment
213
+ - Over 1000ms: Definitely adjust
214
+
215
+ ### Audio Quality Check
216
+ - [ ] No clipped words at step boundaries
217
+ - [ ] Volume consistent across steps
218
+ - [ ] No unexpected pauses
219
+
220
+ ## Final Checklist
221
+
222
+ - [ ] All steps have acceptable timing variance
223
+ - [ ] Final video plays smoothly
224
+ - [ ] Voiceover syncs with actions
225
+ - [ ] No dead air or overlap issues
226
+ - [ ] Intro provides adequate context
227
+ - [ ] Outro concludes naturally
228
+ - [ ] Overall demo flows professionally
@@ -0,0 +1,237 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import {
6
+ buildContext,
7
+ previewContext,
8
+ deployContext,
9
+ listDeployedContext,
10
+ removeContext,
11
+ getSkill,
12
+ listSkills,
13
+ hasTemplates,
14
+ } from "../context-builder";
15
+
16
+ describe("context-builder", () => {
17
+ let tempDir: string;
18
+
19
+ beforeEach(async () => {
20
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "sceneforge-test-"));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(tempDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("hasTemplates", () => {
28
+ it("returns true when templates exist", async () => {
29
+ const result = await hasTemplates();
30
+ expect(result).toBe(true);
31
+ });
32
+ });
33
+
34
+ describe("buildContext", () => {
35
+ it("builds context for claude with all stages", async () => {
36
+ const content = await buildContext("claude", "all");
37
+ expect(content).toContain("SceneForge");
38
+ expect(content).toContain("Claude Code");
39
+ });
40
+
41
+ it("builds context for specific stage", async () => {
42
+ const content = await buildContext("cursor", "actions");
43
+ expect(content).toContain("Cursor");
44
+ });
45
+
46
+ it("includes base templates in output", async () => {
47
+ const content = await buildContext("claude", "all");
48
+ expect(content).toContain("YAML");
49
+ expect(content).toContain("DemoAction");
50
+ });
51
+
52
+ it("applies variable interpolation", async () => {
53
+ const content = await buildContext("claude", "all", {
54
+ customVar: "CustomValue",
55
+ });
56
+ // Variables not in templates won't appear, but function should not throw
57
+ expect(content.length).toBeGreaterThan(0);
58
+ });
59
+ });
60
+
61
+ describe("previewContext", () => {
62
+ it("returns preview result with content", async () => {
63
+ const result = await previewContext("claude", "all");
64
+ expect(result.tool).toBe("claude");
65
+ expect(result.stage).toBeUndefined();
66
+ expect(result.content.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it("includes stage in result when specified", async () => {
70
+ const result = await previewContext("cursor", "actions");
71
+ expect(result.tool).toBe("cursor");
72
+ expect(result.stage).toBe("actions");
73
+ });
74
+ });
75
+
76
+ describe("deployContext", () => {
77
+ it("deploys combined file for single tool", async () => {
78
+ const results = await deployContext({
79
+ target: "claude",
80
+ stage: "all",
81
+ format: "combined",
82
+ outputDir: tempDir,
83
+ });
84
+
85
+ expect(results.length).toBe(1);
86
+ expect(results[0].created).toBe(true);
87
+ expect(results[0].tool).toBe("claude");
88
+
89
+ const filePath = path.join(tempDir, "CLAUDE.md");
90
+ const content = await fs.readFile(filePath, "utf-8");
91
+ expect(content).toContain("SceneForge");
92
+ });
93
+
94
+ it("deploys files for all tools", async () => {
95
+ const results = await deployContext({
96
+ target: "all",
97
+ stage: "all",
98
+ format: "combined",
99
+ outputDir: tempDir,
100
+ });
101
+
102
+ expect(results.length).toBe(4);
103
+ expect(results.every((r) => r.created)).toBe(true);
104
+
105
+ // Check each file exists
106
+ const claudeFile = await fs.readFile(path.join(tempDir, "CLAUDE.md"), "utf-8");
107
+ expect(claudeFile).toContain("Claude Code");
108
+
109
+ const cursorFile = await fs.readFile(path.join(tempDir, ".cursorrules"), "utf-8");
110
+ expect(cursorFile).toContain("Cursor");
111
+ });
112
+
113
+ it("creates directory structure for split format", async () => {
114
+ const results = await deployContext({
115
+ target: "claude",
116
+ stage: "actions",
117
+ format: "split",
118
+ outputDir: tempDir,
119
+ });
120
+
121
+ expect(results.length).toBe(1);
122
+ expect(results[0].created).toBe(true);
123
+
124
+ const splitDir = path.join(tempDir, ".claude/rules");
125
+ const files = await fs.readdir(splitDir);
126
+ expect(files.length).toBeGreaterThan(0);
127
+ });
128
+
129
+ it("deploys multiple stages in split format", async () => {
130
+ const results = await deployContext({
131
+ target: "cursor",
132
+ stage: "all",
133
+ format: "split",
134
+ outputDir: tempDir,
135
+ });
136
+
137
+ expect(results.length).toBe(4);
138
+ expect(results.every((r) => r.created)).toBe(true);
139
+ });
140
+ });
141
+
142
+ describe("listDeployedContext", () => {
143
+ it("returns empty list for empty directory", async () => {
144
+ const result = await listDeployedContext(tempDir);
145
+ expect(result.files.every((f) => !f.exists)).toBe(true);
146
+ });
147
+
148
+ it("detects deployed files", async () => {
149
+ // First deploy
150
+ await deployContext({
151
+ target: "claude",
152
+ stage: "all",
153
+ format: "combined",
154
+ outputDir: tempDir,
155
+ });
156
+
157
+ const result = await listDeployedContext(tempDir);
158
+ const existing = result.files.filter((f) => f.exists);
159
+ expect(existing.length).toBeGreaterThan(0);
160
+ expect(existing.some((f) => f.tool === "claude")).toBe(true);
161
+ });
162
+ });
163
+
164
+ describe("removeContext", () => {
165
+ it("removes deployed files for specific tool", async () => {
166
+ // Deploy first
167
+ await deployContext({
168
+ target: "claude",
169
+ stage: "all",
170
+ format: "combined",
171
+ outputDir: tempDir,
172
+ });
173
+
174
+ // Verify file exists
175
+ const filePath = path.join(tempDir, "CLAUDE.md");
176
+ await expect(fs.access(filePath)).resolves.toBeUndefined();
177
+
178
+ // Remove
179
+ const results = await removeContext(tempDir, "claude");
180
+ expect(results.some((r) => r.removed)).toBe(true);
181
+
182
+ // Verify file removed
183
+ await expect(fs.access(filePath)).rejects.toThrow();
184
+ });
185
+
186
+ it("removes all deployed files when target is all", async () => {
187
+ // Deploy all
188
+ await deployContext({
189
+ target: "all",
190
+ stage: "all",
191
+ format: "combined",
192
+ outputDir: tempDir,
193
+ });
194
+
195
+ // Remove all
196
+ await removeContext(tempDir, "all");
197
+
198
+ // Verify all removed
199
+ const result = await listDeployedContext(tempDir);
200
+ const existing = result.files.filter((f) => f.exists);
201
+ expect(existing.length).toBe(0);
202
+ });
203
+
204
+ it("handles non-existent files gracefully", async () => {
205
+ const results = await removeContext(tempDir, "claude");
206
+ // Should not error, just not mark as removed
207
+ expect(results.every((r) => !r.error || r.error === undefined)).toBe(true);
208
+ });
209
+ });
210
+
211
+ describe("getSkill", () => {
212
+ it("returns skill content for valid skill", async () => {
213
+ const skill = await getSkill("generate-actions");
214
+ expect(skill).not.toBeNull();
215
+ expect(skill?.name).toBe("generate-actions");
216
+ expect(skill?.content).toContain("Generate");
217
+ });
218
+
219
+ it("returns null for invalid skill", async () => {
220
+ const skill = await getSkill("non-existent-skill");
221
+ expect(skill).toBeNull();
222
+ });
223
+ });
224
+
225
+ describe("listSkills", () => {
226
+ it("returns all available skills", async () => {
227
+ const skills = await listSkills();
228
+ expect(skills).toContain("generate-actions");
229
+ expect(skills).toContain("write-step-script");
230
+ expect(skills).toContain("balance-timing");
231
+ expect(skills).toContain("review-demo-yaml");
232
+ expect(skills).toContain("debug-selector");
233
+ expect(skills).toContain("optimize-demo");
234
+ expect(skills.length).toBe(6);
235
+ });
236
+ });
237
+ });