@openhands/extensions 0.2.0 → 0.3.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.2.0"
2
+ ".": "0.3.0"
3
3
  }
package/index.d.ts CHANGED
@@ -15,3 +15,5 @@ export {
15
15
  } from "./integrations/index.js";
16
16
  export { AUTOMATION_CATALOG } from "./automations/index.js";
17
17
  export type { RecommendedAutomation } from "./automations/index.js";
18
+ export { SKILLS_CATALOG } from "./skills/index.js";
19
+ export type { SkillCatalogEntry } from "./skills/index.js";
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { INTEGRATION_CATALOG } from "./integrations/index.js";
2
2
  export { AUTOMATION_CATALOG } from "./automations/index.js";
3
+ export { SKILLS_CATALOG } from "./skills/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,9 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/OpenHands/extensions"
10
10
  },
11
+ "scripts": {
12
+ "build:skills": "node scripts/build-skills-catalog.mjs"
13
+ },
11
14
  "engines": {
12
15
  "node": ">=18.20.0"
13
16
  },
@@ -42,6 +45,11 @@
42
45
  "default": "./integrations/logos.js"
43
46
  },
44
47
  "./integrations/catalog/*.json": "./integrations/catalog/*.json",
48
+ "./skills": {
49
+ "types": "./skills/index.d.ts",
50
+ "import": "./skills/index.js",
51
+ "default": "./skills/index.js"
52
+ },
45
53
  "./skills/*": "./skills/*",
46
54
  "./plugins/*": "./plugins/*",
47
55
  "./marketplaces/*.json": "./marketplaces/*.json",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "extensions"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "OpenHands extensions, plugins, and skills"
5
5
  requires-python = ">=3.12"
6
6
 
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generates skills/index.js from the SKILL.md source files.
4
+ *
5
+ * Run manually or via `npm run build:skills` before publishing.
6
+ * The generated file is checked into git so consumers get a normal
7
+ * JS module import — no build-time filesystem reads needed.
8
+ */
9
+ import { readdirSync, readFileSync, writeFileSync, statSync, existsSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const SKILLS_DIR = join(__dirname, "..", "skills");
15
+ const OUTPUT = join(SKILLS_DIR, "index.js");
16
+
17
+ /** Minimal YAML frontmatter parser for the flat format used by SKILL.md. */
18
+ export function parseFrontmatter(raw) {
19
+ const result = {};
20
+ let currentKey = null;
21
+
22
+ for (const line of raw.split("\n")) {
23
+ // List items
24
+ if (/^\s*-\s/.test(line)) {
25
+ if (currentKey) {
26
+ if (!Array.isArray(result[currentKey])) result[currentKey] = [];
27
+ result[currentKey].push(line.replace(/^\s*-\s*/, ""));
28
+ }
29
+ continue;
30
+ }
31
+ // Key: value (allow hyphens in key names, e.g. event-key)
32
+ const match = line.match(/^([\w-]+):\s*(.*)/);
33
+ if (match) {
34
+ currentKey = match[1];
35
+ const value = match[2].trim();
36
+ result[currentKey] = value && value !== ">" && value !== ">-" && value !== "|" ? value : "";
37
+ continue;
38
+ }
39
+ // Continuation line
40
+ if (currentKey && typeof result[currentKey] === "string" && line.trim()) {
41
+ result[currentKey] = (result[currentKey] + " " + line.trim()).trim();
42
+ }
43
+ }
44
+
45
+ return {
46
+ name: result.name ?? "",
47
+ description: result.description ?? "",
48
+ triggers: Array.isArray(result.triggers) ? result.triggers : [],
49
+ ...(result.license ? { license: result.license } : {}),
50
+ ...(result.compatibility ? { compatibility: result.compatibility } : {}),
51
+ };
52
+ }
53
+
54
+ /** Build the catalog from SKILL.md files in the given directory. */
55
+ export function buildCatalog(skillsDir) {
56
+ const entries = [];
57
+
58
+ for (const dir of readdirSync(skillsDir).sort()) {
59
+ const dirPath = join(skillsDir, dir);
60
+ if (!statSync(dirPath).isDirectory()) continue;
61
+ const skillMd = join(dirPath, "SKILL.md");
62
+ if (!existsSync(skillMd)) continue;
63
+
64
+ const raw = readFileSync(skillMd, "utf-8");
65
+ const parts = raw.split("---");
66
+ if (parts.length < 3) {
67
+ console.warn(`Warning: ${skillMd} missing frontmatter sections, skipping`);
68
+ continue;
69
+ }
70
+
71
+ const fm = parseFrontmatter(parts[1]);
72
+ const body = parts.slice(2).join("---").trim();
73
+
74
+ entries.push({
75
+ name: fm.name?.trim() || dir,
76
+ description: fm.description,
77
+ triggers: fm.triggers,
78
+ content: body,
79
+ ...(fm.license ? { license: fm.license } : {}),
80
+ ...(fm.compatibility ? { compatibility: fm.compatibility } : {}),
81
+ });
82
+ }
83
+
84
+ return entries;
85
+ }
86
+
87
+ // Run codegen when executed directly (not when imported as a module).
88
+ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
89
+ if (isMain) {
90
+ const entries = buildCatalog(SKILLS_DIR);
91
+
92
+ const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit.
93
+ // Source of truth: skills/*/SKILL.md
94
+ export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)};
95
+ export default SKILLS_CATALOG;
96
+ `;
97
+
98
+ writeFileSync(OUTPUT, source);
99
+ console.log(`Generated ${OUTPUT} with ${entries.length} skills`);
100
+ }
@@ -76,7 +76,7 @@ For comments spanning multiple lines, add `start_line` to specify the range:
76
76
  }
77
77
  ```
78
78
 
79
- **Important**: The suggestion must have the same number of lines as the range (e.g., lines 10-12 = 3 lines).
79
+ **`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. See the next section for the exact semantics; getting this wrong is how suggestions silently delete or duplicate code.
80
80
 
81
81
  ## Priority Labels
82
82
 
@@ -114,6 +114,44 @@ Use suggestions for: renaming, typos, small refactors (1-5 lines), type hints, d
114
114
 
115
115
  Avoid for: large refactors, architectural changes, ambiguous improvements.
116
116
 
117
+ ### How Suggestions Actually Work (READ THIS BEFORE WRITING ONE)
118
+
119
+ A suggestion block **replaces** the targeted range with its contents. The replaced range is:
120
+
121
+ - `line` only → the single line `line` (replaces 1 line)
122
+ - `start_line` + `line` → the inclusive range `start_line..line` (replaces `line - start_line + 1` lines)
123
+
124
+ The suggestion content can be **any number of lines** — 0 (deletion), 1, or many. It does not have to match the range size. Whatever is between the ` ```suggestion ` and closing ` ``` ` fences becomes the new content of those lines.
125
+
126
+ Writing the wrong combination of `start_line`/`line` and suggestion body is what causes accepted suggestions to **duplicate** or **delete** code. Use the table below as your contract:
127
+
128
+ | Intent | `start_line` | `line` | Suggestion body must contain |
129
+ |--------|--------------|--------|-------------------------------|
130
+ | Change line N | omit | N | the new content for line N |
131
+ | Change lines N..M | N | M | the new content for the whole block |
132
+ | **Add** a line **after** line N (keep line N) | omit | N | line N's exact current text, then the new line(s) |
133
+ | **Add** a line **before** line N (keep line N) | omit | N | the new line(s), then line N's exact current text |
134
+ | **Insert** lines inside range N..M (keep N..M) | N | M | every original line in N..M plus the new lines, in the final desired order |
135
+ | **Delete** line N | omit | N | empty body (just an empty ` ```suggestion ``` ` block) |
136
+ | **Delete** lines N..M | N | M | empty body |
137
+
138
+ ### Common Mistakes That Break Code
139
+
140
+ 1. **Duplicated lines.** You copy a neighboring line (N-1 or N+1) into the suggestion body as context — that line is still present in the file outside the replaced range, so accepting the suggestion inserts a second copy of it. Fix: only include lines that fall within the targeted range, plus any genuinely new content.
141
+ 2. **Disappearing lines.** You target `start_line=10, line=12` to comment on a 3-line block, but your suggestion body only contains 1 line because you "only want to change line 11". Accepting that suggestion deletes lines 10 and 12. Fix: either narrow the range to just line 11, or include lines 10 and 12 verbatim in the body.
142
+ 3. **Description does not match the suggestion.** The prose says "rename this variable" but the suggestion replaces an entire function. Or the prose says "add a None check" but the suggestion only contains the check (deleting the original code). Fix: after writing the suggestion, re-read the prose and confirm the resulting file would match it line-for-line.
143
+
144
+ ### Mandatory Verification Before Posting
145
+
146
+ For every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON:
147
+
148
+ 1. Read the actual file lines that will be replaced: `sed -n '<start_line>,<line>p' <path>` (or `sed -n '<line>p' <path>` for a single-line target).
149
+ 2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context.
150
+ 3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one.
151
+ 4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead.
152
+
153
+ If you are not 100% sure the suggestion will produce the exact code you described, drop the ` ```suggestion ``` ` block and leave a regular inline comment. A correct prose comment is always better than a one-click suggestion that silently corrupts the file.
154
+
117
155
  ## Finding Line Numbers
118
156
 
119
157
  ```bash
@@ -143,6 +181,6 @@ curl -X POST \
143
181
  3. Post **ONE** review using `gh api --input /tmp/review.json`
144
182
  4. Use priority labels (🔴🟠🟡) on every comment
145
183
  5. Do NOT post comments for code that is acceptable — only comment when action is needed
146
- 6. Use suggestion syntax for concrete code changes
184
+ 6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see "How Suggestions Actually Work")
147
185
  7. Keep the review body brief (details go in inline comments)
148
186
  8. If no issues: post a short approval message with no inline comments
@@ -0,0 +1,11 @@
1
+ export interface SkillCatalogEntry {
2
+ name: string;
3
+ description: string;
4
+ triggers: string[];
5
+ content: string;
6
+ license?: string;
7
+ compatibility?: string;
8
+ }
9
+
10
+ export const SKILLS_CATALOG: SkillCatalogEntry[];
11
+ export default SKILLS_CATALOG;