Skip to content

Commit 125f20f

Browse files
author
xuhongbin
committed
chore: Update package version to 1.0.9 and implement support for copying skills for Cursor due to symlink limitations
1 parent 5fce2c0 commit 125f20f

File tree

9 files changed

+96
-36
lines changed

9 files changed

+96
-36
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/bigo/.emp-agent/skills/react-best-practices

.superset/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"setup": [],
3+
"teardown": []
4+
}

packages/eskill/README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ The CLI automatically detects and links to:
7878
- **ClawdBot** - `~/.openclaw/skills/` or `~/.clawdbot/skills/`
7979
- **Cline** - `~/.cline/skills/`
8080
- **Codex** - `~/.codex/skills/`
81-
- **Cursor** - `~/.cursor/skills/`
81+
- **Cursor** - `~/.cursor/skills/` (uses copy instead of symlink due to [Cursor bug](https://forum.cursor.com/t/cursor-doesnt-follow-symlinks-to-discover-skills/149693))
8282
- **Droid** - `~/.factory/skills/`
8383
- **Gemini** - `~/.gemini/skills/`
8484
- **GitHub Copilot** - `~/.copilot/skills/`
@@ -101,9 +101,9 @@ The CLI automatically detects and links to:
101101
├── config.json
102102
└── cache/
103103
104-
# Symlinks to AI agents
104+
# Symlinks (or copies for Cursor) to AI agents
105105
~/.claude/skills/skill-name -> ~/.emp-agent/skills/skill-name
106-
~/.cursor/skills/skill-name -> ~/.emp-agent/skills/skill-name
106+
~/.cursor/skills/skill-name/ # Copy (Cursor doesn't follow symlinks)
107107
~/.windsurf/skills/skill-name -> ~/.emp-agent/skills/skill-name
108108
```
109109

@@ -264,6 +264,22 @@ ls -la ~/.claude/skills/
264264
# Should show symlink arrows (->)
265265
```
266266

267+
### Cursor not recognizing skills
268+
269+
Cursor has a [known bug](https://forum.cursor.com/t/cursor-doesnt-follow-symlinks-to-discover-skills/149693) where it does not follow symlinks to discover skills. eskill now **copies** (instead of symlinking) skills to `~/.cursor/skills/` for Cursor.
270+
271+
If you installed skills before this fix, reinstall to get the copy:
272+
273+
```bash
274+
# Reinstall for Cursor only (replaces symlink with copy)
275+
eskill install <skill-name> --force --agent cursor
276+
277+
# Or reinstall for all agents
278+
eskill install <skill-name> --force
279+
```
280+
281+
Then restart Cursor. Skills appear in **Cursor Settings → Rules → Agent Decides**.
282+
267283
## License
268284

269285
MIT License - EMP Team

packages/eskill/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@empjs/skill",
3-
"version": "1.0.7",
3+
"version": "1.0.9",
44
"description": "Unified CLI tool for managing AI agent skills across Claude Code, Cursor, Windsurf, and more",
55
"type": "module",
66
"bin": {

packages/eskill/src/commands/install.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -318,22 +318,25 @@ export async function install(skillNameOrPath: string, options: InstallOptions =
318318

319319
// Target path in shared directory
320320
const targetPath = getSharedSkillPath(skillName)
321+
const sourceIsTarget = path.resolve(skillPath) === path.resolve(targetPath)
321322

322-
// Check if already exists
323-
if (fs.existsSync(targetPath)) {
323+
// Check if already exists (skip if source is the shared dir - e.g. reinstalling for Cursor only)
324+
const alreadyExists = fs.existsSync(targetPath) && !sourceIsTarget
325+
if (alreadyExists) {
324326
if (options.force) {
325327
logger.warn('Removing existing installation...')
326328
fs.rmSync(targetPath, {recursive: true, force: true})
327329
} else {
328-
logger.error(`Skill already exists`)
329-
logger.dim(`Location: ${shortenPath(targetPath)}`)
330-
logger.dim(`Tip: Add --force to overwrite`)
331-
process.exit(1)
330+
logger.info(`Skill already exists, updating agent links...`)
332331
}
333332
}
334333

335-
// Copy or link to shared directory
336-
if (options.link) {
334+
// Copy or link to shared directory (skip if already exists without --force, or source is shared dir)
335+
if (sourceIsTarget) {
336+
logger.info(`Skill already in shared directory, updating agent links...`)
337+
} else if (alreadyExists && !options.force) {
338+
// Skip copy, just update agent links
339+
} else if (options.link) {
337340
// Dev mode: create symlink
338341
fs.symlinkSync(skillPath, targetPath, 'dir')
339342
logger.success(`Linked to shared directory (dev mode)`)

packages/eskill/src/commands/list.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,25 @@ export function list(): void {
100100

101101
console.log(chalk.green('📦') + ` ${chalk.bold(skill)}${versionDisplay ? ' ' + versionDisplay : ''}${devTag}`)
102102

103-
// Check which agents are linked
103+
// Check which agents are linked (symlink) or copied (e.g. Cursor)
104104
const cwd = process.cwd()
105105
const linkedAgents: string[] = []
106106
for (const agent of AGENTS) {
107107
const skillsDirs = getAgentSkillsDirs(agent, cwd)
108-
const hasSymlink = skillsDirs.some(dir => {
108+
const hasRef = skillsDirs.some(dir => {
109109
const agentSkillPath = path.join(dir, skill)
110-
if (fs.existsSync(agentSkillPath) && isSymlink(agentSkillPath)) {
110+
if (!fs.existsSync(agentSkillPath)) return false
111+
if (isSymlink(agentSkillPath)) {
111112
const target = readSymlink(agentSkillPath)
112113
return target === skillPath
113114
}
115+
// Cursor uses copy instead of symlink
116+
if (agent.useCopyInsteadOfSymlink) {
117+
return fs.statSync(agentSkillPath).isDirectory()
118+
}
114119
return false
115120
})
116-
if (hasSymlink) {
121+
if (hasRef) {
117122
linkedAgents.push(agent.displayName)
118123
}
119124
}

packages/eskill/src/commands/remove.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path'
33
import {AGENTS, getAgentSkillsDirs} from '../config/agents.js'
44
import {logger} from '../utils/logger.js'
55
import {detectInstalledAgents, extractSkillName, getAgentSkillPaths, getSharedSkillPath} from '../utils/paths.js'
6-
import {isSymlink, removeSymlink} from '../utils/symlink.js'
6+
import {removeSymlink} from '../utils/symlink.js'
77

88
export interface RemoveOptions {
99
agent?: string // specific agent or 'all'
@@ -77,24 +77,22 @@ export async function remove(skillName: string, options: RemoveOptions = {}): Pr
7777
process.exit(1)
7878
}
7979

80-
// Check if there are any remaining symlinks in other agents
81-
const remainingSymlinks: string[] = []
80+
// Check if there are any remaining links/copies in other agents
81+
const remainingRefs: string[] = []
8282
for (const agent of AGENTS) {
8383
const agentSkillPaths = getAgentSkillPaths(agent.name, extractedName, cwd)
84-
const hasSymlink = agentSkillPaths.some(path => {
85-
return fs.existsSync(path) && isSymlink(path)
86-
})
87-
if (hasSymlink) {
88-
remainingSymlinks.push(agent.displayName)
84+
const hasRef = agentSkillPaths.some(p => fs.existsSync(p))
85+
if (hasRef) {
86+
remainingRefs.push(agent.displayName)
8987
}
9088
}
9189

92-
if (remainingSymlinks.length > 0) {
93-
logger.warn(`\n⚠️ Warning: Found remaining symlinks in:`)
94-
for (const agentName of remainingSymlinks) {
90+
if (remainingRefs.length > 0) {
91+
logger.warn(`\n⚠️ Warning: Found remaining references in:`)
92+
for (const agentName of remainingRefs) {
9593
logger.info(` - ${agentName}`)
9694
}
97-
logger.info('\nYou may need to manually remove these symlinks')
95+
logger.info('\nYou may need to manually remove these')
9896
} else {
9997
logger.success(`✅ Skill "${extractedName}" removed successfully!`)
10098
}

packages/eskill/src/config/agents.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface AgentConfig {
88
skillsDir?: string // Single directory (backward compatibility)
99
skillsDirs?: string[] | ((cwd?: string) => string[]) // Multiple directories or function to get directories
1010
enabled: boolean
11+
/** Use copy instead of symlink (e.g. Cursor doesn't follow symlinks) */
12+
useCopyInsteadOfSymlink?: boolean
1113
}
1214

1315
const HOME = os.homedir()
@@ -92,6 +94,8 @@ export const AGENTS: AgentConfig[] = [
9294
displayName: 'Cursor',
9395
skillsDir: path.join(HOME, '.cursor', 'skills'),
9496
enabled: true,
97+
/** Cursor does not follow symlinks to discover skills (known bug). Use copy instead. */
98+
useCopyInsteadOfSymlink: true,
9599
},
96100
{
97101
name: 'droid',

packages/eskill/src/utils/symlink.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,26 @@ import {logger} from './logger.js'
66
import {getAgentSkillPaths, getSharedSkillPath} from './paths.js'
77

88
/**
9-
* Create symlink from shared directory to agent directory(ies)
9+
* Recursively copy directory (excludes node_modules and hidden files)
10+
*/
11+
function copyDir(src: string, dest: string): void {
12+
fs.mkdirSync(dest, {recursive: true})
13+
const entries = fs.readdirSync(src, {withFileTypes: true})
14+
for (const entry of entries) {
15+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
16+
const srcPath = path.join(src, entry.name)
17+
const destPath = path.join(dest, entry.name)
18+
if (entry.isDirectory()) {
19+
copyDir(srcPath, destPath)
20+
} else {
21+
fs.copyFileSync(srcPath, destPath)
22+
}
23+
}
24+
}
25+
26+
/**
27+
* Create symlink (or copy for Cursor) from shared directory to agent directory(ies)
28+
* Cursor does not follow symlinks to discover skills - use copy instead.
1029
*/
1130
export function createSymlink(skillName: string, agent: AgentConfig, cwd?: string): boolean {
1231
const source = getSharedSkillPath(skillName)
@@ -17,10 +36,10 @@ export function createSymlink(skillName: string, agent: AgentConfig, cwd?: strin
1736
}
1837

1938
const targets = getAgentSkillPaths(agent.name, skillName, cwd)
39+
const useCopy = agent.useCopyInsteadOfSymlink === true
2040
let successCount = 0
2141

2242
for (const target of targets) {
23-
// Ensure target directory exists
2443
const targetDir = path.dirname(target)
2544
if (!fs.existsSync(targetDir)) {
2645
try {
@@ -37,8 +56,10 @@ export function createSymlink(skillName: string, agent: AgentConfig, cwd?: strin
3756
const stats = fs.lstatSync(target)
3857
if (stats.isSymbolicLink()) {
3958
fs.unlinkSync(target)
59+
} else if (stats.isDirectory()) {
60+
fs.rmSync(target, {recursive: true, force: true})
4061
} else {
41-
logger.warn(`Target exists but is not a symlink, skipping: ${target}`)
62+
logger.warn(`Target exists but is not a symlink/dir, skipping: ${target}`)
4263
continue
4364
}
4465
} catch (error: any) {
@@ -48,10 +69,14 @@ export function createSymlink(skillName: string, agent: AgentConfig, cwd?: strin
4869
}
4970

5071
try {
51-
fs.symlinkSync(source, target, 'dir')
72+
if (useCopy) {
73+
copyDir(source, target)
74+
} else {
75+
fs.symlinkSync(source, target, 'dir')
76+
}
5277
successCount++
5378
} catch (error: any) {
54-
logger.error(`Failed to create symlink at ${target}: ${error.message}`)
79+
logger.error(`Failed to ${useCopy ? 'copy' : 'symlink'} at ${target}: ${error.message}`)
5580
}
5681
}
5782

@@ -65,7 +90,8 @@ export function createSymlink(skillName: string, agent: AgentConfig, cwd?: strin
6590
}
6691

6792
/**
68-
* Remove symlink from agent directory(ies)
93+
* Remove symlink or copied directory from agent directory(ies)
94+
* Handles both symlinks (most agents) and directories (Cursor uses copy)
6995
*/
7096
export function removeSymlink(skillName: string, agent: AgentConfig, cwd?: string): boolean {
7197
const targets = getAgentSkillPaths(agent.name, skillName, cwd)
@@ -81,11 +107,14 @@ export function removeSymlink(skillName: string, agent: AgentConfig, cwd?: strin
81107
if (stats.isSymbolicLink()) {
82108
fs.unlinkSync(target)
83109
removedCount++
110+
} else if (stats.isDirectory()) {
111+
fs.rmSync(target, {recursive: true, force: true})
112+
removedCount++
84113
} else {
85-
logger.warn(`Not a symlink: ${target}`)
114+
logger.warn(`Not a symlink or directory: ${target}`)
86115
}
87116
} catch (error: any) {
88-
logger.error(`Failed to remove symlink at ${target}: ${error.message}`)
117+
logger.error(`Failed to remove at ${target}: ${error.message}`)
89118
}
90119
}
91120

0 commit comments

Comments
 (0)