Skip to content

Commit 3251f0a

Browse files
committed
Release v1.5.0
1 parent 7464397 commit 3251f0a

File tree

14 files changed

+515
-29
lines changed

14 files changed

+515
-29
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.5.0] - 2026-01-17
9+
10+
### Added
11+
12+
- **`openskills update`** - Refresh installed skills from their recorded source (default: all)
13+
- **Source metadata tracking** - Install now records origin info for reliable updates
14+
15+
### Changed
16+
17+
- **Multi-skill read** - `openskills read` supports comma-separated names
18+
- **Generated usage text** - Clarified read invocation for shell usage
19+
- **README** - Added update guidance and human usage tips
20+
21+
### Fixed
22+
23+
- **Update UX** - Skips skills without source metadata and lists them for re-install
24+
825
## [1.3.0] - 2025-12-14
926

1027
### Added
@@ -121,6 +138,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
121138
[1.3.1]: https://github.com/numman-ali/openskills/compare/v1.3.0...v1.3.1
122139
[1.3.2]: https://github.com/numman-ali/openskills/compare/v1.3.1...v1.3.2
123140
[1.4.0]: https://github.com/numman-ali/openskills/compare/v1.3.2...v1.4.0
141+
[1.5.0]: https://github.com/numman-ali/openskills/compare/v1.4.0...v1.5.0
124142
[1.2.1]: https://github.com/numman-ali/openskills/compare/v1.2.0...v1.2.1
125143
[1.2.0]: https://github.com/numman-ali/openskills/compare/v1.1.0...v1.2.0
126144
[1.1.0]: https://github.com/numman-ali/openskills/compare/v1.0.0...v1.1.0

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ So any agent that can read `AGENTS.md` can use Claude Code skills without needin
9797
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively.
9898

9999
How to use skills:
100-
- Invoke: Bash("npx openskills read <skill-name>")
100+
- Invoke: `npx openskills read <skill-name>` (run in your shell)
101101
- The skill content will load with detailed instructions
102102
- Base directory provided in output for resolving bundled resources
103103

@@ -175,6 +175,7 @@ npx openskills install <source> [options] # Install from GitHub, local path, or
175175
npx openskills sync [-y] [-o <path>] # Update AGENTS.md (or custom output)
176176
npx openskills list # Show installed skills
177177
npx openskills read <name> # Load skill (for agents)
178+
npx openskills update [name...] # Update installed skills (default: all)
178179
npx openskills manage # Remove skills (interactive)
179180
npx openskills remove <name> # Remove specific skill
180181
```
@@ -252,6 +253,31 @@ npx openskills read skill-creator
252253

253254
---
254255

256+
## 🔄 Updating Skills
257+
258+
If you installed skills from a git repo, you can refresh them anytime:
259+
260+
```bash
261+
npx openskills update
262+
```
263+
264+
To update specific skills, pass a comma-separated list:
265+
266+
```bash
267+
npx openskills update git-workflow,check-branch-first
268+
```
269+
270+
If a skill was installed before updates were tracked, re-install it once to record its source.
271+
272+
---
273+
274+
## ✅ Tips
275+
276+
- You can always run OpenSkills via `npx`; a global install is optional.
277+
- For multiple reads, prefer comma-separated names: `npx openskills read foo,bar`.
278+
279+
---
280+
255281
## ❓ FAQ
256282

257283
### Why CLI instead of MCP?

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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.4.0",
3+
"version": "1.5.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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { readSkill } from './commands/read.js';
77
import { removeSkill } from './commands/remove.js';
88
import { manageSkills } from './commands/manage.js';
99
import { syncAgentsMd } from './commands/sync.js';
10+
import { updateSkills } from './commands/update.js';
1011
import { createRequire } from 'module';
1112

1213
const program = new Command();
@@ -49,10 +50,15 @@ program
4950
.action(installSkill);
5051

5152
program
52-
.command('read <skill-name>')
53-
.description('Read skill to stdout (for AI agents)')
53+
.command('read <skill-names...>')
54+
.description('Read skill(s) to stdout (for AI agents)')
5455
.action(readSkill);
5556

57+
program
58+
.command('update [skill-names...]')
59+
.description('Update installed skills from their source (default: all)')
60+
.action(updateSkills);
61+
5662
program
5763
.command('sync')
5864
.description('Update AGENTS.md with installed skills (interactive, pre-selects current state)')

src/commands/install.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFileSync, readdirSync, existsSync, mkdirSync, rmSync, cpSync, statSync } from 'fs';
2-
import { join, basename, resolve, sep } from 'path';
2+
import { join, basename, resolve, sep, relative } from 'path';
33
import { homedir } from 'os';
44
import { execSync } from 'child_process';
55
import chalk from 'chalk';
@@ -8,7 +8,16 @@ import { checkbox, confirm } from '@inquirer/prompts';
88
import { ExitPromptError } from '@inquirer/core';
99
import { hasValidFrontmatter, extractYamlField } from '../utils/yaml.js';
1010
import { ANTHROPIC_MARKETPLACE_SKILLS } from '../utils/marketplace-skills.js';
11+
import { writeSkillMetadata } from '../utils/skill-metadata.js';
1112
import type { InstallOptions } from '../types.js';
13+
import type { SkillSourceMetadata, SkillSourceType } from '../utils/skill-metadata.js';
14+
15+
interface InstallSourceInfo {
16+
source: string;
17+
sourceType: SkillSourceType;
18+
repoUrl?: string;
19+
localRoot?: string;
20+
}
1221

1322
/**
1423
* Check if source is a local path
@@ -101,7 +110,12 @@ export async function installSkill(source: string, options: InstallOptions): Pro
101110
// Handle local path installation
102111
if (isLocalPath(source)) {
103112
const localPath = expandPath(source);
104-
await installFromLocal(localPath, targetDir, options);
113+
const sourceInfo: InstallSourceInfo = {
114+
source,
115+
sourceType: 'local',
116+
localRoot: localPath,
117+
};
118+
await installFromLocal(localPath, targetDir, options, sourceInfo);
105119
printPostInstallHints(isProject);
106120
return;
107121
}
@@ -131,6 +145,11 @@ export async function installSkill(source: string, options: InstallOptions): Pro
131145
// Clone and install from git
132146
const tempDir = join(homedir(), `.openskills-temp-${Date.now()}`);
133147
mkdirSync(tempDir, { recursive: true });
148+
const sourceInfo: InstallSourceInfo = {
149+
source,
150+
sourceType: 'git',
151+
repoUrl,
152+
};
134153

135154
try {
136155
const spinner = ora('Cloning repository...').start();
@@ -152,10 +171,10 @@ export async function installSkill(source: string, options: InstallOptions): Pro
152171
const repoDir = join(tempDir, 'repo');
153172

154173
if (skillSubpath) {
155-
await installSpecificSkill(repoDir, skillSubpath, targetDir, isProject, options);
174+
await installSpecificSkill(repoDir, skillSubpath, targetDir, isProject, options, sourceInfo);
156175
} else {
157176
const repoName = getRepoName(repoUrl);
158-
await installFromRepo(repoDir, targetDir, options, repoName || undefined);
177+
await installFromRepo(repoDir, targetDir, options, repoName || undefined, sourceInfo);
159178
}
160179
} finally {
161180
rmSync(tempDir, { recursive: true, force: true });
@@ -177,7 +196,12 @@ function printPostInstallHints(isProject: boolean): void {
177196
/**
178197
* Install from local path (directory containing skills or single skill)
179198
*/
180-
async function installFromLocal(localPath: string, targetDir: string, options: InstallOptions): Promise<void> {
199+
async function installFromLocal(
200+
localPath: string,
201+
targetDir: string,
202+
options: InstallOptions,
203+
sourceInfo: InstallSourceInfo
204+
): Promise<void> {
181205
if (!existsSync(localPath)) {
182206
console.error(chalk.red(`Error: Path does not exist: ${localPath}`));
183207
process.exit(1);
@@ -194,10 +218,10 @@ async function installFromLocal(localPath: string, targetDir: string, options: I
194218
if (existsSync(skillMdPath)) {
195219
// Single skill directory
196220
const isProject = targetDir.includes(process.cwd());
197-
await installSingleLocalSkill(localPath, targetDir, isProject, options);
221+
await installSingleLocalSkill(localPath, targetDir, isProject, options, sourceInfo);
198222
} else {
199223
// Directory containing multiple skills
200-
await installFromRepo(localPath, targetDir, options);
224+
await installFromRepo(localPath, targetDir, options, undefined, sourceInfo);
201225
}
202226
}
203227

@@ -208,7 +232,8 @@ async function installSingleLocalSkill(
208232
skillDir: string,
209233
targetDir: string,
210234
isProject: boolean,
211-
options: InstallOptions
235+
options: InstallOptions,
236+
sourceInfo: InstallSourceInfo
212237
): Promise<void> {
213238
const skillMdPath = join(skillDir, 'SKILL.md');
214239
const content = readFileSync(skillMdPath, 'utf-8');
@@ -235,6 +260,7 @@ async function installSingleLocalSkill(
235260
}
236261

237262
cpSync(skillDir, targetPath, { recursive: true, dereference: true });
263+
writeSkillMetadata(targetPath, buildLocalMetadata(sourceInfo, skillDir));
238264

239265
console.log(chalk.green(`✅ Installed: ${skillName}`));
240266
console.log(` Location: ${targetPath}`);
@@ -248,7 +274,8 @@ async function installSpecificSkill(
248274
skillSubpath: string,
249275
targetDir: string,
250276
isProject: boolean,
251-
options: InstallOptions
277+
options: InstallOptions,
278+
sourceInfo: InstallSourceInfo
252279
): Promise<void> {
253280
const skillDir = join(repoDir, skillSubpath);
254281
const skillMdPath = join(skillDir, 'SKILL.md');
@@ -282,6 +309,7 @@ async function installSpecificSkill(
282309
process.exit(1);
283310
}
284311
cpSync(skillDir, targetPath, { recursive: true, dereference: true });
312+
writeSkillMetadata(targetPath, buildGitMetadata(sourceInfo, skillSubpath));
285313

286314
console.log(chalk.green(`✅ Installed: ${skillName}`));
287315
console.log(` Location: ${targetPath}`);
@@ -294,7 +322,8 @@ async function installFromRepo(
294322
repoDir: string,
295323
targetDir: string,
296324
options: InstallOptions,
297-
repoName?: string
325+
repoName: string | undefined,
326+
sourceInfo: InstallSourceInfo
298327
): Promise<void> {
299328
const rootSkillPath = join(repoDir, 'SKILL.md');
300329
let skillInfos: Array<{
@@ -444,6 +473,7 @@ async function installFromRepo(
444473
continue;
445474
}
446475
cpSync(info.skillDir, info.targetPath, { recursive: true, dereference: true });
476+
writeSkillMetadata(info.targetPath, buildMetadataFromSource(sourceInfo, info.skillDir, repoDir));
447477

448478
console.log(chalk.green(`✅ Installed: ${info.skillName}`));
449479
installedCount++;
@@ -452,6 +482,38 @@ async function installFromRepo(
452482
console.log(chalk.green(`\n✅ Installation complete: ${installedCount} skill(s) installed`));
453483
}
454484

485+
function buildMetadataFromSource(
486+
sourceInfo: InstallSourceInfo,
487+
skillDir: string,
488+
repoDir: string
489+
): SkillSourceMetadata {
490+
if (sourceInfo.sourceType === 'local') {
491+
return buildLocalMetadata(sourceInfo, skillDir);
492+
}
493+
const subpath = relative(repoDir, skillDir);
494+
const normalizedSubpath = subpath === '' ? '' : subpath;
495+
return buildGitMetadata(sourceInfo, normalizedSubpath);
496+
}
497+
498+
function buildGitMetadata(sourceInfo: InstallSourceInfo, subpath: string): SkillSourceMetadata {
499+
return {
500+
source: sourceInfo.source,
501+
sourceType: 'git',
502+
repoUrl: sourceInfo.repoUrl,
503+
subpath,
504+
installedAt: new Date().toISOString(),
505+
};
506+
}
507+
508+
function buildLocalMetadata(sourceInfo: InstallSourceInfo, skillDir: string): SkillSourceMetadata {
509+
return {
510+
source: sourceInfo.source,
511+
sourceType: 'local',
512+
localPath: skillDir,
513+
installedAt: new Date().toISOString(),
514+
};
515+
}
516+
455517
/**
456518
* Warn if installing could conflict with Claude Code marketplace
457519
* Returns true if should proceed, false if should skip

src/commands/read.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import { readFileSync } from 'fs';
22
import { findSkill } from '../utils/skills.js';
3+
import { normalizeSkillNames } from '../utils/skill-names.js';
34

45
/**
56
* Read skill to stdout (for AI agents)
67
*/
7-
export function readSkill(skillName: string): void {
8-
const skill = findSkill(skillName);
8+
export function readSkill(skillNames: string[] | string): void {
9+
const names = normalizeSkillNames(skillNames);
10+
if (names.length === 0) {
11+
console.error('Error: No skill names provided');
12+
process.exit(1);
13+
}
14+
const resolved = [];
15+
const missing = [];
916

10-
if (!skill) {
11-
console.error(`Error: Skill '${skillName}' not found`);
17+
for (const name of names) {
18+
const skill = findSkill(name);
19+
if (!skill) {
20+
missing.push(name);
21+
continue;
22+
}
23+
resolved.push({ name, skill });
24+
}
25+
26+
if (missing.length > 0) {
27+
console.error(`Error: Skill(s) not found: ${missing.join(', ')}`);
1228
console.error('\nSearched:');
1329
console.error(' .agent/skills/ (project universal)');
1430
console.error(' ~/.agent/skills/ (global universal)');
@@ -18,13 +34,15 @@ export function readSkill(skillName: string): void {
1834
process.exit(1);
1935
}
2036

21-
const content = readFileSync(skill.path, 'utf-8');
37+
for (const { name, skill } of resolved) {
38+
const content = readFileSync(skill.path, 'utf-8');
2239

23-
// Output in Claude Code format
24-
console.log(`Reading: ${skillName}`);
25-
console.log(`Base directory: ${skill.baseDir}`);
26-
console.log('');
27-
console.log(content);
28-
console.log('');
29-
console.log(`Skill read: ${skillName}`);
40+
// Output in Claude Code format
41+
console.log(`Reading: ${name}`);
42+
console.log(`Base directory: ${skill.baseDir}`);
43+
console.log('');
44+
console.log(content);
45+
console.log('');
46+
console.log(`Skill read: ${name}`);
47+
}
3048
}

0 commit comments

Comments
 (0)