@openhands/extensions 0.10.0 → 0.12.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 (54) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/AGENTS.md +2 -1
  3. package/MIGRATION.md +31 -0
  4. package/README.md +12 -9
  5. package/automations/README.md +251 -8
  6. package/automations/catalog/github-pr-reviewer/manifest.json +82 -0
  7. package/automations/catalog/github-repo-monitor/manifest.json +82 -0
  8. package/automations/catalog/incident-retrospective-drafter/manifest.json +100 -0
  9. package/automations/catalog/{jira-issue-to-pr.json → jira-issue-to-pr/manifest.json} +10 -5
  10. package/automations/catalog/{linear-triage-assistant.json → linear-triage-assistant/manifest.json} +8 -4
  11. package/automations/catalog/{research-brief-writer.json → research-brief-writer/manifest.json} +11 -5
  12. package/automations/catalog/{slack-channel-monitor.json → slack-channel-monitor/manifest.json} +7 -2
  13. package/automations/catalog/{slack-standup-digest.json → slack-standup-digest/manifest.json} +8 -5
  14. package/automations/catalog-index.js +23 -0
  15. package/automations/catalog.schema.json +273 -0
  16. package/automations/index.d.ts +111 -3
  17. package/automations/index.js +21 -18
  18. package/index.d.ts +16 -1
  19. package/integrations/catalog/atlassian-rovo.json +44 -0
  20. package/integrations/catalog/datadog.json +3 -3
  21. package/integrations/catalog/github.json +1 -1
  22. package/integrations/catalog/linear.json +1 -1
  23. package/integrations/catalog/slack.json +2 -2
  24. package/integrations/catalog-index.js +30 -28
  25. package/integrations/index.d.ts +1 -1
  26. package/marketplaces/openhands-extensions.json +9 -0
  27. package/package.json +6 -3
  28. package/pyproject.toml +1 -1
  29. package/python/openhands_extensions/__init__.py +0 -4
  30. package/python/openhands_extensions/_version.py +3 -2
  31. package/python/openhands_extensions/integrations.py +8 -30
  32. package/scripts/build-automation-catalog.mjs +51 -0
  33. package/skills/bitbucket/SKILL.md +0 -1
  34. package/skills/github/SKILL.md +0 -1
  35. package/skills/gitlab/SKILL.md +0 -1
  36. package/skills/index.js +11 -8
  37. package/skills/iterate/README.md +4 -1
  38. package/skills/iterate/SKILL.md +22 -2
  39. package/skills/openhands-sdk/SKILL.md +2 -0
  40. package/skills/technical-writing/.plugin/plugin.json +15 -0
  41. package/skills/technical-writing/README.md +14 -0
  42. package/skills/technical-writing/SKILL.md +106 -0
  43. package/tests/fixtures/automations/capabilities.json +92 -0
  44. package/tests/fixtures/automations/github-pr-reviewer.json +251 -0
  45. package/tests/fixtures/automations/github-repo-monitor.json +180 -0
  46. package/tests/fixtures/automations/incident-retrospective-drafter.json +91 -0
  47. package/tests/test_automation_setup.py +526 -0
  48. package/tests/test_catalogs.py +104 -12
  49. package/tests/test_deprecations.py +39 -4
  50. package/tests/test_integration_catalog_in_sync.py +50 -60
  51. package/tests/test_live_integration_smoke.py +366 -0
  52. package/automations/catalog/github-pr-reviewer.json +0 -13
  53. package/automations/catalog/github-repo-monitor.json +0 -11
  54. package/automations/catalog/incident-retrospective-drafter.json +0 -15
@@ -0,0 +1,100 @@
1
+ {
2
+ "id": "incident-retrospective-drafter",
3
+ "name": "Incident retrospective drafter",
4
+ "category": "Reliability",
5
+ "description": "Collect incident chatter and issue updates, then draft a timeline and follow-up checklist.",
6
+ "requires": {
7
+ "integrations": {
8
+ "slack": {
9
+ "message": "Reads incident discussion and timestamps."
10
+ },
11
+ "linear": {
12
+ "message": "Reads follow-up tickets, owners, and status."
13
+ },
14
+ "notion": {
15
+ "message": "Publishes the drafted retrospective. Can be connected later during setup.",
16
+ "required": false
17
+ }
18
+ },
19
+ "features": [
20
+ "mcpTools",
21
+ "conversationDispatch"
22
+ ]
23
+ },
24
+ "popularityRank": 78,
25
+ "estimatedSetupMinutes": 8,
26
+ "skill": "incident-retrospective",
27
+ "exampleImplementation": "Trigger: manual, cron, or incident label added\nRequired MCPs: Slack, Linear, Notion\n\n1. Gather incident messages, timestamps, and linked issue references.\n2. Pull Linear follow-up tickets, owners, and status.\n3. TODO: define the incident identifier format and approved Notion template.\n4. Build a timeline, impact summary, root-cause hypotheses, and action items.\n5. Publish a draft retrospective and notify the incident owner.",
28
+ "setup": {
29
+ "version": "1.0",
30
+ "mode": "assisted",
31
+ "form": {
32
+ "note": "These answers start the conversation. Anything left blank is something the agent will ask about.",
33
+ "args": {
34
+ "incidentChannel": {
35
+ "type": "text",
36
+ "label": "Incident channel",
37
+ "help": "The Slack channel where incidents are discussed.",
38
+ "placeholder": "#incidents",
39
+ "required": false,
40
+ "constraints": {
41
+ "maxLength": 100
42
+ }
43
+ },
44
+ "linearTeam": {
45
+ "type": "text",
46
+ "label": "Linear team",
47
+ "help": "The team whose follow-up tickets belong in the retrospective.",
48
+ "required": false,
49
+ "constraints": {
50
+ "maxLength": 100
51
+ }
52
+ },
53
+ "notionDestination": {
54
+ "type": "text",
55
+ "label": "Notion destination",
56
+ "help": "Page or database where drafts should be published. Leave blank to decide during setup.",
57
+ "required": false,
58
+ "constraints": {
59
+ "maxLength": 200
60
+ }
61
+ },
62
+ "triggerPreference": {
63
+ "type": "select",
64
+ "label": "How should it run?",
65
+ "help": "A starting preference only. The agent confirms the exact trigger before creating anything.",
66
+ "default": "undecided",
67
+ "required": false,
68
+ "options": [
69
+ {
70
+ "value": "undecided",
71
+ "label": "Let the agent recommend one"
72
+ },
73
+ {
74
+ "value": "scheduled",
75
+ "label": "On a schedule"
76
+ },
77
+ {
78
+ "value": "onIncidentLabel",
79
+ "label": "When an incident is labelled"
80
+ },
81
+ {
82
+ "value": "manual",
83
+ "label": "Only when I run it"
84
+ }
85
+ ]
86
+ },
87
+ "notes": {
88
+ "type": "textarea",
89
+ "label": "Anything else the agent should know?",
90
+ "help": "Incident naming conventions, an existing retro template, who should be notified.",
91
+ "required": false,
92
+ "constraints": {
93
+ "maxLength": 2000
94
+ }
95
+ }
96
+ }
97
+ },
98
+ "message": "The user supplied these starting points. Confirm each one, ask about anything left blank, then create the automation.\n\n- Incident channel: {{form.incidentChannel}}\n- Linear team: {{form.linearTeam}}\n- Notion destination: {{form.notionDestination}}\n- Preferred trigger: {{form.triggerPreference}}\n- Notes: {{form.notes}}\n\nStill undecided and required before creation: the incident identifier format and the approved Notion retrospective template."
99
+ }
100
+ }
@@ -3,12 +3,17 @@
3
3
  "name": "Jira issue to GitHub PR",
4
4
  "category": "Project management",
5
5
  "description": "Watch a Jira Cloud project for issues with a configurable label and automatically open a GitHub pull request for each new issue found. The target GitHub repo is read from the ticket body - no repo parameter required at deploy time.",
6
- "requiredIntegrationIds": [
7
- "jira",
8
- "github"
9
- ],
6
+ "requires": {
7
+ "integrations": {
8
+ "jira": {
9
+ "message": "Reads the project for issues carrying the trigger label."
10
+ },
11
+ "github": {
12
+ "message": "Opens a pull request on the repository named in the ticket."
13
+ }
14
+ }
15
+ },
10
16
  "popularityRank": 85,
11
17
  "estimatedSetupMinutes": 5,
12
- "prompt": "Set up a Jira automation to create pull requests",
13
18
  "exampleImplementation": "Trigger: cron polling (e.g. every 5 minutes)\nRequired secrets: Jira API token, GitHub personal access token (repo + workflow scope)\n\n1. Collect Jira base URL, email, API token secret name, label to watch, and cron schedule from the user. No GitHub repo is needed at deploy time - each ticket must include the target repo (owner/repo) in its body.\n2. Poll POST /rest/api/3/search/jql on the Jira Cloud instance to find open issues carrying the configured label.\n3. Deduplicate against a KV-store-backed set of already-processed issue keys so re-runs never create duplicate PRs.\n4. For each new issue, start an independent OpenHands agent conversation that extracts the GitHub repo from the ticket body, clones it, creates a branch named after the Jira key, implements or scaffolds the requested change, and opens a pull request.\n5. Immediately after the conversation is created, post a Jira comment on the issue: 'I'm on it: <conversation URL>'.\n6. Persist the processed issue key immediately after dispatching so the next poll skips it."
14
19
  }
@@ -3,11 +3,15 @@
3
3
  "name": "Linear issue triage assistant",
4
4
  "category": "Project management",
5
5
  "description": "Classify new Linear issues, suggest labels, find duplicates, and ask clarifying questions.",
6
- "requiredIntegrationIds": [
7
- "linear"
8
- ],
6
+ "requires": {
7
+ "integrations": {
8
+ "linear": {
9
+ "message": "Reads new issues and applies labels, comments, and duplicate links."
10
+ }
11
+ }
12
+ },
9
13
  "popularityRank": 90,
10
14
  "estimatedSetupMinutes": 3,
11
- "prompt": "/linear-triage:setup",
15
+ "skill": "linear-triage",
12
16
  "exampleImplementation": "Trigger: linear Issue create\nRequired MCP: Linear\n\n1. Load the new issue, team context, project, labels, and recent similar issues.\n2. Score priority and classify the request as bug, feature, support, or chore.\n3. TODO: map local team labels and priority conventions.\n4. Add a triage comment with rationale and suggested next actions.\n5. Optionally update labels, priority, and assignee."
13
17
  }
@@ -3,12 +3,18 @@
3
3
  "name": "Research brief writer",
4
4
  "category": "Research",
5
5
  "description": "Monitor a topic, gather sources from the web, and publish a short brief for your team.",
6
- "requiredIntegrationIds": [
7
- "tavily",
8
- "notion"
9
- ],
6
+ "requires": {
7
+ "integrations": {
8
+ "tavily": {
9
+ "message": "Searches the web for sources on the topic."
10
+ },
11
+ "notion": {
12
+ "message": "Publishes the finished brief."
13
+ }
14
+ }
15
+ },
10
16
  "popularityRank": 84,
11
17
  "estimatedSetupMinutes": 7,
12
- "prompt": "/research-brief:setup",
18
+ "skill": "research-brief",
13
19
  "exampleImplementation": "Trigger: cron, weekly or daily\nRequired MCPs: Tavily, Notion\n\n1. Run focused Tavily searches for configured topics and competitors.\n2. Deduplicate sources and rank them by freshness and authority.\n3. TODO: confirm citation format and destination Notion database schema.\n4. Write an executive summary, implications, and recommended actions.\n5. Create or update a Notion page with citations and source links."
14
20
  }
@@ -3,9 +3,14 @@
3
3
  "name": "Slack channel monitor",
4
4
  "category": "Team communication",
5
5
  "description": "Watch Slack channels for @openhands mentions, open a conversation with the message context, reply when the agent finishes, and continue the same conversation from triggered Slack thread follow-ups.",
6
- "requiredIntegrationIds": ["slack"],
6
+ "requires": {
7
+ "integrations": {
8
+ "slack": {
9
+ "message": "Reads channel messages and posts replies back to the thread."
10
+ }
11
+ }
12
+ },
7
13
  "popularityRank": 92,
8
14
  "estimatedSetupMinutes": 7,
9
- "prompt": "/slack-monitor:poll",
10
15
  "exampleImplementation": "Trigger: cron, every minute\nRequired secret: SLACK_BOT_TOKEN or SLACK_USER_TOKEN\n\n1. Poll up to 10 configured Slack channels for new messages since the last run.\n2. Match messages containing the trigger phrase (default: @openhands).\n3. React with 👀 and start an OpenHands conversation with the message and recent channel context.\n4. Post a thread reply with a link to the conversation, then post the agent response when done.\n5. Keep the thread in a short follow-up watch window and forward only replies that repeat the trigger phrase to the same conversation, with per-thread backoff for quiet threads."
11
16
  }
@@ -2,12 +2,15 @@
2
2
  "id": "slack-standup-digest",
3
3
  "name": "Slack standup digest",
4
4
  "category": "Team updates",
5
- "description": "Summarize yesterday\u2019s Slack activity into an async standup note with blockers, decisions, and owners.",
6
- "requiredIntegrationIds": [
7
- "slack"
8
- ],
5
+ "description": "Summarize yesterday’s Slack activity into an async standup note with blockers, decisions, and owners.",
6
+ "requires": {
7
+ "integrations": {
8
+ "slack": {
9
+ "message": "Reads recent channel activity and posts the digest."
10
+ }
11
+ }
12
+ },
9
13
  "popularityRank": 94,
10
14
  "estimatedSetupMinutes": 5,
11
- "prompt": "/standup-digest:setup",
12
15
  "exampleImplementation": "Trigger: cron, weekday mornings\nRequired MCP: Slack\n\n1. Search configured channels for messages since the last digest window.\n2. Cluster updates into shipped work, active work, blockers, and decisions.\n3. TODO: choose a safe allowlist for channels and whether DMs are excluded.\n4. Draft a Slack message with owners, links, and unanswered questions.\n5. Optionally post to the configured standup channel."
13
16
  }
@@ -0,0 +1,23 @@
1
+ // This file is auto-generated by scripts/build-automation-catalog.mjs.
2
+ // Do not edit it manually. To update it after changing automations/catalog/*/manifest.json,
3
+ // run: npm run build:automations
4
+
5
+ import entry0 from "./catalog/github-pr-reviewer/manifest.json" with { type: "json" };
6
+ import entry1 from "./catalog/github-repo-monitor/manifest.json" with { type: "json" };
7
+ import entry2 from "./catalog/slack-standup-digest/manifest.json" with { type: "json" };
8
+ import entry3 from "./catalog/slack-channel-monitor/manifest.json" with { type: "json" };
9
+ import entry4 from "./catalog/linear-triage-assistant/manifest.json" with { type: "json" };
10
+ import entry5 from "./catalog/jira-issue-to-pr/manifest.json" with { type: "json" };
11
+ import entry6 from "./catalog/research-brief-writer/manifest.json" with { type: "json" };
12
+ import entry7 from "./catalog/incident-retrospective-drafter/manifest.json" with { type: "json" };
13
+
14
+ export const AUTOMATION_CATALOG_ENTRIES = [
15
+ entry0,
16
+ entry1,
17
+ entry2,
18
+ entry3,
19
+ entry4,
20
+ entry5,
21
+ entry6,
22
+ entry7,
23
+ ];
@@ -0,0 +1,273 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/OpenHands/extensions/automations/catalog.schema.json",
4
+ "title": "AutomationCatalogEntry",
5
+ "description": "Strict schema for automations/catalog/<id>/manifest.json. Mirrors automations/index.d.ts. An entry carries the card metadata Agent Canvas renders plus, optionally, the extension-owned setup contract for that automation. It states only what varies between automations, and states each of those things once: anything the host can derive from another field is absent by design.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "id",
10
+ "name",
11
+ "category",
12
+ "description",
13
+ "requires",
14
+ "popularityRank",
15
+ "estimatedSetupMinutes",
16
+ "exampleImplementation"
17
+ ],
18
+ "properties": {
19
+ "id": {
20
+ "type": "string",
21
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
22
+ "description": "Stable identifier. Must equal the name of the directory holding this manifest."
23
+ },
24
+ "name": { "type": "string", "minLength": 1 },
25
+ "category": { "type": "string", "minLength": 1 },
26
+ "description": { "type": "string", "minLength": 1 },
27
+ "requires": { "$ref": "#/$defs/requires" },
28
+ "popularityRank": { "type": "integer", "minimum": 0 },
29
+ "estimatedSetupMinutes": { "type": "integer", "minimum": 1 },
30
+ "skill": {
31
+ "description": "The skills/ directory that builds this automation today. Defaults to `id`, so it is stated only where the two differ. How that skill is invoked is not repeated here: the command lives once, in the skill's own `triggers:` frontmatter, and the skills catalog exposes it.",
32
+ "type": "string",
33
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
34
+ },
35
+ "exampleImplementation": {
36
+ "description": "What the skill builds. Documentation for authors; Agent Canvas does not render it.",
37
+ "type": "string",
38
+ "minLength": 1
39
+ },
40
+ "setup": { "$ref": "#/$defs/setup" }
41
+ },
42
+ "$defs": {
43
+ "setup": {
44
+ "description": "The extension-owned configuration experience for this automation: what the deployment must support, what must be connected first, what the user is asked, how a draft is validated, what request is sent, and which analytics stages are emitted. It never describes what the automation does at runtime - that is the preset, owned by OpenHands/automation. It is data, not code - there is no key that accepts JavaScript, no markup in copy, no free-form URL, and no secret value.",
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "required": [
48
+ "version",
49
+ "mode",
50
+ "form"
51
+ ],
52
+ "properties": {
53
+ "version": {
54
+ "const": "1.0",
55
+ "description": "Selects how this block is interpreted. A future format ships a new constant."
56
+ },
57
+ "mode": {
58
+ "enum": ["direct", "assisted"],
59
+ "description": "direct - the form produces a request body. assisted - the form seeds an agent conversation that finishes setup."
60
+ },
61
+ "form": { "$ref": "#/$defs/form" },
62
+ "prompt": {
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
+ "$ref": "#/$defs/templateValue"
65
+ },
66
+ "filter": {
67
+ "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
+ "$ref": "#/$defs/templateValue"
69
+ },
70
+ "message": {
71
+ "description": "assisted only. Setup context handed to the agent conversation that finishes setup. The command that opens it comes from the skill, so it is not repeated here. Capped so this never becomes a channel for runtime instructions.",
72
+ "allOf": [{ "$ref": "#/$defs/templateCopy" }, { "maxLength": 2000 }]
73
+ }
74
+ },
75
+ "allOf": [
76
+ {
77
+ "if": {
78
+ "properties": { "mode": { "const": "direct" } },
79
+ "required": ["mode"]
80
+ },
81
+ "then": {
82
+ "required": ["prompt"],
83
+ "not": { "required": ["message"] }
84
+ }
85
+ },
86
+ {
87
+ "if": {
88
+ "properties": { "mode": { "const": "assisted" } },
89
+ "required": ["mode"]
90
+ },
91
+ "then": {
92
+ "required": ["message"],
93
+ "allOf": [
94
+ { "not": { "required": ["prompt"] } },
95
+ { "not": { "required": ["filter"] } }
96
+ ]
97
+ }
98
+ },
99
+ {
100
+ "if": {
101
+ "properties": {
102
+ "form": {
103
+ "properties": {
104
+ "triggers": { "not": { "required": ["event"] } }
105
+ },
106
+ "required": ["triggers"]
107
+ }
108
+ },
109
+ "required": ["form"]
110
+ },
111
+ "then": { "not": { "required": ["filter"] } }
112
+ }
113
+ ]
114
+ },
115
+ "copy": {
116
+ "description": "Literal user-visible copy. Carries no markup, because a setup block must never inject HTML into the host.",
117
+ "type": "string",
118
+ "minLength": 1,
119
+ "not": { "pattern": "<[A-Za-z/!]" }
120
+ },
121
+ "placeholders": {
122
+ "$comment": "Every {{ must open a known placeholder namespace and close immediately. `form` resolves against the values the user entered; `automation` against the catalog entry this setup block belongs to. There is deliberately no secrets namespace.",
123
+ "not": {
124
+ "pattern": "\\{\\{(?!(form|automation)\\.[A-Za-z0-9_.]+\\}\\})"
125
+ }
126
+ },
127
+ "templateCopy": {
128
+ "description": "User-visible copy that may embed {{namespace.key}} placeholders.",
129
+ "allOf": [
130
+ { "$ref": "#/$defs/copy" },
131
+ { "$ref": "#/$defs/placeholders" }
132
+ ]
133
+ },
134
+ "templateValue": {
135
+ "description": "A request-body string that may embed placeholders. Not rendered, so expression syntax such as a JMESPath filter is allowed.",
136
+ "type": "string",
137
+ "minLength": 1,
138
+ "allOf": [{ "$ref": "#/$defs/placeholders" }]
139
+ },
140
+ "requires": {
141
+ "description": "What must be connected, and what the deployment must offer, for this automation to be usable. Every entry carries it, whether or not it ships a setup block, so the integrations an automation needs are recorded in exactly one place. The host decides what to do when a requirement is unmet; an entry only states the requirement and why it exists.",
142
+ "type": "object",
143
+ "additionalProperties": false,
144
+ "required": ["integrations"],
145
+ "properties": {
146
+ "integrations": {
147
+ "description": "Keyed by integration id, each of which must resolve to an integrations/catalog entry.",
148
+ "type": "object",
149
+ "minProperties": 1,
150
+ "propertyNames": { "type": "string", "minLength": 1 },
151
+ "additionalProperties": {
152
+ "type": "object",
153
+ "additionalProperties": false,
154
+ "required": ["message"],
155
+ "properties": {
156
+ "message": {
157
+ "description": "Why this automation needs the integration. Required, so an integration is never listed without saying what it is for.",
158
+ "$ref": "#/$defs/copy"
159
+ },
160
+ "required": {
161
+ "description": "Defaults to true. Set false to let setup continue while the integration is still unconnected.",
162
+ "const": false
163
+ }
164
+ }
165
+ }
166
+ },
167
+ "features": {
168
+ "description": "Deployment capabilities this automation cannot run without. Names are matched against the capabilities endpoint the host queries.",
169
+ "type": "array",
170
+ "minItems": 1,
171
+ "uniqueItems": true,
172
+ "items": { "type": "string", "minLength": 1 }
173
+ }
174
+ }
175
+ },
176
+ "form": {
177
+ "description": "The inputs the user is asked for. These constraints are also what the host validates locally, so no separate validation block restates them.",
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "required": ["args"],
181
+ "properties": {
182
+ "note": { "$ref": "#/$defs/copy" },
183
+ "triggers": {
184
+ "description": "Inputs that decide when the automation runs, keyed by trigger kind. The key set is the trigger kinds the deployment must support, so it is not restated anywhere else.",
185
+ "type": "object",
186
+ "additionalProperties": false,
187
+ "minProperties": 1,
188
+ "properties": {
189
+ "cron": { "$ref": "#/$defs/formFields" },
190
+ "event": { "$ref": "#/$defs/formFields" }
191
+ }
192
+ },
193
+ "args": {
194
+ "description": "Every other input: the arguments to the automation itself.",
195
+ "$ref": "#/$defs/formFields"
196
+ }
197
+ }
198
+ },
199
+ "formFields": {
200
+ "description": "Keyed by field name, which is what {{form.<name>}} resolves against.",
201
+ "type": "object",
202
+ "minProperties": 1,
203
+ "propertyNames": { "pattern": "^[a-z][A-Za-z0-9]*$" },
204
+ "additionalProperties": { "$ref": "#/$defs/formField" }
205
+ },
206
+ "formField": {
207
+ "type": "object",
208
+ "additionalProperties": false,
209
+ "required": ["type", "label", "help", "required"],
210
+ "properties": {
211
+ "type": {
212
+ "description": "Semantic field types. The host renders a cron field without knowing what cron means to an automation, and resolves the deployment's schedule limits and timezone list for the cron and timezone types, so an entry never restates them.",
213
+ "enum": [
214
+ "text",
215
+ "textarea",
216
+ "select",
217
+ "cron",
218
+ "timezone",
219
+ "repo-picker"
220
+ ]
221
+ },
222
+ "label": { "$ref": "#/$defs/copy" },
223
+ "help": { "$ref": "#/$defs/copy" },
224
+ "placeholder": { "$ref": "#/$defs/copy" },
225
+ "default": { "type": "string" },
226
+ "required": { "type": "boolean" },
227
+ "provider": { "enum": ["github", "gitlab", "bitbucket"] },
228
+ "options": {
229
+ "type": "array",
230
+ "minItems": 1,
231
+ "items": {
232
+ "type": "object",
233
+ "additionalProperties": false,
234
+ "required": ["value", "label"],
235
+ "properties": {
236
+ "value": { "type": "string", "minLength": 1 },
237
+ "label": { "$ref": "#/$defs/copy" }
238
+ }
239
+ }
240
+ },
241
+ "constraints": {
242
+ "type": "object",
243
+ "additionalProperties": false,
244
+ "minProperties": 1,
245
+ "properties": {
246
+ "minLength": { "type": "integer", "minimum": 0 },
247
+ "maxLength": { "type": "integer", "minimum": 1 },
248
+ "format": {
249
+ "description": "A host-implemented check, named from a closed set. Setup blocks cannot supply their own regex, so one cannot hand the host a pathological pattern. safeExpressionLiteral marks a value that is interpolated into a filter expression string literal, so quotes and backslashes must be rejected before they can break the expression.",
250
+ "enum": ["safeExpressionLiteral"]
251
+ }
252
+ }
253
+ }
254
+ },
255
+ "allOf": [
256
+ {
257
+ "if": {
258
+ "properties": { "type": { "const": "repo-picker" } },
259
+ "required": ["type"]
260
+ },
261
+ "then": { "required": ["provider"] }
262
+ },
263
+ {
264
+ "if": {
265
+ "properties": { "type": { "not": { "const": "select" } } },
266
+ "required": ["type"]
267
+ },
268
+ "then": { "not": { "required": ["options"] } }
269
+ }
270
+ ]
271
+ }
272
+ }
273
+ }
@@ -3,12 +3,120 @@ export interface RecommendedAutomation {
3
3
  name: string;
4
4
  category: string;
5
5
  description: string;
6
- prompt: string;
7
- exampleImplementation: string;
8
- requiredIntegrationIds: string[];
6
+ requires: AutomationPrerequisites;
9
7
  popularityRank: number;
10
8
  estimatedSetupMinutes: number;
9
+ /**
10
+ * The `skills/` directory that builds this automation today. Defaults to
11
+ * `id`; present only where the two differ. Look the launch command up from
12
+ * that skill's entry in `SKILLS_CATALOG` rather than storing it here.
13
+ */
14
+ skill?: string;
15
+ exampleImplementation: string;
16
+ /** Present when this automation ships an extension-owned setup experience. */
17
+ setup?: AutomationSetup;
18
+ }
19
+
20
+ /**
21
+ * The extension-owned configuration experience for one automation.
22
+ *
23
+ * Mirrors the `setup` block in `automations/catalog.schema.json`, which is
24
+ * authoritative. It describes how an automation is *configured*; it never
25
+ * describes what the automation does at runtime.
26
+ *
27
+ * It states only what varies between automations, and states each of those
28
+ * things once. Everything else is the same for every automation and is the
29
+ * host's to generate: the slash command (`/<id>:setup`), the setup route
30
+ * (`/automations/new/<id>`), the capabilities check, the preflight call, the
31
+ * mapping from a rejected payload path back to the input at fault, the review
32
+ * screen, the navigation after a success, and the analytics stages.
33
+ */
34
+
35
+ export type AutomationSetupMode = "direct" | "assisted";
36
+ export type AutomationFieldType =
37
+ | "text"
38
+ | "textarea"
39
+ | "select"
40
+ | "cron"
41
+ | "timezone"
42
+ | "repo-picker";
43
+ export type AutomationGitProvider = "github" | "gitlab" | "bitbucket";
44
+ export type AutomationTriggerKind = "cron" | "event";
45
+
46
+ export interface AutomationFieldOption {
47
+ value: string;
48
+ label: string;
49
+ }
50
+
51
+ export interface AutomationFieldConstraints {
52
+ minLength?: number;
53
+ maxLength?: number;
54
+ /** Host-implemented check, named from a closed set. Entries supply no regex. */
55
+ format?: "safeExpressionLiteral";
56
+ }
57
+
58
+ export interface AutomationFormField {
59
+ type: AutomationFieldType;
60
+ label: string;
61
+ help: string;
62
+ placeholder?: string;
63
+ default?: string;
64
+ required: boolean;
65
+ provider?: AutomationGitProvider;
66
+ options?: AutomationFieldOption[];
67
+ constraints?: AutomationFieldConstraints;
68
+ }
69
+
70
+ /** Keyed by field name, which is what `{{form.<name>}}` resolves against. */
71
+ export type AutomationFormFields = Record<string, AutomationFormField>;
72
+
73
+ export interface AutomationIntegrationRequirement {
74
+ /** Why this automation needs it. Omitted when there is no setup flow to show it in. */
75
+ message?: string;
76
+ /** Defaults to true. `false` lets setup continue while it is unconnected. */
77
+ required?: false;
78
+ }
79
+
80
+ export interface AutomationPrerequisites {
81
+ /** Keyed by integration catalog id. */
82
+ integrations: Record<string, AutomationIntegrationRequirement>;
83
+ /** Deployment capabilities this automation cannot run without. */
84
+ features?: string[];
85
+ }
86
+
87
+ /** The inputs that decide when the automation runs, keyed by trigger kind. */
88
+ export type AutomationTriggerForm = Partial<
89
+ Record<AutomationTriggerKind, AutomationFormFields>
90
+ >;
91
+
92
+ export interface AutomationForm {
93
+ note?: string;
94
+ triggers?: AutomationTriggerForm;
95
+ /** Every other input: the arguments to the automation itself. */
96
+ args: AutomationFormFields;
97
+ }
98
+
99
+ export interface AutomationSetup {
100
+ version: "1.0";
101
+ mode: AutomationSetupMode;
102
+ form: AutomationForm;
103
+ /** direct only. What the automation is told to do. */
104
+ prompt?: string;
105
+ /** direct only, event trigger only. Which delivered events belong to it. */
106
+ filter?: string;
107
+ /** assisted only. Setup context for the conversation that finishes setup. */
108
+ message?: string;
11
109
  }
12
110
 
13
111
  export const AUTOMATION_CATALOG: RecommendedAutomation[];
112
+ /**
113
+ * Return the full automation catalog.
114
+ * Reads the generated static import index over `automations/catalog/<id>/manifest.json`.
115
+ * Returns an independent copy.
116
+ */
117
+ export function listAutomationCatalog(): RecommendedAutomation[];
118
+ /** Return one automation catalog entry by id as an independent copy. */
119
+ export function getAutomationCatalogEntry(
120
+ id: string,
121
+ ): RecommendedAutomation | undefined;
14
122
  export default AUTOMATION_CATALOG;