ai-agent-skills 1.2.0 → 1.2.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/README.md +7 -11
- package/cli.js +113 -42
- package/package.json +2 -2
- package/skills.json +6 -6
- package/.github/FUNDING.yml +0 -2
- package/.github/ISSUE_TEMPLATE/bug-report.yml +0 -41
- package/.github/ISSUE_TEMPLATE/config.yml +0 -11
- package/.github/ISSUE_TEMPLATE/skill-request.yml +0 -54
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -23
- package/.github/workflows/validate.yml +0 -188
- package/CHANGELOG.md +0 -120
- package/CONTRIBUTING.md +0 -84
- package/test.js +0 -180
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Homebrew for AI agent skills.</strong><br>
|
|
9
|
-
|
|
9
|
+
Universal Skills installer for any agent that follows the open standard spec
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -179,10 +179,8 @@ cp -r Ai-Agent-Skills/skills/pdf ~/.claude/skills/
|
|
|
179
179
|
|
|
180
180
|
## Create Your Own
|
|
181
181
|
|
|
182
|
-
Two options:
|
|
183
182
|
|
|
184
|
-
1. **
|
|
185
|
-
2. **Build manually**: Follow the [Agent Skills spec](https://agentskills.io/specification)
|
|
183
|
+
1. **Build manually**: Follow the [Agent Skills spec](https://agentskills.io/specification)
|
|
186
184
|
|
|
187
185
|
## What Are Agent Skills?
|
|
188
186
|
|
|
@@ -209,19 +207,17 @@ We review all contributions for quality and spec compliance.
|
|
|
209
207
|
|
|
210
208
|
## Links
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
|
|
213
211
|
- [Agent Skills Spec](https://agentskills.io) - Official format documentation
|
|
214
|
-
- [Browse Skills](https://skillcreator.ai/
|
|
215
|
-
- [Create Skills](https://skillcreator.ai/build) - Generate skills
|
|
212
|
+
- [Browse Skills](https://skillcreator.ai/explore) - Visual skill gallery with one-click install
|
|
213
|
+
- [Create Skills](https://skillcreator.ai/build) - Generate skills (waitlist)
|
|
216
214
|
- [Anthropic Skills](https://github.com/anthropics/skills) - Official example skills
|
|
217
215
|
|
|
218
216
|
## See Also
|
|
219
217
|
|
|
220
|
-
**[openskills](https://github.com/numman-ali/openskills)** -
|
|
221
|
-
|
|
222
|
-
**We took a different approach**: Homebrew-style simplicity. No global install, no sync step, no AGENTS.md. Just `npx ai-agent-skills install pdf` and it lands in the right folder for your agent. Curated catalog, native paths, zero config.
|
|
218
|
+
**[openskills](https://github.com/numman-ali/openskills)** - another universal skills loader that inspired parts of this project (created pre the open agent skills standard) & Requires global install, AGENTS.md sync, and Bash calls. Great for flexibility.
|
|
223
219
|
|
|
224
|
-
|
|
220
|
+
**ai-agent-skills** - Just `npx`, installs to native agent folders. Homebrew for skills.
|
|
225
221
|
|
|
226
222
|
---
|
|
227
223
|
|
package/cli.js
CHANGED
|
@@ -202,7 +202,10 @@ function parseArgs(args) {
|
|
|
202
202
|
|
|
203
203
|
// ============ SAFE FILE OPERATIONS ============
|
|
204
204
|
|
|
205
|
-
function copyDir(src, dest, currentSize = { total: 0 }) {
|
|
205
|
+
function copyDir(src, dest, currentSize = { total: 0 }, rootSrc = null) {
|
|
206
|
+
// Track root source to prevent path escape attacks
|
|
207
|
+
if (rootSrc === null) rootSrc = src;
|
|
208
|
+
|
|
206
209
|
try {
|
|
207
210
|
if (fs.existsSync(dest)) {
|
|
208
211
|
fs.rmSync(dest, { recursive: true });
|
|
@@ -211,13 +214,32 @@ function copyDir(src, dest, currentSize = { total: 0 }) {
|
|
|
211
214
|
|
|
212
215
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
213
216
|
|
|
217
|
+
// Files/folders to skip during copy
|
|
218
|
+
const skipList = ['.git', '.github', 'node_modules', '.DS_Store'];
|
|
219
|
+
|
|
214
220
|
for (const entry of entries) {
|
|
221
|
+
// Skip unnecessary files/folders
|
|
222
|
+
if (skipList.includes(entry.name)) continue;
|
|
223
|
+
|
|
224
|
+
// Skip symlinks to prevent path escape attacks
|
|
225
|
+
if (entry.isSymbolicLink()) {
|
|
226
|
+
warn(`Skipping symlink: ${entry.name}`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
215
230
|
const srcPath = path.join(src, entry.name);
|
|
216
231
|
const destPath = path.join(dest, entry.name);
|
|
217
232
|
|
|
233
|
+
// Verify resolved path stays within source directory (prevent path traversal)
|
|
234
|
+
const resolvedSrc = fs.realpathSync(srcPath);
|
|
235
|
+
if (!resolvedSrc.startsWith(fs.realpathSync(rootSrc))) {
|
|
236
|
+
warn(`Skipping file outside source directory: ${entry.name}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
218
240
|
if (entry.isDirectory()) {
|
|
219
|
-
copyDir(srcPath, destPath, currentSize);
|
|
220
|
-
} else {
|
|
241
|
+
copyDir(srcPath, destPath, currentSize, rootSrc);
|
|
242
|
+
} else if (entry.isFile()) {
|
|
221
243
|
const stat = fs.statSync(srcPath);
|
|
222
244
|
currentSize.total += stat.size;
|
|
223
245
|
|
|
@@ -227,6 +249,7 @@ function copyDir(src, dest, currentSize = { total: 0 }) {
|
|
|
227
249
|
|
|
228
250
|
fs.copyFileSync(srcPath, destPath);
|
|
229
251
|
}
|
|
252
|
+
// Skip any other special file types (sockets, devices, etc.)
|
|
230
253
|
}
|
|
231
254
|
} catch (e) {
|
|
232
255
|
// Clean up partial install on failure
|
|
@@ -395,7 +418,6 @@ function getInstalledSkills(agent = 'claude') {
|
|
|
395
418
|
function listInstalledSkills(agent = 'claude') {
|
|
396
419
|
const installed = getInstalledSkills(agent);
|
|
397
420
|
const destDir = AGENT_PATHS[agent] || AGENT_PATHS.claude;
|
|
398
|
-
const data = loadSkillsJson();
|
|
399
421
|
|
|
400
422
|
if (installed.length === 0) {
|
|
401
423
|
warn(`No skills installed for ${agent}`);
|
|
@@ -406,43 +428,12 @@ function listInstalledSkills(agent = 'claude') {
|
|
|
406
428
|
log(`\n${colors.bold}Installed Skills${colors.reset} (${installed.length} for ${agent})\n`);
|
|
407
429
|
log(`${colors.dim}Location: ${destDir}${colors.reset}\n`);
|
|
408
430
|
|
|
409
|
-
// Check for updates
|
|
410
|
-
let updatesAvailable = 0;
|
|
411
|
-
|
|
412
431
|
installed.forEach(name => {
|
|
413
|
-
|
|
414
|
-
const hasUpdate = skill && skill.lastUpdated && isUpdateAvailable(name, agent, skill.lastUpdated);
|
|
415
|
-
|
|
416
|
-
if (hasUpdate) {
|
|
417
|
-
updatesAvailable++;
|
|
418
|
-
log(` ${colors.green}${name}${colors.reset} ${colors.yellow}(update available)${colors.reset}`);
|
|
419
|
-
} else {
|
|
420
|
-
log(` ${colors.green}${name}${colors.reset}`);
|
|
421
|
-
}
|
|
432
|
+
log(` ${colors.green}${name}${colors.reset}`);
|
|
422
433
|
});
|
|
423
434
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
log(`${colors.dim}Run: npx ai-agent-skills update <name> --agent ${agent}${colors.reset}`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
log(`\n${colors.dim}Uninstall: npx ai-agent-skills uninstall <name> --agent ${agent}${colors.reset}`);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function isUpdateAvailable(skillName, agent, repoLastUpdated) {
|
|
433
|
-
// Simple check based on file modification time
|
|
434
|
-
const destDir = AGENT_PATHS[agent] || AGENT_PATHS.claude;
|
|
435
|
-
const skillPath = path.join(destDir, skillName, 'SKILL.md');
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
if (!fs.existsSync(skillPath)) return false;
|
|
439
|
-
const stat = fs.statSync(skillPath);
|
|
440
|
-
const installedTime = stat.mtime.getTime();
|
|
441
|
-
const repoTime = new Date(repoLastUpdated).getTime();
|
|
442
|
-
return repoTime > installedTime;
|
|
443
|
-
} catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
435
|
+
log(`\n${colors.dim}Update: npx ai-agent-skills update <name> --agent ${agent}${colors.reset}`);
|
|
436
|
+
log(`${colors.dim}Uninstall: npx ai-agent-skills uninstall <name> --agent ${agent}${colors.reset}`);
|
|
446
437
|
}
|
|
447
438
|
|
|
448
439
|
function updateSkill(skillName, agent = 'claude', dryRun = false) {
|
|
@@ -768,11 +759,28 @@ async function browseSkills(agent = 'claude') {
|
|
|
768
759
|
// ============ EXTERNAL INSTALL (GitHub/Local) ============
|
|
769
760
|
|
|
770
761
|
function isGitHubUrl(source) {
|
|
771
|
-
|
|
762
|
+
// Must have owner/repo format, not start with path indicators
|
|
763
|
+
return source.includes('/') &&
|
|
764
|
+
!source.startsWith('./') &&
|
|
765
|
+
!source.startsWith('../') &&
|
|
766
|
+
!source.startsWith('/') &&
|
|
767
|
+
!source.startsWith('~') &&
|
|
768
|
+
!isWindowsPath(source);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function isWindowsPath(source) {
|
|
772
|
+
// Match Windows absolute paths like C:\, D:\, etc.
|
|
773
|
+
return /^[a-zA-Z]:[\\\/]/.test(source);
|
|
772
774
|
}
|
|
773
775
|
|
|
774
776
|
function isLocalPath(source) {
|
|
775
|
-
|
|
777
|
+
// Explicit local paths: ./ or / or ~/ or Windows paths like C:\
|
|
778
|
+
// Also accept ../ as local path (will be resolved)
|
|
779
|
+
return source.startsWith('./') ||
|
|
780
|
+
source.startsWith('../') ||
|
|
781
|
+
source.startsWith('/') ||
|
|
782
|
+
source.startsWith('~/') ||
|
|
783
|
+
isWindowsPath(source);
|
|
776
784
|
}
|
|
777
785
|
|
|
778
786
|
function expandPath(p) {
|
|
@@ -782,8 +790,23 @@ function expandPath(p) {
|
|
|
782
790
|
return path.resolve(p);
|
|
783
791
|
}
|
|
784
792
|
|
|
793
|
+
// Validate GitHub owner/repo names (alphanumeric, hyphens, underscores, dots)
|
|
794
|
+
function validateGitHubName(name, type = 'name') {
|
|
795
|
+
if (!name || typeof name !== 'string') {
|
|
796
|
+
throw new Error(`Invalid GitHub ${type}`);
|
|
797
|
+
}
|
|
798
|
+
// GitHub allows: alphanumeric, hyphens, underscores, dots (no leading/trailing dots for repos)
|
|
799
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
800
|
+
throw new Error(`Invalid GitHub ${type}: "${name}" contains invalid characters`);
|
|
801
|
+
}
|
|
802
|
+
if (name.length > 100) {
|
|
803
|
+
throw new Error(`GitHub ${type} too long: ${name.length} > 100 characters`);
|
|
804
|
+
}
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
|
|
785
808
|
async function installFromGitHub(source, agent = 'claude', dryRun = false) {
|
|
786
|
-
const {
|
|
809
|
+
const { execFileSync } = require('child_process');
|
|
787
810
|
|
|
788
811
|
// Parse owner/repo format
|
|
789
812
|
const parts = source.split('/');
|
|
@@ -796,6 +819,18 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
|
|
|
796
819
|
const repo = parts[1];
|
|
797
820
|
const skillName = parts[2]; // Optional specific skill
|
|
798
821
|
|
|
822
|
+
// Validate owner and repo to prevent injection attacks
|
|
823
|
+
try {
|
|
824
|
+
validateGitHubName(owner, 'owner');
|
|
825
|
+
validateGitHubName(repo, 'repository');
|
|
826
|
+
if (skillName) {
|
|
827
|
+
validateSkillName(skillName);
|
|
828
|
+
}
|
|
829
|
+
} catch (e) {
|
|
830
|
+
error(e.message);
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
799
834
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
800
835
|
const tempDir = path.join(os.tmpdir(), `ai-skills-${Date.now()}`);
|
|
801
836
|
|
|
@@ -809,13 +844,17 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
|
|
|
809
844
|
|
|
810
845
|
try {
|
|
811
846
|
info(`Cloning ${owner}/${repo}...`);
|
|
812
|
-
|
|
847
|
+
// Use execFileSync with args array to prevent shell injection
|
|
848
|
+
execFileSync('git', ['clone', '--depth', '1', repoUrl, tempDir], { stdio: 'pipe' });
|
|
813
849
|
|
|
814
850
|
// Find skills in the cloned repo
|
|
815
851
|
const skillsDir = fs.existsSync(path.join(tempDir, 'skills'))
|
|
816
852
|
? path.join(tempDir, 'skills')
|
|
817
853
|
: tempDir;
|
|
818
854
|
|
|
855
|
+
// Check if repo root IS a skill (has SKILL.md at root)
|
|
856
|
+
const isRootSkill = fs.existsSync(path.join(tempDir, 'SKILL.md'));
|
|
857
|
+
|
|
819
858
|
if (skillName) {
|
|
820
859
|
// Install specific skill
|
|
821
860
|
const skillPath = path.join(skillsDir, skillName);
|
|
@@ -835,6 +874,29 @@ async function installFromGitHub(source, agent = 'claude', dryRun = false) {
|
|
|
835
874
|
copyDir(skillPath, destPath);
|
|
836
875
|
success(`\nInstalled: ${skillName} from ${owner}/${repo}`);
|
|
837
876
|
info(`Location: ${destPath}`);
|
|
877
|
+
} else if (isRootSkill) {
|
|
878
|
+
// Repo itself is a single skill
|
|
879
|
+
// Sanitize repo name to valid skill name (lowercase, alphanumeric + hyphens)
|
|
880
|
+
const skillName = repo.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
validateSkillName(skillName);
|
|
884
|
+
} catch (e) {
|
|
885
|
+
error(`Cannot install: repo name "${repo}" cannot be converted to valid skill name`);
|
|
886
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const destDir = AGENT_PATHS[agent] || AGENT_PATHS.claude;
|
|
891
|
+
const destPath = path.join(destDir, skillName);
|
|
892
|
+
|
|
893
|
+
if (!fs.existsSync(destDir)) {
|
|
894
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
copyDir(tempDir, destPath);
|
|
898
|
+
success(`\nInstalled: ${skillName} from ${owner}/${repo}`);
|
|
899
|
+
info(`Location: ${destPath}`);
|
|
838
900
|
} else {
|
|
839
901
|
// Install all skills from repo
|
|
840
902
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
@@ -970,6 +1032,7 @@ ${colors.bold}Commands:${colors.reset}
|
|
|
970
1032
|
${colors.green}search <query>${colors.reset} Search skills by name, description, or tags
|
|
971
1033
|
${colors.green}info <name>${colors.reset} Show skill details
|
|
972
1034
|
${colors.green}config${colors.reset} Show/edit configuration
|
|
1035
|
+
${colors.green}version${colors.reset} Show version number
|
|
973
1036
|
${colors.green}help${colors.reset} Show this help
|
|
974
1037
|
|
|
975
1038
|
${colors.bold}Options:${colors.reset}
|
|
@@ -978,6 +1041,7 @@ ${colors.bold}Options:${colors.reset}
|
|
|
978
1041
|
${colors.cyan}--dry-run, -n${colors.reset} Preview changes without applying
|
|
979
1042
|
${colors.cyan}--category <c>${colors.reset} Filter by category
|
|
980
1043
|
${colors.cyan}--all${colors.reset} Apply to all (with update)
|
|
1044
|
+
${colors.cyan}--version, -v${colors.reset} Show version number
|
|
981
1045
|
|
|
982
1046
|
${colors.bold}Agents:${colors.reset}
|
|
983
1047
|
${colors.cyan}claude${colors.reset} (default) ~/.claude/skills/
|
|
@@ -1202,6 +1266,13 @@ switch (command || 'help') {
|
|
|
1202
1266
|
showHelp();
|
|
1203
1267
|
break;
|
|
1204
1268
|
|
|
1269
|
+
case 'version':
|
|
1270
|
+
case '--version':
|
|
1271
|
+
case '-v':
|
|
1272
|
+
const pkg = require('./package.json');
|
|
1273
|
+
log(`ai-agent-skills v${pkg.version}`);
|
|
1274
|
+
break;
|
|
1275
|
+
|
|
1205
1276
|
default:
|
|
1206
1277
|
// If command looks like a skill name, try to install it
|
|
1207
1278
|
if (getAvailableSkills().includes(command)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-agent-skills",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Install curated AI agent skills with one command. Works with Claude Code, Cursor, Amp, VS Code, and all Agent Skills compatible tools.",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"skills": "./cli.js"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=14.
|
|
11
|
+
"node": ">=14.14.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node test.js",
|
package/skills.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.
|
|
3
|
-
"updated": "2025-12-
|
|
2
|
+
"version": "1.2.2",
|
|
3
|
+
"updated": "2025-12-25T00:00:00Z",
|
|
4
4
|
"total": 39,
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
@@ -516,7 +516,7 @@
|
|
|
516
516
|
"id": "development",
|
|
517
517
|
"name": "Development",
|
|
518
518
|
"description": "Coding and software development skills",
|
|
519
|
-
"count":
|
|
519
|
+
"count": 13
|
|
520
520
|
},
|
|
521
521
|
{
|
|
522
522
|
"id": "document",
|
|
@@ -528,19 +528,19 @@
|
|
|
528
528
|
"id": "creative",
|
|
529
529
|
"name": "Creative",
|
|
530
530
|
"description": "Design and creative skills",
|
|
531
|
-
"count":
|
|
531
|
+
"count": 6
|
|
532
532
|
},
|
|
533
533
|
{
|
|
534
534
|
"id": "business",
|
|
535
535
|
"name": "Business",
|
|
536
536
|
"description": "Business and communication skills",
|
|
537
|
-
"count":
|
|
537
|
+
"count": 5
|
|
538
538
|
},
|
|
539
539
|
{
|
|
540
540
|
"id": "productivity",
|
|
541
541
|
"name": "Productivity",
|
|
542
542
|
"description": "Productivity and workflow skills",
|
|
543
|
-
"count":
|
|
543
|
+
"count": 11
|
|
544
544
|
}
|
|
545
545
|
]
|
|
546
546
|
}
|
package/.github/FUNDING.yml
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
name: Bug Report
|
|
2
|
-
description: Report an issue with a skill
|
|
3
|
-
title: "[Bug] "
|
|
4
|
-
labels: ["bug"]
|
|
5
|
-
body:
|
|
6
|
-
- type: input
|
|
7
|
-
id: skill
|
|
8
|
-
attributes:
|
|
9
|
-
label: Skill Name
|
|
10
|
-
description: Which skill has the issue?
|
|
11
|
-
placeholder: e.g., frontend-design
|
|
12
|
-
validations:
|
|
13
|
-
required: true
|
|
14
|
-
|
|
15
|
-
- type: textarea
|
|
16
|
-
id: description
|
|
17
|
-
attributes:
|
|
18
|
-
label: What happened?
|
|
19
|
-
description: Describe the bug
|
|
20
|
-
validations:
|
|
21
|
-
required: true
|
|
22
|
-
|
|
23
|
-
- type: textarea
|
|
24
|
-
id: expected
|
|
25
|
-
attributes:
|
|
26
|
-
label: Expected behavior
|
|
27
|
-
description: What should have happened?
|
|
28
|
-
|
|
29
|
-
- type: input
|
|
30
|
-
id: agent
|
|
31
|
-
attributes:
|
|
32
|
-
label: Agent
|
|
33
|
-
description: Which AI agent are you using?
|
|
34
|
-
placeholder: e.g., Claude Code, Cursor, Amp
|
|
35
|
-
|
|
36
|
-
- type: textarea
|
|
37
|
-
id: logs
|
|
38
|
-
attributes:
|
|
39
|
-
label: Relevant logs
|
|
40
|
-
description: Any error messages or output
|
|
41
|
-
render: shell
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
blank_issues_enabled: false
|
|
2
|
-
contact_links:
|
|
3
|
-
- name: Browse Skills
|
|
4
|
-
url: https://skillcreator.ai/discover
|
|
5
|
-
about: Find and install skills from our gallery
|
|
6
|
-
- name: Create a Skill
|
|
7
|
-
url: https://skillcreator.ai/build
|
|
8
|
-
about: Generate a custom skill in 30 seconds
|
|
9
|
-
- name: Follow Updates
|
|
10
|
-
url: https://x.com/skillcreatorai
|
|
11
|
-
about: Get notified about new skills and features
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
name: Skill Request
|
|
2
|
-
description: Request a new skill to be added
|
|
3
|
-
title: "[Skill Request] "
|
|
4
|
-
labels: ["skill-request"]
|
|
5
|
-
body:
|
|
6
|
-
- type: markdown
|
|
7
|
-
attributes:
|
|
8
|
-
value: |
|
|
9
|
-
Thanks for suggesting a skill! We review all requests.
|
|
10
|
-
|
|
11
|
-
- type: input
|
|
12
|
-
id: skill-name
|
|
13
|
-
attributes:
|
|
14
|
-
label: Skill Name
|
|
15
|
-
description: What should this skill be called?
|
|
16
|
-
placeholder: e.g., slack-automation
|
|
17
|
-
validations:
|
|
18
|
-
required: true
|
|
19
|
-
|
|
20
|
-
- type: textarea
|
|
21
|
-
id: description
|
|
22
|
-
attributes:
|
|
23
|
-
label: What should this skill do?
|
|
24
|
-
description: Describe the use case and functionality
|
|
25
|
-
placeholder: |
|
|
26
|
-
I want a skill that can...
|
|
27
|
-
|
|
28
|
-
Use cases:
|
|
29
|
-
- ...
|
|
30
|
-
- ...
|
|
31
|
-
validations:
|
|
32
|
-
required: true
|
|
33
|
-
|
|
34
|
-
- type: dropdown
|
|
35
|
-
id: category
|
|
36
|
-
attributes:
|
|
37
|
-
label: Category
|
|
38
|
-
options:
|
|
39
|
-
- Development
|
|
40
|
-
- Documents
|
|
41
|
-
- Creative
|
|
42
|
-
- Productivity
|
|
43
|
-
- Other
|
|
44
|
-
validations:
|
|
45
|
-
required: true
|
|
46
|
-
|
|
47
|
-
- type: textarea
|
|
48
|
-
id: examples
|
|
49
|
-
attributes:
|
|
50
|
-
label: Example prompts
|
|
51
|
-
description: How would you use this skill?
|
|
52
|
-
placeholder: |
|
|
53
|
-
"Create a Slack message to #engineering about the deployment"
|
|
54
|
-
"Summarize this channel's messages from today"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
## Summary
|
|
2
|
-
|
|
3
|
-
Brief description of the changes.
|
|
4
|
-
|
|
5
|
-
## Type
|
|
6
|
-
|
|
7
|
-
- [ ] New skill
|
|
8
|
-
- [ ] Skill update/fix
|
|
9
|
-
- [ ] Documentation
|
|
10
|
-
- [ ] Other
|
|
11
|
-
|
|
12
|
-
## Checklist
|
|
13
|
-
|
|
14
|
-
- [ ] SKILL.md has valid YAML frontmatter with `name` and `description`
|
|
15
|
-
- [ ] Skill name is lowercase with hyphens only
|
|
16
|
-
- [ ] Added entry to `skills.json`
|
|
17
|
-
- [ ] Tested the skill works as expected
|
|
18
|
-
|
|
19
|
-
## Skill Details (if adding new skill)
|
|
20
|
-
|
|
21
|
-
**Name:**
|
|
22
|
-
**Category:**
|
|
23
|
-
**Description:**
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
name: Validate Skills
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
validate:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
|
|
15
|
-
- name: Setup Node.js
|
|
16
|
-
uses: actions/setup-node@v4
|
|
17
|
-
with:
|
|
18
|
-
node-version: '18'
|
|
19
|
-
|
|
20
|
-
- name: Validate skills.json schema
|
|
21
|
-
run: |
|
|
22
|
-
echo "Validating skills.json..."
|
|
23
|
-
node -e "
|
|
24
|
-
const fs = require('fs');
|
|
25
|
-
const data = JSON.parse(fs.readFileSync('skills.json', 'utf8'));
|
|
26
|
-
|
|
27
|
-
// Check structure
|
|
28
|
-
if (!Array.isArray(data.skills)) {
|
|
29
|
-
console.error('ERROR: skills must be an array');
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const required = ['name', 'description', 'category', 'author', 'license'];
|
|
34
|
-
const validCategories = ['development', 'document', 'creative', 'business', 'productivity'];
|
|
35
|
-
const names = new Set();
|
|
36
|
-
let errors = 0;
|
|
37
|
-
|
|
38
|
-
data.skills.forEach((skill, i) => {
|
|
39
|
-
// Check required fields
|
|
40
|
-
required.forEach(field => {
|
|
41
|
-
if (!skill[field]) {
|
|
42
|
-
console.error('ERROR: Skill ' + skill.name + ' missing ' + field);
|
|
43
|
-
errors++;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Check category
|
|
48
|
-
if (skill.category && !validCategories.includes(skill.category)) {
|
|
49
|
-
console.error('ERROR: Invalid category ' + skill.category + ' for ' + skill.name);
|
|
50
|
-
errors++;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check for duplicates
|
|
54
|
-
if (names.has(skill.name)) {
|
|
55
|
-
console.error('ERROR: Duplicate skill name: ' + skill.name);
|
|
56
|
-
errors++;
|
|
57
|
-
}
|
|
58
|
-
names.add(skill.name);
|
|
59
|
-
|
|
60
|
-
// Check name format
|
|
61
|
-
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(skill.name)) {
|
|
62
|
-
console.error('ERROR: Invalid skill name format: ' + skill.name);
|
|
63
|
-
errors++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check description length
|
|
67
|
-
if (skill.description && skill.description.length < 10) {
|
|
68
|
-
console.error('WARNING: Short description for ' + skill.name);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (errors > 0) {
|
|
73
|
-
console.error('Found ' + errors + ' errors');
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.log('✓ skills.json is valid (' + data.skills.length + ' skills)');
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
- name: Check skill folders match JSON
|
|
81
|
-
run: |
|
|
82
|
-
echo "Checking skill folders..."
|
|
83
|
-
node -e "
|
|
84
|
-
const fs = require('fs');
|
|
85
|
-
const path = require('path');
|
|
86
|
-
|
|
87
|
-
const data = JSON.parse(fs.readFileSync('skills.json', 'utf8'));
|
|
88
|
-
const jsonSkills = new Set(data.skills.map(s => s.name));
|
|
89
|
-
|
|
90
|
-
const skillsDir = 'skills';
|
|
91
|
-
const folders = fs.readdirSync(skillsDir).filter(f =>
|
|
92
|
-
fs.statSync(path.join(skillsDir, f)).isDirectory()
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
let errors = 0;
|
|
96
|
-
|
|
97
|
-
// Check each folder has an entry in JSON
|
|
98
|
-
folders.forEach(folder => {
|
|
99
|
-
if (!jsonSkills.has(folder)) {
|
|
100
|
-
console.error('ERROR: Folder ' + folder + ' not in skills.json');
|
|
101
|
-
errors++;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Check SKILL.md exists
|
|
105
|
-
const skillMd = path.join(skillsDir, folder, 'SKILL.md');
|
|
106
|
-
if (!fs.existsSync(skillMd)) {
|
|
107
|
-
console.error('ERROR: Missing SKILL.md in ' + folder);
|
|
108
|
-
errors++;
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Check each JSON entry has a folder
|
|
113
|
-
jsonSkills.forEach(name => {
|
|
114
|
-
if (!folders.includes(name)) {
|
|
115
|
-
console.error('ERROR: skills.json has ' + name + ' but folder missing');
|
|
116
|
-
errors++;
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (errors > 0) {
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log('✓ All ' + folders.length + ' skill folders match skills.json');
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
- name: Validate SKILL.md frontmatter
|
|
128
|
-
run: |
|
|
129
|
-
echo "Validating frontmatter..."
|
|
130
|
-
node -e "
|
|
131
|
-
const fs = require('fs');
|
|
132
|
-
const path = require('path');
|
|
133
|
-
|
|
134
|
-
const skillsDir = 'skills';
|
|
135
|
-
const folders = fs.readdirSync(skillsDir).filter(f =>
|
|
136
|
-
fs.statSync(path.join(skillsDir, f)).isDirectory()
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
let errors = 0;
|
|
140
|
-
|
|
141
|
-
folders.forEach(folder => {
|
|
142
|
-
const skillMd = path.join(skillsDir, folder, 'SKILL.md');
|
|
143
|
-
if (!fs.existsSync(skillMd)) return;
|
|
144
|
-
|
|
145
|
-
const content = fs.readFileSync(skillMd, 'utf8');
|
|
146
|
-
|
|
147
|
-
// Check for frontmatter
|
|
148
|
-
if (!content.startsWith('---')) {
|
|
149
|
-
console.error('ERROR: ' + folder + '/SKILL.md missing frontmatter');
|
|
150
|
-
errors++;
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Extract frontmatter
|
|
155
|
-
const endIndex = content.indexOf('---', 3);
|
|
156
|
-
if (endIndex === -1) {
|
|
157
|
-
console.error('ERROR: ' + folder + '/SKILL.md has unclosed frontmatter');
|
|
158
|
-
errors++;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const frontmatter = content.slice(3, endIndex);
|
|
163
|
-
|
|
164
|
-
// Check for name field
|
|
165
|
-
if (!frontmatter.includes('name:')) {
|
|
166
|
-
console.error('ERROR: ' + folder + '/SKILL.md missing name in frontmatter');
|
|
167
|
-
errors++;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Check for description field
|
|
171
|
-
if (!frontmatter.includes('description:')) {
|
|
172
|
-
console.error('ERROR: ' + folder + '/SKILL.md missing description in frontmatter');
|
|
173
|
-
errors++;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (errors > 0) {
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
console.log('✓ All SKILL.md files have valid frontmatter');
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
- name: Run tests
|
|
185
|
-
run: node test.js
|
|
186
|
-
|
|
187
|
-
- name: Validate CLI loads
|
|
188
|
-
run: node cli.js list > /dev/null && echo "✓ CLI loads successfully"
|
package/CHANGELOG.md
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
## [1.1.0] - 2024-12-20
|
|
6
|
-
|
|
7
|
-
### Added
|
|
8
|
-
- **Dry-run mode**: Preview installations with `--dry-run` flag
|
|
9
|
-
- **Config file support**: `~/.agent-skills.json` for default settings
|
|
10
|
-
- **Update notifications**: See available updates when listing installed skills
|
|
11
|
-
- **Update all**: `update --all` to update all installed skills at once
|
|
12
|
-
- **Category filter**: `list --category development` to filter skills
|
|
13
|
-
- **Tag support**: Search now includes tags
|
|
14
|
-
- **"Did you mean" suggestions**: Typo-tolerant skill name matching
|
|
15
|
-
- **Config command**: `config --default-agent cursor` to set defaults
|
|
16
|
-
- **Security validation**: Block path traversal attacks in skill names
|
|
17
|
-
- **Size limit**: 50MB max skill size to prevent abuse
|
|
18
|
-
- **Proper error handling**: Graceful failures with helpful messages
|
|
19
|
-
- **Test suite**: `npm test` runs validation tests
|
|
20
|
-
- **Enhanced CI**: Schema validation, duplicate detection, frontmatter checks
|
|
21
|
-
|
|
22
|
-
### Changed
|
|
23
|
-
- Bump to version 1.1.0 (semver: new features)
|
|
24
|
-
- Node.js 14+ now required (was implicit, now explicit)
|
|
25
|
-
- CLI shows skill size on install
|
|
26
|
-
- Better help output with categories section
|
|
27
|
-
|
|
28
|
-
### Fixed
|
|
29
|
-
- JSON parsing errors now show helpful messages instead of crashing
|
|
30
|
-
- File operation errors properly caught and reported
|
|
31
|
-
- Partial installs cleaned up on failure
|
|
32
|
-
|
|
33
|
-
### Security
|
|
34
|
-
- Skill names validated against path traversal patterns (`../`, `\`)
|
|
35
|
-
- Max file size enforced during copy operations
|
|
36
|
-
|
|
37
|
-
## [1.0.8] - 2024-12-20
|
|
38
|
-
|
|
39
|
-
### Added
|
|
40
|
-
- `uninstall` command to remove installed skills
|
|
41
|
-
- `update` command to update skills to latest version
|
|
42
|
-
- `list --installed` flag to show installed skills per agent
|
|
43
|
-
- Letta agent support (`--agent letta`)
|
|
44
|
-
- Command aliases: `add`, `remove`, `rm`, `find`, `show`, `upgrade`
|
|
45
|
-
|
|
46
|
-
### Fixed
|
|
47
|
-
- Description truncation now only adds "..." when actually truncated
|
|
48
|
-
|
|
49
|
-
## [1.0.7] - 2024-12-19
|
|
50
|
-
|
|
51
|
-
### Added
|
|
52
|
-
- Credits & Attribution section in README
|
|
53
|
-
- npm downloads badge
|
|
54
|
-
- Full skill listing in README (all 38 skills now documented)
|
|
55
|
-
|
|
56
|
-
### Fixed
|
|
57
|
-
- `--agent` flag parsing improvements
|
|
58
|
-
- Codex agent support
|
|
59
|
-
|
|
60
|
-
## [1.0.6] - 2024-12-18
|
|
61
|
-
|
|
62
|
-
### Added
|
|
63
|
-
- 15 new skills from ComposioHQ ecosystem:
|
|
64
|
-
- `artifacts-builder` - Interactive React/Tailwind components
|
|
65
|
-
- `changelog-generator` - Generate changelogs from git commits
|
|
66
|
-
- `competitive-ads-extractor` - Analyze competitor ads
|
|
67
|
-
- `content-research-writer` - Research and write with citations
|
|
68
|
-
- `developer-growth-analysis` - Track developer metrics
|
|
69
|
-
- `domain-name-brainstormer` - Generate domain name ideas
|
|
70
|
-
- `file-organizer` - Organize files and find duplicates
|
|
71
|
-
- `image-enhancer` - Improve image quality
|
|
72
|
-
- `invoice-organizer` - Organize invoices for tax prep
|
|
73
|
-
- `lead-research-assistant` - Identify and qualify leads
|
|
74
|
-
- `meeting-insights-analyzer` - Analyze meeting transcripts
|
|
75
|
-
- `raffle-winner-picker` - Select contest winners
|
|
76
|
-
- `slack-gif-creator` - Create animated GIFs for Slack
|
|
77
|
-
- `theme-factory` - Professional font and color themes
|
|
78
|
-
- `video-downloader` - Download videos from platforms
|
|
79
|
-
- Cross-link to Awesome Agent Skills repository
|
|
80
|
-
|
|
81
|
-
## [1.0.5] - 2024-12-17
|
|
82
|
-
|
|
83
|
-
### Fixed
|
|
84
|
-
- VS Code install message now correctly shows `.github/skills/`
|
|
85
|
-
|
|
86
|
-
## [1.0.4] - 2024-12-17
|
|
87
|
-
|
|
88
|
-
### Fixed
|
|
89
|
-
- VS Code path corrected to `.github/skills/` (was `.vscode/`)
|
|
90
|
-
|
|
91
|
-
## [1.0.3] - 2024-12-17
|
|
92
|
-
|
|
93
|
-
### Added
|
|
94
|
-
- `job-application` skill for cover letters and applications
|
|
95
|
-
|
|
96
|
-
## [1.0.2] - 2024-12-17
|
|
97
|
-
|
|
98
|
-
### Added
|
|
99
|
-
- Multi-agent support with `--agent` flag
|
|
100
|
-
- Support for Claude Code, Cursor, Amp, VS Code, Goose, OpenCode
|
|
101
|
-
- Portable install option with `--agent project`
|
|
102
|
-
|
|
103
|
-
## [1.0.1] - 2024-12-16
|
|
104
|
-
|
|
105
|
-
### Added
|
|
106
|
-
- `qa-regression` skill for automated Playwright testing
|
|
107
|
-
- `jira-issues` skill for Jira integration
|
|
108
|
-
- GitHub issue templates and PR templates
|
|
109
|
-
- CI validation workflow
|
|
110
|
-
- Funding configuration
|
|
111
|
-
|
|
112
|
-
## [1.0.0] - 2024-12-16
|
|
113
|
-
|
|
114
|
-
### Added
|
|
115
|
-
- Initial release with 20 curated skills
|
|
116
|
-
- NPX installer (`npx ai-agent-skills install <name>`)
|
|
117
|
-
- Skills from Anthropic's official examples
|
|
118
|
-
- Core document skills: `pdf`, `xlsx`, `docx`, `pptx`
|
|
119
|
-
- Development skills: `frontend-design`, `mcp-builder`, `skill-creator`
|
|
120
|
-
- Creative skills: `canvas-design`, `algorithmic-art`
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# Contributing to Agent Skills
|
|
2
|
-
|
|
3
|
-
We welcome contributions to the universal skill repository.
|
|
4
|
-
|
|
5
|
-
## Adding a Skill
|
|
6
|
-
|
|
7
|
-
### Requirements
|
|
8
|
-
|
|
9
|
-
1. Your skill must follow the [Agent Skills specification](https://agentskills.io/specification)
|
|
10
|
-
2. The `SKILL.md` file must include valid YAML frontmatter with `name` and `description`
|
|
11
|
-
3. The skill name must be lowercase with hyphens only (e.g., `my-skill`)
|
|
12
|
-
4. The skill must actually work and provide value
|
|
13
|
-
|
|
14
|
-
### Process
|
|
15
|
-
|
|
16
|
-
1. Fork this repository
|
|
17
|
-
2. Create your skill folder: `skills/<skill-name>/`
|
|
18
|
-
3. Add your `SKILL.md` file with proper frontmatter:
|
|
19
|
-
|
|
20
|
-
```yaml
|
|
21
|
-
---
|
|
22
|
-
name: my-skill
|
|
23
|
-
description: What this skill does and when to use it.
|
|
24
|
-
license: MIT
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
# My Skill
|
|
28
|
-
|
|
29
|
-
Instructions for the agent...
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
4. Add an entry to `skills.json`:
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"name": "my-skill",
|
|
37
|
-
"description": "What this skill does and when to use it.",
|
|
38
|
-
"category": "development",
|
|
39
|
-
"author": "your-username",
|
|
40
|
-
"source": "your-username/your-repo",
|
|
41
|
-
"license": "MIT",
|
|
42
|
-
"path": "skills/my-skill",
|
|
43
|
-
"stars": 0,
|
|
44
|
-
"downloads": 0,
|
|
45
|
-
"featured": false,
|
|
46
|
-
"verified": false
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
5. Submit a pull request
|
|
51
|
-
|
|
52
|
-
### Categories
|
|
53
|
-
|
|
54
|
-
Use one of these categories:
|
|
55
|
-
|
|
56
|
-
- `development` - Coding, debugging, testing, tooling
|
|
57
|
-
- `document` - PDF, Word, Excel, presentations
|
|
58
|
-
- `creative` - Art, design, visuals
|
|
59
|
-
- `business` - Communication, research, productivity
|
|
60
|
-
- `data` - Analytics, visualization, databases
|
|
61
|
-
- `content` - Writing, research, media
|
|
62
|
-
- `lifestyle` - Personal productivity
|
|
63
|
-
|
|
64
|
-
### Review Process
|
|
65
|
-
|
|
66
|
-
We review all submissions for:
|
|
67
|
-
|
|
68
|
-
- **Spec compliance**: Valid SKILL.md format
|
|
69
|
-
- **Quality**: Clear instructions, actually useful
|
|
70
|
-
- **Safety**: No malicious or harmful content
|
|
71
|
-
- **Uniqueness**: Not duplicating existing skills
|
|
72
|
-
|
|
73
|
-
## Updating Existing Skills
|
|
74
|
-
|
|
75
|
-
If you find a bug or want to improve an existing skill:
|
|
76
|
-
|
|
77
|
-
1. Fork the repo
|
|
78
|
-
2. Make your changes
|
|
79
|
-
3. Test that the skill still works
|
|
80
|
-
4. Submit a PR with a clear description of the change
|
|
81
|
-
|
|
82
|
-
## Questions?
|
|
83
|
-
|
|
84
|
-
Open an issue or visit [skillcreator.ai](https://skillcreator.ai).
|
package/test.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Test suite for ai-agent-skills CLI
|
|
5
|
-
* Run with: node test.js
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const { execSync } = require('child_process');
|
|
11
|
-
|
|
12
|
-
const colors = {
|
|
13
|
-
reset: '\x1b[0m',
|
|
14
|
-
green: '\x1b[32m',
|
|
15
|
-
red: '\x1b[31m',
|
|
16
|
-
yellow: '\x1b[33m',
|
|
17
|
-
dim: '\x1b[2m'
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
let passed = 0;
|
|
21
|
-
let failed = 0;
|
|
22
|
-
|
|
23
|
-
function test(name, fn) {
|
|
24
|
-
try {
|
|
25
|
-
fn();
|
|
26
|
-
console.log(`${colors.green}✓${colors.reset} ${name}`);
|
|
27
|
-
passed++;
|
|
28
|
-
} catch (e) {
|
|
29
|
-
console.log(`${colors.red}✗${colors.reset} ${name}`);
|
|
30
|
-
console.log(` ${colors.dim}${e.message}${colors.reset}`);
|
|
31
|
-
failed++;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function assert(condition, message) {
|
|
36
|
-
if (!condition) throw new Error(message || 'Assertion failed');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function assertEqual(a, b, message) {
|
|
40
|
-
if (a !== b) throw new Error(message || `Expected ${b}, got ${a}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function assertContains(str, substr, message) {
|
|
44
|
-
if (!str.includes(substr)) throw new Error(message || `Expected "${str}" to contain "${substr}"`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function run(cmd) {
|
|
48
|
-
try {
|
|
49
|
-
return execSync(`node cli.js ${cmd}`, { encoding: 'utf8', cwd: __dirname });
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return e.stdout || e.message;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
console.log('\n🧪 Running tests...\n');
|
|
56
|
-
|
|
57
|
-
// ============ SKILLS.JSON TESTS ============
|
|
58
|
-
|
|
59
|
-
test('skills.json exists and is valid JSON', () => {
|
|
60
|
-
const content = fs.readFileSync(path.join(__dirname, 'skills.json'), 'utf8');
|
|
61
|
-
const data = JSON.parse(content);
|
|
62
|
-
assert(Array.isArray(data.skills), 'skills should be an array');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('skills.json has skills with required fields', () => {
|
|
66
|
-
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'skills.json'), 'utf8'));
|
|
67
|
-
const required = ['name', 'description', 'category', 'author', 'license'];
|
|
68
|
-
|
|
69
|
-
data.skills.forEach(skill => {
|
|
70
|
-
required.forEach(field => {
|
|
71
|
-
assert(skill[field], `Skill ${skill.name} missing ${field}`);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('skill names match folder names', () => {
|
|
77
|
-
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'skills.json'), 'utf8'));
|
|
78
|
-
const skillsDir = path.join(__dirname, 'skills');
|
|
79
|
-
|
|
80
|
-
data.skills.forEach(skill => {
|
|
81
|
-
const skillPath = path.join(skillsDir, skill.name);
|
|
82
|
-
assert(fs.existsSync(skillPath), `Folder missing for skill: ${skill.name}`);
|
|
83
|
-
assert(fs.existsSync(path.join(skillPath, 'SKILL.md')), `SKILL.md missing for: ${skill.name}`);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('no duplicate skill names', () => {
|
|
88
|
-
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'skills.json'), 'utf8'));
|
|
89
|
-
const names = data.skills.map(s => s.name);
|
|
90
|
-
const unique = [...new Set(names)];
|
|
91
|
-
assertEqual(names.length, unique.length, 'Duplicate skill names found');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('all categories are valid', () => {
|
|
95
|
-
const validCategories = ['development', 'document', 'creative', 'business', 'productivity'];
|
|
96
|
-
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'skills.json'), 'utf8'));
|
|
97
|
-
|
|
98
|
-
data.skills.forEach(skill => {
|
|
99
|
-
assert(
|
|
100
|
-
validCategories.includes(skill.category),
|
|
101
|
-
`Invalid category "${skill.category}" for skill ${skill.name}`
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// ============ CLI TESTS ============
|
|
107
|
-
|
|
108
|
-
test('help command works', () => {
|
|
109
|
-
const output = run('help');
|
|
110
|
-
assertContains(output, 'AI Agent Skills');
|
|
111
|
-
assertContains(output, 'install');
|
|
112
|
-
assertContains(output, 'uninstall');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('list command works', () => {
|
|
116
|
-
const output = run('list');
|
|
117
|
-
assertContains(output, 'Available Skills');
|
|
118
|
-
assertContains(output, 'DEVELOPMENT');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('search command works', () => {
|
|
122
|
-
const output = run('search pdf');
|
|
123
|
-
assertContains(output, 'pdf');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('info command works', () => {
|
|
127
|
-
const output = run('info pdf');
|
|
128
|
-
assertContains(output, 'pdf');
|
|
129
|
-
assertContains(output, 'Category:');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('invalid skill name rejected', () => {
|
|
133
|
-
const output = run('install ../etc/passwd');
|
|
134
|
-
assertContains(output, 'Invalid skill name');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('dry-run shows preview', () => {
|
|
138
|
-
const output = run('install pdf --dry-run');
|
|
139
|
-
assertContains(output, 'Dry Run');
|
|
140
|
-
assertContains(output, 'Would install');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('config command works', () => {
|
|
144
|
-
const output = run('config');
|
|
145
|
-
assertContains(output, 'Configuration');
|
|
146
|
-
assertContains(output, 'defaultAgent');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test('unknown command shows error', () => {
|
|
150
|
-
const output = run('notacommand');
|
|
151
|
-
assertContains(output, 'Unknown command');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('category filter works', () => {
|
|
155
|
-
const output = run('list --category document');
|
|
156
|
-
assertContains(output, 'DOCUMENT');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// ============ SECURITY TESTS ============
|
|
160
|
-
|
|
161
|
-
test('path traversal blocked in skill names', () => {
|
|
162
|
-
const output = run('install ../../etc');
|
|
163
|
-
assertContains(output, 'Invalid skill name');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test('backslash path traversal blocked', () => {
|
|
167
|
-
const output = run('install ..\\..\\etc');
|
|
168
|
-
assertContains(output, 'Invalid skill name');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// ============ SUMMARY ============
|
|
172
|
-
|
|
173
|
-
console.log('\n' + '─'.repeat(40));
|
|
174
|
-
console.log(`${colors.green}Passed: ${passed}${colors.reset}`);
|
|
175
|
-
if (failed > 0) {
|
|
176
|
-
console.log(`${colors.red}Failed: ${failed}${colors.reset}`);
|
|
177
|
-
}
|
|
178
|
-
console.log('─'.repeat(40) + '\n');
|
|
179
|
-
|
|
180
|
-
process.exit(failed > 0 ? 1 : 0);
|