@openhands/extensions 0.13.0 → 0.15.0

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.
@@ -14,6 +14,62 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const SKILLS_DIR = join(__dirname, "..", "skills");
15
15
  const OUTPUT = join(SKILLS_DIR, "index.js");
16
16
 
17
+ const MARKETPLACES_DIR = join(__dirname, "..", "marketplaces");
18
+ const SKILL_SOURCE_PREFIX = "./skills/";
19
+
20
+ /**
21
+ * Categories for skill entries, consumed by the agent-canvas /skills facet rail.
22
+ *
23
+ * Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing and keeps its own values.
24
+ */
25
+ export const SKILL_CATEGORY_IDS = [
26
+ "automations",
27
+ "environment",
28
+ "code-hosting",
29
+ "agent-authoring",
30
+ "code-quality",
31
+ "integrations",
32
+ "writing",
33
+ "design",
34
+ "other",
35
+ ];
36
+
37
+ const FALLBACK_CATEGORY = "other";
38
+
39
+ /** Build a `skill directory name -> {category, file}` map from every manifest. */
40
+ export function buildCategoryMap(marketplacesDir) {
41
+ const map = new Map();
42
+
43
+ for (const filename of readdirSync(marketplacesDir).filter((f) => f.endsWith(".json")).sort()) {
44
+ const manifest = JSON.parse(readFileSync(join(marketplacesDir, filename), "utf-8"));
45
+
46
+ for (const entry of manifest.plugins ?? []) {
47
+ const source = entry.source ?? "";
48
+ if (!source.startsWith(SKILL_SOURCE_PREFIX)) continue;
49
+
50
+ const name = source.slice(SKILL_SOURCE_PREFIX.length);
51
+ const { category } = entry;
52
+
53
+ if (!SKILL_CATEGORY_IDS.includes(category)) {
54
+ throw new Error(
55
+ `${filename}: skill "${name}" has category "${category}", expected one of: ${SKILL_CATEGORY_IDS.join(", ")}`,
56
+ );
57
+ }
58
+
59
+ const existing = map.get(name);
60
+ if (existing && existing.category !== category) {
61
+ throw new Error(
62
+ `Conflicting categories for skill "${name}": ${existing.file} says "${existing.category}", ${filename} says "${category}"`,
63
+ );
64
+ }
65
+
66
+ map.set(name, { category, file: filename });
67
+ }
68
+ }
69
+
70
+ return map;
71
+ }
72
+
17
73
  /** Minimal YAML frontmatter parser for the flat format used by SKILL.md. */
18
74
  export function parseFrontmatter(raw) {
19
75
  const result = {};
@@ -51,9 +107,15 @@ export function parseFrontmatter(raw) {
51
107
  };
52
108
  }
53
109
 
54
- /** Build the catalog from SKILL.md files in the given directory. */
55
- export function buildCatalog(skillsDir) {
110
+ /**
111
+ * Build the catalog from SKILL.md files in the given directory.
112
+ *
113
+ * Pass an isolated `marketplacesDir` when building from fixtures; the default reads this repo's real manifests.
114
+ */
115
+ export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
56
116
  const entries = [];
117
+ const categories = buildCategoryMap(marketplacesDir);
118
+ const uncategorized = [];
57
119
 
58
120
  for (const dir of readdirSync(skillsDir).sort()) {
59
121
  const dirPath = join(skillsDir, dir);
@@ -71,16 +133,26 @@ export function buildCatalog(skillsDir) {
71
133
  const fm = parseFrontmatter(parts[1]);
72
134
  const body = parts.slice(2).join("---").trim();
73
135
 
136
+ const mapped = categories.get(dir);
137
+ if (!mapped) uncategorized.push(dir);
138
+
74
139
  entries.push({
75
140
  name: fm.name?.trim() || dir,
76
141
  description: fm.description,
77
142
  triggers: fm.triggers,
78
143
  content: body,
144
+ category: mapped?.category ?? FALLBACK_CATEGORY,
79
145
  ...(fm.license ? { license: fm.license } : {}),
80
146
  ...(fm.compatibility ? { compatibility: fm.compatibility } : {}),
81
147
  });
82
148
  }
83
149
 
150
+ if (uncategorized.length > 0) {
151
+ console.warn(
152
+ `Warning: no marketplace entry, category defaults to "${FALLBACK_CATEGORY}": ${uncategorized.join(", ")}`,
153
+ );
154
+ }
155
+
84
156
  return entries;
85
157
  }
86
158
 
@@ -90,7 +162,8 @@ if (isMain) {
90
162
  const entries = buildCatalog(SKILLS_DIR);
91
163
 
92
164
  const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit.
93
- // Source of truth: skills/*/SKILL.md
165
+ // Source of truth: skills/*/SKILL.md and marketplaces/*.json (category)
166
+ export const SKILL_CATEGORY_IDS = ${JSON.stringify(SKILL_CATEGORY_IDS)};
94
167
  export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)};
95
168
  export default SKILLS_CATALOG;
96
169
  `;
package/skills/index.d.ts CHANGED
@@ -1,8 +1,29 @@
1
+ /**
2
+ * Categories for skill entries, consumed by the agent-canvas /skills facet rail.
3
+ *
4
+ * Sourced from the `category` field on marketplace entries whose `source` starts with `./skills/`.
5
+ * Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing.
6
+ */
7
+ export type SkillCategoryId =
8
+ | "automations"
9
+ | "environment"
10
+ | "code-hosting"
11
+ | "agent-authoring"
12
+ | "code-quality"
13
+ | "integrations"
14
+ | "writing"
15
+ | "design"
16
+ | "other";
17
+
18
+ export const SKILL_CATEGORY_IDS: readonly SkillCategoryId[];
19
+
1
20
  export interface SkillCatalogEntry {
2
21
  name: string;
3
22
  description: string;
4
23
  triggers: string[];
5
24
  content: string;
25
+ /** `"other"` when the skill has no marketplace entry. */
26
+ category: SkillCategoryId;
6
27
  license?: string;
7
28
  compatibility?: string;
8
29
  }