@openhands/extensions 0.15.0 → 0.17.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.
Files changed (45) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/automations/README.md +97 -18
  4. package/automations/bundle-index.js +9 -0
  5. package/automations/catalog/github-pr-reviewer/manifest.json +23 -7
  6. package/automations/catalog/github-repo-monitor/manifest.json +28 -34
  7. package/automations/catalog/jira-issue-to-pr/manifest.json +3 -3
  8. package/automations/catalog.schema.json +102 -1
  9. package/automations/index.d.ts +174 -1
  10. package/automations/index.js +14 -0
  11. package/automations/interface.json +93 -6
  12. package/automations/interface.schema.json +273 -9
  13. package/index.d.ts +1 -1
  14. package/index.js +1 -1
  15. package/integrations/catalog/posthog.json +41 -3
  16. package/integrations/catalog/sonarqube.json +78 -0
  17. package/integrations/catalog-index.js +65 -63
  18. package/marketplaces/openhands-extensions.json +11 -0
  19. package/package.json +1 -1
  20. package/plugins/qa-changes/action.yml +1 -1
  21. package/pyproject.toml +1 -1
  22. package/python/openhands_extensions/_version.py +1 -1
  23. package/scripts/build-automation-catalog.mjs +30 -0
  24. package/scripts/build-skills-catalog.mjs +24 -7
  25. package/skills/add-skill/SKILL.md +1 -1
  26. package/skills/github-pr-reviewer/README.md +12 -6
  27. package/skills/github-pr-reviewer/SKILL.md +93 -31
  28. package/skills/github-pr-reviewer/commands/pr-reviewer-setup.md +1 -1
  29. package/skills/github-pr-reviewer/references/state-schema.md +68 -18
  30. package/skills/github-pr-reviewer/scripts/main.py +540 -103
  31. package/skills/github-pr-reviewer/tests/test_main.py +503 -0
  32. package/skills/index.d.ts +6 -0
  33. package/skills/index.js +29 -17
  34. package/skills/jira-issue-to-pr/scripts/main.py +4 -2
  35. package/skills/openhands-sdk/SKILL.md +2 -0
  36. package/skills/skill-creator/SKILL.md +1 -1
  37. package/skills/slack-channel-monitor/scripts/main.py +3 -2
  38. package/tests/fixtures/automations/capabilities.json +30 -0
  39. package/tests/fixtures/automations/github-pr-reviewer.json +319 -55
  40. package/tests/fixtures/automations/github-repo-monitor.json +153 -62
  41. package/tests/test_automation_setup.py +226 -31
  42. package/tests/test_catalogs.py +43 -0
  43. package/tests/test_interface_manifest.py +20 -0
  44. package/tests/test_skills_catalog.py +182 -36
  45. package/tests/test_slack_channel_monitor.py +1 -1
@@ -63,6 +63,10 @@
63
63
  "description": "direct only. What the automation is told to do. The rest of the create request restates the form, so it is not written here: the automation's name comes from this entry, the repository from the repo-picker field, and the trigger from the key and fields under form.triggers.",
64
64
  "$ref": "#/$defs/templateValue"
65
65
  },
66
+ "bundle": {
67
+ "description": "direct only, and the alternative to prompt. The script tarball this entry ships: the automation is deterministic machinery rather than judgement, so it runs as its own code and the host creates it through the raw create endpoint instead of a preset. The rest of the create request still restates the form, so it is not written here either.",
68
+ "$ref": "#/$defs/bundle"
69
+ },
66
70
  "filter": {
67
71
  "description": "direct only, and only for an event trigger. The expression deciding whether a delivered event belongs to this automation. It composes form values into a JMESPath expression, which is the one part of an event trigger that cannot be read off the form.",
68
72
  "$ref": "#/$defs/templateValue"
@@ -79,7 +83,10 @@
79
83
  "required": ["mode"]
80
84
  },
81
85
  "then": {
82
- "required": ["prompt"]
86
+ "oneOf": [
87
+ { "required": ["prompt"], "not": { "required": ["bundle"] } },
88
+ { "required": ["bundle"], "not": { "required": ["prompt"] } }
89
+ ]
83
90
  }
84
91
  },
85
92
  {
@@ -91,6 +98,7 @@
91
98
  "required": ["message"],
92
99
  "allOf": [
93
100
  { "not": { "required": ["prompt"] } },
101
+ { "not": { "required": ["bundle"] } },
94
102
  { "not": { "required": ["filter"] } }
95
103
  ]
96
104
  }
@@ -108,6 +116,88 @@
108
116
  "required": ["form"]
109
117
  },
110
118
  "then": { "not": { "required": ["filter"] } }
119
+ },
120
+ {
121
+ "if": { "required": ["bundle"] },
122
+ "then": { "not": { "required": ["filter"] } }
123
+ }
124
+ ]
125
+ },
126
+ "bundle": {
127
+ "description": "What the host packs into a .tar.gz and uploads before creating the automation. It names files inside this repository and the command that runs them - never a host, a URL, or code to evaluate.",
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "required": ["version", "entrypoint", "files", "config"],
131
+ "properties": {
132
+ "version": {
133
+ "description": "The version of this bundle, recorded as the created automation's template provenance. Bump it when the shipped files or the config shape change: it is what tells an already-enabled deployment that what it installed is no longer current. Not the package version, which moves for reasons that have nothing to do with this entry.",
134
+ "type": "string",
135
+ "minLength": 1,
136
+ "maxLength": 50,
137
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
138
+ },
139
+ "entrypoint": {
140
+ "description": "The command the automation service runs inside the extracted tarball, such as `python3 main.py`. No shell metacharacters: the service rejects them, and a bundle has no reason to need one.",
141
+ "type": "string",
142
+ "minLength": 1,
143
+ "maxLength": 500,
144
+ "pattern": "^[A-Za-z0-9 ._/-]+$"
145
+ },
146
+ "setupScript": {
147
+ "description": "Path inside the tarball to a script run once before the entrypoint, such as `setup.sh`. Absent when the bundle needs no installation step, which is the case for a standard-library script.",
148
+ "type": "string",
149
+ "minLength": 1,
150
+ "maxLength": 255,
151
+ "pattern": "^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$",
152
+ "not": { "pattern": "(^|/)\\.\\.?(/|$)" }
153
+ },
154
+ "timeout": {
155
+ "description": "Seconds a single run may take, when this entry needs more than the service default. A poll that fetches an archive per queued item needs longer than one that only calls an API.",
156
+ "type": "integer",
157
+ "minimum": 1,
158
+ "maximum": 86400
159
+ },
160
+ "files": {
161
+ "description": "The tarball's contents, keyed by the path each file takes inside the archive and valued by where it lives in this repository. Stated rather than derived from the entry directory, so a script shipped by both a skill and the catalog has one copy rather than two.",
162
+ "type": "object",
163
+ "minProperties": 1,
164
+ "maxProperties": 32,
165
+ "propertyNames": {
166
+ "pattern": "^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$",
167
+ "not": { "pattern": "(^|/)\\.\\.?(/|$)" },
168
+ "maxLength": 255
169
+ },
170
+ "additionalProperties": { "$ref": "#/$defs/bundleSource" }
171
+ },
172
+ "config": {
173
+ "description": "The config.json packed beside the entrypoint, rendered from the form. This is the bundle's analogue of prompt: the one thing that cannot be read off the form, because only the entry knows which key of its own script each field fills.",
174
+ "$ref": "#/$defs/bundleConfig"
175
+ }
176
+ }
177
+ },
178
+ "bundleSource": {
179
+ "description": "A file in this repository, as a repository-relative path under skills/ or automations/. No absolute path, no traversal, and no scheme, so a bundle cannot name anything outside the published package.",
180
+ "type": "string",
181
+ "minLength": 1,
182
+ "maxLength": 255,
183
+ "pattern": "^(skills|automations)/[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$",
184
+ "not": { "pattern": "(^|/)\\.\\.?(/|$)" }
185
+ },
186
+ "bundleConfig": {
187
+ "description": "The JSON document written to config.json. Its string leaves may embed the same placeholders every other value uses. It carries no markup rule, because it is written to a file rather than rendered.",
188
+ "type": "object",
189
+ "minProperties": 1,
190
+ "additionalProperties": { "$ref": "#/$defs/bundleConfigValue" }
191
+ },
192
+ "bundleConfigValue": {
193
+ "description": "A config leaf: a templated string, a number, a boolean, null, or an array or object of the same.",
194
+ "anyOf": [
195
+ { "$ref": "#/$defs/templateValue" },
196
+ { "type": ["number", "boolean", "null"] },
197
+ { "type": "array", "items": { "$ref": "#/$defs/bundleConfigValue" } },
198
+ {
199
+ "type": "object",
200
+ "additionalProperties": { "$ref": "#/$defs/bundleConfigValue" }
111
201
  }
112
202
  ]
113
203
  },
@@ -224,6 +314,10 @@
224
314
  "default": { "type": "string" },
225
315
  "required": { "type": "boolean" },
226
316
  "provider": { "enum": ["github", "gitlab", "bitbucket"] },
317
+ "multiple": {
318
+ "description": "repo-picker only. The field collects several repositories rather than one, and its value is a list. A whole-value placeholder resolves to that list, so a payload can state `\"repos\": \"{{form.repositories}}\"` and get an array.",
319
+ "const": true
320
+ },
227
321
  "options": {
228
322
  "type": "array",
229
323
  "minItems": 1,
@@ -259,6 +353,13 @@
259
353
  },
260
354
  "then": { "required": ["provider"] }
261
355
  },
356
+ {
357
+ "if": {
358
+ "properties": { "type": { "not": { "const": "repo-picker" } } },
359
+ "required": ["type"]
360
+ },
361
+ "then": { "not": { "required": ["multiple"] } }
362
+ },
262
363
  {
263
364
  "if": {
264
365
  "properties": { "type": { "not": { "const": "select" } } },
@@ -63,6 +63,11 @@ export interface AutomationFormField {
63
63
  default?: string;
64
64
  required: boolean;
65
65
  provider?: AutomationGitProvider;
66
+ /**
67
+ * repo-picker only. The field collects several repositories rather than one,
68
+ * and its value is a list. A whole-value placeholder resolves to that list.
69
+ */
70
+ multiple?: true;
66
71
  options?: AutomationFieldOption[];
67
72
  constraints?: AutomationFieldConstraints;
68
73
  }
@@ -96,12 +101,40 @@ export interface AutomationForm {
96
101
  args: AutomationFormFields;
97
102
  }
98
103
 
104
+ /** A config.json leaf: templated string, number, boolean, null, or a nesting of those. */
105
+ export type AutomationBundleConfigValue =
106
+ | string
107
+ | number
108
+ | boolean
109
+ | null
110
+ | AutomationBundleConfigValue[]
111
+ | { [key: string]: AutomationBundleConfigValue };
112
+
113
+ /**
114
+ * The script tarball a direct entry may ship instead of a prompt, for an
115
+ * automation that is deterministic machinery rather than judgement.
116
+ */
117
+ export interface AutomationBundle {
118
+ /** The command run inside the extracted tarball. */
119
+ entrypoint: string;
120
+ /** Script run once before the entrypoint. Absent when nothing to install. */
121
+ setupScript?: string;
122
+ /** Seconds a run may take, when the service default is not enough. */
123
+ timeout?: number;
124
+ /** Packed path -> the repository path the file is read from at build time. */
125
+ files: Record<string, string>;
126
+ /** Rendered from the form and packed as config.json beside the entrypoint. */
127
+ config: Record<string, AutomationBundleConfigValue>;
128
+ }
129
+
99
130
  export interface AutomationSetup {
100
131
  version: "1.0";
101
132
  mode: AutomationSetupMode;
102
133
  form: AutomationForm;
103
134
  /** direct only. What the automation is told to do. */
104
135
  prompt?: string;
136
+ /** direct only, and the alternative to `prompt`. Exactly one is present. */
137
+ bundle?: AutomationBundle;
105
138
  /** direct only, event trigger only. Which delivered events belong to it. */
106
139
  filter?: string;
107
140
  /**
@@ -130,17 +163,142 @@ export interface AutomationInterfaceRoutes {
130
163
  setup: string;
131
164
  /** Carries the `:automationId` segment the host substitutes. */
132
165
  detail: string;
166
+ /** The templates sub-page. Static: there is no parameter to substitute. */
167
+ templates: string;
168
+ }
169
+
170
+ /** An icon name from the host's closed icon map. */
171
+ export type AutomationIconSlug =
172
+ | "layout-dashboard"
173
+ | "sparkles"
174
+ | "bot"
175
+ | "circle-alert"
176
+ | "activity"
177
+ | "timer";
178
+
179
+ /** The pages a sub-page navigation item may point at. */
180
+ export type AutomationSubPageId = "list" | "templates";
181
+
182
+ export interface AutomationSubPageNavItem {
183
+ /** The `pages` entry this item navigates to; its route comes from `routes`. */
184
+ page: AutomationSubPageId;
185
+ label: string;
186
+ icon: AutomationIconSlug;
133
187
  }
134
188
 
135
189
  export interface AutomationInterfaceNavigation {
136
190
  sidebar: { label: string };
137
191
  commandMenu: { title: string; description: string; keywords: string };
192
+ /** The ordered sub-page navigation of the Automation interface. */
193
+ subPages: AutomationSubPageNavItem[];
194
+ }
195
+
196
+ /** A value the host computes; a tile picks and captions it, never defines it. */
197
+ export type AutomationOverviewMetric =
198
+ | "automations"
199
+ | "needs-attention"
200
+ | "total-runs"
201
+ | "average-duration";
202
+
203
+ /**
204
+ * One summary tile. `detail` captions the value; `zeroDetail`, when present,
205
+ * replaces it while the value is zero. Both are plain substitution over the
206
+ * metric's placeholder namespace: only the `automations` metric exposes
207
+ * `{{active}}`, so every other tile's copy is literal.
208
+ */
209
+ export interface AutomationOverviewTile {
210
+ metric: AutomationOverviewMetric;
211
+ label: string;
212
+ detail: string;
213
+ zeroDetail?: string;
214
+ icon: AutomationIconSlug;
215
+ }
216
+
217
+ export interface AutomationOverview {
218
+ /** Names the tiles section for assistive technology. */
219
+ label: string;
220
+ tiles: AutomationOverviewTile[];
221
+ }
222
+
223
+ export type AutomationStatusFilterValue =
224
+ | "all"
225
+ | "active"
226
+ | "failing"
227
+ | "disabled";
228
+ export type AutomationTriggerFilterValue = "all" | "schedule" | "event";
229
+
230
+ /**
231
+ * A filter dropdown. Values name predicates the host implements; the manifest
232
+ * supplies which appear and their labels. `label` is the control's accessible
233
+ * name. The `all` option is the host's default and reset target.
234
+ */
235
+ export interface AutomationStatusFilter {
236
+ id: "status";
237
+ label: string;
238
+ options: { value: AutomationStatusFilterValue; label: string }[];
239
+ }
240
+
241
+ export interface AutomationTriggerFilter {
242
+ id: "trigger";
243
+ label: string;
244
+ options: { value: AutomationTriggerFilterValue; label: string }[];
245
+ }
246
+
247
+ export type AutomationDashboardFilter =
248
+ | AutomationStatusFilter
249
+ | AutomationTriggerFilter;
250
+
251
+ /** A comparator the host implements, named from a closed set. */
252
+ export type AutomationSortValue = "last-run" | "runs" | "name";
253
+
254
+ export interface AutomationDashboardSort {
255
+ /** The control's accessible name. */
256
+ label: string;
257
+ options: { value: AutomationSortValue; label: string }[];
258
+ /** Must be one of the declared option values. */
259
+ default: AutomationSortValue;
260
+ }
261
+
262
+ /**
263
+ * Copy for the per-automation run insights on cards and rows. The states,
264
+ * precedence, sampling, and value formatting are the host's; the manifest
265
+ * names them.
266
+ */
267
+ export interface AutomationListInsights {
268
+ health: {
269
+ healthy: string;
270
+ failing: string;
271
+ running: string;
272
+ disabled: string;
273
+ neverRun: string;
274
+ checking: string;
275
+ };
276
+ lastRun: { label: string; never: string; justNow: string };
277
+ stats: { runs: string; recentSuccess: string; averageDuration: string };
278
+ }
279
+
280
+ /**
281
+ * The templates sub-page identity. Its body - the catalog cards and their
282
+ * launch behavior - is the host's existing catalog surface.
283
+ */
284
+ export interface AutomationTemplatesPage {
285
+ title: string;
286
+ description: string;
138
287
  }
139
288
 
140
289
  export interface AutomationInterfacePages {
141
- list: { title: string; subtitle: string };
290
+ list: {
291
+ title: string;
292
+ subtitle: string;
293
+ overview: AutomationOverview;
294
+ /** The filter dropdowns of the list page, in render order. */
295
+ filters: AutomationDashboardFilter[];
296
+ sort: AutomationDashboardSort;
297
+ insights: AutomationListInsights;
298
+ };
142
299
  detail: { backLabel: string };
143
300
  edit: { title: string };
301
+ templates: AutomationTemplatesPage;
144
302
  }
145
303
 
146
304
  export type AutomationAttributeType =
@@ -212,6 +370,10 @@ export interface AutomationInterfaceEndpoints {
212
370
  validate: string;
213
371
  createPrompt: string;
214
372
  createPlugin: string;
373
+ /** The raw create endpoint, which a bundle entry is created through. */
374
+ createBundle: string;
375
+ /** Where a bundle's tarball is uploaded before that create call. */
376
+ uploads: string;
215
377
  }
216
378
 
217
379
  export interface AutomationInterfaceManifest {
@@ -240,4 +402,15 @@ export function listAutomationCatalog(): RecommendedAutomation[];
240
402
  export function getAutomationCatalogEntry(
241
403
  id: string,
242
404
  ): RecommendedAutomation | undefined;
405
+ /**
406
+ * Return the files a bundle entry ships, keyed by the path each takes inside
407
+ * the tarball, as an independent copy. Undefined for an entry with no bundle.
408
+ *
409
+ * The contents are inlined at build time from the repository paths
410
+ * `setup.bundle.files` names, because a host packing the tarball has the
411
+ * published package but not the repository.
412
+ */
413
+ export function getAutomationBundleFiles(
414
+ id: string,
415
+ ): Record<string, string> | undefined;
243
416
  export default AUTOMATION_CATALOG;
@@ -5,6 +5,7 @@
5
5
  * directory. `catalog-index.js` is generated from that directory so the JS
6
6
  * package can statically import each JSON file without an aggregate JSON asset.
7
7
  */
8
+ import { AUTOMATION_BUNDLE_FILES } from "./bundle-index.js";
8
9
  import { AUTOMATION_CATALOG_ENTRIES } from "./catalog-index.js";
9
10
  import interfaceManifest from "./interface.json" with { type: "json" };
10
11
 
@@ -21,6 +22,19 @@ export const getAutomationCatalogEntry = (id) => {
21
22
 
22
23
  export const AUTOMATION_CATALOG = clone(AUTOMATIONS);
23
24
 
25
+ /**
26
+ * The files a bundle entry ships, keyed by entry id and then by the path each
27
+ * file takes inside the tarball. Generated from the repository paths the
28
+ * entry's `setup.bundle.files` names, because a host packing the tarball has
29
+ * the package but not the repository.
30
+ *
31
+ * Absent for every entry that does not ship a bundle.
32
+ */
33
+ export const getAutomationBundleFiles = (id) => {
34
+ const files = AUTOMATION_BUNDLE_FILES[id];
35
+ return files ? clone(files) : undefined;
36
+ };
37
+
24
38
  /**
25
39
  * The production Automation interface manifest: the domain-level facts of the
26
40
  * interface, hand-authored in `automations/interface.json`.
@@ -3,7 +3,8 @@
3
3
  "routes": {
4
4
  "list": "/automations",
5
5
  "setup": "/automations/new/:automationId",
6
- "detail": "/automations/:automationId"
6
+ "detail": "/automations/:automationId",
7
+ "templates": "/automations/templates"
7
8
  },
8
9
  "navigation": {
9
10
  "sidebar": { "label": "Automate" },
@@ -11,15 +12,99 @@
11
12
  "title": "Automations",
12
13
  "description": "Review scheduled and webhook automations.",
13
14
  "keywords": "automate cron schedule webhook jobs"
14
- }
15
+ },
16
+ "subPages": [
17
+ { "page": "list", "label": "Dashboard", "icon": "layout-dashboard" },
18
+ { "page": "templates", "label": "Templates", "icon": "sparkles" }
19
+ ]
15
20
  },
16
21
  "pages": {
17
22
  "list": {
18
- "title": "Automations",
19
- "subtitle": "View active and inactive automations, search by metadata, and inspect read-only details."
23
+ "title": "Dashboard",
24
+ "subtitle": "Health, activity, and run performance across your automations.",
25
+ "overview": {
26
+ "label": "Automation overview",
27
+ "tiles": [
28
+ {
29
+ "metric": "automations",
30
+ "label": "Automations",
31
+ "detail": "{{active}} active",
32
+ "icon": "bot"
33
+ },
34
+ {
35
+ "metric": "needs-attention",
36
+ "label": "Needs attention",
37
+ "detail": "Latest run failed",
38
+ "zeroDetail": "No latest-run failures",
39
+ "icon": "circle-alert"
40
+ },
41
+ {
42
+ "metric": "total-runs",
43
+ "label": "Total runs",
44
+ "detail": "Across loaded automations",
45
+ "icon": "activity"
46
+ },
47
+ {
48
+ "metric": "average-duration",
49
+ "label": "Average duration",
50
+ "detail": "Recent completed runs",
51
+ "icon": "timer"
52
+ }
53
+ ]
54
+ },
55
+ "filters": [
56
+ {
57
+ "id": "status",
58
+ "label": "Filter by status",
59
+ "options": [
60
+ { "value": "all", "label": "All statuses" },
61
+ { "value": "active", "label": "Active" },
62
+ { "value": "failing", "label": "Needs attention" },
63
+ { "value": "disabled", "label": "Disabled" }
64
+ ]
65
+ },
66
+ {
67
+ "id": "trigger",
68
+ "label": "Filter by trigger",
69
+ "options": [
70
+ { "value": "all", "label": "All triggers" },
71
+ { "value": "schedule", "label": "Scheduled" },
72
+ { "value": "event", "label": "Event-driven" }
73
+ ]
74
+ }
75
+ ],
76
+ "sort": {
77
+ "label": "Sort automations",
78
+ "default": "last-run",
79
+ "options": [
80
+ { "value": "last-run", "label": "Latest run" },
81
+ { "value": "runs", "label": "Most runs" },
82
+ { "value": "name", "label": "Name" }
83
+ ]
84
+ },
85
+ "insights": {
86
+ "health": {
87
+ "healthy": "Healthy",
88
+ "failing": "Failing",
89
+ "running": "Running",
90
+ "disabled": "Disabled",
91
+ "neverRun": "Never run",
92
+ "checking": "Checking"
93
+ },
94
+ "lastRun": { "label": "Last run", "never": "Never", "justNow": "Just now" },
95
+ "stats": {
96
+ "runs": "Runs",
97
+ "recentSuccess": "Recent success",
98
+ "averageDuration": "Avg. duration"
99
+ }
100
+ }
20
101
  },
21
102
  "detail": { "backLabel": "Back to Automations" },
22
- "edit": { "title": "Edit automation" }
103
+ "edit": { "title": "Edit automation" },
104
+ "templates": {
105
+ "title": "Templates",
106
+ "description": "Browse proven automations and beta ideas, then launch one into a conversation to tailor it to your work."
107
+ }
23
108
  },
24
109
  "docsUrl": "https://docs.openhands.dev/openhands/usage/automations/overview",
25
110
  "attributes": {
@@ -59,7 +144,9 @@
59
144
  "capabilities": "/v1/capabilities",
60
145
  "validate": "/v1/validate",
61
146
  "createPrompt": "/v1/preset/prompt",
62
- "createPlugin": "/v1/preset/plugin"
147
+ "createPlugin": "/v1/preset/plugin",
148
+ "createBundle": "/v1",
149
+ "uploads": "/v1/uploads"
63
150
  },
64
151
  "featuredAutomationIds": [
65
152
  "github-pr-reviewer",