@andysama/openskills 1.3.2
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/LICENSE +17 -0
- package/README.md +535 -0
- package/dist/cli.js +732 -0
- package/examples/my-first-skill/SKILL.md +61 -0
- package/examples/my-first-skill/references/skill-format.md +77 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/list.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
// src/utils/skills.ts
|
|
10
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
11
|
+
import { join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/dirs.ts
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
function getSearchDirs() {
|
|
17
|
+
return [
|
|
18
|
+
join(process.cwd(), ".agent/skills"),
|
|
19
|
+
// 1. Project universal (.agent)
|
|
20
|
+
join(homedir(), ".agent/skills"),
|
|
21
|
+
// 2. Global universal (.agent)
|
|
22
|
+
join(process.cwd(), ".claude/skills"),
|
|
23
|
+
// 3. Project claude
|
|
24
|
+
join(homedir(), ".claude/skills")
|
|
25
|
+
// 4. Global claude
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/utils/yaml.ts
|
|
30
|
+
function extractYamlField(content, field) {
|
|
31
|
+
const match = content.match(new RegExp(`^${field}:\\s*(.+?)$`, "m"));
|
|
32
|
+
return match ? match[1].trim() : "";
|
|
33
|
+
}
|
|
34
|
+
function hasValidFrontmatter(content) {
|
|
35
|
+
return content.trim().startsWith("---");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/utils/skills.ts
|
|
39
|
+
function isDirectoryOrSymlinkToDirectory(entry, parentDir) {
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (entry.isSymbolicLink()) {
|
|
44
|
+
try {
|
|
45
|
+
const fullPath = join2(parentDir, entry.name);
|
|
46
|
+
const stats = statSync(fullPath);
|
|
47
|
+
return stats.isDirectory();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function findAllSkills() {
|
|
55
|
+
const skills = [];
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
const dirs = getSearchDirs();
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
if (!existsSync(dir)) continue;
|
|
60
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (isDirectoryOrSymlinkToDirectory(entry, dir)) {
|
|
63
|
+
if (seen.has(entry.name)) continue;
|
|
64
|
+
const skillPath = join2(dir, entry.name, "SKILL.md");
|
|
65
|
+
if (existsSync(skillPath)) {
|
|
66
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
67
|
+
const isProjectLocal = dir.includes(process.cwd());
|
|
68
|
+
skills.push({
|
|
69
|
+
name: entry.name,
|
|
70
|
+
description: extractYamlField(content, "description"),
|
|
71
|
+
location: isProjectLocal ? "project" : "global",
|
|
72
|
+
path: join2(dir, entry.name)
|
|
73
|
+
});
|
|
74
|
+
seen.add(entry.name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return skills;
|
|
80
|
+
}
|
|
81
|
+
function findSkill(skillName) {
|
|
82
|
+
const dirs = getSearchDirs();
|
|
83
|
+
for (const dir of dirs) {
|
|
84
|
+
const skillPath = join2(dir, skillName, "SKILL.md");
|
|
85
|
+
if (existsSync(skillPath)) {
|
|
86
|
+
return {
|
|
87
|
+
path: skillPath,
|
|
88
|
+
baseDir: join2(dir, skillName),
|
|
89
|
+
source: dir
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/commands/list.ts
|
|
97
|
+
function listSkills() {
|
|
98
|
+
console.log(chalk.bold("Available Skills:\n"));
|
|
99
|
+
const skills = findAllSkills();
|
|
100
|
+
if (skills.length === 0) {
|
|
101
|
+
console.log("No skills installed.\n");
|
|
102
|
+
console.log("Install skills:");
|
|
103
|
+
console.log(` ${chalk.cyan("openskills install anthropics/skills")} ${chalk.dim("# Project (default)")}`);
|
|
104
|
+
console.log(` ${chalk.cyan("openskills install owner/skill --global")} ${chalk.dim("# Global (advanced)")}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const sorted = skills.sort((a, b) => {
|
|
108
|
+
if (a.location !== b.location) {
|
|
109
|
+
return a.location === "project" ? -1 : 1;
|
|
110
|
+
}
|
|
111
|
+
return a.name.localeCompare(b.name);
|
|
112
|
+
});
|
|
113
|
+
for (const skill of sorted) {
|
|
114
|
+
const locationLabel = skill.location === "project" ? chalk.blue("(project)") : chalk.dim("(global)");
|
|
115
|
+
console.log(` ${chalk.bold(skill.name.padEnd(25))} ${locationLabel}`);
|
|
116
|
+
console.log(` ${chalk.dim(skill.description)}
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
const projectCount = skills.filter((s) => s.location === "project").length;
|
|
120
|
+
const globalCount = skills.filter((s) => s.location === "global").length;
|
|
121
|
+
console.log(chalk.dim(`Summary: ${projectCount} project, ${globalCount} global (${skills.length} total)`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/commands/install.ts
|
|
125
|
+
import { ExitPromptError } from "@inquirer/core";
|
|
126
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
127
|
+
import chalk2 from "chalk";
|
|
128
|
+
import { execSync } from "child_process";
|
|
129
|
+
import { cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
|
|
130
|
+
import ora from "ora";
|
|
131
|
+
import { homedir as homedir2 } from "os";
|
|
132
|
+
import { basename, join as join3, resolve, sep } from "path";
|
|
133
|
+
|
|
134
|
+
// src/utils/marketplace-skills.ts
|
|
135
|
+
var ANTHROPIC_MARKETPLACE_SKILLS = [
|
|
136
|
+
// document-skills plugin
|
|
137
|
+
"xlsx",
|
|
138
|
+
"docx",
|
|
139
|
+
"pptx",
|
|
140
|
+
"pdf",
|
|
141
|
+
// example-skills plugin
|
|
142
|
+
"algorithmic-art",
|
|
143
|
+
"artifacts-builder",
|
|
144
|
+
"brand-guidelines",
|
|
145
|
+
"canvas-design",
|
|
146
|
+
"internal-comms",
|
|
147
|
+
"mcp-builder",
|
|
148
|
+
"skill-creator",
|
|
149
|
+
"slack-gif-creator",
|
|
150
|
+
"template-skill",
|
|
151
|
+
"theme-factory",
|
|
152
|
+
"webapp-testing"
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// src/commands/install.ts
|
|
156
|
+
function isLocalPath(source) {
|
|
157
|
+
return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~/") || /^[a-zA-Z]:[\\/]/.test(source);
|
|
158
|
+
}
|
|
159
|
+
function isGitUrl(source) {
|
|
160
|
+
return source.startsWith("git@") || source.startsWith("git://") || source.startsWith("http://") || source.startsWith("https://") || source.endsWith(".git");
|
|
161
|
+
}
|
|
162
|
+
function expandPath(source) {
|
|
163
|
+
if (source.startsWith("~/")) {
|
|
164
|
+
return join3(homedir2(), source.slice(2));
|
|
165
|
+
}
|
|
166
|
+
return resolve(source);
|
|
167
|
+
}
|
|
168
|
+
async function installSkill(source, options) {
|
|
169
|
+
const folder = options.universal ? ".agent/skills" : ".claude/skills";
|
|
170
|
+
const isProject = !options.global;
|
|
171
|
+
const targetDir = isProject ? join3(process.cwd(), folder) : join3(homedir2(), folder);
|
|
172
|
+
const location = isProject ? chalk2.blue(`project (${folder})`) : chalk2.dim(`global (~/${folder})`);
|
|
173
|
+
console.log(`Installing from: ${chalk2.cyan(source)}`);
|
|
174
|
+
console.log(`Location: ${location}
|
|
175
|
+
`);
|
|
176
|
+
if (isLocalPath(source)) {
|
|
177
|
+
const localPath = expandPath(source);
|
|
178
|
+
await installFromLocal(localPath, targetDir, options);
|
|
179
|
+
printPostInstallHints(isProject);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let repoUrl;
|
|
183
|
+
let skillSubpath = "";
|
|
184
|
+
if (isGitUrl(source)) {
|
|
185
|
+
repoUrl = source;
|
|
186
|
+
} else {
|
|
187
|
+
const parts = source.split("/");
|
|
188
|
+
if (parts.length === 2) {
|
|
189
|
+
repoUrl = `https://github.com/${source}`;
|
|
190
|
+
} else if (parts.length > 2) {
|
|
191
|
+
repoUrl = `https://github.com/${parts[0]}/${parts[1]}`;
|
|
192
|
+
skillSubpath = parts.slice(2).join("/");
|
|
193
|
+
} else {
|
|
194
|
+
console.error(chalk2.red("Error: Invalid source format"));
|
|
195
|
+
console.error("Expected: owner/repo, owner/repo/skill-name, git URL, or local path");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const tempDir = join3(homedir2(), `.openskills-temp-${Date.now()}`);
|
|
200
|
+
mkdirSync(tempDir, { recursive: true });
|
|
201
|
+
try {
|
|
202
|
+
const spinner = ora("Cloning repository...").start();
|
|
203
|
+
try {
|
|
204
|
+
execSync(`git clone --depth 1 --quiet "${repoUrl}" "${tempDir}/repo"`, {
|
|
205
|
+
stdio: "pipe"
|
|
206
|
+
});
|
|
207
|
+
spinner.succeed("Repository cloned");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
spinner.fail("Failed to clone repository");
|
|
210
|
+
const err = error;
|
|
211
|
+
if (err.stderr) {
|
|
212
|
+
console.error(chalk2.dim(err.stderr.toString().trim()));
|
|
213
|
+
}
|
|
214
|
+
console.error(chalk2.yellow("\nTip: For private repos, ensure git SSH keys or credentials are configured"));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
const repoDir = join3(tempDir, "repo");
|
|
218
|
+
if (skillSubpath) {
|
|
219
|
+
await installSpecificSkill(repoDir, skillSubpath, targetDir, isProject, options);
|
|
220
|
+
} else {
|
|
221
|
+
await installFromRepo(repoDir, targetDir, options);
|
|
222
|
+
}
|
|
223
|
+
} finally {
|
|
224
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
printPostInstallHints(isProject);
|
|
227
|
+
}
|
|
228
|
+
function printPostInstallHints(isProject) {
|
|
229
|
+
console.log(`
|
|
230
|
+
${chalk2.dim("Read skill:")} ${chalk2.cyan("openskills read <skill-name>")}`);
|
|
231
|
+
if (isProject) {
|
|
232
|
+
console.log(`${chalk2.dim("Sync to AGENTS.md:")} ${chalk2.cyan("openskills sync")}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function installFromLocal(localPath, targetDir, options) {
|
|
236
|
+
if (!existsSync2(localPath)) {
|
|
237
|
+
console.error(chalk2.red(`Error: Path does not exist: ${localPath}`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const stats = statSync2(localPath);
|
|
241
|
+
if (!stats.isDirectory()) {
|
|
242
|
+
console.error(chalk2.red("Error: Path must be a directory"));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
const skillMdPath = join3(localPath, "SKILL.md");
|
|
246
|
+
if (existsSync2(skillMdPath)) {
|
|
247
|
+
const isProject = targetDir.includes(process.cwd());
|
|
248
|
+
await installSingleLocalSkill(localPath, targetDir, isProject, options);
|
|
249
|
+
} else {
|
|
250
|
+
await installFromRepo(localPath, targetDir, options);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function installSingleLocalSkill(skillDir, targetDir, isProject, options) {
|
|
254
|
+
const skillMdPath = join3(skillDir, "SKILL.md");
|
|
255
|
+
const content = readFileSync2(skillMdPath, "utf-8");
|
|
256
|
+
if (!hasValidFrontmatter(content)) {
|
|
257
|
+
console.error(chalk2.red("Error: Invalid SKILL.md (missing YAML frontmatter)"));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const skillName = basename(skillDir);
|
|
261
|
+
const targetPath = join3(targetDir, skillName);
|
|
262
|
+
const shouldInstall = await warnIfConflict(skillName, targetPath, isProject, options.yes);
|
|
263
|
+
if (!shouldInstall) {
|
|
264
|
+
console.log(chalk2.yellow(`Skipped: ${skillName}`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
mkdirSync(targetDir, { recursive: true });
|
|
268
|
+
const resolvedTargetPath = resolve(targetPath);
|
|
269
|
+
const resolvedTargetDir = resolve(targetDir);
|
|
270
|
+
if (!resolvedTargetPath.startsWith(resolvedTargetDir + sep)) {
|
|
271
|
+
console.error(chalk2.red(`Security error: Installation path outside target directory`));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
cpSync(skillDir, targetPath, { recursive: true, dereference: true });
|
|
275
|
+
console.log(chalk2.green(`\u2705 Installed: ${skillName}`));
|
|
276
|
+
console.log(` Location: ${targetPath}`);
|
|
277
|
+
}
|
|
278
|
+
async function installSpecificSkill(repoDir, skillSubpath, targetDir, isProject, options) {
|
|
279
|
+
const skillDir = join3(repoDir, skillSubpath);
|
|
280
|
+
const skillMdPath = join3(skillDir, "SKILL.md");
|
|
281
|
+
if (!existsSync2(skillMdPath)) {
|
|
282
|
+
console.error(chalk2.red(`Error: SKILL.md not found at ${skillSubpath}`));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const content = readFileSync2(skillMdPath, "utf-8");
|
|
286
|
+
if (!hasValidFrontmatter(content)) {
|
|
287
|
+
console.error(chalk2.red("Error: Invalid SKILL.md (missing YAML frontmatter)"));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
const skillName = basename(skillSubpath);
|
|
291
|
+
const targetPath = join3(targetDir, skillName);
|
|
292
|
+
const shouldInstall = await warnIfConflict(skillName, targetPath, isProject, options.yes);
|
|
293
|
+
if (!shouldInstall) {
|
|
294
|
+
console.log(chalk2.yellow(`Skipped: ${skillName}`));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
mkdirSync(targetDir, { recursive: true });
|
|
298
|
+
const resolvedTargetPath = resolve(targetPath);
|
|
299
|
+
const resolvedTargetDir = resolve(targetDir);
|
|
300
|
+
if (!resolvedTargetPath.startsWith(resolvedTargetDir + sep)) {
|
|
301
|
+
console.error(chalk2.red(`Security error: Installation path outside target directory`));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
cpSync(skillDir, targetPath, { recursive: true, dereference: true });
|
|
305
|
+
console.log(chalk2.green(`\u2705 Installed: ${skillName}`));
|
|
306
|
+
console.log(` Location: ${targetPath}`);
|
|
307
|
+
}
|
|
308
|
+
async function installFromRepo(repoDir, targetDir, options) {
|
|
309
|
+
const findSkills = (dir) => {
|
|
310
|
+
const skills = [];
|
|
311
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
const fullPath = join3(dir, entry.name);
|
|
314
|
+
if (entry.isDirectory()) {
|
|
315
|
+
if (existsSync2(join3(fullPath, "SKILL.md"))) {
|
|
316
|
+
skills.push(fullPath);
|
|
317
|
+
} else {
|
|
318
|
+
skills.push(...findSkills(fullPath));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return skills;
|
|
323
|
+
};
|
|
324
|
+
const skillDirs = findSkills(repoDir);
|
|
325
|
+
if (skillDirs.length === 0) {
|
|
326
|
+
console.error(chalk2.red("Error: No SKILL.md files found in repository"));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
console.log(chalk2.dim(`Found ${skillDirs.length} skill(s)
|
|
330
|
+
`));
|
|
331
|
+
const skillInfos = skillDirs.map((skillDir) => {
|
|
332
|
+
const skillMdPath = join3(skillDir, "SKILL.md");
|
|
333
|
+
const content = readFileSync2(skillMdPath, "utf-8");
|
|
334
|
+
if (!hasValidFrontmatter(content)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const skillName = basename(skillDir);
|
|
338
|
+
const description = extractYamlField(content, "description");
|
|
339
|
+
const targetPath = join3(targetDir, skillName);
|
|
340
|
+
const size = getDirectorySize(skillDir);
|
|
341
|
+
return {
|
|
342
|
+
skillDir,
|
|
343
|
+
skillName,
|
|
344
|
+
description,
|
|
345
|
+
targetPath,
|
|
346
|
+
size
|
|
347
|
+
};
|
|
348
|
+
}).filter((info) => info !== null);
|
|
349
|
+
if (skillInfos.length === 0) {
|
|
350
|
+
console.error(chalk2.red("Error: No valid SKILL.md files found"));
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
let skillsToInstall = skillInfos;
|
|
354
|
+
if (!options.yes && skillInfos.length > 1) {
|
|
355
|
+
try {
|
|
356
|
+
const choices = skillInfos.map((info) => ({
|
|
357
|
+
name: `${chalk2.bold(info.skillName.padEnd(25))} ${chalk2.dim(formatSize(info.size))}`,
|
|
358
|
+
value: info.skillName,
|
|
359
|
+
description: info.description.slice(0, 80),
|
|
360
|
+
checked: true
|
|
361
|
+
// Check all by default
|
|
362
|
+
}));
|
|
363
|
+
const selected = await checkbox({
|
|
364
|
+
message: "Select skills to install",
|
|
365
|
+
choices,
|
|
366
|
+
pageSize: 15
|
|
367
|
+
});
|
|
368
|
+
if (selected.length === 0) {
|
|
369
|
+
console.log(chalk2.yellow("No skills selected. Installation cancelled."));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
skillsToInstall = skillInfos.filter((info) => selected.includes(info.skillName));
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof ExitPromptError) {
|
|
375
|
+
console.log(chalk2.yellow("\n\nCancelled by user"));
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const isProject = targetDir === join3(process.cwd(), ".claude/skills");
|
|
382
|
+
let installedCount = 0;
|
|
383
|
+
for (const info of skillsToInstall) {
|
|
384
|
+
const shouldInstall = await warnIfConflict(info.skillName, info.targetPath, isProject, options.yes);
|
|
385
|
+
if (!shouldInstall) {
|
|
386
|
+
console.log(chalk2.yellow(`Skipped: ${info.skillName}`));
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
mkdirSync(targetDir, { recursive: true });
|
|
390
|
+
const resolvedTargetPath = resolve(info.targetPath);
|
|
391
|
+
const resolvedTargetDir = resolve(targetDir);
|
|
392
|
+
if (!resolvedTargetPath.startsWith(resolvedTargetDir + sep)) {
|
|
393
|
+
console.error(chalk2.red(`Security error: Installation path outside target directory`));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
cpSync(info.skillDir, info.targetPath, { recursive: true, dereference: true });
|
|
397
|
+
console.log(chalk2.green(`\u2705 Installed: ${info.skillName}`));
|
|
398
|
+
installedCount++;
|
|
399
|
+
}
|
|
400
|
+
console.log(chalk2.green(`
|
|
401
|
+
\u2705 Installation complete: ${installedCount} skill(s) installed`));
|
|
402
|
+
}
|
|
403
|
+
async function warnIfConflict(skillName, targetPath, isProject, skipPrompt = false) {
|
|
404
|
+
if (existsSync2(targetPath)) {
|
|
405
|
+
if (skipPrompt) {
|
|
406
|
+
console.log(chalk2.dim(`Overwriting: ${skillName}`));
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const shouldOverwrite = await confirm({
|
|
411
|
+
message: chalk2.yellow(`Skill '${skillName}' already exists. Overwrite?`),
|
|
412
|
+
default: false
|
|
413
|
+
});
|
|
414
|
+
if (!shouldOverwrite) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof ExitPromptError) {
|
|
419
|
+
console.log(chalk2.yellow("\n\nCancelled by user"));
|
|
420
|
+
process.exit(0);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!isProject && ANTHROPIC_MARKETPLACE_SKILLS.includes(skillName)) {
|
|
426
|
+
console.warn(chalk2.yellow(`
|
|
427
|
+
\u26A0\uFE0F Warning: '${skillName}' matches an Anthropic marketplace skill`));
|
|
428
|
+
console.warn(chalk2.dim(" Installing globally may conflict with Claude Code plugins."));
|
|
429
|
+
console.warn(chalk2.dim(" If you re-enable Claude plugins, this will be overwritten."));
|
|
430
|
+
console.warn(chalk2.dim(" Recommend: Use --project flag for conflict-free installation.\n"));
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
function getDirectorySize(dirPath) {
|
|
435
|
+
let size = 0;
|
|
436
|
+
const entries = readdirSync2(dirPath, { withFileTypes: true });
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
const fullPath = join3(dirPath, entry.name);
|
|
439
|
+
if (entry.isFile()) {
|
|
440
|
+
size += statSync2(fullPath).size;
|
|
441
|
+
} else if (entry.isDirectory()) {
|
|
442
|
+
size += getDirectorySize(fullPath);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return size;
|
|
446
|
+
}
|
|
447
|
+
function formatSize(bytes) {
|
|
448
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
449
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
450
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/read.ts
|
|
454
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
455
|
+
function readSkill(skillName) {
|
|
456
|
+
const skill = findSkill(skillName);
|
|
457
|
+
if (!skill) {
|
|
458
|
+
console.error(`Error: Skill '${skillName}' not found`);
|
|
459
|
+
console.error("\nSearched:");
|
|
460
|
+
console.error(" .agent/skills/ (project universal)");
|
|
461
|
+
console.error(" ~/.agent/skills/ (global universal)");
|
|
462
|
+
console.error(" .claude/skills/ (project)");
|
|
463
|
+
console.error(" ~/.claude/skills/ (global)");
|
|
464
|
+
console.error("\nInstall skills: openskills install owner/repo");
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
const content = readFileSync3(skill.path, "utf-8");
|
|
468
|
+
console.log(`Reading: ${skillName}`);
|
|
469
|
+
console.log(`Base directory: ${skill.baseDir}`);
|
|
470
|
+
console.log("");
|
|
471
|
+
console.log(content);
|
|
472
|
+
console.log("");
|
|
473
|
+
console.log(`Skill read: ${skillName}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/commands/remove.ts
|
|
477
|
+
import { rmSync as rmSync2 } from "fs";
|
|
478
|
+
import { homedir as homedir3 } from "os";
|
|
479
|
+
function removeSkill(skillName) {
|
|
480
|
+
const skill = findSkill(skillName);
|
|
481
|
+
if (!skill) {
|
|
482
|
+
console.error(`Error: Skill '${skillName}' not found`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
rmSync2(skill.baseDir, { recursive: true, force: true });
|
|
486
|
+
const location = skill.source.includes(homedir3()) ? "global" : "project";
|
|
487
|
+
console.log(`\u2705 Removed: ${skillName}`);
|
|
488
|
+
console.log(` From: ${location} (${skill.source})`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/commands/manage.ts
|
|
492
|
+
import { rmSync as rmSync3 } from "fs";
|
|
493
|
+
import chalk3 from "chalk";
|
|
494
|
+
import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
495
|
+
import { ExitPromptError as ExitPromptError2 } from "@inquirer/core";
|
|
496
|
+
async function manageSkills() {
|
|
497
|
+
const skills = findAllSkills();
|
|
498
|
+
if (skills.length === 0) {
|
|
499
|
+
console.log("No skills installed.");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const sorted = skills.sort((a, b) => {
|
|
504
|
+
if (a.location !== b.location) {
|
|
505
|
+
return a.location === "project" ? -1 : 1;
|
|
506
|
+
}
|
|
507
|
+
return a.name.localeCompare(b.name);
|
|
508
|
+
});
|
|
509
|
+
const choices = sorted.map((skill) => ({
|
|
510
|
+
name: `${chalk3.bold(skill.name.padEnd(25))} ${skill.location === "project" ? chalk3.blue("(project)") : chalk3.dim("(global)")}`,
|
|
511
|
+
value: skill.name,
|
|
512
|
+
checked: false
|
|
513
|
+
// Nothing checked by default
|
|
514
|
+
}));
|
|
515
|
+
const toRemove = await checkbox2({
|
|
516
|
+
message: "Select skills to remove",
|
|
517
|
+
choices,
|
|
518
|
+
pageSize: 15
|
|
519
|
+
});
|
|
520
|
+
if (toRemove.length === 0) {
|
|
521
|
+
console.log(chalk3.yellow("No skills selected for removal."));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
for (const skillName of toRemove) {
|
|
525
|
+
const skill = findSkill(skillName);
|
|
526
|
+
if (skill) {
|
|
527
|
+
rmSync3(skill.baseDir, { recursive: true, force: true });
|
|
528
|
+
const location = skill.source.includes(process.cwd()) ? "project" : "global";
|
|
529
|
+
console.log(chalk3.green(`\u2705 Removed: ${skillName} (${location})`));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
console.log(chalk3.green(`
|
|
533
|
+
\u2705 Removed ${toRemove.length} skill(s)`));
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error instanceof ExitPromptError2) {
|
|
536
|
+
console.log(chalk3.yellow("\n\nCancelled by user"));
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/sync.ts
|
|
544
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
545
|
+
import { dirname, basename as basename2 } from "path";
|
|
546
|
+
import chalk4 from "chalk";
|
|
547
|
+
import { checkbox as checkbox3 } from "@inquirer/prompts";
|
|
548
|
+
import { ExitPromptError as ExitPromptError3 } from "@inquirer/core";
|
|
549
|
+
|
|
550
|
+
// src/utils/agents-md.ts
|
|
551
|
+
function parseCurrentSkills(content) {
|
|
552
|
+
const skillNames = [];
|
|
553
|
+
const skillRegex = /<skill>[\s\S]*?<name>([^<]+)<\/name>[\s\S]*?<\/skill>/g;
|
|
554
|
+
let match;
|
|
555
|
+
while ((match = skillRegex.exec(content)) !== null) {
|
|
556
|
+
skillNames.push(match[1].trim());
|
|
557
|
+
}
|
|
558
|
+
return skillNames;
|
|
559
|
+
}
|
|
560
|
+
function generateSkillsXml(skills) {
|
|
561
|
+
const skillTags = skills.map(
|
|
562
|
+
(s) => `<skill>
|
|
563
|
+
<name>${s.name}</name>
|
|
564
|
+
<description>${s.description}</description>
|
|
565
|
+
<location>${s.location}</location>
|
|
566
|
+
</skill>`
|
|
567
|
+
).join("\n\n");
|
|
568
|
+
return `<skills_system priority="1">
|
|
569
|
+
|
|
570
|
+
## Available Skills
|
|
571
|
+
|
|
572
|
+
<!-- SKILLS_TABLE_START -->
|
|
573
|
+
<usage>
|
|
574
|
+
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
|
575
|
+
|
|
576
|
+
How to use skills:
|
|
577
|
+
- Invoke: Bash("openskills read <skill-name>")
|
|
578
|
+
- The skill content will load with detailed instructions on how to complete the task
|
|
579
|
+
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
|
|
580
|
+
|
|
581
|
+
Usage notes:
|
|
582
|
+
- Only use skills listed in <available_skills> below
|
|
583
|
+
- Do not invoke a skill that is already loaded in your context
|
|
584
|
+
- Each skill invocation is stateless
|
|
585
|
+
</usage>
|
|
586
|
+
|
|
587
|
+
<available_skills>
|
|
588
|
+
|
|
589
|
+
${skillTags}
|
|
590
|
+
|
|
591
|
+
</available_skills>
|
|
592
|
+
<!-- SKILLS_TABLE_END -->
|
|
593
|
+
|
|
594
|
+
</skills_system>`;
|
|
595
|
+
}
|
|
596
|
+
function replaceSkillsSection(content, newSection) {
|
|
597
|
+
const startMarker = "<skills_system";
|
|
598
|
+
const endMarker = "</skills_system>";
|
|
599
|
+
if (content.includes(startMarker)) {
|
|
600
|
+
const regex = /<skills_system[^>]*>[\s\S]*?<\/skills_system>/;
|
|
601
|
+
return content.replace(regex, newSection);
|
|
602
|
+
}
|
|
603
|
+
const htmlStartMarker = "<!-- SKILLS_TABLE_START -->";
|
|
604
|
+
const htmlEndMarker = "<!-- SKILLS_TABLE_END -->";
|
|
605
|
+
if (content.includes(htmlStartMarker)) {
|
|
606
|
+
const innerContent = newSection.replace(/<skills_system[^>]*>|<\/skills_system>/g, "");
|
|
607
|
+
const regex = new RegExp(
|
|
608
|
+
`${htmlStartMarker}[\\s\\S]*?${htmlEndMarker}`,
|
|
609
|
+
"g"
|
|
610
|
+
);
|
|
611
|
+
return content.replace(regex, `${htmlStartMarker}
|
|
612
|
+
${innerContent}
|
|
613
|
+
${htmlEndMarker}`);
|
|
614
|
+
}
|
|
615
|
+
return content.trimEnd() + "\n\n" + newSection + "\n";
|
|
616
|
+
}
|
|
617
|
+
function removeSkillsSection(content) {
|
|
618
|
+
const startMarker = "<skills_system";
|
|
619
|
+
const endMarker = "</skills_system>";
|
|
620
|
+
if (content.includes(startMarker)) {
|
|
621
|
+
const regex = /<skills_system[^>]*>[\s\S]*?<\/skills_system>/;
|
|
622
|
+
return content.replace(regex, "<!-- Skills section removed -->");
|
|
623
|
+
}
|
|
624
|
+
const htmlStartMarker = "<!-- SKILLS_TABLE_START -->";
|
|
625
|
+
const htmlEndMarker = "<!-- SKILLS_TABLE_END -->";
|
|
626
|
+
if (content.includes(htmlStartMarker)) {
|
|
627
|
+
const regex = new RegExp(
|
|
628
|
+
`${htmlStartMarker}[\\s\\S]*?${htmlEndMarker}`,
|
|
629
|
+
"g"
|
|
630
|
+
);
|
|
631
|
+
return content.replace(regex, `${htmlStartMarker}
|
|
632
|
+
<!-- Skills section removed -->
|
|
633
|
+
${htmlEndMarker}`);
|
|
634
|
+
}
|
|
635
|
+
return content;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/commands/sync.ts
|
|
639
|
+
async function syncAgentsMd(options = {}) {
|
|
640
|
+
const outputPath = options.output || "AGENTS.md";
|
|
641
|
+
const outputName = basename2(outputPath);
|
|
642
|
+
if (!outputPath.endsWith(".md")) {
|
|
643
|
+
console.error(chalk4.red("Error: Output file must be a markdown file (.md)"));
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
if (!existsSync3(outputPath)) {
|
|
647
|
+
const dir = dirname(outputPath);
|
|
648
|
+
if (dir && dir !== "." && !existsSync3(dir)) {
|
|
649
|
+
mkdirSync2(dir, { recursive: true });
|
|
650
|
+
}
|
|
651
|
+
writeFileSync(outputPath, `# ${outputName.replace(".md", "")}
|
|
652
|
+
|
|
653
|
+
`);
|
|
654
|
+
console.log(chalk4.dim(`Created ${outputPath}`));
|
|
655
|
+
}
|
|
656
|
+
let skills = findAllSkills();
|
|
657
|
+
if (skills.length === 0) {
|
|
658
|
+
console.log("No skills installed. Install skills first:");
|
|
659
|
+
console.log(` ${chalk4.cyan("openskills install anthropics/skills --project")}`);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!options.yes) {
|
|
663
|
+
try {
|
|
664
|
+
const content2 = readFileSync4(outputPath, "utf-8");
|
|
665
|
+
const currentSkills = parseCurrentSkills(content2);
|
|
666
|
+
const sorted = skills.sort((a, b) => {
|
|
667
|
+
if (a.location !== b.location) {
|
|
668
|
+
return a.location === "project" ? -1 : 1;
|
|
669
|
+
}
|
|
670
|
+
return a.name.localeCompare(b.name);
|
|
671
|
+
});
|
|
672
|
+
const choices = sorted.map((skill) => ({
|
|
673
|
+
name: `${chalk4.bold(skill.name.padEnd(25))} ${skill.location === "project" ? chalk4.blue("(project)") : chalk4.dim("(global)")}`,
|
|
674
|
+
value: skill.name,
|
|
675
|
+
description: skill.description.slice(0, 70),
|
|
676
|
+
// Pre-select if currently in file, otherwise default to project skills
|
|
677
|
+
checked: currentSkills.includes(skill.name) || currentSkills.length === 0 && skill.location === "project"
|
|
678
|
+
}));
|
|
679
|
+
const selected = await checkbox3({
|
|
680
|
+
message: `Select skills to sync to ${outputName}`,
|
|
681
|
+
choices,
|
|
682
|
+
pageSize: 15
|
|
683
|
+
});
|
|
684
|
+
if (selected.length === 0) {
|
|
685
|
+
const content3 = readFileSync4(outputPath, "utf-8");
|
|
686
|
+
const updated2 = removeSkillsSection(content3);
|
|
687
|
+
writeFileSync(outputPath, updated2);
|
|
688
|
+
console.log(chalk4.green(`\u2705 Removed all skills from ${outputName}`));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
skills = skills.filter((s) => selected.includes(s.name));
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (error instanceof ExitPromptError3) {
|
|
694
|
+
console.log(chalk4.yellow("\n\nCancelled by user"));
|
|
695
|
+
process.exit(0);
|
|
696
|
+
}
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const xml = generateSkillsXml(skills);
|
|
701
|
+
const content = readFileSync4(outputPath, "utf-8");
|
|
702
|
+
const updated = replaceSkillsSection(content, xml);
|
|
703
|
+
writeFileSync(outputPath, updated);
|
|
704
|
+
const hadMarkers = content.includes("<skills_system") || content.includes("<!-- SKILLS_TABLE_START -->");
|
|
705
|
+
if (hadMarkers) {
|
|
706
|
+
console.log(chalk4.green(`\u2705 Synced ${skills.length} skill(s) to ${outputName}`));
|
|
707
|
+
} else {
|
|
708
|
+
console.log(chalk4.green(`\u2705 Added skills section to ${outputName} (${skills.length} skill(s))`));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/cli.ts
|
|
713
|
+
var program = new Command();
|
|
714
|
+
program.name("openskills").description("Universal skills loader for AI coding agents").version("1.2.1").showHelpAfterError(false).exitOverride((err) => {
|
|
715
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version") {
|
|
716
|
+
process.exit(0);
|
|
717
|
+
}
|
|
718
|
+
if (err.code === "commander.missingArgument" || err.code === "commander.missingMandatoryOptionValue") {
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
if (err.code === "commander.unknownOption" || err.code === "commander.invalidArgument") {
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
process.exit(err.exitCode || 1);
|
|
725
|
+
});
|
|
726
|
+
program.command("list").description("List all installed skills").action(listSkills);
|
|
727
|
+
program.command("install <source>").description("Install skill from GitHub or Git URL").option("-g, --global", "Install globally (default: project install)").option("-u, --universal", "Install to .agent/skills/ (for universal AGENTS.md usage)").option("-y, --yes", "Skip interactive selection, install all skills found").action(installSkill);
|
|
728
|
+
program.command("read <skill-name>").description("Read skill to stdout (for AI agents)").action(readSkill);
|
|
729
|
+
program.command("sync").description("Update AGENTS.md with installed skills (interactive, pre-selects current state)").option("-y, --yes", "Skip interactive selection, sync all skills").option("-o, --output <path>", "Output file path (default: AGENTS.md)").action(syncAgentsMd);
|
|
730
|
+
program.command("manage").description("Interactively manage (remove) installed skills").action(manageSkills);
|
|
731
|
+
program.command("remove <skill-name>").alias("rm").description("Remove specific skill (for scripts, use manage for interactive)").action(removeSkill);
|
|
732
|
+
program.parse();
|