emberflow-skills 1.11.0 → 1.13.0
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/README.md +31 -9
- package/bin/install.js +210 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,12 +8,21 @@ Publish beautiful docs from your AI coding tools to [Emberflow](https://www.embe
|
|
|
8
8
|
npx emberflow-skills
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The installer auto-detects
|
|
11
|
+
The installer auto-detects which AI coding tools you use and installs skills in the correct format for each.
|
|
12
|
+
|
|
13
|
+
| Tool | Install location | How to invoke |
|
|
14
|
+
|------|-----------------|---------------|
|
|
15
|
+
| **Claude Code** | `.claude/skills/` | `/ember-publish [topic]` |
|
|
16
|
+
| **Codex** | `.agents/skills/` | `$ember-publish [topic]` |
|
|
17
|
+
| **Cursor** | `.cursor/rules/*.mdc` | `@ember-publish-explainer` or ask naturally |
|
|
18
|
+
| **Windsurf** | `.windsurf/rules/*.md` | Ask naturally: "publish this to Emberflow" |
|
|
19
|
+
|
|
20
|
+
If multiple tools are detected, skills are installed for all of them.
|
|
12
21
|
|
|
13
22
|
### Options
|
|
14
23
|
|
|
15
24
|
```bash
|
|
16
|
-
# Install to current project (default)
|
|
25
|
+
# Install to current project (default — auto-detects tools)
|
|
17
26
|
npx emberflow-skills
|
|
18
27
|
|
|
19
28
|
# Install globally for Claude Code (available in all projects)
|
|
@@ -22,9 +31,13 @@ npx emberflow-skills --global
|
|
|
22
31
|
|
|
23
32
|
### What the installer does
|
|
24
33
|
|
|
25
|
-
1. Detects
|
|
26
|
-
2.
|
|
27
|
-
|
|
34
|
+
1. Detects tool configs in your project (`.claude/`, `.agents/`, `.cursor/`, `.windsurf/`)
|
|
35
|
+
2. Installs skills in each tool's native format:
|
|
36
|
+
- Claude Code & Codex: `SKILL.md` with frontmatter (native skill system)
|
|
37
|
+
- Cursor: `.mdc` rules with auto-attach descriptions
|
|
38
|
+
- Windsurf: `.md` rules
|
|
39
|
+
3. Copies reference templates for the explainer skill
|
|
40
|
+
4. Done — use the skills in your next conversation
|
|
28
41
|
|
|
29
42
|
## Skills
|
|
30
43
|
|
|
@@ -62,7 +75,7 @@ Publish CSV files as an interactive dataset viewer with virtual scroll (handles
|
|
|
62
75
|
|
|
63
76
|
### `/ember-publish-explainer`
|
|
64
77
|
|
|
65
|
-
Generate interactive visual explainers — the AI chooses the best visualization type (
|
|
78
|
+
Generate interactive visual explainers — the AI chooses the best visualization type (funnel, heatmap, radar chart, waterfall, timeline, architecture diagram, etc.) for the topic. Slide-based with animated transitions.
|
|
66
79
|
|
|
67
80
|
```
|
|
68
81
|
/ember-publish-explainer how our CI/CD pipeline works
|
|
@@ -95,12 +108,21 @@ If you prefer not to use npx:
|
|
|
95
108
|
|
|
96
109
|
```bash
|
|
97
110
|
git clone https://github.com/pmccurley87/emberflow-skills.git
|
|
111
|
+
|
|
112
|
+
# Claude Code
|
|
98
113
|
cp -r emberflow-skills/skills/* .claude/skills/
|
|
114
|
+
|
|
115
|
+
# Codex — same SKILL.md format
|
|
116
|
+
cp -r emberflow-skills/skills/* .agents/skills/
|
|
117
|
+
|
|
118
|
+
# Cursor — copy as .mdc rules (or use npx for auto-conversion)
|
|
119
|
+
# Windsurf — copy to .windsurf/rules/
|
|
99
120
|
```
|
|
100
121
|
|
|
101
122
|
## Works with
|
|
102
123
|
|
|
103
|
-
- **Claude Code** —
|
|
104
|
-
- **
|
|
105
|
-
- **
|
|
124
|
+
- **Claude Code** — native skill system, invoke with `/ember-publish`
|
|
125
|
+
- **Codex** — native skill system, invoke with `$ember-publish`
|
|
126
|
+
- **Cursor** — auto-attached rules, invoke with `@ember-publish-explainer` or ask naturally
|
|
127
|
+
- **Windsurf** — rules loaded automatically, ask naturally
|
|
106
128
|
- **Any MCP-compatible tool** — Emberflow also provides an MCP server
|
package/bin/install.js
CHANGED
|
@@ -18,13 +18,39 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
|
18
18
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
19
19
|
const orange = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
// ── Tool definitions ──
|
|
22
|
+
|
|
23
|
+
const TOOLS = [
|
|
24
|
+
{
|
|
25
|
+
type: 'claude',
|
|
26
|
+
detect: ['.claude'],
|
|
27
|
+
projectDir: '.claude/skills',
|
|
28
|
+
globalDir: path.join(os.homedir(), '.claude', 'skills'),
|
|
29
|
+
label: 'Claude Code',
|
|
30
|
+
usage: '/ember-publish',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'cursor',
|
|
34
|
+
detect: ['.cursor', '.cursorrules'],
|
|
35
|
+
projectDir: '.cursor/rules',
|
|
36
|
+
label: 'Cursor',
|
|
37
|
+
usage: '"publish this to Emberflow"',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'windsurf',
|
|
41
|
+
detect: ['.windsurf', '.windsurfrules'],
|
|
42
|
+
projectDir: '.windsurf/rules',
|
|
43
|
+
label: 'Windsurf',
|
|
44
|
+
usage: '"publish this to Emberflow"',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'codex',
|
|
48
|
+
detect: ['.agents', 'AGENTS.md'],
|
|
49
|
+
projectDir: '.agents/skills',
|
|
50
|
+
globalDir: path.join(os.homedir(), '.agents', 'skills'),
|
|
51
|
+
label: 'Codex',
|
|
52
|
+
usage: '$ember-publish',
|
|
53
|
+
},
|
|
28
54
|
];
|
|
29
55
|
|
|
30
56
|
const args = process.argv.slice(2);
|
|
@@ -77,7 +103,22 @@ function sleep(ms) {
|
|
|
77
103
|
return new Promise((r) => setTimeout(r, ms));
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
// ──
|
|
106
|
+
// ── SKILL.md parsing ──
|
|
107
|
+
|
|
108
|
+
function parseFrontmatter(content) {
|
|
109
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
110
|
+
if (!match) return { meta: {}, body: content };
|
|
111
|
+
const meta = {};
|
|
112
|
+
for (const line of match[1].split('\n')) {
|
|
113
|
+
const idx = line.indexOf(':');
|
|
114
|
+
if (idx > 0) {
|
|
115
|
+
meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { meta, body: match[2] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── File helpers ──
|
|
81
122
|
|
|
82
123
|
function copyDirRecursive(src, dest) {
|
|
83
124
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -92,25 +133,138 @@ function copyDirRecursive(src, dest) {
|
|
|
92
133
|
}
|
|
93
134
|
}
|
|
94
135
|
|
|
95
|
-
function
|
|
136
|
+
function rewriteTemplatePaths(body, templatesRelPath) {
|
|
137
|
+
// Rewrite "Read templates/" to use the full relative path from project root
|
|
138
|
+
return body.replace(
|
|
139
|
+
/Read templates\//g,
|
|
140
|
+
`Read ${templatesRelPath}/`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Installers per tool type ──
|
|
145
|
+
|
|
146
|
+
function installClaude(name, destDir) {
|
|
147
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
148
|
+
const skillDir = path.join(destDir, name);
|
|
149
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
150
|
+
fs.copyFileSync(path.join(srcDir, 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
|
|
151
|
+
|
|
152
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
153
|
+
if (fs.existsSync(templatesDir)) {
|
|
154
|
+
copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function installCursor(name, destDir, cwd) {
|
|
159
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
160
|
+
const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
161
|
+
const { meta, body } = parseFrontmatter(skillMd);
|
|
162
|
+
|
|
163
|
+
// Copy templates to .cursor/rules/<name>/templates/
|
|
164
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
165
|
+
const hasTemplates = fs.existsSync(templatesDir);
|
|
166
|
+
if (hasTemplates) {
|
|
167
|
+
copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Rewrite template paths relative to project root
|
|
171
|
+
let content = body;
|
|
172
|
+
if (hasTemplates) {
|
|
173
|
+
const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
|
|
174
|
+
content = rewriteTemplatePaths(content, relPath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Write .mdc file with Cursor frontmatter
|
|
178
|
+
const description = meta.description || name;
|
|
179
|
+
const hint = meta['argument-hint'] ? ` — ${meta['argument-hint']}` : '';
|
|
180
|
+
const mdc = `---
|
|
181
|
+
description: ${description}${hint}
|
|
182
|
+
globs:
|
|
183
|
+
alwaysApply: false
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
${content}`;
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
189
|
+
fs.writeFileSync(path.join(destDir, `${name}.mdc`), mdc);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function installWindsurf(name, destDir, cwd) {
|
|
193
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
194
|
+
const skillMd = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
|
|
195
|
+
const { body } = parseFrontmatter(skillMd);
|
|
196
|
+
|
|
197
|
+
// Copy templates
|
|
198
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
199
|
+
const hasTemplates = fs.existsSync(templatesDir);
|
|
200
|
+
if (hasTemplates) {
|
|
201
|
+
copyDirRecursive(templatesDir, path.join(destDir, name, 'templates'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Rewrite template paths
|
|
205
|
+
let content = body;
|
|
206
|
+
if (hasTemplates) {
|
|
207
|
+
const relPath = path.relative(cwd, path.join(destDir, name, 'templates'));
|
|
208
|
+
content = rewriteTemplatePaths(content, relPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
212
|
+
fs.writeFileSync(path.join(destDir, `${name}.md`), content);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function installCodex(name, destDir, cwd) {
|
|
216
|
+
// Codex uses the same SKILL.md format as Claude Code, discovered from .agents/skills/<name>/
|
|
217
|
+
const srcDir = path.join(SKILLS_DIR, name);
|
|
218
|
+
const skillDir = path.join(destDir, name);
|
|
219
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
220
|
+
fs.copyFileSync(path.join(srcDir, 'SKILL.md'), path.join(skillDir, 'SKILL.md'));
|
|
221
|
+
|
|
222
|
+
// Copy templates
|
|
223
|
+
const templatesDir = path.join(srcDir, 'templates');
|
|
224
|
+
if (fs.existsSync(templatesDir)) {
|
|
225
|
+
copyDirRecursive(templatesDir, path.join(skillDir, 'templates'));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Main install function ──
|
|
230
|
+
|
|
231
|
+
function install(destDir, tool, cwd) {
|
|
96
232
|
for (const name of SKILL_NAMES) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
233
|
+
switch (tool.type) {
|
|
234
|
+
case 'claude':
|
|
235
|
+
installClaude(name, destDir);
|
|
236
|
+
break;
|
|
237
|
+
case 'cursor':
|
|
238
|
+
installCursor(name, destDir, cwd);
|
|
239
|
+
break;
|
|
240
|
+
case 'windsurf':
|
|
241
|
+
installWindsurf(name, destDir, cwd);
|
|
242
|
+
break;
|
|
243
|
+
case 'codex':
|
|
244
|
+
installCodex(name, destDir, cwd);
|
|
245
|
+
break;
|
|
107
246
|
}
|
|
108
|
-
|
|
109
|
-
console.log(` ${green('✓')}
|
|
247
|
+
const relDest = path.relative(cwd, destDir) || destDir;
|
|
248
|
+
console.log(` ${green('✓')} ${name} → ${dim(relDest)} ${dim(`(${tool.label})`)}`);
|
|
110
249
|
}
|
|
111
250
|
return true;
|
|
112
251
|
}
|
|
113
252
|
|
|
253
|
+
// ── Detection ──
|
|
254
|
+
|
|
255
|
+
function detectTools(cwd) {
|
|
256
|
+
const detected = [];
|
|
257
|
+
for (const tool of TOOLS) {
|
|
258
|
+
for (const marker of tool.detect) {
|
|
259
|
+
if (fs.existsSync(path.join(cwd, marker))) {
|
|
260
|
+
detected.push(tool);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return detected;
|
|
266
|
+
}
|
|
267
|
+
|
|
114
268
|
// ── Auth flow ──
|
|
115
269
|
|
|
116
270
|
function hasValidToken() {
|
|
@@ -128,7 +282,6 @@ async function authenticate() {
|
|
|
128
282
|
console.log(` ${dim('Your published docs will be attributed to your account.')}`);
|
|
129
283
|
console.log();
|
|
130
284
|
|
|
131
|
-
// Request device code
|
|
132
285
|
let resp;
|
|
133
286
|
try {
|
|
134
287
|
resp = await request('POST', `${EMBERFLOW_URL}/api/device-code`);
|
|
@@ -151,7 +304,6 @@ async function authenticate() {
|
|
|
151
304
|
console.log(` Your code: ${bold(code)}`);
|
|
152
305
|
console.log();
|
|
153
306
|
|
|
154
|
-
// Try to open the URL automatically
|
|
155
307
|
try {
|
|
156
308
|
const { exec } = require('child_process');
|
|
157
309
|
if (process.platform === 'win32') {
|
|
@@ -165,8 +317,7 @@ async function authenticate() {
|
|
|
165
317
|
|
|
166
318
|
process.stdout.write(` ${dim('Waiting for approval...')}`);
|
|
167
319
|
|
|
168
|
-
|
|
169
|
-
const maxAttempts = 60; // 3 minutes at 3s intervals
|
|
320
|
+
const maxAttempts = 60;
|
|
170
321
|
for (let i = 0; i < maxAttempts; i++) {
|
|
171
322
|
await sleep(3000);
|
|
172
323
|
|
|
@@ -174,7 +325,6 @@ async function authenticate() {
|
|
|
174
325
|
const status = await request('GET', `${EMBERFLOW_URL}/api/device-code/${code}`);
|
|
175
326
|
|
|
176
327
|
if (status.data.status === 'approved' && status.data.session_token) {
|
|
177
|
-
// Strip cookie name prefix if present (e.g. "__Secure-better-auth.session_token=VALUE" -> "VALUE")
|
|
178
328
|
const raw = status.data.session_token.replace(/^(?:__Secure-)?better-auth\.session_token=/, '');
|
|
179
329
|
fs.mkdirSync(path.dirname(TOKEN_PATH), { recursive: true });
|
|
180
330
|
fs.writeFileSync(TOKEN_PATH, JSON.stringify({ token: raw }, null, 2));
|
|
@@ -194,7 +344,6 @@ async function authenticate() {
|
|
|
194
344
|
// Network error, keep polling
|
|
195
345
|
}
|
|
196
346
|
|
|
197
|
-
// Spinner
|
|
198
347
|
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
199
348
|
process.stdout.clearLine(0);
|
|
200
349
|
process.stdout.cursorTo(0);
|
|
@@ -217,38 +366,54 @@ async function main() {
|
|
|
217
366
|
console.log();
|
|
218
367
|
|
|
219
368
|
if (!authOnly) {
|
|
369
|
+
const cwd = process.cwd();
|
|
220
370
|
let installed = 0;
|
|
221
371
|
|
|
222
372
|
if (isGlobal) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const cwd = process.cwd();
|
|
229
|
-
const detected = [];
|
|
230
|
-
|
|
231
|
-
for (const t of targets) {
|
|
232
|
-
const parent = path.dirname(path.join(cwd, t.dir));
|
|
233
|
-
if (fs.existsSync(path.join(cwd, t.dir)) || fs.existsSync(parent)) {
|
|
234
|
-
detected.push(t);
|
|
373
|
+
// Global install — Claude Code + Codex (both support global skills)
|
|
374
|
+
for (const tool of TOOLS) {
|
|
375
|
+
if (tool.globalDir) {
|
|
376
|
+
install(tool.globalDir, tool, cwd);
|
|
377
|
+
installed++;
|
|
235
378
|
}
|
|
236
379
|
}
|
|
380
|
+
} else {
|
|
381
|
+
const detected = detectTools(cwd);
|
|
237
382
|
|
|
238
383
|
if (detected.length === 0) {
|
|
239
|
-
|
|
384
|
+
// Default to Claude Code
|
|
385
|
+
detected.push(TOOLS[0]);
|
|
386
|
+
console.log(` ${dim('No tool config detected — defaulting to Claude Code')}`);
|
|
387
|
+
console.log();
|
|
388
|
+
} else {
|
|
389
|
+
console.log(` ${dim(`Detected: ${detected.map(t => t.label).join(', ')}`)}`);
|
|
390
|
+
console.log();
|
|
240
391
|
}
|
|
241
392
|
|
|
242
|
-
for (const
|
|
243
|
-
|
|
393
|
+
for (const tool of detected) {
|
|
394
|
+
const destDir = path.join(cwd, tool.projectDir);
|
|
395
|
+
install(destDir, tool, cwd);
|
|
244
396
|
installed++;
|
|
397
|
+
console.log();
|
|
245
398
|
}
|
|
246
399
|
}
|
|
247
400
|
|
|
248
401
|
if (installed > 0) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
402
|
+
const tools = isGlobal ? TOOLS.filter(t => t.globalDir) : (detectTools(cwd).length > 0 ? detectTools(cwd) : [TOOLS[0]]);
|
|
403
|
+
|
|
404
|
+
for (const tool of tools) {
|
|
405
|
+
if (tool.type === 'claude') {
|
|
406
|
+
console.log(` ${bold('Claude Code:')} ${cyan('/ember-publish')} ${dim('[topic]')} — auto-picks format`);
|
|
407
|
+
console.log(` ${cyan('/ember-publish-doc')} ${cyan('/ember-publish-json')} ${cyan('/ember-publish-explainer')} ${cyan('/ember-publish-space')}`);
|
|
408
|
+
} else if (tool.type === 'codex') {
|
|
409
|
+
console.log(` ${bold('Codex:')} ${cyan('$ember-publish')} ${dim('[topic]')} — invoke skills with $ prefix`);
|
|
410
|
+
console.log(` ${cyan('$ember-publish-doc')} ${cyan('$ember-publish-json')} ${cyan('$ember-publish-explainer')} ${cyan('$ember-publish-space')}`);
|
|
411
|
+
} else if (tool.type === 'cursor') {
|
|
412
|
+
console.log(` ${bold('Cursor:')} Type ${cyan('@ember-publish-explainer')} or ask ${cyan('"publish this to Emberflow"')}`);
|
|
413
|
+
} else if (tool.type === 'windsurf') {
|
|
414
|
+
console.log(` ${bold('Windsurf:')} Ask ${cyan('"publish this to Emberflow"')} or ${cyan('"create an Emberflow explainer"')}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
252
417
|
}
|
|
253
418
|
}
|
|
254
419
|
|