learn-skill.md 1.0.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 +64 -0
- package/bin/cli.mjs +99 -0
- package/package.json +40 -0
- package/src/agents.mjs +34 -0
- package/src/api.mjs +27 -0
- package/src/install.mjs +99 -0
- package/src/prompt.mjs +54 -0
- package/src/search.mjs +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# learn-skill
|
|
2
|
+
|
|
3
|
+
Install AI agent skills from [learn-skill.md](https://learn-skill.md) — the skill marketplace for AI agents.
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx learn-skill add <slug>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# install a skill (interactive agent picker)
|
|
15
|
+
npx learn-skill add my-skill
|
|
16
|
+
|
|
17
|
+
# install to a specific agent
|
|
18
|
+
npx learn-skill add my-skill -a claude
|
|
19
|
+
npx learn-skill add my-skill -a cursor
|
|
20
|
+
|
|
21
|
+
# skip confirmation
|
|
22
|
+
npx learn-skill add my-skill -a claude -y
|
|
23
|
+
|
|
24
|
+
# search for skills
|
|
25
|
+
npx learn-skill search "unit test"
|
|
26
|
+
|
|
27
|
+
# list recent skills
|
|
28
|
+
npx learn-skill list
|
|
29
|
+
|
|
30
|
+
# show supported agents
|
|
31
|
+
npx learn-skill agents
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Supported Agents
|
|
35
|
+
|
|
36
|
+
| Agent | Skills Directory |
|
|
37
|
+
|-------|-----------------|
|
|
38
|
+
| Claude Code | `~/.claude/skills/` |
|
|
39
|
+
| Cursor | `~/.cursor/skills/` |
|
|
40
|
+
| Codex | `~/.codex/skills/` |
|
|
41
|
+
| Windsurf | `~/.windsurf/skills/` |
|
|
42
|
+
| Continue | `~/.continue/skills/` |
|
|
43
|
+
| OpenCode | `~/.opencode/skills/` |
|
|
44
|
+
| Amp | `~/.amp/skills/` |
|
|
45
|
+
|
|
46
|
+
The CLI auto-detects which agents are installed on your machine.
|
|
47
|
+
|
|
48
|
+
## How It Works
|
|
49
|
+
|
|
50
|
+
1. Fetches the skill from learn-skill.md
|
|
51
|
+
2. Asks which agent(s) to install to (or use `-a` flag)
|
|
52
|
+
3. Downloads the skill as `.tar.gz`
|
|
53
|
+
4. Extracts to the agent's skills directory
|
|
54
|
+
|
|
55
|
+
## Also works with
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bunx learn-skill add my-skill
|
|
59
|
+
pnpm dlx learn-skill add my-skill
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from 'node:util'
|
|
4
|
+
import { add } from '../src/install.mjs'
|
|
5
|
+
import { search, list } from '../src/search.mjs'
|
|
6
|
+
import { AGENTS, detectAgents } from '../src/agents.mjs'
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
learn-skill.md — install AI agent skills from learn-skill.md
|
|
10
|
+
|
|
11
|
+
usage:
|
|
12
|
+
learn-skill.md add <slug> install a skill
|
|
13
|
+
learn-skill.md add <slug> -a claude install to a specific agent
|
|
14
|
+
learn-skill.md search <query> search for skills
|
|
15
|
+
learn-skill.md list list recent skills
|
|
16
|
+
learn-skill.md agents show supported agents
|
|
17
|
+
|
|
18
|
+
options:
|
|
19
|
+
-a, --agent <name> target agent (claude, cursor, codex, windsurf, etc.)
|
|
20
|
+
-y, --yes skip confirmation
|
|
21
|
+
-h, --help show this help
|
|
22
|
+
-v, --version show version
|
|
23
|
+
|
|
24
|
+
examples:
|
|
25
|
+
npx learn-skill.md add my-skill
|
|
26
|
+
npx learn-skill.md add my-skill -a cursor
|
|
27
|
+
npx learn-skill.md search "unit test"
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
const { values, positionals } = parseArgs({
|
|
31
|
+
allowPositionals: true,
|
|
32
|
+
options: {
|
|
33
|
+
agent: { type: 'string', short: 'a' },
|
|
34
|
+
yes: { type: 'boolean', short: 'y', default: false },
|
|
35
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
36
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const [command, ...args] = positionals
|
|
41
|
+
|
|
42
|
+
if (values.version) {
|
|
43
|
+
console.log('learn-skill.md v1.0.0')
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (values.help || !command) {
|
|
48
|
+
console.log(HELP)
|
|
49
|
+
process.exit(0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
switch (command) {
|
|
54
|
+
case 'add':
|
|
55
|
+
case 'install': {
|
|
56
|
+
const slug = args[0]
|
|
57
|
+
if (!slug) {
|
|
58
|
+
console.error(' error: missing skill slug\n usage: learn-skill.md add <slug>')
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
await add(slug, { agent: values.agent, yes: values.yes })
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
case 'search':
|
|
65
|
+
case 'find': {
|
|
66
|
+
const query = args.join(' ')
|
|
67
|
+
if (!query) {
|
|
68
|
+
console.error(' error: missing search query\n usage: learn-skill.md search <query>')
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
await search(query)
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
case 'list':
|
|
75
|
+
case 'ls': {
|
|
76
|
+
await list()
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
case 'agents': {
|
|
80
|
+
console.log('\n supported agents:\n')
|
|
81
|
+
const detected = detectAgents()
|
|
82
|
+
for (const a of AGENTS) {
|
|
83
|
+
const installed = detected.includes(a.id) ? ' (detected)' : ''
|
|
84
|
+
console.log(` ${a.id.padEnd(14)} ${a.dir}${installed}`)
|
|
85
|
+
}
|
|
86
|
+
console.log()
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
console.error(` unknown command: ${command}`)
|
|
91
|
+
console.log(HELP)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
console.error(`\n error: ${err.message}`)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "learn-skill.md",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install AI agent skills from learn-skill.md — the skill marketplace for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"learn-skill.md": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"skills",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"cursor",
|
|
22
|
+
"codex",
|
|
23
|
+
"windsurf",
|
|
24
|
+
"continue",
|
|
25
|
+
"opencode",
|
|
26
|
+
"amp",
|
|
27
|
+
"agent",
|
|
28
|
+
"skill-installer",
|
|
29
|
+
"skill-marketplace",
|
|
30
|
+
"learn-skill",
|
|
31
|
+
"npx"
|
|
32
|
+
],
|
|
33
|
+
"author": "froogooofficial",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/froogooofficial/lear-skill.com"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://learn-skill.md"
|
|
40
|
+
}
|
package/src/agents.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const home = homedir()
|
|
6
|
+
|
|
7
|
+
export const AGENTS = [
|
|
8
|
+
{ id: 'claude', name: 'Claude Code', dir: '.claude/skills', configDir: '.claude' },
|
|
9
|
+
{ id: 'cursor', name: 'Cursor', dir: '.cursor/skills', configDir: '.cursor' },
|
|
10
|
+
{ id: 'codex', name: 'Codex', dir: '.codex/skills', configDir: '.codex' },
|
|
11
|
+
{ id: 'windsurf', name: 'Windsurf', dir: '.windsurf/skills', configDir: '.windsurf' },
|
|
12
|
+
{ id: 'continue', name: 'Continue', dir: '.continue/skills', configDir: '.continue' },
|
|
13
|
+
{ id: 'opencode', name: 'OpenCode', dir: '.opencode/skills', configDir: '.opencode' },
|
|
14
|
+
{ id: 'amp', name: 'Amp', dir: '.amp/skills', configDir: '.amp' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export function detectAgents() {
|
|
18
|
+
const found = []
|
|
19
|
+
for (const agent of AGENTS) {
|
|
20
|
+
const configPath = join(home, agent.configDir)
|
|
21
|
+
if (existsSync(configPath)) {
|
|
22
|
+
found.push(agent.id)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return found
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getAgent(id) {
|
|
29
|
+
return AGENTS.find((a) => a.id === id)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSkillDir(agent) {
|
|
33
|
+
return join(home, agent.dir)
|
|
34
|
+
}
|
package/src/api.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const BASE_URL = 'https://learn-skill.md'
|
|
2
|
+
|
|
3
|
+
export async function get(path) {
|
|
4
|
+
const url = `${BASE_URL}${path}`
|
|
5
|
+
const res = await fetch(url)
|
|
6
|
+
if (!res.ok) {
|
|
7
|
+
throw new Error(`API error: ${res.status} ${res.statusText} (${path})`)
|
|
8
|
+
}
|
|
9
|
+
return res.json()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function download(path) {
|
|
13
|
+
const url = `${BASE_URL}${path}`
|
|
14
|
+
const res = await fetch(url)
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText} (${path})`)
|
|
17
|
+
}
|
|
18
|
+
return res
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rawUrl(slug) {
|
|
22
|
+
return `${BASE_URL}/raw/${slug}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installUrl(slug) {
|
|
26
|
+
return `${BASE_URL}/install/${slug}`
|
|
27
|
+
}
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, createWriteStream } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { pipeline } from 'node:stream/promises'
|
|
6
|
+
import { Readable } from 'node:stream'
|
|
7
|
+
|
|
8
|
+
import { AGENTS, detectAgents, getAgent, getSkillDir } from './agents.mjs'
|
|
9
|
+
import { get, installUrl } from './api.mjs'
|
|
10
|
+
import { choose, chooseMultiple, confirm } from './prompt.mjs'
|
|
11
|
+
|
|
12
|
+
export async function add(slug, opts = {}) {
|
|
13
|
+
// 1. fetch skill metadata
|
|
14
|
+
console.log(`\n fetching skill: ${slug}...`)
|
|
15
|
+
const data = await get(`/api/skills/${slug}`)
|
|
16
|
+
const skill = data.skill
|
|
17
|
+
|
|
18
|
+
if (!skill) {
|
|
19
|
+
throw new Error(`skill "${slug}" not found`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(` found: ${skill.title}`)
|
|
23
|
+
if (skill.description) console.log(` ${skill.description}`)
|
|
24
|
+
console.log(` files: ${skill.file_count} | size: ${(skill.file_size / 1024).toFixed(1)}KB`)
|
|
25
|
+
|
|
26
|
+
// 2. pick agent(s)
|
|
27
|
+
let agents = []
|
|
28
|
+
|
|
29
|
+
if (opts.agent) {
|
|
30
|
+
const agent = getAgent(opts.agent)
|
|
31
|
+
if (!agent) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`unknown agent "${opts.agent}". supported: ${AGENTS.map((a) => a.id).join(', ')}`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
agents = [agent]
|
|
37
|
+
} else {
|
|
38
|
+
const detected = detectAgents()
|
|
39
|
+
const agentOptions = AGENTS.map((a) => ({
|
|
40
|
+
label: `${a.name}${detected.includes(a.id) ? ' (detected)' : ''}`,
|
|
41
|
+
value: a,
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const selected = await chooseMultiple('install to which agent(s)?', agentOptions)
|
|
45
|
+
agents = selected.map((s) => s.value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (agents.length === 0) {
|
|
49
|
+
console.log(' no agents selected, aborting.')
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. confirm
|
|
54
|
+
if (!opts.yes) {
|
|
55
|
+
console.log('\n will install to:')
|
|
56
|
+
for (const a of agents) {
|
|
57
|
+
console.log(` - ${a.name} (${getSkillDir(a)})`)
|
|
58
|
+
}
|
|
59
|
+
const ok = await confirm('proceed?')
|
|
60
|
+
if (!ok) {
|
|
61
|
+
console.log(' cancelled.')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. download tar.gz to temp
|
|
67
|
+
console.log('\n downloading...')
|
|
68
|
+
const url = installUrl(slug)
|
|
69
|
+
const res = await fetch(url)
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error(`download failed: ${res.status}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tmpFile = join(tmpdir(), `learn-skill-${slug}-${Date.now()}.tar.gz`)
|
|
75
|
+
const fileStream = createWriteStream(tmpFile)
|
|
76
|
+
await pipeline(Readable.fromWeb(res.body), fileStream)
|
|
77
|
+
|
|
78
|
+
// 5. extract to each agent's skill dir
|
|
79
|
+
for (const agent of agents) {
|
|
80
|
+
const skillDir = getSkillDir(agent)
|
|
81
|
+
mkdirSync(skillDir, { recursive: true })
|
|
82
|
+
|
|
83
|
+
console.log(` installing to ${agent.name}...`)
|
|
84
|
+
try {
|
|
85
|
+
execSync(`tar -xzf "${tmpFile}" -C "${skillDir}"`, { stdio: 'pipe' })
|
|
86
|
+
console.log(` done: ${skillDir}/${slug}/`)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(` failed to extract to ${agent.name}: ${err.message}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 6. cleanup
|
|
93
|
+
try {
|
|
94
|
+
const { unlinkSync } = await import('node:fs')
|
|
95
|
+
unlinkSync(tmpFile)
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
console.log(`\n skill "${skill.title}" installed successfully!\n`)
|
|
99
|
+
}
|
package/src/prompt.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
|
+
|
|
3
|
+
const rl = () =>
|
|
4
|
+
createInterface({ input: process.stdin, output: process.stdout })
|
|
5
|
+
|
|
6
|
+
export function ask(question) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const r = rl()
|
|
9
|
+
r.question(question, (answer) => {
|
|
10
|
+
r.close()
|
|
11
|
+
resolve(answer.trim())
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function choose(label, options) {
|
|
17
|
+
console.log(`\n ${label}\n`)
|
|
18
|
+
options.forEach((opt, i) => {
|
|
19
|
+
console.log(` ${i + 1}) ${opt.label}`)
|
|
20
|
+
})
|
|
21
|
+
console.log()
|
|
22
|
+
|
|
23
|
+
while (true) {
|
|
24
|
+
const answer = await ask(` select [1-${options.length}]: `)
|
|
25
|
+
const num = parseInt(answer, 10)
|
|
26
|
+
if (num >= 1 && num <= options.length) {
|
|
27
|
+
return options[num - 1]
|
|
28
|
+
}
|
|
29
|
+
console.log(` invalid choice, enter 1-${options.length}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function chooseMultiple(label, options) {
|
|
34
|
+
console.log(`\n ${label}\n`)
|
|
35
|
+
options.forEach((opt, i) => {
|
|
36
|
+
console.log(` ${i + 1}) ${opt.label}`)
|
|
37
|
+
})
|
|
38
|
+
console.log()
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
const answer = await ask(` select [1-${options.length}, comma-separated]: `)
|
|
42
|
+
const nums = answer.split(',').map((s) => parseInt(s.trim(), 10))
|
|
43
|
+
const valid = nums.every((n) => n >= 1 && n <= options.length)
|
|
44
|
+
if (valid && nums.length > 0) {
|
|
45
|
+
return nums.map((n) => options[n - 1])
|
|
46
|
+
}
|
|
47
|
+
console.log(` invalid choice, enter numbers 1-${options.length} separated by commas`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function confirm(message) {
|
|
52
|
+
const answer = await ask(` ${message} [y/N]: `)
|
|
53
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'
|
|
54
|
+
}
|
package/src/search.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { get } from './api.mjs'
|
|
2
|
+
|
|
3
|
+
export async function search(query) {
|
|
4
|
+
const data = await get(`/api/skills?q=${encodeURIComponent(query)}&limit=20`)
|
|
5
|
+
const skills = data.skills || []
|
|
6
|
+
|
|
7
|
+
if (skills.length === 0) {
|
|
8
|
+
console.log(`\n no skills found for "${query}"\n`)
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(`\n ${skills.length} skill(s) found:\n`)
|
|
13
|
+
for (const s of skills) {
|
|
14
|
+
const tags = s.tags?.length ? ` [${s.tags.join(', ')}]` : ''
|
|
15
|
+
console.log(` ${s.slug.padEnd(30)} ${s.description || ''}${tags}`)
|
|
16
|
+
}
|
|
17
|
+
console.log(`\n install: npx learn-skill add <slug>\n`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function list() {
|
|
21
|
+
const data = await get('/api/skills/recent')
|
|
22
|
+
const skills = data.skills || []
|
|
23
|
+
|
|
24
|
+
if (skills.length === 0) {
|
|
25
|
+
console.log('\n no skills available yet\n')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('\n recent skills:\n')
|
|
30
|
+
for (const s of skills) {
|
|
31
|
+
const tags = s.tags?.length ? ` [${s.tags.join(', ')}]` : ''
|
|
32
|
+
console.log(` ${s.slug.padEnd(30)} ${s.description || ''}${tags}`)
|
|
33
|
+
}
|
|
34
|
+
console.log(`\n install: npx learn-skill add <slug>\n`)
|
|
35
|
+
}
|