awesome-agents 0.1.3 → 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/AGENTS.md +7 -4
- package/CHANGELOG.md +35 -0
- package/README.md +45 -17
- package/docs/cli.md +29 -8
- package/docs/product/README.md +1 -1
- package/docs/product/command-model.md +14 -9
- package/docs/product/harness-targets.md +26 -3
- package/docs/product/product-scope.md +7 -2
- package/docs/product/profile-source-format.md +28 -12
- package/package.json +3 -2
- package/src/cli.js +140 -10
- package/src/constants.js +14 -3
- package/src/help.js +14 -12
- package/src/installer.js +148 -14
- package/src/renderers.js +117 -4
- package/src/skills.js +397 -0
- package/src/source.js +162 -20
package/src/renderers.js
CHANGED
|
@@ -20,7 +20,7 @@ export function normalizeAgentList(input, options = {}) {
|
|
|
20
20
|
|
|
21
21
|
const rawAgents = flattenValues(input);
|
|
22
22
|
if (rawAgents.length === 0) {
|
|
23
|
-
return
|
|
23
|
+
return arrayify(options.defaultAgent);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (rawAgents.includes("*")) {
|
|
@@ -41,6 +41,9 @@ export function renderForAgent(profile, agent, context) {
|
|
|
41
41
|
if (normalized === "opencode") {
|
|
42
42
|
return renderOpenCode(profile, context);
|
|
43
43
|
}
|
|
44
|
+
if (normalized === "tenex-edge") {
|
|
45
|
+
return renderTenexEdge(profile, context);
|
|
46
|
+
}
|
|
44
47
|
throw new Error(`Unsupported agent "${agent}"`);
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -85,6 +88,15 @@ export function resolveTargetPath(profile, agent, options = {}) {
|
|
|
85
88
|
return path.join(opencodeHome, "agents", `${profile.slug}.md`);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
if (normalized === "tenex-edge") {
|
|
92
|
+
const tenexEdgeHome = options.tenexEdgeHome
|
|
93
|
+
? path.resolve(expandHome(options.tenexEdgeHome, home))
|
|
94
|
+
: process.env.TENEX_EDGE_HOME
|
|
95
|
+
? path.resolve(expandHome(process.env.TENEX_EDGE_HOME, home))
|
|
96
|
+
: path.join(home, ".tenex-edge");
|
|
97
|
+
return path.join(tenexEdgeHome, "agents", `${profile.slug}.json`);
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
throw new Error(`Unsupported agent "${agent}"`);
|
|
89
101
|
}
|
|
90
102
|
|
|
@@ -105,9 +117,9 @@ function renderCodex(profile, context) {
|
|
|
105
117
|
`# ${GENERATED_MARKER}.`,
|
|
106
118
|
`# Source: ${context.source}`,
|
|
107
119
|
`# Profile: ${profile.slug}`,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
`# Display name: ${tomlComment(profile.name)}`,
|
|
121
|
+
`# Summary: ${tomlComment(profile.summary || profile.name)}`,
|
|
122
|
+
""
|
|
111
123
|
];
|
|
112
124
|
|
|
113
125
|
if (model && model !== "inherit") {
|
|
@@ -159,6 +171,38 @@ function renderOpenCode(profile, context) {
|
|
|
159
171
|
return stringifyFrontmatter(attributes, `${marker}\n\n${buildInstructionBody(profile, undefined, "opencode")}`);
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
function renderTenexEdge(profile, context) {
|
|
175
|
+
const existing = tenexEdgeKeyMaterial(context.existingContent);
|
|
176
|
+
const keyMaterial = existing ?? generateNostrKeypair();
|
|
177
|
+
const agent = {
|
|
178
|
+
description: profile.summary || profile.name,
|
|
179
|
+
prompt: buildInstructionBody(profile, profile.adapters["tenex-edge"] ?? profile.adapters["claude-code"], "tenex-edge")
|
|
180
|
+
};
|
|
181
|
+
const model = chooseClaudeModel(profile);
|
|
182
|
+
const effort = profile.attributes.recommended_reasoning_effort;
|
|
183
|
+
|
|
184
|
+
if (model && model !== "inherit") {
|
|
185
|
+
agent.model = model;
|
|
186
|
+
}
|
|
187
|
+
if (effort && effort !== "inherit" && ["low", "medium", "high", "xhigh", "max"].includes(effort)) {
|
|
188
|
+
agent.effort = effort;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stored = {
|
|
192
|
+
slug: profile.slug,
|
|
193
|
+
secret_key: keyMaterial.secret_key,
|
|
194
|
+
public_key: keyMaterial.public_key,
|
|
195
|
+
created_at: keyMaterial.created_at ?? Math.floor(Date.now() / 1000),
|
|
196
|
+
command: ["claude"],
|
|
197
|
+
agent,
|
|
198
|
+
byline: profile.summary || profile.name,
|
|
199
|
+
managed_by: GENERATED_MARKER,
|
|
200
|
+
source: context.source
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return `${JSON.stringify(stored, null, 2)}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
162
206
|
function buildInstructionBody(profile, adapter, harness) {
|
|
163
207
|
const parts = [
|
|
164
208
|
profile.body.trimEnd(),
|
|
@@ -173,6 +217,20 @@ function buildInstructionBody(profile, adapter, harness) {
|
|
|
173
217
|
"- The runtime agent manages any profile-specific home directory and notes at task time."
|
|
174
218
|
];
|
|
175
219
|
|
|
220
|
+
if (profile.installedSkills?.length) {
|
|
221
|
+
const skillBase = path.join(path.dirname(profile.installedSkills[0].path), "<skill>");
|
|
222
|
+
parts.push(
|
|
223
|
+
"",
|
|
224
|
+
"## Immediately Relevant Skills",
|
|
225
|
+
"",
|
|
226
|
+
`Immediately relevant skills; you should load these right away from \`${skillBase}\`.`,
|
|
227
|
+
""
|
|
228
|
+
);
|
|
229
|
+
for (const skill of profile.installedSkills) {
|
|
230
|
+
parts.push(`- \`${skill.name}\`: \`${skill.path}\``);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
176
234
|
if (adapter?.body) {
|
|
177
235
|
parts.push("", "## Harness Adapter", "", adapter.body.trimEnd());
|
|
178
236
|
}
|
|
@@ -219,10 +277,65 @@ function chooseOpenCodeModel(profile) {
|
|
|
219
277
|
return candidates.find((model) => model.includes("/"));
|
|
220
278
|
}
|
|
221
279
|
|
|
280
|
+
function tenexEdgeKeyMaterial(content) {
|
|
281
|
+
if (!content) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let parsed;
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(content);
|
|
288
|
+
} catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isHex64(parsed.secret_key)) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const derived = derivePublicKey(parsed.secret_key);
|
|
297
|
+
if (!derived) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
secret_key: parsed.secret_key,
|
|
303
|
+
public_key: isHex64(parsed.public_key) ? parsed.public_key : derived,
|
|
304
|
+
created_at: Number.isInteger(parsed.created_at) ? parsed.created_at : undefined
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function generateNostrKeypair() {
|
|
309
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
310
|
+
const publicKey = ecdh.generateKeys(undefined, "uncompressed");
|
|
311
|
+
return {
|
|
312
|
+
secret_key: ecdh.getPrivateKey("hex"),
|
|
313
|
+
public_key: publicKey.subarray(1, 33).toString("hex")
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function derivePublicKey(secretKey) {
|
|
318
|
+
try {
|
|
319
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
320
|
+
ecdh.setPrivateKey(Buffer.from(secretKey, "hex"));
|
|
321
|
+
return ecdh.getPublicKey(undefined, "uncompressed").subarray(1, 33).toString("hex");
|
|
322
|
+
} catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isHex64(value) {
|
|
328
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
222
331
|
function tomlString(value) {
|
|
223
332
|
return JSON.stringify(String(value));
|
|
224
333
|
}
|
|
225
334
|
|
|
335
|
+
function tomlComment(value) {
|
|
336
|
+
return String(value).replace(/\s+/g, " ").trim();
|
|
337
|
+
}
|
|
338
|
+
|
|
226
339
|
function tomlMultiline(value) {
|
|
227
340
|
const text = String(value).trimEnd();
|
|
228
341
|
if (!text.includes("'''")) {
|
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
|
+
}
|