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/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
- await assertAgentProfileLayout(sourcePath);
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 gitUrl = normalizeGithubUrl(source);
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 clone = spawnSync("git", ["clone", "--depth=1", gitUrl, cloneDir], {
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
- await assertAgentProfileLayout(cloneDir);
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) {