Skip to content

Commit 48fc0bc

Browse files
Numman Aliclaude
andcommitted
feat: add --universal flag and make project install default (v1.2.0)
Major changes: 1. Added --universal flag for .agent/skills installation - Default: .claude/skills (Claude Code compatible) - With --universal: .agent/skills (universal AGENTS.md for all agents) - Works with both project and global installs 2. Changed default from global to project install - No flags → .claude/skills (project, recommended) - --global → ~/.claude/skills (advanced users) - Project install is now the default behavior 3. Support for 4 installation locations with priority - .agent/skills (project universal) - priority 1 - ~/.agent/skills (global universal) - priority 2 - .claude/skills (project) - priority 3 - ~/.claude/skills (global) - priority 4 4. Deduplication in list/read commands - Skills with same name only show once (highest priority wins) - Prevents duplicates when skills exist in both .agent and .claude Breaking changes: - Removed --project flag, replaced with --global flag - Default install location changed from global to project Migration: - Old: openskills install X --project - New: openskills install X (project is now default) - Old: openskills install X - New: openskills install X --global Use cases: - Claude Code + other agents: Use --universal to install to .agent/skills - Claude Code only: Use default (installs to .claude/skills) - Share skills across projects: Use --global 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 22ef434 commit 48fc0bc

File tree

10 files changed

+55
-27
lines changed

10 files changed

+55
-27
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openskills",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Universal skills loader for AI coding agents - install and load Anthropic SKILL.md format skills in any agent",
55
"type": "module",
66
"main": "./dist/cli.js",

src/cli.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const program = new Command();
1313
program
1414
.name('openskills')
1515
.description('Universal skills loader for AI coding agents')
16-
.version('1.0.0')
16+
.version('1.2.0')
1717
.showHelpAfterError(false)
1818
.exitOverride((err) => {
1919
// Handle all commander errors gracefully (no stack traces)
@@ -40,7 +40,8 @@ program
4040
program
4141
.command('install <source>')
4242
.description('Install skill from GitHub or Git URL')
43-
.option('-p, --project', 'Install to project .claude/skills/ (recommended)')
43+
.option('-g, --global', 'Install globally (default: project install)')
44+
.option('-u, --universal', 'Install to .agent/skills/ (for universal AGENTS.md usage)')
4445
.option('-y, --yes', 'Skip interactive selection, install all skills found')
4546
.action(installSkill);
4647

src/commands/install.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import type { InstallOptions } from '../types.js';
1414
* Install skill from GitHub or Git URL
1515
*/
1616
export async function installSkill(source: string, options: InstallOptions): Promise<void> {
17-
const targetDir = options.project
18-
? join(process.cwd(), '.claude/skills')
19-
: join(homedir(), '.claude/skills');
17+
const folder = options.universal ? '.agent/skills' : '.claude/skills';
18+
const isProject = !options.global; // Default to project unless --global specified
19+
const targetDir = isProject
20+
? join(process.cwd(), folder)
21+
: join(homedir(), folder);
2022

21-
const location = options.project
22-
? chalk.blue('project (.claude/skills)')
23-
: chalk.dim('global (~/.claude/skills)');
23+
const location = isProject
24+
? chalk.blue(`project (${folder})`)
25+
: chalk.dim(`global (~/${folder})`);
2426

2527
console.log(`Installing from: ${chalk.cyan(source)}`);
2628
console.log(`Location: ${location}\n`);
@@ -63,7 +65,7 @@ export async function installSkill(source: string, options: InstallOptions): Pro
6365

6466
if (skillSubpath) {
6567
// Specific skill path provided - install directly
66-
await installSpecificSkill(repoDir, skillSubpath, targetDir, options.project || false);
68+
await installSpecificSkill(repoDir, skillSubpath, targetDir, isProject);
6769
} else {
6870
// Find all skills in repo
6971
await installFromRepo(repoDir, targetDir, options);
@@ -74,7 +76,7 @@ export async function installSkill(source: string, options: InstallOptions): Pro
7476
}
7577

7678
console.log(`\n${chalk.dim('Read skill:')} ${chalk.cyan('openskills read <skill-name>')}`);
77-
if (options.project) {
79+
if (isProject) {
7880
console.log(`${chalk.dim('Sync to AGENTS.md:')} ${chalk.cyan('openskills sync')}`);
7981
}
8082
}

src/commands/list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export function listSkills(): void {
1212
if (skills.length === 0) {
1313
console.log('No skills installed.\n');
1414
console.log('Install skills:');
15-
console.log(` ${chalk.cyan('openskills install anthropics/skills --project')} ${chalk.dim('# Recommended')}`);
16-
console.log(` ${chalk.cyan('openskills install owner/unique-skill')} ${chalk.dim('# Global (advanced)')}`);
15+
console.log(` ${chalk.cyan('openskills install anthropics/skills')} ${chalk.dim('# Project (default)')}`);
16+
console.log(` ${chalk.cyan('openskills install owner/skill --global')} ${chalk.dim('# Global (advanced)')}`);
1717
return;
1818
}
1919

src/commands/manage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export async function manageSkills(): Promise<void> {
4646
const skill = findSkill(skillName);
4747
if (skill) {
4848
rmSync(skill.baseDir, { recursive: true, force: true });
49-
const location = skill.source.includes('/.claude/skills') ? 'project' : 'global';
49+
const location = skill.source.includes(process.cwd()) ? 'project' : 'global';
5050
console.log(chalk.green(`✅ Removed: ${skillName} (${location})`));
5151
}
5252
}

src/commands/read.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export function readSkill(skillName: string): void {
1010
if (!skill) {
1111
console.error(`Error: Skill '${skillName}' not found`);
1212
console.error('\nSearched:');
13+
console.error(' .agent/skills/ (project universal)');
14+
console.error(' ~/.agent/skills/ (global universal)');
1315
console.error(' .claude/skills/ (project)');
1416
console.error(' ~/.claude/skills/ (global)');
1517
console.error('\nInstall skills: openskills install owner/repo');

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export interface SkillLocation {
1212
}
1313

1414
export interface InstallOptions {
15-
project?: boolean;
15+
global?: boolean;
16+
universal?: boolean;
1617
yes?: boolean;
1718
}
1819

src/utils/dirs.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { homedir } from 'os';
44
/**
55
* Get skills directory path
66
*/
7-
export function getSkillsDir(projectLocal: boolean = false): string {
7+
export function getSkillsDir(projectLocal: boolean = false, universal: boolean = false): string {
8+
const folder = universal ? '.agent/skills' : '.claude/skills';
89
return projectLocal
9-
? join(process.cwd(), '.claude/skills')
10-
: join(homedir(), '.claude/skills');
10+
? join(process.cwd(), folder)
11+
: join(homedir(), folder);
1112
}
1213

1314
/**
1415
* Get all searchable skill directories in priority order
16+
* Priority: project .agent > global .agent > project .claude > global .claude
1517
*/
1618
export function getSearchDirs(): string[] {
1719
return [
18-
join(process.cwd(), '.claude/skills'), // Project-local first
19-
join(homedir(), '.claude/skills'), // Global second
20+
join(process.cwd(), '.agent/skills'), // 1. Project universal (.agent)
21+
join(homedir(), '.agent/skills'), // 2. Global universal (.agent)
22+
join(process.cwd(), '.claude/skills'), // 3. Project claude
23+
join(homedir(), '.claude/skills'), // 4. Global claude
2024
];
2125
}

src/utils/skills.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Skill, SkillLocation } from '../types.js';
99
*/
1010
export function findAllSkills(): Skill[] {
1111
const skills: Skill[] = [];
12+
const seen = new Set<string>();
1213
const dirs = getSearchDirs();
1314

1415
for (const dir of dirs) {
@@ -18,17 +19,22 @@ export function findAllSkills(): Skill[] {
1819

1920
for (const entry of entries) {
2021
if (entry.isDirectory()) {
22+
// Deduplicate: only add if we haven't seen this skill name yet
23+
if (seen.has(entry.name)) continue;
24+
2125
const skillPath = join(dir, entry.name, 'SKILL.md');
2226
if (existsSync(skillPath)) {
2327
const content = readFileSync(skillPath, 'utf-8');
24-
const isProjectLocal = dir === join(process.cwd(), '.claude/skills');
28+
const isProjectLocal = dir.includes(process.cwd());
2529

2630
skills.push({
2731
name: entry.name,
2832
description: extractYamlField(content, 'description'),
2933
location: isProjectLocal ? 'project' : 'global',
3034
path: join(dir, entry.name),
3135
});
36+
37+
seen.add(entry.name);
3238
}
3339
}
3440
}

tests/utils/dirs.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,34 @@ import { homedir } from 'os';
44
import { getSkillsDir, getSearchDirs } from '../../src/utils/dirs.js';
55

66
describe('getSkillsDir', () => {
7-
it('should return global dir by default', () => {
7+
it('should return global .claude dir by default', () => {
88
const dir = getSkillsDir();
99
expect(dir).toBe(join(homedir(), '.claude/skills'));
1010
});
1111

12-
it('should return project dir when projectLocal is true', () => {
12+
it('should return project .claude dir when projectLocal is true', () => {
1313
const dir = getSkillsDir(true);
1414
expect(dir).toBe(join(process.cwd(), '.claude/skills'));
1515
});
16+
17+
it('should return global .agent dir when universal is true', () => {
18+
const dir = getSkillsDir(false, true);
19+
expect(dir).toBe(join(homedir(), '.agent/skills'));
20+
});
21+
22+
it('should return project .agent dir when both projectLocal and universal are true', () => {
23+
const dir = getSkillsDir(true, true);
24+
expect(dir).toBe(join(process.cwd(), '.agent/skills'));
25+
});
1626
});
1727

1828
describe('getSearchDirs', () => {
19-
it('should return dirs in priority order', () => {
29+
it('should return all 4 dirs in priority order', () => {
2030
const dirs = getSearchDirs();
21-
expect(dirs).toHaveLength(2);
22-
expect(dirs[0]).toBe(join(process.cwd(), '.claude/skills'));
23-
expect(dirs[1]).toBe(join(homedir(), '.claude/skills'));
31+
expect(dirs).toHaveLength(4);
32+
expect(dirs[0]).toBe(join(process.cwd(), '.agent/skills')); // 1. Project universal
33+
expect(dirs[1]).toBe(join(homedir(), '.agent/skills')); // 2. Global universal
34+
expect(dirs[2]).toBe(join(process.cwd(), '.claude/skills')); // 3. Project claude
35+
expect(dirs[3]).toBe(join(homedir(), '.claude/skills')); // 4. Global claude
2436
});
2537
});

0 commit comments

Comments
 (0)