@deckio/deck-engine 1.7.5 → 1.7.7

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.
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Generate an image using OpenAI chatgpt-image-latest (Image API) and save
4
- * it for use in slides.
5
- *
6
- * Requires OPENAI_API_KEY environment variable.
7
- *
8
- * Usage:
9
- * node scripts/generate-image.mjs --prompt "a bridge icon" --name bridge-icon
10
- * node scripts/generate-image.mjs --prompt "..." --name hero --size 1536x1024
11
- * node scripts/generate-image.mjs --prompt "..." --name icon --quality high
12
- * node scripts/generate-image.mjs --prompt "..." --name x --model gpt-image-1.5
13
- */
14
- import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'
15
- import path from 'path'
16
-
17
- const root = process.cwd()
18
- const args = process.argv.slice(2)
19
- const arg = (name, fb) => {
20
- const i = args.indexOf(`--${name}`)
21
- return i !== -1 && args[i + 1] ? args[i + 1] : fb
22
- }
23
-
24
- const outputDir = arg('out-dir', path.join('src', 'data', 'generated'))
25
- const imagesDir = path.resolve(root, outputDir)
26
- const envPath = path.join(root, '.env')
27
-
28
- if (existsSync(envPath)) {
29
- for (const line of readFileSync(envPath, 'utf-8').split(/\r?\n/)) {
30
- const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/)
31
- if (m) process.env[m[1]] = m[2].trim()
32
- }
33
- }
34
-
35
- const PROMPT = arg('prompt', null)
36
- const NAME = arg('name', null)
37
- const SIZE = arg('size', '1024x1024')
38
- const QUALITY = arg('quality', 'auto')
39
- const MODEL = arg('model', 'chatgpt-image-latest')
40
-
41
- const API_KEY = process.env.OPENAI_API_KEY
42
- if (!API_KEY) {
43
- console.error('❌ Set OPENAI_API_KEY environment variable')
44
- process.exit(1)
45
- }
46
- if (!PROMPT) {
47
- console.error('❌ --prompt is required')
48
- process.exit(1)
49
- }
50
- if (!NAME) {
51
- console.error('❌ --name is required (used as filename)')
52
- process.exit(1)
53
- }
54
-
55
- async function main() {
56
- mkdirSync(imagesDir, { recursive: true })
57
- console.log(`🎨 Generating image: "${PROMPT}"`)
58
- console.log(` Model: ${MODEL} Size: ${SIZE} Quality: ${QUALITY}`)
59
-
60
- const body = {
61
- model: MODEL,
62
- prompt: PROMPT,
63
- n: 1,
64
- size: SIZE,
65
- quality: QUALITY,
66
- }
67
-
68
- const res = await fetch('https://api.openai.com/v1/images/generations', {
69
- method: 'POST',
70
- headers: {
71
- 'Content-Type': 'application/json',
72
- Authorization: `Bearer ${API_KEY}`,
73
- },
74
- body: JSON.stringify(body),
75
- })
76
-
77
- if (!res.ok) {
78
- const err = await res.text()
79
- console.error(`❌ API error (${res.status}): ${err}`)
80
- process.exit(1)
81
- }
82
-
83
- const data = await res.json()
84
- const imageB64 = data.data?.[0]?.b64_json || null
85
- const imageUrl = data.data?.[0]?.url || null
86
-
87
- if (!imageB64 && !imageUrl) {
88
- console.error('❌ Could not extract image from response.')
89
- console.error(JSON.stringify(data).slice(0, 2000))
90
- process.exit(1)
91
- }
92
-
93
- const filepath = path.join(imagesDir, `${NAME}.png`)
94
- let imgBuffer
95
- if (imageB64) {
96
- imgBuffer = Buffer.from(imageB64, 'base64')
97
- } else {
98
- const dl = await fetch(imageUrl)
99
- imgBuffer = Buffer.from(await dl.arrayBuffer())
100
- }
101
- writeFileSync(filepath, imgBuffer)
102
- console.log(`📸 Saved: ${path.relative(root, filepath)}`)
103
- console.log(`\n📦 Import in a slide:`)
104
- console.log(` import ${NAME}Img from '../../data/generated/${NAME}.png'`)
105
- }
106
-
107
- main().catch(err => {
108
- console.error('❌', err?.message || err)
109
- process.exit(1)
110
- })
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate an image using OpenAI chatgpt-image-latest (Image API) and save
4
+ * it for use in slides.
5
+ *
6
+ * Requires OPENAI_API_KEY environment variable.
7
+ *
8
+ * Usage:
9
+ * node scripts/generate-image.mjs --prompt "a bridge icon" --name bridge-icon
10
+ * node scripts/generate-image.mjs --prompt "..." --name hero --size 1536x1024
11
+ * node scripts/generate-image.mjs --prompt "..." --name icon --quality high
12
+ * node scripts/generate-image.mjs --prompt "..." --name x --model gpt-image-1.5
13
+ */
14
+ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'
15
+ import path from 'path'
16
+
17
+ const root = process.cwd()
18
+ const args = process.argv.slice(2)
19
+ const arg = (name, fb) => {
20
+ const i = args.indexOf(`--${name}`)
21
+ return i !== -1 && args[i + 1] ? args[i + 1] : fb
22
+ }
23
+
24
+ const outputDir = arg('out-dir', path.join('src', 'data', 'generated'))
25
+ const imagesDir = path.resolve(root, outputDir)
26
+ const envPath = path.join(root, '.env')
27
+
28
+ if (existsSync(envPath)) {
29
+ for (const line of readFileSync(envPath, 'utf-8').split(/\r?\n/)) {
30
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/)
31
+ if (m) process.env[m[1]] = m[2].trim()
32
+ }
33
+ }
34
+
35
+ const PROMPT = arg('prompt', null)
36
+ const NAME = arg('name', null)
37
+ const SIZE = arg('size', '1024x1024')
38
+ const QUALITY = arg('quality', 'auto')
39
+ const MODEL = arg('model', 'chatgpt-image-latest')
40
+
41
+ const API_KEY = process.env.OPENAI_API_KEY
42
+ if (!API_KEY) {
43
+ console.error('❌ Set OPENAI_API_KEY environment variable')
44
+ process.exit(1)
45
+ }
46
+ if (!PROMPT) {
47
+ console.error('❌ --prompt is required')
48
+ process.exit(1)
49
+ }
50
+ if (!NAME) {
51
+ console.error('❌ --name is required (used as filename)')
52
+ process.exit(1)
53
+ }
54
+
55
+ async function main() {
56
+ mkdirSync(imagesDir, { recursive: true })
57
+ console.log(`🎨 Generating image: "${PROMPT}"`)
58
+ console.log(` Model: ${MODEL} Size: ${SIZE} Quality: ${QUALITY}`)
59
+
60
+ const body = {
61
+ model: MODEL,
62
+ prompt: PROMPT,
63
+ n: 1,
64
+ size: SIZE,
65
+ quality: QUALITY,
66
+ }
67
+
68
+ const res = await fetch('https://api.openai.com/v1/images/generations', {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ Authorization: `Bearer ${API_KEY}`,
73
+ },
74
+ body: JSON.stringify(body),
75
+ })
76
+
77
+ if (!res.ok) {
78
+ const err = await res.text()
79
+ console.error(`❌ API error (${res.status}): ${err}`)
80
+ process.exit(1)
81
+ }
82
+
83
+ const data = await res.json()
84
+ const imageB64 = data.data?.[0]?.b64_json || null
85
+ const imageUrl = data.data?.[0]?.url || null
86
+
87
+ if (!imageB64 && !imageUrl) {
88
+ console.error('❌ Could not extract image from response.')
89
+ console.error(JSON.stringify(data).slice(0, 2000))
90
+ process.exit(1)
91
+ }
92
+
93
+ const filepath = path.join(imagesDir, `${NAME}.png`)
94
+ let imgBuffer
95
+ if (imageB64) {
96
+ imgBuffer = Buffer.from(imageB64, 'base64')
97
+ } else {
98
+ const dl = await fetch(imageUrl)
99
+ imgBuffer = Buffer.from(await dl.arrayBuffer())
100
+ }
101
+ writeFileSync(filepath, imgBuffer)
102
+ console.log(`📸 Saved: ${path.relative(root, filepath)}`)
103
+ console.log(`\n📦 Import in a slide:`)
104
+ console.log(` import ${NAME}Img from '../../data/generated/${NAME}.png'`)
105
+ }
106
+
107
+ main().catch(err => {
108
+ console.error('❌', err?.message || err)
109
+ process.exit(1)
110
+ })
@@ -1,188 +1,214 @@
1
- #!/usr/bin/env node
2
- /**
3
- * deck-engine init — provisions Copilot skills and state into a deck project.
4
- *
5
- * Copies .github/skills/ from the engine package and bootstraps
6
- * .github/memory/state.md with project metadata from deck.config.js.
7
- *
8
- * Usage:
9
- * node node_modules/@deckio/deck-engine/scripts/init-project.mjs
10
- * npx deck-init (if bin is configured)
11
- *
12
- * Idempotent — safe to re-run. Updates skills, preserves state.
13
- */
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, copyFileSync, rmSync } from 'fs'
15
- import { join, dirname } from 'path'
16
- import { fileURLToPath } from 'url'
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url))
19
- const engineRoot = join(__dirname, '..')
20
- const projectRoot = process.cwd()
21
-
22
- // ── Discover project metadata from deck.config.js ──
23
-
24
- function readProjectMeta() {
25
- const configPath = join(projectRoot, 'deck.config.js')
26
- if (!existsSync(configPath)) {
27
- console.error('❌ No deck.config.js found in', projectRoot)
28
- process.exit(1)
29
- }
30
- const content = readFileSync(configPath, 'utf-8')
31
- const str = (key) => {
32
- const m = content.match(new RegExp(`${key}:\\s*['"\`]([^'"\`]+)['"\`]`))
33
- return m ? m[1] : null
34
- }
35
- return {
36
- id: str('id') || 'unknown',
37
- title: str('title') || 'Deck Project',
38
- }
39
- }
40
-
41
- // ── Copy skills ──
42
-
43
- function copySkills() {
44
- const srcSkills = join(engineRoot, 'skills')
45
- if (!existsSync(srcSkills)) {
46
- console.warn('⚠️ No skills directory in engine package')
47
- return 0
48
- }
49
-
50
- const destSkills = join(projectRoot, '.github', 'skills')
51
- let count = 0
52
-
53
- // Collect engine skill names to detect stale skills
54
- const engineSkillNames = new Set()
55
- for (const entry of readdirSync(srcSkills, { withFileTypes: true })) {
56
- if (!entry.isDirectory()) continue
57
- const skillFile = join(srcSkills, entry.name, 'SKILL.md')
58
- if (!existsSync(skillFile)) continue
59
- engineSkillNames.add(entry.name)
60
-
61
- const destDir = join(destSkills, entry.name)
62
- mkdirSync(destDir, { recursive: true })
63
- copyFileSync(skillFile, join(destDir, 'SKILL.md'))
64
- count++
65
- }
66
-
67
- // Remove stale skills no longer in the engine
68
- if (existsSync(destSkills)) {
69
- for (const entry of readdirSync(destSkills, { withFileTypes: true })) {
70
- if (!entry.isDirectory()) continue
71
- if (!engineSkillNames.has(entry.name)) {
72
- rmSync(join(destSkills, entry.name), { recursive: true, force: true })
73
- console.log(` Removed stale skill: ${entry.name}`)
74
- }
75
- }
76
- }
77
-
78
- return count
79
- }
80
-
81
- // ── Copy instructions ──
82
-
83
- function copyInstructions() {
84
- const srcInstructions = join(engineRoot, 'instructions')
85
- if (!existsSync(srcInstructions)) {
86
- console.warn('⚠️ No instructions directory in engine package')
87
- return 0
88
- }
89
-
90
- const destInstructions = join(projectRoot, '.github', 'instructions')
91
- mkdirSync(destInstructions, { recursive: true })
92
- let count = 0
93
-
94
- // Collect engine instruction names to detect stale instructions
95
- const engineInstrNames = new Set()
96
- for (const entry of readdirSync(srcInstructions, { withFileTypes: true })) {
97
- if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
98
- engineInstrNames.add(entry.name)
99
- copyFileSync(
100
- join(srcInstructions, entry.name),
101
- join(destInstructions, entry.name)
102
- )
103
- count++
104
- }
105
-
106
- // Remove stale instructions no longer in the engine
107
- for (const entry of readdirSync(destInstructions, { withFileTypes: true })) {
108
- if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
109
- if (!engineInstrNames.has(entry.name)) {
110
- rmSync(join(destInstructions, entry.name), { force: true })
111
- console.log(` Removed stale instruction: ${entry.name}`)
112
- }
113
- }
114
-
115
- return count
116
- }
117
-
118
- // ── Copy AGENTS.md ──
119
-
120
- function copyAgentsMd() {
121
- const src = join(engineRoot, 'instructions', 'AGENTS.md')
122
- if (!existsSync(src)) return false
123
- copyFileSync(src, join(projectRoot, 'AGENTS.md'))
124
- return true
125
- }
126
-
127
- // ── Bootstrap state.md ──
128
-
129
- function bootstrapState(meta) {
130
- const stateDir = join(projectRoot, '.github', 'memory')
131
- const statePath = join(stateDir, 'state.md')
132
-
133
- // Don't overwrite existing state (preserves user's port/url config)
134
- if (existsSync(statePath)) {
135
- console.log(' state.md already exists — preserved')
136
- return
137
- }
138
-
139
- mkdirSync(stateDir, { recursive: true })
140
- const content = `# Deck State
141
-
142
- ## Project
143
- id: ${meta.id}
144
- title: ${meta.title}
145
-
146
- ## Dev Server
147
- port: 5173
148
- url: http://localhost:5173/
149
- `
150
- writeFileSync(statePath, content, 'utf-8')
151
- console.log(' Created .github/memory/state.md')
152
- }
153
-
154
- // ── Create eyes directory ──
155
-
156
- function createEyesDir() {
157
- const eyesDir = join(projectRoot, '.github', 'eyes')
158
- mkdirSync(eyesDir, { recursive: true })
159
-
160
- // Add to .gitignore if not already there
161
- const gitignorePath = join(projectRoot, '.gitignore')
162
- if (existsSync(gitignorePath)) {
163
- const gitignore = readFileSync(gitignorePath, 'utf-8')
164
- if (!gitignore.includes('.github/eyes')) {
165
- writeFileSync(gitignorePath, gitignore.trimEnd() + '\n.github/eyes/\n', 'utf-8')
166
- console.log(' Added .github/eyes/ to .gitignore')
167
- }
168
- }
169
- }
170
-
171
- // ── Main ──
172
-
173
- const meta = readProjectMeta()
174
- console.log(`\n🎯 Initializing deck project: ${meta.title} (${meta.id})`)
175
-
176
- const skillCount = copySkills()
177
- console.log(` Copied ${skillCount} Copilot skills to .github/skills/`)
178
-
179
- const instrCount = copyInstructions()
180
- console.log(` Copied ${instrCount} Copilot instructions to .github/instructions/`)
181
-
182
- const agentsCopied = copyAgentsMd()
183
- if (agentsCopied) console.log(' Copied AGENTS.md to project root')
184
-
185
- bootstrapState(meta)
186
- createEyesDir()
187
-
188
- console.log(`\n✅ Done. Run \`copilot --yolo\` to start editing with Copilot skills.\n`)
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deck-engine init — provisions Copilot skills and state into a deck project.
4
+ *
5
+ * Copies .github/skills/ from the engine package and bootstraps
6
+ * .github/memory/state.md with project metadata from deck.config.js.
7
+ *
8
+ * Usage:
9
+ * node node_modules/@deckio/deck-engine/scripts/init-project.mjs
10
+ * npx deck-init (if bin is configured)
11
+ *
12
+ * Idempotent — safe to re-run. Updates skills, preserves state.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, copyFileSync, rmSync } from 'fs'
15
+ import { join, dirname } from 'path'
16
+ import { fileURLToPath } from 'url'
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url))
19
+ const engineRoot = join(__dirname, '..')
20
+ const projectRoot = process.cwd()
21
+
22
+ // ── Discover project metadata from deck.config.js ──
23
+
24
+ function readProjectMeta() {
25
+ const configPath = join(projectRoot, 'deck.config.js')
26
+ if (!existsSync(configPath)) {
27
+ console.error('❌ No deck.config.js found in', projectRoot)
28
+ process.exit(1)
29
+ }
30
+ const content = readFileSync(configPath, 'utf-8')
31
+ const str = (key) => {
32
+ const m = content.match(new RegExp(`${key}:\\s*['"\`]([^'"\`]+)['"\`]`))
33
+ return m ? m[1] : null
34
+ }
35
+ return {
36
+ id: str('id') || 'unknown',
37
+ title: str('title') || 'Deck Project',
38
+ }
39
+ }
40
+
41
+ // ── Copy skills ──
42
+
43
+ function copySkills() {
44
+ const srcSkills = join(engineRoot, 'skills')
45
+ if (!existsSync(srcSkills)) {
46
+ console.warn('⚠️ No skills directory in engine package')
47
+ return 0
48
+ }
49
+
50
+ const destSkills = join(projectRoot, '.github', 'skills')
51
+ let count = 0
52
+
53
+ // Collect engine skill names to detect stale skills
54
+ const engineSkillNames = new Set()
55
+ for (const entry of readdirSync(srcSkills, { withFileTypes: true })) {
56
+ if (!entry.isDirectory()) continue
57
+ const skillFile = join(srcSkills, entry.name, 'SKILL.md')
58
+ if (!existsSync(skillFile)) continue
59
+ engineSkillNames.add(entry.name)
60
+
61
+ const destDir = join(destSkills, entry.name)
62
+ mkdirSync(destDir, { recursive: true })
63
+ copyFileSync(skillFile, join(destDir, 'SKILL.md'))
64
+ count++
65
+ }
66
+
67
+ // Remove stale skills no longer in the engine
68
+ if (existsSync(destSkills)) {
69
+ for (const entry of readdirSync(destSkills, { withFileTypes: true })) {
70
+ if (!entry.isDirectory()) continue
71
+ if (!engineSkillNames.has(entry.name)) {
72
+ rmSync(join(destSkills, entry.name), { recursive: true, force: true })
73
+ console.log(` Removed stale skill: ${entry.name}`)
74
+ }
75
+ }
76
+ }
77
+
78
+ return count
79
+ }
80
+
81
+ // ── Copy instructions ──
82
+
83
+ function copyInstructions() {
84
+ const srcInstructions = join(engineRoot, 'instructions')
85
+ if (!existsSync(srcInstructions)) {
86
+ console.warn('⚠️ No instructions directory in engine package')
87
+ return 0
88
+ }
89
+
90
+ const destInstructions = join(projectRoot, '.github', 'instructions')
91
+ mkdirSync(destInstructions, { recursive: true })
92
+ let count = 0
93
+
94
+ // Collect engine instruction names to detect stale instructions
95
+ const engineInstrNames = new Set()
96
+ for (const entry of readdirSync(srcInstructions, { withFileTypes: true })) {
97
+ if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
98
+ engineInstrNames.add(entry.name)
99
+ copyFileSync(
100
+ join(srcInstructions, entry.name),
101
+ join(destInstructions, entry.name)
102
+ )
103
+ count++
104
+ }
105
+
106
+ // Remove stale instructions no longer in the engine
107
+ for (const entry of readdirSync(destInstructions, { withFileTypes: true })) {
108
+ if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
109
+ if (!engineInstrNames.has(entry.name)) {
110
+ rmSync(join(destInstructions, entry.name), { force: true })
111
+ console.log(` Removed stale instruction: ${entry.name}`)
112
+ }
113
+ }
114
+
115
+ return count
116
+ }
117
+
118
+ // ── Copy AGENTS.md ──
119
+
120
+ function copyAgentsMd() {
121
+ const src = join(engineRoot, 'instructions', 'AGENTS.md')
122
+ if (!existsSync(src)) return false
123
+ copyFileSync(src, join(projectRoot, 'AGENTS.md'))
124
+ return true
125
+ }
126
+
127
+ // ── Bootstrap state.md ──
128
+
129
+ function bootstrapState(meta) {
130
+ const stateDir = join(projectRoot, '.github', 'memory')
131
+ const statePath = join(stateDir, 'state.md')
132
+
133
+ // Don't overwrite existing state (preserves user's port/url config)
134
+ if (existsSync(statePath)) {
135
+ console.log(' state.md already exists — preserved')
136
+ return
137
+ }
138
+
139
+ mkdirSync(stateDir, { recursive: true })
140
+ const content = `# Deck State
141
+
142
+ ## Project
143
+ id: ${meta.id}
144
+ title: ${meta.title}
145
+
146
+ ## Dev Server
147
+ port: 5173
148
+ url: http://localhost:5173/
149
+ `
150
+ writeFileSync(statePath, content, 'utf-8')
151
+ console.log(' Created .github/memory/state.md')
152
+ }
153
+
154
+ // ── Create eyes directory ──
155
+
156
+ function createEyesDir() {
157
+ const eyesDir = join(projectRoot, '.github', 'eyes')
158
+ mkdirSync(eyesDir, { recursive: true })
159
+
160
+ // Add to .gitignore if not already there
161
+ const gitignorePath = join(projectRoot, '.gitignore')
162
+ if (existsSync(gitignorePath)) {
163
+ const gitignore = readFileSync(gitignorePath, 'utf-8')
164
+ if (!gitignore.includes('.github/eyes')) {
165
+ writeFileSync(gitignorePath, gitignore.trimEnd() + '\n.github/eyes/\n', 'utf-8')
166
+ console.log(' Added .github/eyes/ to .gitignore')
167
+ }
168
+ }
169
+ }
170
+
171
+ function ensureVSCodeSettings() {
172
+ const vscodeDir = join(projectRoot, '.vscode')
173
+ const settingsPath = join(vscodeDir, 'settings.json')
174
+
175
+ mkdirSync(vscodeDir, { recursive: true })
176
+
177
+ let settings = {}
178
+ if (existsSync(settingsPath)) {
179
+ try {
180
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
181
+ } catch {
182
+ console.warn('⚠️ Could not parse .vscode/settings.json — leaving it unchanged')
183
+ return
184
+ }
185
+ }
186
+
187
+ if (settings['simpleBrowser.useIntegratedBrowser'] === true) {
188
+ return
189
+ }
190
+
191
+ settings['simpleBrowser.useIntegratedBrowser'] = true
192
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
193
+ console.log(' Ensured .vscode/settings.json uses the integrated browser')
194
+ }
195
+
196
+ // ── Main ──
197
+
198
+ const meta = readProjectMeta()
199
+ console.log(`\n🎯 Initializing deck project: ${meta.title} (${meta.id})`)
200
+
201
+ const skillCount = copySkills()
202
+ console.log(` Copied ${skillCount} Copilot skills to .github/skills/`)
203
+
204
+ const instrCount = copyInstructions()
205
+ console.log(` Copied ${instrCount} Copilot instructions to .github/instructions/`)
206
+
207
+ const agentsCopied = copyAgentsMd()
208
+ if (agentsCopied) console.log(' Copied AGENTS.md to project root')
209
+
210
+ bootstrapState(meta)
211
+ createEyesDir()
212
+ ensureVSCodeSettings()
213
+
214
+ console.log(`\n✅ Done. Run \`copilot --yolo\` to start editing with Copilot skills.\n`)