@framers/agentos-skills 0.3.0 → 0.4.1
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/CONTRIBUTING.md +231 -0
- package/README.md +93 -58
- package/package.json +19 -31
- package/registry/community/.gitkeep +0 -0
- package/registry/curated/1password/SKILL.md +53 -0
- package/registry/curated/account-manager/SKILL.md +60 -0
- package/registry/curated/agent-config/SKILL.md +22 -0
- package/registry/curated/amazon-polly/SKILL.md +74 -0
- package/registry/curated/apple-notes/SKILL.md +45 -0
- package/registry/curated/apple-reminders/SKILL.md +46 -0
- package/registry/curated/audio-generation/SKILL.md +231 -0
- package/registry/curated/blog-publisher/SKILL.md +110 -0
- package/registry/curated/bluesky-bot/SKILL.md +93 -0
- package/registry/curated/cli-tools/SKILL.md +137 -0
- package/registry/curated/cloud-ops/SKILL.md +124 -0
- package/registry/curated/code-safety/SKILL.md +42 -0
- package/registry/curated/coding-agent/SKILL.md +40 -0
- package/registry/curated/company-research/SKILL.md +46 -0
- package/registry/curated/content-creator/SKILL.md +53 -0
- package/registry/curated/deep-research/SKILL.md +56 -0
- package/registry/curated/diarization/SKILL.md +83 -0
- package/registry/curated/discord-helper/SKILL.md +43 -0
- package/registry/curated/document-export/SKILL.md +54 -0
- package/registry/curated/email-intelligence/SKILL.md +41 -0
- package/registry/curated/emergent-tools/SKILL.md +225 -0
- package/registry/curated/endpoint-semantic/SKILL.md +72 -0
- package/registry/curated/facebook-bot/SKILL.md +94 -0
- package/registry/curated/git/SKILL.md +49 -0
- package/registry/curated/github/SKILL.md +142 -0
- package/registry/curated/google-cloud-stt/SKILL.md +71 -0
- package/registry/curated/google-cloud-tts/SKILL.md +71 -0
- package/registry/curated/grounding-guard/SKILL.md +38 -0
- package/registry/curated/healthcheck/SKILL.md +43 -0
- package/registry/curated/image-editing/SKILL.md +25 -0
- package/registry/curated/image-gen/SKILL.md +141 -0
- package/registry/curated/instagram-bot/SKILL.md +60 -0
- package/registry/curated/interactive-widgets/SKILL.md +85 -0
- package/registry/curated/linkedin-bot/SKILL.md +86 -0
- package/registry/curated/mastodon-bot/SKILL.md +104 -0
- package/registry/curated/memory-manager/SKILL.md +127 -0
- package/registry/curated/ml-content-classifier/SKILL.md +38 -0
- package/registry/curated/movie-lookup/SKILL.md +48 -0
- package/registry/curated/multimodal-rag/SKILL.md +153 -0
- package/registry/curated/notion/SKILL.md +43 -0
- package/registry/curated/obsidian/SKILL.md +42 -0
- package/registry/curated/openwakeword/SKILL.md +75 -0
- package/registry/curated/pii-redaction/SKILL.md +56 -0
- package/registry/curated/pinterest-bot/SKILL.md +45 -0
- package/registry/curated/piper/SKILL.md +72 -0
- package/registry/curated/porcupine/SKILL.md +74 -0
- package/registry/curated/reddit-bot/SKILL.md +74 -0
- package/registry/curated/seo-campaign/SKILL.md +51 -0
- package/registry/curated/site-deploy/SKILL.md +119 -0
- package/registry/curated/slack-helper/SKILL.md +43 -0
- package/registry/curated/social-broadcast/SKILL.md +145 -0
- package/registry/curated/spotify-player/SKILL.md +45 -0
- package/registry/curated/streaming-stt-deepgram/SKILL.md +84 -0
- package/registry/curated/streaming-stt-whisper/SKILL.md +82 -0
- package/registry/curated/streaming-tts-elevenlabs/SKILL.md +84 -0
- package/registry/curated/streaming-tts-openai/SKILL.md +83 -0
- package/registry/curated/structured-output/SKILL.md +22 -0
- package/registry/curated/summarize/SKILL.md +40 -0
- package/registry/curated/threads-bot/SKILL.md +82 -0
- package/registry/curated/tiktok-bot/SKILL.md +104 -0
- package/registry/curated/topicality/SKILL.md +37 -0
- package/registry/curated/trello/SKILL.md +44 -0
- package/registry/curated/twitter-bot/SKILL.md +63 -0
- package/registry/curated/video-generation/SKILL.md +225 -0
- package/registry/curated/vision-ocr/SKILL.md +82 -0
- package/registry/curated/voice-conversation/SKILL.md +65 -0
- package/registry/curated/vosk/SKILL.md +74 -0
- package/registry/curated/weather/SKILL.md +37 -0
- package/registry/curated/web-scraper/SKILL.md +60 -0
- package/registry/curated/web-search/SKILL.md +49 -0
- package/registry/curated/whisper-transcribe/SKILL.md +58 -0
- package/registry/curated/youtube-bot/SKILL.md +104 -0
- package/registry.json +2446 -0
- package/scripts/update-registry.mjs +126 -0
- package/scripts/validate-skill.mjs +304 -0
- package/types.d.ts +160 -0
- package/dist/SkillLoader.d.ts +0 -50
- package/dist/SkillLoader.d.ts.map +0 -1
- package/dist/SkillLoader.js +0 -291
- package/dist/SkillLoader.js.map +0 -1
- package/dist/SkillRegistry.d.ts +0 -135
- package/dist/SkillRegistry.d.ts.map +0 -1
- package/dist/SkillRegistry.js +0 -455
- package/dist/SkillRegistry.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -13
- package/dist/index.js.map +0 -1
- package/dist/paths.d.ts +0 -35
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -71
- package/dist/paths.js.map +0 -1
- package/dist/types.d.ts +0 -231
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -21
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regenerate skills registry.json from SKILL.md files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import YAML from 'yaml';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const packageRoot = path.join(__dirname, '..');
|
|
15
|
+
const registryPath = path.join(packageRoot, 'registry.json');
|
|
16
|
+
const curatedRoot = path.join(packageRoot, 'registry', 'curated');
|
|
17
|
+
const communityRoot = path.join(packageRoot, 'registry', 'community');
|
|
18
|
+
|
|
19
|
+
function readFrontmatter(filePath) {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
22
|
+
if (!match) return {};
|
|
23
|
+
try {
|
|
24
|
+
const parsed = YAML.parse(match[1]);
|
|
25
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listSkillDirs(root) {
|
|
32
|
+
if (!fs.existsSync(root)) return [];
|
|
33
|
+
return fs
|
|
34
|
+
.readdirSync(root, { withFileTypes: true })
|
|
35
|
+
.filter((d) => d.isDirectory() && fs.existsSync(path.join(root, d.name, 'SKILL.md')))
|
|
36
|
+
.map((d) => d.name)
|
|
37
|
+
.sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanObject(obj) {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
43
|
+
if (v === undefined) continue;
|
|
44
|
+
if (Array.isArray(v) && v.length === 0) continue;
|
|
45
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) continue;
|
|
46
|
+
out[k] = v;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toStringArray(value) {
|
|
52
|
+
if (!Array.isArray(value)) return [];
|
|
53
|
+
return value.filter((v) => typeof v === 'string' && v.trim().length > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function slugToDisplayName(slug) {
|
|
57
|
+
return slug
|
|
58
|
+
.split(/[-_]/g)
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
61
|
+
.join(' ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildEntry(name, source, updatedAt) {
|
|
65
|
+
const skillPath = path.join(packageRoot, 'registry', source, name, 'SKILL.md');
|
|
66
|
+
const fm = readFrontmatter(skillPath);
|
|
67
|
+
const metadata = fm?.metadata?.agentos && typeof fm.metadata.agentos === 'object'
|
|
68
|
+
? fm.metadata.agentos
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
return cleanObject({
|
|
72
|
+
id: source === 'curated' ? `com.framers.skill.${name}` : `com.community.skill.${name}`,
|
|
73
|
+
name,
|
|
74
|
+
displayName:
|
|
75
|
+
typeof fm.name === 'string' && fm.name.trim().length > 0
|
|
76
|
+
? fm.name
|
|
77
|
+
: slugToDisplayName(name),
|
|
78
|
+
version: typeof fm.version === 'string' && fm.version ? fm.version : '1.0.0',
|
|
79
|
+
path: `registry/${source}/${name}`,
|
|
80
|
+
description: typeof fm.description === 'string' ? fm.description : '',
|
|
81
|
+
category: typeof fm.category === 'string' && fm.category ? fm.category : 'uncategorized',
|
|
82
|
+
namespace: typeof fm.namespace === 'string' && fm.namespace ? fm.namespace : 'wunderland',
|
|
83
|
+
verified: source === 'curated',
|
|
84
|
+
source,
|
|
85
|
+
verifiedAt: source === 'curated' ? updatedAt : undefined,
|
|
86
|
+
keywords: toStringArray(fm.tags),
|
|
87
|
+
requiredSecrets: toStringArray(fm.requires_secrets),
|
|
88
|
+
requiredTools: toStringArray(fm.requires_tools),
|
|
89
|
+
metadata: metadata && typeof metadata === 'object' ? metadata : undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateRegistry() {
|
|
94
|
+
const updated = new Date().toISOString();
|
|
95
|
+
const curatedNames = listSkillDirs(curatedRoot);
|
|
96
|
+
const communityNames = listSkillDirs(communityRoot);
|
|
97
|
+
|
|
98
|
+
const curated = curatedNames.map((name) => buildEntry(name, 'curated', updated));
|
|
99
|
+
const community = communityNames.map((name) => buildEntry(name, 'community', updated));
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
updated,
|
|
104
|
+
categories: {
|
|
105
|
+
curated: curatedNames,
|
|
106
|
+
community: communityNames,
|
|
107
|
+
},
|
|
108
|
+
skills: {
|
|
109
|
+
curated,
|
|
110
|
+
community,
|
|
111
|
+
},
|
|
112
|
+
stats: {
|
|
113
|
+
totalSkills: curated.length + community.length,
|
|
114
|
+
curatedCount: curated.length,
|
|
115
|
+
communityCount: community.length,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const registry = generateRegistry();
|
|
121
|
+
fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
122
|
+
|
|
123
|
+
console.log('✅ Skills registry updated');
|
|
124
|
+
console.log(` curated: ${registry.stats.curatedCount}`);
|
|
125
|
+
console.log(` community: ${registry.stats.communityCount}`);
|
|
126
|
+
console.log(` total: ${registry.stats.totalSkills}`);
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SKILL.md validator for @framers/agentos-skills
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/validate-skill.mjs registry/community/my-skill/SKILL.md
|
|
8
|
+
* node scripts/validate-skill.mjs # validates ALL skills
|
|
9
|
+
*
|
|
10
|
+
* Exit code 0 on pass, 1 on fail.
|
|
11
|
+
* Zero external dependencies — uses only Node.js built-ins.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
15
|
+
import { resolve, basename, dirname, join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const VALID_CATEGORIES = new Set([
|
|
22
|
+
'automation',
|
|
23
|
+
'communication',
|
|
24
|
+
'content',
|
|
25
|
+
'creative',
|
|
26
|
+
'developer-tools',
|
|
27
|
+
'devops',
|
|
28
|
+
'information',
|
|
29
|
+
'infrastructure',
|
|
30
|
+
'marketing',
|
|
31
|
+
'media',
|
|
32
|
+
'productivity',
|
|
33
|
+
'research',
|
|
34
|
+
'security',
|
|
35
|
+
'social-automation',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const VALID_NAMESPACES = new Set(['wunderland', 'community']);
|
|
39
|
+
|
|
40
|
+
const REQUIRED_FIELDS = ['name', 'version', 'description', 'author', 'namespace', 'category', 'tags'];
|
|
41
|
+
|
|
42
|
+
const MAX_DESCRIPTION_LENGTH = 200;
|
|
43
|
+
|
|
44
|
+
/** Patterns that suggest accidentally-committed secrets. */
|
|
45
|
+
const SECRET_PATTERNS = [
|
|
46
|
+
/(?:api[_-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9_\-]{16,}/i,
|
|
47
|
+
/(?:secret|token|password|passwd|pwd)\s*[:=]\s*["']?[A-Za-z0-9_\-]{16,}/i,
|
|
48
|
+
/sk-[A-Za-z0-9]{20,}/, // OpenAI-style keys
|
|
49
|
+
/ghp_[A-Za-z0-9]{36,}/, // GitHub personal access tokens
|
|
50
|
+
/gho_[A-Za-z0-9]{36,}/, // GitHub OAuth tokens
|
|
51
|
+
/xox[bpors]-[A-Za-z0-9\-]{10,}/, // Slack tokens
|
|
52
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key IDs
|
|
53
|
+
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, // Private keys
|
|
54
|
+
/Bearer\s+[A-Za-z0-9_\-\.]{20,}/, // Bearer tokens
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Simple YAML frontmatter parser (no dependencies)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse basic YAML key-value pairs from frontmatter lines.
|
|
63
|
+
* Handles:
|
|
64
|
+
* key: value -> string
|
|
65
|
+
* key: 'value' -> string (strip quotes)
|
|
66
|
+
* key: "value" -> string (strip quotes)
|
|
67
|
+
* key: [a, b, c] -> string[]
|
|
68
|
+
* key: [] -> string[] (empty)
|
|
69
|
+
*
|
|
70
|
+
* Nested keys (like metadata.agentos.emoji) are ignored — only top-level
|
|
71
|
+
* keys matter for required-field validation.
|
|
72
|
+
*/
|
|
73
|
+
function parseSimpleYaml(yamlText) {
|
|
74
|
+
const result = {};
|
|
75
|
+
const lines = yamlText.split('\n');
|
|
76
|
+
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
// Skip blank lines and comments
|
|
79
|
+
const trimmed = line.trimStart();
|
|
80
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
81
|
+
|
|
82
|
+
// Only parse top-level keys (no leading whitespace)
|
|
83
|
+
if (line[0] === ' ' || line[0] === '\t') continue;
|
|
84
|
+
|
|
85
|
+
const colonIndex = trimmed.indexOf(':');
|
|
86
|
+
if (colonIndex === -1) continue;
|
|
87
|
+
|
|
88
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
89
|
+
let value = trimmed.slice(colonIndex + 1).trim();
|
|
90
|
+
|
|
91
|
+
if (!key) continue;
|
|
92
|
+
|
|
93
|
+
// Inline array: [a, b, c]
|
|
94
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
95
|
+
const inner = value.slice(1, -1).trim();
|
|
96
|
+
if (inner === '') {
|
|
97
|
+
result[key] = [];
|
|
98
|
+
} else {
|
|
99
|
+
result[key] = inner.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Strip surrounding quotes
|
|
105
|
+
if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
|
|
106
|
+
value = value.slice(1, -1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
result[key] = value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Validation
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate a single SKILL.md file. Returns an object with:
|
|
121
|
+
* { path, passed, errors[] }
|
|
122
|
+
*/
|
|
123
|
+
function validateSkill(filePath) {
|
|
124
|
+
const errors = [];
|
|
125
|
+
const absolutePath = resolve(filePath);
|
|
126
|
+
|
|
127
|
+
// --- Read file --------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
let content;
|
|
130
|
+
try {
|
|
131
|
+
content = readFileSync(absolutePath, 'utf-8');
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { path: absolutePath, passed: false, errors: [`Cannot read file: ${err.message}`] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Split frontmatter / body ----------------------------------------
|
|
137
|
+
|
|
138
|
+
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
139
|
+
const match = content.match(fmRegex);
|
|
140
|
+
|
|
141
|
+
if (!match) {
|
|
142
|
+
return {
|
|
143
|
+
path: absolutePath,
|
|
144
|
+
passed: false,
|
|
145
|
+
errors: ['File does not contain valid YAML frontmatter (expected --- delimiters).'],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const yamlText = match[1];
|
|
150
|
+
const markdownBody = match[2];
|
|
151
|
+
|
|
152
|
+
// --- Parse YAML -------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
const meta = parseSimpleYaml(yamlText);
|
|
155
|
+
|
|
156
|
+
// --- Required fields --------------------------------------------------
|
|
157
|
+
|
|
158
|
+
for (const field of REQUIRED_FIELDS) {
|
|
159
|
+
if (meta[field] === undefined || meta[field] === '') {
|
|
160
|
+
errors.push(`Missing required field: ${field}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Category ---------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
if (meta.category && !VALID_CATEGORIES.has(meta.category)) {
|
|
167
|
+
errors.push(
|
|
168
|
+
`Invalid category "${meta.category}". Must be one of: ${[...VALID_CATEGORIES].join(', ')}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Namespace --------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
if (meta.namespace && !VALID_NAMESPACES.has(meta.namespace)) {
|
|
175
|
+
errors.push(
|
|
176
|
+
`Invalid namespace "${meta.namespace}". Must be one of: ${[...VALID_NAMESPACES].join(', ')}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Description length -----------------------------------------------
|
|
181
|
+
|
|
182
|
+
if (meta.description && typeof meta.description === 'string' && meta.description.length > MAX_DESCRIPTION_LENGTH) {
|
|
183
|
+
errors.push(
|
|
184
|
+
`Description is ${meta.description.length} characters (max ${MAX_DESCRIPTION_LENGTH}).`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Tags -------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
if (meta.tags !== undefined) {
|
|
191
|
+
if (!Array.isArray(meta.tags)) {
|
|
192
|
+
errors.push('tags must be an array.');
|
|
193
|
+
} else if (meta.tags.length === 0) {
|
|
194
|
+
errors.push('tags array must contain at least 1 tag.');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Name matches directory -------------------------------------------
|
|
199
|
+
|
|
200
|
+
const dirName = basename(dirname(absolutePath));
|
|
201
|
+
if (meta.name && meta.name !== dirName) {
|
|
202
|
+
errors.push(
|
|
203
|
+
`Skill name "${meta.name}" does not match directory name "${dirName}".`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Markdown body not empty ------------------------------------------
|
|
208
|
+
|
|
209
|
+
if (!markdownBody || markdownBody.trim().length === 0) {
|
|
210
|
+
errors.push('Markdown body is empty. Include usage instructions, Examples, and Constraints.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Secret pattern scan ----------------------------------------------
|
|
214
|
+
|
|
215
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
216
|
+
if (pattern.test(content)) {
|
|
217
|
+
errors.push(`Possible secret or credential detected (pattern: ${pattern.source}). Remove it before submitting.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { path: absolutePath, passed: errors.length === 0, errors };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Discover all SKILL.md files
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function discoverSkills(rootDir) {
|
|
229
|
+
const skills = [];
|
|
230
|
+
const tiers = ['curated', 'community'];
|
|
231
|
+
|
|
232
|
+
for (const tier of tiers) {
|
|
233
|
+
const tierDir = join(rootDir, 'registry', tier);
|
|
234
|
+
if (!existsSync(tierDir)) continue;
|
|
235
|
+
|
|
236
|
+
for (const entry of readdirSync(tierDir)) {
|
|
237
|
+
const skillDir = join(tierDir, entry);
|
|
238
|
+
if (!statSync(skillDir).isDirectory()) continue;
|
|
239
|
+
|
|
240
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
241
|
+
if (existsSync(skillFile)) {
|
|
242
|
+
skills.push(skillFile);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return skills;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Main
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
function main() {
|
|
255
|
+
const args = process.argv.slice(2);
|
|
256
|
+
|
|
257
|
+
let filesToValidate;
|
|
258
|
+
|
|
259
|
+
if (args.length === 0) {
|
|
260
|
+
// Validate all skills
|
|
261
|
+
const rootDir = resolve(dirname(new URL(import.meta.url).pathname), '..');
|
|
262
|
+
filesToValidate = discoverSkills(rootDir);
|
|
263
|
+
if (filesToValidate.length === 0) {
|
|
264
|
+
console.log('No SKILL.md files found in registry/curated/ or registry/community/.');
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
console.log(`Validating ${filesToValidate.length} skill(s)...\n`);
|
|
268
|
+
} else {
|
|
269
|
+
filesToValidate = [resolve(args[0])];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let allPassed = true;
|
|
273
|
+
|
|
274
|
+
for (const filePath of filesToValidate) {
|
|
275
|
+
const result = validateSkill(filePath);
|
|
276
|
+
|
|
277
|
+
if (result.passed) {
|
|
278
|
+
console.log(` PASS ${result.path}`);
|
|
279
|
+
} else {
|
|
280
|
+
allPassed = false;
|
|
281
|
+
console.log(` FAIL ${result.path}`);
|
|
282
|
+
for (const err of result.errors) {
|
|
283
|
+
console.log(` - ${err}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
if (allPassed) {
|
|
291
|
+
console.log(`All ${filesToValidate.length} skill(s) passed validation.`);
|
|
292
|
+
process.exit(0);
|
|
293
|
+
} else {
|
|
294
|
+
const failCount = filesToValidate.length - filesToValidate.filter((_, i) => {
|
|
295
|
+
// Re-validate to count (cheap for small set)
|
|
296
|
+
return validateSkill(filesToValidate[i]).passed;
|
|
297
|
+
}).length;
|
|
298
|
+
// Simpler: just fail
|
|
299
|
+
console.log('Validation failed. Fix the errors above and re-run.');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main();
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for the `@framers/agentos-skills` content package.
|
|
3
|
+
*
|
|
4
|
+
* Describes the shape of `registry.json` — the machine-readable index of all
|
|
5
|
+
* bundled SKILL.md prompt modules. These types are consumed by
|
|
6
|
+
* `@framers/agentos-skills-registry` (the catalog SDK) and any tooling that
|
|
7
|
+
* reads the registry directly.
|
|
8
|
+
*
|
|
9
|
+
* @module @framers/agentos-skills
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Supported package-manager / install strategies for skill prerequisites. */
|
|
13
|
+
export type SkillInstallKind = 'brew' | 'apt' | 'node' | 'go' | 'uv' | 'download';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Describes how to install a single prerequisite binary for a skill.
|
|
17
|
+
*
|
|
18
|
+
* Each install spec maps to a concrete command the agent or user can run
|
|
19
|
+
* (e.g. `brew install gh`, `npm install -g typescript`).
|
|
20
|
+
*/
|
|
21
|
+
export interface SkillInstallSpec {
|
|
22
|
+
/** Unique ID for this install option (e.g. 'brew-gh') */
|
|
23
|
+
id?: string;
|
|
24
|
+
/** Package manager or install strategy */
|
|
25
|
+
kind: SkillInstallKind;
|
|
26
|
+
/** Human-readable label for the install option */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Binary names this install provides (used for verification) */
|
|
29
|
+
bins?: string[];
|
|
30
|
+
/** OS restrictions — only show this option on matching platforms */
|
|
31
|
+
os?: readonly string[];
|
|
32
|
+
/** Homebrew formula name */
|
|
33
|
+
formula?: string;
|
|
34
|
+
/** npm / pip / go package name */
|
|
35
|
+
package?: string;
|
|
36
|
+
/** Node module name (for `npx`-style invocation) */
|
|
37
|
+
module?: string;
|
|
38
|
+
/** Direct download URL */
|
|
39
|
+
url?: string;
|
|
40
|
+
/** Archive filename within the download */
|
|
41
|
+
archive?: string;
|
|
42
|
+
/** Whether to extract the download */
|
|
43
|
+
extract?: boolean;
|
|
44
|
+
/** Number of directory levels to strip when extracting */
|
|
45
|
+
stripComponents?: number;
|
|
46
|
+
/** Target directory for the extracted/downloaded binary */
|
|
47
|
+
targetDir?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Binary, environment variable, and config requirements that must be met
|
|
52
|
+
* before a skill is eligible to run.
|
|
53
|
+
*/
|
|
54
|
+
export interface SkillRequirements {
|
|
55
|
+
/** All of these binaries must be on PATH */
|
|
56
|
+
bins?: string[];
|
|
57
|
+
/** At least one of these binaries must be on PATH */
|
|
58
|
+
anyBins?: string[];
|
|
59
|
+
/** All of these environment variables must be set */
|
|
60
|
+
env?: string[];
|
|
61
|
+
/** All of these config file paths must exist */
|
|
62
|
+
config?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Structured metadata extracted from the `metadata.agentos` block in SKILL.md
|
|
67
|
+
* frontmatter. Controls eligibility, discoverability, and installation UX.
|
|
68
|
+
*/
|
|
69
|
+
export interface SkillMetadata {
|
|
70
|
+
/** If true, skill is always included in agent context (no opt-in needed) */
|
|
71
|
+
always?: boolean;
|
|
72
|
+
/** Override key used for per-skill config in agent.config.json */
|
|
73
|
+
skillKey?: string;
|
|
74
|
+
/** Primary environment variable this skill needs (e.g. 'GITHUB_TOKEN') */
|
|
75
|
+
primaryEnv?: string;
|
|
76
|
+
/** Emoji icon for display in catalogs and CLI */
|
|
77
|
+
emoji?: string;
|
|
78
|
+
/** URL to the skill's homepage or docs */
|
|
79
|
+
homepage?: string;
|
|
80
|
+
/** OS restrictions — skill only available on these platforms */
|
|
81
|
+
os?: readonly string[];
|
|
82
|
+
/** Binary and env requirements for eligibility checking */
|
|
83
|
+
requires?: SkillRequirements;
|
|
84
|
+
/** Installation options for missing prerequisites */
|
|
85
|
+
install?: SkillInstallSpec[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shape of a single skill entry in `registry.json`.
|
|
90
|
+
*
|
|
91
|
+
* Each entry maps to one `registry/curated/{name}/SKILL.md` file and includes
|
|
92
|
+
* all metadata needed for catalog queries without reading the file from disk.
|
|
93
|
+
*/
|
|
94
|
+
export interface SkillRegistryEntry {
|
|
95
|
+
/** Fully-qualified ID (e.g. 'com.framers.skill.github') */
|
|
96
|
+
id: string;
|
|
97
|
+
/** Unique slug matching the directory name under registry/curated/ */
|
|
98
|
+
name: string;
|
|
99
|
+
/** Human-readable display name (e.g. 'GitHub') */
|
|
100
|
+
displayName?: string;
|
|
101
|
+
/** Semantic version from the SKILL.md frontmatter */
|
|
102
|
+
version: string;
|
|
103
|
+
/** Relative path from package root to the skill directory */
|
|
104
|
+
path: string;
|
|
105
|
+
/** Brief description of the skill's capabilities */
|
|
106
|
+
description: string;
|
|
107
|
+
/** Grouping category (e.g. 'developer', 'social', 'voice') */
|
|
108
|
+
category?: string;
|
|
109
|
+
/** Namespace used for registry scoping */
|
|
110
|
+
namespace?: string;
|
|
111
|
+
/** Whether this skill has been staff-verified */
|
|
112
|
+
verified: boolean;
|
|
113
|
+
/** Provenance: curated (staff-maintained) or community-submitted */
|
|
114
|
+
source?: 'curated' | 'community';
|
|
115
|
+
/** ISO timestamp of last verification */
|
|
116
|
+
verifiedAt?: string;
|
|
117
|
+
/** Searchable keywords */
|
|
118
|
+
keywords?: string[];
|
|
119
|
+
/** Secret identifiers the skill requires (e.g. 'github.token') */
|
|
120
|
+
requiredSecrets?: string[];
|
|
121
|
+
/** Tool identifiers the skill depends on */
|
|
122
|
+
requiredTools?: string[];
|
|
123
|
+
/** Structured metadata from the SKILL.md frontmatter */
|
|
124
|
+
metadata?: SkillMetadata;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Aggregate counts for the registry. */
|
|
128
|
+
export interface SkillsRegistryStats {
|
|
129
|
+
totalSkills: number;
|
|
130
|
+
curatedCount: number;
|
|
131
|
+
communityCount: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Shape of the full `registry.json` file.
|
|
136
|
+
*
|
|
137
|
+
* Auto-generated by `scripts/update-registry.mjs` from the SKILL.md files
|
|
138
|
+
* in `registry/curated/` and `registry/community/`.
|
|
139
|
+
*/
|
|
140
|
+
export interface SkillsRegistry {
|
|
141
|
+
/** Schema version */
|
|
142
|
+
version: string;
|
|
143
|
+
/** ISO timestamp of last regeneration */
|
|
144
|
+
updated: string;
|
|
145
|
+
/** Category names present in each source */
|
|
146
|
+
categories: {
|
|
147
|
+
curated: string[];
|
|
148
|
+
community: string[];
|
|
149
|
+
};
|
|
150
|
+
/** Skill entries grouped by source */
|
|
151
|
+
skills: {
|
|
152
|
+
curated: SkillRegistryEntry[];
|
|
153
|
+
community: SkillRegistryEntry[];
|
|
154
|
+
};
|
|
155
|
+
/** Aggregate statistics */
|
|
156
|
+
stats: SkillsRegistryStats;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
declare const registry: SkillsRegistry;
|
|
160
|
+
export default registry;
|
package/dist/SkillLoader.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Skill Loader for AgentOS
|
|
3
|
-
* @module @framers/agentos-skills/SkillLoader
|
|
4
|
-
*
|
|
5
|
-
* Loads skills from directories by parsing SKILL.md files with YAML frontmatter.
|
|
6
|
-
* Skills are modular capabilities that extend agent functionality.
|
|
7
|
-
*/
|
|
8
|
-
import type { SkillEntry, SkillMetadata, SkillEligibilityContext, ParsedSkillFrontmatter } from './types.js';
|
|
9
|
-
/**
|
|
10
|
-
* Parse YAML frontmatter from SKILL.md content.
|
|
11
|
-
* Supports the standard `---` delimited format.
|
|
12
|
-
*/
|
|
13
|
-
export declare function parseSkillFrontmatter(content: string): {
|
|
14
|
-
frontmatter: ParsedSkillFrontmatter;
|
|
15
|
-
body: string;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Extract SkillMetadata from parsed frontmatter.
|
|
19
|
-
*/
|
|
20
|
-
export declare function extractMetadata(frontmatter: ParsedSkillFrontmatter): SkillMetadata | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* Load a single skill from a directory.
|
|
23
|
-
*
|
|
24
|
-
* @param skillDir - Path to skill directory (should contain SKILL.md)
|
|
25
|
-
* @returns SkillEntry or null if invalid
|
|
26
|
-
*/
|
|
27
|
-
export declare function loadSkillFromDir(skillDir: string): Promise<SkillEntry | null>;
|
|
28
|
-
/**
|
|
29
|
-
* Load all skills from a directory.
|
|
30
|
-
*
|
|
31
|
-
* @param dir - Parent directory containing skill subdirectories
|
|
32
|
-
* @returns Array of SkillEntry objects
|
|
33
|
-
*/
|
|
34
|
-
export declare function loadSkillsFromDir(dir: string): Promise<SkillEntry[]>;
|
|
35
|
-
/**
|
|
36
|
-
* Filter skill entries by platform.
|
|
37
|
-
*/
|
|
38
|
-
export declare function filterByPlatform(entries: SkillEntry[], platform: string): SkillEntry[];
|
|
39
|
-
/**
|
|
40
|
-
* Filter skill entries by eligibility context.
|
|
41
|
-
*/
|
|
42
|
-
export declare function filterByEligibility(entries: SkillEntry[], context: SkillEligibilityContext): SkillEntry[];
|
|
43
|
-
/**
|
|
44
|
-
* Check if all binary requirements for a skill are met.
|
|
45
|
-
*/
|
|
46
|
-
export declare function checkBinaryRequirements(entry: SkillEntry, hasBin: (bin: string) => boolean): {
|
|
47
|
-
met: boolean;
|
|
48
|
-
missing: string[];
|
|
49
|
-
};
|
|
50
|
-
//# sourceMappingURL=SkillLoader.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SkillLoader.d.ts","sourceRoot":"","sources":["../src/SkillLoader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAEV,UAAU,EACV,aAAa,EACb,uBAAuB,EACvB,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AAQpB;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG;IACtD,WAAW,EAAE,sBAAsB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;CACd,CAkCA;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,sBAAsB,GAAG,aAAa,GAAG,SAAS,CAoC9F;AAiED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAyCnF;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuB1E;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAStF;AAaD;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,uBAAuB,GAC/B,UAAU,EAAE,CA+Bd;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAC/B;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAgBrC"}
|