awesome-agents 0.1.5 → 0.1.8
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 +44 -0
- package/README.md +36 -9
- package/docs/cli.md +33 -7
- package/docs/plans/public-builder-profiles-agent-model-cards/plan.md +105 -0
- package/docs/product/README.md +2 -1
- package/docs/product/command-model.md +13 -12
- package/docs/product/harness-targets.md +50 -1
- package/docs/product/open-questions.md +6 -0
- package/docs/product/product-scope.md +4 -4
- package/docs/product/profile-source-format.md +29 -0
- package/docs/product/site-profiles-and-model-cards.md +135 -0
- package/package.json +4 -2
- package/scripts/release.js +0 -4
- package/src/cli.js +140 -10
- package/src/constants.js +10 -4
- package/src/help.js +13 -10
- package/src/installer.js +114 -11
- package/src/renderers.js +222 -3
- package/src/skills.js +397 -0
- package/src/source.js +26 -4
package/src/renderers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { AGENT_ALIASES,
|
|
4
|
+
import { AGENT_ALIASES, GENERATED_MARKER, SUPPORTED_AGENTS } from "./constants.js";
|
|
5
5
|
import { stringifyFrontmatter } from "./frontmatter.js";
|
|
6
6
|
import { expandHome } from "./source.js";
|
|
7
7
|
|
|
@@ -20,8 +20,7 @@ export function normalizeAgentList(input, options = {}) {
|
|
|
20
20
|
|
|
21
21
|
const rawAgents = flattenValues(input);
|
|
22
22
|
if (rawAgents.length === 0) {
|
|
23
|
-
|
|
24
|
-
return fallback.length > 0 ? fallback : [DEFAULT_AGENT];
|
|
23
|
+
return arrayify(options.defaultAgent);
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
if (rawAgents.includes("*")) {
|
|
@@ -42,6 +41,12 @@ export function renderForAgent(profile, agent, context) {
|
|
|
42
41
|
if (normalized === "opencode") {
|
|
43
42
|
return renderOpenCode(profile, context);
|
|
44
43
|
}
|
|
44
|
+
if (normalized === "goose") {
|
|
45
|
+
return renderGoose(profile, context);
|
|
46
|
+
}
|
|
47
|
+
if (normalized === "tenex-edge") {
|
|
48
|
+
return renderTenexEdge(profile, context);
|
|
49
|
+
}
|
|
45
50
|
throw new Error(`Unsupported agent "${agent}"`);
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -86,6 +91,28 @@ export function resolveTargetPath(profile, agent, options = {}) {
|
|
|
86
91
|
return path.join(opencodeHome, "agents", `${profile.slug}.md`);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
if (normalized === "goose") {
|
|
95
|
+
if (scope === "project") {
|
|
96
|
+
return path.join(cwd, ".agents", "agents", `${profile.slug}.md`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const gooseHome = options.gooseHome
|
|
100
|
+
? path.resolve(expandHome(options.gooseHome, home))
|
|
101
|
+
: process.env.GOOSE_HOME
|
|
102
|
+
? path.resolve(expandHome(process.env.GOOSE_HOME, home))
|
|
103
|
+
: path.join(home, ".agents");
|
|
104
|
+
return path.join(gooseHome, "agents", `${profile.slug}.md`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (normalized === "tenex-edge") {
|
|
108
|
+
const tenexEdgeHome = options.tenexEdgeHome
|
|
109
|
+
? path.resolve(expandHome(options.tenexEdgeHome, home))
|
|
110
|
+
: process.env.TENEX_EDGE_HOME
|
|
111
|
+
? path.resolve(expandHome(process.env.TENEX_EDGE_HOME, home))
|
|
112
|
+
: path.join(home, ".tenex-edge");
|
|
113
|
+
return path.join(tenexEdgeHome, "agents", `${profile.slug}.json`);
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
throw new Error(`Unsupported agent "${agent}"`);
|
|
90
117
|
}
|
|
91
118
|
|
|
@@ -160,6 +187,98 @@ function renderOpenCode(profile, context) {
|
|
|
160
187
|
return stringifyFrontmatter(attributes, `${marker}\n\n${buildInstructionBody(profile, undefined, "opencode")}`);
|
|
161
188
|
}
|
|
162
189
|
|
|
190
|
+
function renderGoose(profile, context) {
|
|
191
|
+
const attributes = {
|
|
192
|
+
name: profile.slug,
|
|
193
|
+
description: profile.summary || profile.name
|
|
194
|
+
};
|
|
195
|
+
const model = chooseGooseModel(profile);
|
|
196
|
+
if (model && model !== "inherit") {
|
|
197
|
+
attributes.model = model;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const marker = htmlMarker(profile, "goose", context);
|
|
201
|
+
return stringifyFrontmatter(attributes, `${marker}\n\n${buildInstructionBody(profile, profile.adapters.goose ?? profile.adapters["claude-code"], "goose")}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderTenexEdge(profile, context) {
|
|
205
|
+
const existing = tenexEdgeKeyMaterial(context.existingContent);
|
|
206
|
+
const keyMaterial = existing ?? generateNostrKeypair();
|
|
207
|
+
const commands = tenexEdgeCommands(profile, context);
|
|
208
|
+
const agent = tenexEdgeInlineAgent(profile);
|
|
209
|
+
|
|
210
|
+
const stored = {
|
|
211
|
+
slug: profile.slug,
|
|
212
|
+
secret_key: keyMaterial.secret_key,
|
|
213
|
+
public_key: keyMaterial.public_key,
|
|
214
|
+
created_at: keyMaterial.created_at ?? Math.floor(Date.now() / 1000),
|
|
215
|
+
commands,
|
|
216
|
+
byline: profile.summary || profile.name,
|
|
217
|
+
managed_by: GENERATED_MARKER,
|
|
218
|
+
source: context.source
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (hasInlineClaudeCommand(commands)) {
|
|
222
|
+
stored.agent = agent;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `${JSON.stringify(stored, null, 2)}\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function tenexEdgeInlineAgent(profile) {
|
|
229
|
+
const agent = {
|
|
230
|
+
description: profile.summary || profile.name,
|
|
231
|
+
prompt: buildInstructionBody(profile, profile.adapters["tenex-edge"] ?? profile.adapters["claude-code"], "tenex-edge")
|
|
232
|
+
};
|
|
233
|
+
const model = chooseClaudeModel(profile);
|
|
234
|
+
const effort = profile.attributes.recommended_reasoning_effort;
|
|
235
|
+
|
|
236
|
+
if (model && model !== "inherit") {
|
|
237
|
+
agent.model = model;
|
|
238
|
+
}
|
|
239
|
+
if (effort && effort !== "inherit" && ["low", "medium", "high", "xhigh", "max"].includes(effort)) {
|
|
240
|
+
agent.effort = effort;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return agent;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function tenexEdgeCommands(profile, context) {
|
|
247
|
+
const commands = [];
|
|
248
|
+
for (const harness of arrayify(context.selectedHarnesses)) {
|
|
249
|
+
if (harness === "codex") {
|
|
250
|
+
commands.push({
|
|
251
|
+
name: "codex",
|
|
252
|
+
argv: ["codex", "--profile", profile.slug]
|
|
253
|
+
});
|
|
254
|
+
} else if (harness === "claude-code") {
|
|
255
|
+
commands.push({
|
|
256
|
+
name: "claude",
|
|
257
|
+
argv: ["claude", "--agent", profile.slug]
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Keep tenex-edge-only installs self-contained. tenex-edge expands this with
|
|
263
|
+
// the inline `agent` definition as `claude --agents ... --agent <slug>`.
|
|
264
|
+
if (!commands.some((command) => command.name === "claude")) {
|
|
265
|
+
commands.push({
|
|
266
|
+
name: "claude",
|
|
267
|
+
argv: ["claude"]
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return uniqueBy(commands, (command) => command.name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function hasInlineClaudeCommand(commands) {
|
|
275
|
+
return commands.some((command) => (
|
|
276
|
+
command.name === "claude" &&
|
|
277
|
+
command.argv.length === 1 &&
|
|
278
|
+
command.argv[0] === "claude"
|
|
279
|
+
));
|
|
280
|
+
}
|
|
281
|
+
|
|
163
282
|
function buildInstructionBody(profile, adapter, harness) {
|
|
164
283
|
const parts = [
|
|
165
284
|
profile.body.trimEnd(),
|
|
@@ -174,6 +293,20 @@ function buildInstructionBody(profile, adapter, harness) {
|
|
|
174
293
|
"- The runtime agent manages any profile-specific home directory and notes at task time."
|
|
175
294
|
];
|
|
176
295
|
|
|
296
|
+
if (profile.installedSkills?.length) {
|
|
297
|
+
const skillBase = path.join(path.dirname(profile.installedSkills[0].path), "<skill>");
|
|
298
|
+
parts.push(
|
|
299
|
+
"",
|
|
300
|
+
"## Immediately Relevant Skills",
|
|
301
|
+
"",
|
|
302
|
+
`Immediately relevant skills; you should load these right away from \`${skillBase}\`.`,
|
|
303
|
+
""
|
|
304
|
+
);
|
|
305
|
+
for (const skill of profile.installedSkills) {
|
|
306
|
+
parts.push(`- \`${skill.name}\`: \`${skill.path}\``);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
177
310
|
if (adapter?.body) {
|
|
178
311
|
parts.push("", "## Harness Adapter", "", adapter.body.trimEnd());
|
|
179
312
|
}
|
|
@@ -220,6 +353,78 @@ function chooseOpenCodeModel(profile) {
|
|
|
220
353
|
return candidates.find((model) => model.includes("/"));
|
|
221
354
|
}
|
|
222
355
|
|
|
356
|
+
function chooseGooseModel(profile) {
|
|
357
|
+
const candidates = [
|
|
358
|
+
profile.attributes.recommended_model,
|
|
359
|
+
...arrayify(profile.attributes.recommended_models)
|
|
360
|
+
].filter(Boolean).map((value) => String(value).toLowerCase());
|
|
361
|
+
|
|
362
|
+
if (candidates.some((model) => model.includes("opus"))) {
|
|
363
|
+
return "claude-3-5-sonnet";
|
|
364
|
+
}
|
|
365
|
+
if (candidates.some((model) => model.includes("sonnet"))) {
|
|
366
|
+
return "claude-3-5-sonnet";
|
|
367
|
+
}
|
|
368
|
+
if (candidates.some((model) => model.includes("haiku"))) {
|
|
369
|
+
return "claude-3-5-haiku";
|
|
370
|
+
}
|
|
371
|
+
if (candidates.some((model) => model.includes("gpt"))) {
|
|
372
|
+
return candidates.find((model) => model.includes("gpt"));
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function tenexEdgeKeyMaterial(content) {
|
|
378
|
+
if (!content) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let parsed;
|
|
383
|
+
try {
|
|
384
|
+
parsed = JSON.parse(content);
|
|
385
|
+
} catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!isHex64(parsed.secret_key)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const derived = derivePublicKey(parsed.secret_key);
|
|
394
|
+
if (!derived) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
secret_key: parsed.secret_key,
|
|
400
|
+
public_key: isHex64(parsed.public_key) ? parsed.public_key : derived,
|
|
401
|
+
created_at: Number.isInteger(parsed.created_at) ? parsed.created_at : undefined
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function generateNostrKeypair() {
|
|
406
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
407
|
+
const publicKey = ecdh.generateKeys(undefined, "uncompressed");
|
|
408
|
+
return {
|
|
409
|
+
secret_key: ecdh.getPrivateKey("hex"),
|
|
410
|
+
public_key: publicKey.subarray(1, 33).toString("hex")
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function derivePublicKey(secretKey) {
|
|
415
|
+
try {
|
|
416
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
417
|
+
ecdh.setPrivateKey(Buffer.from(secretKey, "hex"));
|
|
418
|
+
return ecdh.getPublicKey(undefined, "uncompressed").subarray(1, 33).toString("hex");
|
|
419
|
+
} catch {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isHex64(value) {
|
|
425
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
426
|
+
}
|
|
427
|
+
|
|
223
428
|
function tomlString(value) {
|
|
224
429
|
return JSON.stringify(String(value));
|
|
225
430
|
}
|
|
@@ -250,6 +455,20 @@ function arrayify(value) {
|
|
|
250
455
|
return [value];
|
|
251
456
|
}
|
|
252
457
|
|
|
458
|
+
function uniqueBy(values, keyForValue) {
|
|
459
|
+
const seen = new Set();
|
|
460
|
+
const out = [];
|
|
461
|
+
for (const value of values) {
|
|
462
|
+
const key = keyForValue(value);
|
|
463
|
+
if (seen.has(key)) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
seen.add(key);
|
|
467
|
+
out.push(value);
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
253
472
|
function flattenValues(values) {
|
|
254
473
|
return arrayify(values)
|
|
255
474
|
.flatMap((value) => String(value).split(","))
|
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
|
+
}
|