awesome-agents 0.1.5 → 0.1.6
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/CHANGELOG.md +9 -0
- package/README.md +34 -9
- package/docs/cli.md +28 -7
- package/docs/product/README.md +1 -1
- package/docs/product/command-model.md +13 -12
- package/docs/product/harness-targets.md +23 -1
- package/docs/product/product-scope.md +4 -4
- package/docs/product/profile-source-format.md +17 -0
- package/package.json +3 -2
- package/src/cli.js +140 -10
- package/src/constants.js +8 -4
- package/src/help.js +12 -10
- package/src/installer.js +99 -11
- package/src/renderers.js +111 -3
- package/src/skills.js +397 -0
- package/src/source.js +26 -4
package/src/skills.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
6
|
+
import { expandHome, materializeSource } from "./source.js";
|
|
7
|
+
|
|
8
|
+
const SKILL_CONTAINER_DIRS = [
|
|
9
|
+
"skills",
|
|
10
|
+
"skills/.curated",
|
|
11
|
+
"skills/.experimental",
|
|
12
|
+
"skills/.system",
|
|
13
|
+
".agents/skills",
|
|
14
|
+
".claude/skills",
|
|
15
|
+
".codex/skills",
|
|
16
|
+
".opencode/skills"
|
|
17
|
+
];
|
|
18
|
+
const SKIP_DIRS = new Set([".git", "node_modules", "dist", "build", "__pycache__"]);
|
|
19
|
+
|
|
20
|
+
export async function installProfileSkills(profile, sourceRoot, options = {}) {
|
|
21
|
+
const dependencies = normalizeSkillDependencies(profile.attributes.skills);
|
|
22
|
+
if (dependencies.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const home = path.resolve(expandHome(options.home ?? os.homedir()));
|
|
27
|
+
const installed = [];
|
|
28
|
+
|
|
29
|
+
for (const dependency of dependencies) {
|
|
30
|
+
const skill = await resolveSkillDependency(profile, dependency, sourceRoot, home, options);
|
|
31
|
+
const target = path.join(home, ".agents", "homes", profile.slug, "skills", skill.installName);
|
|
32
|
+
|
|
33
|
+
if (!options.dryRun) {
|
|
34
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
35
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
36
|
+
await fs.cp(skill.path, target, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
dereference: true,
|
|
39
|
+
preserveTimestamps: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
installed.push({
|
|
44
|
+
name: skill.installName,
|
|
45
|
+
declaredName: dependency.selector ?? dependency.source ?? skill.name,
|
|
46
|
+
source: skill.source,
|
|
47
|
+
sourceKind: skill.sourceKind,
|
|
48
|
+
path: target
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return installed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeSkillDependencies(value) {
|
|
56
|
+
if (!value) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
61
|
+
return entries.map(parseSkillDependency);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseSkillDependency(value) {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
const text = value.trim();
|
|
67
|
+
if (!text) {
|
|
68
|
+
throw new Error("Skill dependency entries must not be empty.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const split = text.match(/^(\S+)\s+(.+)$/);
|
|
72
|
+
if (split) {
|
|
73
|
+
return { source: split[1], selector: split[2].trim(), raw: text };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const atSelector = splitAtSelector(text);
|
|
77
|
+
if (atSelector) {
|
|
78
|
+
return { source: atSelector.source, selector: atSelector.selector, raw: text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (looksLikeRemoteSource(text)) {
|
|
82
|
+
return { source: text, selector: undefined, raw: text };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { selector: text, raw: text };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isObject(value)) {
|
|
89
|
+
const source = firstString(value.source, value.package, value.repository, value.repo);
|
|
90
|
+
const selector = firstString(value.skill, value.name, value.slug, value.id);
|
|
91
|
+
if (!source && !selector) {
|
|
92
|
+
throw new Error("Skill dependency objects must include a skill/name or source field.");
|
|
93
|
+
}
|
|
94
|
+
return { source, selector, raw: value };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error("Skill dependencies must be strings or objects.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function splitAtSelector(value) {
|
|
101
|
+
if (value.startsWith("git@")) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const atIndex = value.lastIndexOf("@");
|
|
105
|
+
if (atIndex <= 0 || atIndex === value.length - 1 || atIndex <= value.lastIndexOf("/")) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
source: value.slice(0, atIndex),
|
|
110
|
+
selector: value.slice(atIndex + 1)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function looksLikeRemoteSource(value) {
|
|
115
|
+
return value.includes("/") || value.includes(":") || value.startsWith("~") || value.startsWith(".");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveSkillDependency(profile, dependency, sourceRoot, home, options) {
|
|
119
|
+
if (dependency.source) {
|
|
120
|
+
const materialized = await materializeSource(dependency.source, {
|
|
121
|
+
...options,
|
|
122
|
+
requireAgentProfileLayout: false
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
return await selectSkillFromRoot(materialized.path, dependency.selector, {
|
|
126
|
+
dependency,
|
|
127
|
+
source: materialized.source,
|
|
128
|
+
sourceKind: materialized.kind
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
await materialized.cleanup();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const profileSkill = await findSkillInRoot(profile.agentRoot, dependency.selector, {
|
|
136
|
+
dependency,
|
|
137
|
+
source: profile.agentRoot,
|
|
138
|
+
sourceKind: "profile-local"
|
|
139
|
+
});
|
|
140
|
+
if (profileSkill) {
|
|
141
|
+
return profileSkill;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const sourceSkill = await findSkillInRoot(sourceRoot, dependency.selector, {
|
|
145
|
+
dependency,
|
|
146
|
+
source: sourceRoot,
|
|
147
|
+
sourceKind: "local-source"
|
|
148
|
+
});
|
|
149
|
+
if (sourceSkill) {
|
|
150
|
+
return sourceSkill;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const userSkillRoot = path.join(home, ".agents", "skills");
|
|
154
|
+
const userSkill = await findSkillInRoot(userSkillRoot, dependency.selector, {
|
|
155
|
+
dependency,
|
|
156
|
+
source: userSkillRoot,
|
|
157
|
+
sourceKind: "user-skills"
|
|
158
|
+
});
|
|
159
|
+
if (userSkill) {
|
|
160
|
+
return userSkill;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error(`Skill "${dependency.selector}" was not found in the profile source or ${userSkillRoot}.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function findSkillInRoot(root, selector, context) {
|
|
167
|
+
if (!root || !existsSync(root)) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const skills = await discoverSkills(root);
|
|
172
|
+
if (skills.length === 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return findDiscoveredSkill(skills, selector, context);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function selectSkillFromRoot(root, selector, context) {
|
|
180
|
+
const skills = await discoverSkills(root);
|
|
181
|
+
if (skills.length === 0) {
|
|
182
|
+
throw new Error(`No skills found in ${context.source}. Expected SKILL.md files in the source root or skills/<name>/.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return selectDiscoveredSkill(skills, selector, context);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectDiscoveredSkill(skills, selector, context) {
|
|
189
|
+
if (!selector) {
|
|
190
|
+
if (skills.length === 1) {
|
|
191
|
+
return withSource(skills[0], context);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Skill source "${context.source}" contains multiple skills. Specify one of: ${skills.map((skill) => skill.name).join(", ")}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const normalized = sanitizeSkillName(selector);
|
|
197
|
+
const selected = skills.find((skill) => {
|
|
198
|
+
return sanitizeSkillName(skill.name) === normalized
|
|
199
|
+
|| sanitizeSkillName(skill.directoryName) === normalized
|
|
200
|
+
|| skill.installName === normalized;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!selected) {
|
|
204
|
+
throw new Error(`Skill "${selector}" was not found in ${context.source}. Available skills: ${skills.map((skill) => skill.name).join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return withSource(selected, context);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function findDiscoveredSkill(skills, selector, context) {
|
|
211
|
+
if (!selector) {
|
|
212
|
+
return skills.length === 1 ? withSource(skills[0], context) : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const normalized = sanitizeSkillName(selector);
|
|
216
|
+
const selected = skills.find((skill) => {
|
|
217
|
+
return sanitizeSkillName(skill.name) === normalized
|
|
218
|
+
|| sanitizeSkillName(skill.directoryName) === normalized
|
|
219
|
+
|| skill.installName === normalized;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return selected ? withSource(selected, context) : undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function withSource(skill, context) {
|
|
226
|
+
return {
|
|
227
|
+
...skill,
|
|
228
|
+
source: context.source,
|
|
229
|
+
sourceKind: context.sourceKind
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function discoverSkills(root) {
|
|
234
|
+
const discovered = [];
|
|
235
|
+
const seenPaths = new Set();
|
|
236
|
+
const seenNames = new Set();
|
|
237
|
+
|
|
238
|
+
const addSkillAt = async (skillDir) => {
|
|
239
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
240
|
+
if (!existsSync(skillPath)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resolved = path.resolve(skillDir);
|
|
245
|
+
if (seenPaths.has(resolved)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const skill = await readSkill(skillDir, root);
|
|
250
|
+
if (seenNames.has(skill.installName)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
discovered.push(skill);
|
|
255
|
+
seenPaths.add(resolved);
|
|
256
|
+
seenNames.add(skill.installName);
|
|
257
|
+
return true;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await addSkillAt(root);
|
|
261
|
+
await discoverContainer(root, false, addSkillAt);
|
|
262
|
+
for (const container of SKILL_CONTAINER_DIRS) {
|
|
263
|
+
await discoverContainer(path.join(root, container), true, addSkillAt);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (discovered.length === 0) {
|
|
267
|
+
for (const skillDir of await findSkillDirs(root)) {
|
|
268
|
+
await addSkillAt(skillDir);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return discovered.sort((a, b) => a.installName.localeCompare(b.installName));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function discoverContainer(container, walkGrandchildren, addSkillAt) {
|
|
276
|
+
let entries;
|
|
277
|
+
try {
|
|
278
|
+
entries = await fs.readdir(container, { withFileTypes: true });
|
|
279
|
+
} catch {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const childDir = path.join(container, entry.name);
|
|
289
|
+
if (!await isDirectoryLike(entry, childDir)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const found = await addSkillAt(childDir);
|
|
294
|
+
if (found || !walkGrandchildren) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let grandchildren;
|
|
299
|
+
try {
|
|
300
|
+
grandchildren = await fs.readdir(childDir, { withFileTypes: true });
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
for (const grandchild of grandchildren) {
|
|
305
|
+
if (SKIP_DIRS.has(grandchild.name)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const grandchildDir = path.join(childDir, grandchild.name);
|
|
309
|
+
if (await isDirectoryLike(grandchild, grandchildDir)) {
|
|
310
|
+
await addSkillAt(grandchildDir);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function findSkillDirs(root) {
|
|
317
|
+
const results = [];
|
|
318
|
+
|
|
319
|
+
async function visit(directory) {
|
|
320
|
+
let entries;
|
|
321
|
+
try {
|
|
322
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
323
|
+
} catch {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (existsSync(path.join(directory, "SKILL.md"))) {
|
|
328
|
+
results.push(directory);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const entryPath = path.join(directory, entry.name);
|
|
337
|
+
if (await isDirectoryLike(entry, entryPath)) {
|
|
338
|
+
await visit(entryPath);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await visit(root);
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function readSkill(skillDir, root) {
|
|
348
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
349
|
+
const raw = await fs.readFile(skillPath, "utf8");
|
|
350
|
+
const parsed = parseFrontmatter(raw, skillPath);
|
|
351
|
+
const directoryName = path.basename(skillDir);
|
|
352
|
+
const name = firstString(parsed.attributes.name, directoryName);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
name,
|
|
356
|
+
installName: sanitizeSkillName(name),
|
|
357
|
+
directoryName,
|
|
358
|
+
path: skillDir,
|
|
359
|
+
relativePath: path.relative(root, skillDir)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function isDirectoryLike(entry, entryPath) {
|
|
364
|
+
if (entry.isDirectory()) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (!entry.isSymbolicLink()) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
return (await fs.stat(entryPath)).isDirectory();
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sanitizeSkillName(name) {
|
|
379
|
+
return String(name)
|
|
380
|
+
.toLowerCase()
|
|
381
|
+
.replace(/[^a-z0-9._]+/g, "-")
|
|
382
|
+
.replace(/^[.-]+|[.-]+$/g, "")
|
|
383
|
+
.slice(0, 255) || "unnamed-skill";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function firstString(...values) {
|
|
387
|
+
for (const value of values) {
|
|
388
|
+
if (typeof value === "string" && value.trim()) {
|
|
389
|
+
return value.trim();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isObject(value) {
|
|
396
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
397
|
+
}
|
package/src/source.js
CHANGED
|
@@ -90,6 +90,7 @@ export function normalizeGithubUrl(source) {
|
|
|
90
90
|
export async function materializeSource(sourceInput, options = {}) {
|
|
91
91
|
const home = options.home ?? os.homedir();
|
|
92
92
|
const source = sourceInput;
|
|
93
|
+
const requireAgentProfileLayout = options.requireAgentProfileLayout ?? true;
|
|
93
94
|
|
|
94
95
|
if (!source) {
|
|
95
96
|
throw new Error("Specify a source. Use a local path, owner/repo, or GitHub URL.");
|
|
@@ -97,7 +98,9 @@ export async function materializeSource(sourceInput, options = {}) {
|
|
|
97
98
|
|
|
98
99
|
if (isLocalSource(source, home)) {
|
|
99
100
|
const sourcePath = path.resolve(expandHome(source, home));
|
|
100
|
-
|
|
101
|
+
if (requireAgentProfileLayout) {
|
|
102
|
+
await assertAgentProfileLayout(sourcePath);
|
|
103
|
+
}
|
|
101
104
|
return {
|
|
102
105
|
kind: "local",
|
|
103
106
|
source: sourcePath,
|
|
@@ -106,14 +109,20 @@ export async function materializeSource(sourceInput, options = {}) {
|
|
|
106
109
|
};
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
const
|
|
112
|
+
const { sourceWithoutRef, ref } = splitGitRef(source);
|
|
113
|
+
const gitUrl = normalizeGithubUrl(sourceWithoutRef);
|
|
110
114
|
if (!gitUrl) {
|
|
111
115
|
throw new Error(`Unsupported source "${source}". Use a local path, owner/repo, or GitHub URL.`);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
const tmpRoot = options.tmpRoot ?? os.tmpdir();
|
|
115
119
|
const cloneDir = await fs.mkdtemp(path.join(tmpRoot, "awesome-agents-source-"));
|
|
116
|
-
const
|
|
120
|
+
const cloneArgs = ["clone", "--depth=1"];
|
|
121
|
+
if (ref) {
|
|
122
|
+
cloneArgs.push("--branch", ref);
|
|
123
|
+
}
|
|
124
|
+
cloneArgs.push(gitUrl, cloneDir);
|
|
125
|
+
const clone = spawnSync("git", cloneArgs, {
|
|
117
126
|
encoding: "utf8"
|
|
118
127
|
});
|
|
119
128
|
|
|
@@ -123,7 +132,9 @@ export async function materializeSource(sourceInput, options = {}) {
|
|
|
123
132
|
throw new Error(`Could not clone ${source}: ${detail}`);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
|
|
135
|
+
if (requireAgentProfileLayout) {
|
|
136
|
+
await assertAgentProfileLayout(cloneDir);
|
|
137
|
+
}
|
|
127
138
|
|
|
128
139
|
return {
|
|
129
140
|
kind: "git",
|
|
@@ -136,6 +147,17 @@ export async function materializeSource(sourceInput, options = {}) {
|
|
|
136
147
|
};
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
function splitGitRef(source) {
|
|
151
|
+
const hashIndex = source.lastIndexOf("#");
|
|
152
|
+
if (hashIndex <= 0 || hashIndex === source.length - 1) {
|
|
153
|
+
return { sourceWithoutRef: source, ref: undefined };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
sourceWithoutRef: source.slice(0, hashIndex),
|
|
157
|
+
ref: source.slice(hashIndex + 1)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
async function assertAgentProfileLayout(sourcePath) {
|
|
140
162
|
const entries = await discoverProfileEntries(sourcePath);
|
|
141
163
|
if (entries.length === 0) {
|