@openhands/extensions 0.11.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.
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +2 -1
- package/MIGRATION.md +31 -0
- package/README.md +9 -7
- package/automations/README.md +251 -8
- package/automations/catalog/github-pr-reviewer/manifest.json +82 -0
- package/automations/catalog/github-repo-monitor/manifest.json +82 -0
- package/automations/catalog/incident-retrospective-drafter/manifest.json +100 -0
- package/automations/catalog/{jira-issue-to-pr.json → jira-issue-to-pr/manifest.json} +10 -5
- package/automations/catalog/{linear-triage-assistant.json → linear-triage-assistant/manifest.json} +8 -4
- package/automations/catalog/{research-brief-writer.json → research-brief-writer/manifest.json} +11 -5
- package/automations/catalog/{slack-channel-monitor.json → slack-channel-monitor/manifest.json} +7 -2
- package/automations/catalog/{slack-standup-digest.json → slack-standup-digest/manifest.json} +8 -5
- package/automations/catalog-index.js +23 -0
- package/automations/catalog.schema.json +273 -0
- package/automations/index.d.ts +111 -3
- package/automations/index.js +21 -18
- package/index.d.ts +16 -1
- package/integrations/catalog/datadog.json +3 -3
- package/integrations/index.d.ts +1 -1
- package/package.json +6 -3
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/__init__.py +0 -4
- package/python/openhands_extensions/_version.py +3 -2
- package/python/openhands_extensions/integrations.py +8 -30
- package/scripts/build-automation-catalog.mjs +51 -0
- package/skills/bitbucket/SKILL.md +0 -1
- package/skills/github/SKILL.md +0 -1
- package/skills/gitlab/SKILL.md +0 -1
- package/skills/index.js +4 -7
- package/skills/openhands-sdk/SKILL.md +2 -0
- package/tests/fixtures/automations/capabilities.json +92 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +251 -0
- package/tests/fixtures/automations/github-repo-monitor.json +180 -0
- package/tests/fixtures/automations/incident-retrospective-drafter.json +91 -0
- package/tests/test_automation_setup.py +526 -0
- package/tests/test_catalogs.py +37 -12
- package/tests/test_deprecations.py +39 -4
- package/tests/test_integration_catalog_in_sync.py +50 -60
- package/automations/catalog/github-pr-reviewer.json +0 -13
- package/automations/catalog/github-repo-monitor.json +0 -11
- package/automations/catalog/incident-retrospective-drafter.json +0 -15
|
@@ -39,6 +39,19 @@ def _catalog_entries() -> list[dict]:
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _python_entries(
|
|
43
|
+
mcp: bool | None = None,
|
|
44
|
+
oauth: bool | None = None,
|
|
45
|
+
) -> list[dict]:
|
|
46
|
+
"""Catalog entries as seen through the supported Python model API."""
|
|
47
|
+
return [
|
|
48
|
+
model.model_dump(exclude_none=True)
|
|
49
|
+
for model in openhands_extensions.list_integration_catalog_models(
|
|
50
|
+
mcp=mcp, oauth=oauth
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
42
55
|
def _supports_mcp(entry: dict) -> bool:
|
|
43
56
|
return any(option.get("provider") == "mcp" for option in entry["connectionOptions"])
|
|
44
57
|
|
|
@@ -84,8 +97,8 @@ def test_python_snapshot_is_built_from_catalog_directory() -> None:
|
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
|
|
87
|
-
def
|
|
88
|
-
entries =
|
|
100
|
+
def test_python_catalog_models_expose_no_derived_fields() -> None:
|
|
101
|
+
entries = _python_entries()
|
|
89
102
|
assert entries == _catalog_entries()
|
|
90
103
|
for entry in entries:
|
|
91
104
|
assert "supportsMcp" not in entry
|
|
@@ -99,8 +112,14 @@ def test_python_list_integration_catalog_returns_raw_entries() -> None:
|
|
|
99
112
|
assert "runtimeAvailability" not in entry
|
|
100
113
|
|
|
101
114
|
|
|
115
|
+
def test_raw_dict_accessors_were_removed_in_0_12_0() -> None:
|
|
116
|
+
# Deprecated in 0.10.0, removed in 0.12.0. The typed model API replaces them.
|
|
117
|
+
assert not hasattr(openhands_extensions, "list_integration_catalog")
|
|
118
|
+
assert not hasattr(openhands_extensions, "get_integration_catalog_entry")
|
|
119
|
+
|
|
120
|
+
|
|
102
121
|
def test_python_list_integration_catalog_models_returns_typed_entries() -> None:
|
|
103
|
-
entries =
|
|
122
|
+
entries = _catalog_entries()
|
|
104
123
|
models = openhands_extensions.list_integration_catalog_models()
|
|
105
124
|
|
|
106
125
|
assert [model.model_dump(exclude_none=True) for model in models] == entries
|
|
@@ -189,7 +208,7 @@ def test_catalog_models_require_connectability_and_install_metadata() -> None:
|
|
|
189
208
|
|
|
190
209
|
|
|
191
210
|
def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
|
|
192
|
-
entries =
|
|
211
|
+
entries = _python_entries()
|
|
193
212
|
with_logo = [entry for entry in entries if entry.get("logoUrl")]
|
|
194
213
|
assert with_logo
|
|
195
214
|
assert any(entry["id"].startswith("cloudflare-") for entry in with_logo)
|
|
@@ -199,16 +218,14 @@ def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
|
|
|
199
218
|
assert "react" not in entry["logoUrl"].lower()
|
|
200
219
|
|
|
201
220
|
|
|
202
|
-
def
|
|
203
|
-
sample_id =
|
|
204
|
-
sample = openhands_extensions.
|
|
221
|
+
def test_get_integration_catalog_entry_model_round_trip() -> None:
|
|
222
|
+
sample_id = _python_entries()[0]["id"]
|
|
223
|
+
sample = openhands_extensions.get_integration_catalog_entry_model(sample_id)
|
|
205
224
|
assert sample is not None
|
|
206
|
-
assert sample == next(
|
|
207
|
-
entry
|
|
208
|
-
for entry in openhands_extensions.list_integration_catalog()
|
|
209
|
-
if entry["id"] == sample_id
|
|
225
|
+
assert sample.model_dump(exclude_none=True) == next(
|
|
226
|
+
entry for entry in _python_entries() if entry["id"] == sample_id
|
|
210
227
|
)
|
|
211
|
-
assert openhands_extensions.
|
|
228
|
+
assert openhands_extensions.get_integration_catalog_entry_model("nope") is None
|
|
212
229
|
|
|
213
230
|
|
|
214
231
|
def _js_call(expr: str) -> str:
|
|
@@ -227,16 +244,16 @@ def test_js_reads_the_same_catalog_as_python() -> None:
|
|
|
227
244
|
"import { listIntegrationCatalog } from './integrations/index.js';\n"
|
|
228
245
|
"process.stdout.write(JSON.stringify(listIntegrationCatalog()));"
|
|
229
246
|
)
|
|
230
|
-
assert json.loads(integrations) ==
|
|
247
|
+
assert json.loads(integrations) == _python_entries()
|
|
231
248
|
|
|
232
|
-
sample_id =
|
|
249
|
+
sample_id = _python_entries()[0]["id"]
|
|
233
250
|
sample = _js_call(
|
|
234
251
|
"import { getIntegrationCatalogEntry } from './integrations/index.js';\n"
|
|
235
252
|
f"process.stdout.write(JSON.stringify(getIntegrationCatalogEntry({json.dumps(sample_id)})));"
|
|
236
253
|
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
254
|
+
entry_model = openhands_extensions.get_integration_catalog_entry_model(sample_id)
|
|
255
|
+
assert entry_model is not None
|
|
256
|
+
assert json.loads(sample) == entry_model.model_dump(exclude_none=True)
|
|
240
257
|
|
|
241
258
|
|
|
242
259
|
def _js_filter(mcp, oauth) -> list[str]:
|
|
@@ -249,74 +266,51 @@ def _js_filter(mcp, oauth) -> list[str]:
|
|
|
249
266
|
|
|
250
267
|
|
|
251
268
|
def test_filter_mcp_only() -> None:
|
|
252
|
-
py_ids = {e["id"] for e in
|
|
269
|
+
py_ids = {e["id"] for e in _python_entries(mcp=True)}
|
|
253
270
|
js_ids = set(_js_filter("true", "undefined"))
|
|
254
|
-
all_mcp = {
|
|
255
|
-
e["id"]
|
|
256
|
-
for e in openhands_extensions.list_integration_catalog()
|
|
257
|
-
if _supports_mcp(e)
|
|
258
|
-
}
|
|
271
|
+
all_mcp = {e["id"] for e in _python_entries() if _supports_mcp(e)}
|
|
259
272
|
assert py_ids == js_ids == all_mcp
|
|
260
273
|
assert "filesystem" in py_ids
|
|
261
274
|
|
|
262
275
|
|
|
263
276
|
def test_filter_oauth_only() -> None:
|
|
264
|
-
py_ids = {
|
|
265
|
-
e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True)
|
|
266
|
-
}
|
|
277
|
+
py_ids = {e["id"] for e in _python_entries(oauth=True)}
|
|
267
278
|
js_ids = set(_js_filter("undefined", "true"))
|
|
268
|
-
all_oauth = {
|
|
269
|
-
e["id"]
|
|
270
|
-
for e in openhands_extensions.list_integration_catalog()
|
|
271
|
-
if _supports_oauth(e)
|
|
272
|
-
}
|
|
279
|
+
all_oauth = {e["id"] for e in _python_entries() if _supports_oauth(e)}
|
|
273
280
|
assert py_ids == js_ids == all_oauth
|
|
274
281
|
assert py_ids
|
|
275
282
|
|
|
276
283
|
|
|
277
284
|
def test_filter_oauth_not_mcp() -> None:
|
|
278
|
-
py_ids = {
|
|
279
|
-
e["id"]
|
|
280
|
-
for e in openhands_extensions.list_integration_catalog(oauth=True, mcp=False)
|
|
281
|
-
}
|
|
285
|
+
py_ids = {e["id"] for e in _python_entries(oauth=True, mcp=False)}
|
|
282
286
|
js_ids = set(_js_filter("false", "true"))
|
|
283
287
|
expected = {
|
|
284
288
|
e["id"]
|
|
285
|
-
for e in
|
|
289
|
+
for e in _python_entries()
|
|
286
290
|
if _supports_oauth(e) and not _supports_mcp(e)
|
|
287
291
|
}
|
|
288
292
|
assert py_ids == js_ids == expected
|
|
289
293
|
|
|
290
294
|
|
|
291
295
|
def test_filter_none_returns_all() -> None:
|
|
292
|
-
assert len(
|
|
293
|
-
openhands_extensions.list_integration_catalog(mcp=None, oauth=None)
|
|
294
|
-
)
|
|
296
|
+
assert len(_python_entries()) == len(_python_entries(mcp=None, oauth=None))
|
|
295
297
|
|
|
296
298
|
|
|
297
299
|
def test_accessors_return_independent_copies() -> None:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
assert
|
|
301
|
-
assert
|
|
302
|
-
catalog_a[0]["__mutated"] = True
|
|
303
|
-
assert "__mutated" not in openhands_extensions.list_integration_catalog()[0]
|
|
304
|
-
|
|
305
|
-
sample_id = openhands_extensions.list_integration_catalog()[0]["id"]
|
|
306
|
-
sample = openhands_extensions.get_integration_catalog_entry(sample_id)
|
|
307
|
-
assert sample is not None
|
|
308
|
-
sample["__mutated"] = True
|
|
309
|
-
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry(
|
|
310
|
-
sample_id
|
|
311
|
-
)
|
|
300
|
+
models_a = openhands_extensions.list_integration_catalog_models()
|
|
301
|
+
models_b = openhands_extensions.list_integration_catalog_models()
|
|
302
|
+
assert models_a == models_b
|
|
303
|
+
assert models_a[0] is not models_b[0]
|
|
312
304
|
|
|
305
|
+
# Mutating the exported snapshot must not leak into the cached catalog.
|
|
306
|
+
# The models forbid extra fields, so a leak surfaces as a validation error.
|
|
313
307
|
snapshot = openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT
|
|
314
308
|
snapshot["integrations"][0]["__mutated"] = True
|
|
315
|
-
assert "__mutated" not in
|
|
309
|
+
assert "__mutated" not in _python_entries()[0]
|
|
316
310
|
|
|
317
311
|
|
|
318
312
|
def test_js_accessors_return_independent_copies() -> None:
|
|
319
|
-
sample_id =
|
|
313
|
+
sample_id = _python_entries()[0]["id"]
|
|
320
314
|
expr = f"""
|
|
321
315
|
import {{
|
|
322
316
|
INTEGRATION_CATALOG,
|
|
@@ -342,11 +336,7 @@ def test_js_accessors_return_independent_copies() -> None:
|
|
|
342
336
|
|
|
343
337
|
|
|
344
338
|
def test_oauth_config_lives_on_connection_options() -> None:
|
|
345
|
-
oauth_entries = [
|
|
346
|
-
entry
|
|
347
|
-
for entry in openhands_extensions.list_integration_catalog()
|
|
348
|
-
if _supports_oauth(entry)
|
|
349
|
-
]
|
|
339
|
+
oauth_entries = [entry for entry in _python_entries() if _supports_oauth(entry)]
|
|
350
340
|
assert oauth_entries
|
|
351
341
|
for entry in oauth_entries:
|
|
352
342
|
assert "registrationDefaults" not in entry
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "github-pr-reviewer",
|
|
3
|
-
"name": "GitHub Code Review Agent",
|
|
4
|
-
"category": "Code review",
|
|
5
|
-
"description": "Watch for a configurable label on GitHub pull requests, inspect full PR and repository context, and post an AI review comment once per label event.",
|
|
6
|
-
"requiredIntegrationIds": [
|
|
7
|
-
"github"
|
|
8
|
-
],
|
|
9
|
-
"popularityRank": 100,
|
|
10
|
-
"estimatedSetupMinutes": 4,
|
|
11
|
-
"prompt": "/pr-reviewer:setup",
|
|
12
|
-
"exampleImplementation": "Trigger: cron polling for open GitHub PRs with a configured label such as openhands-review\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Read repository, trigger label, review tone, and polling schedule from setup.\n2. List open PRs and find the latest matching GitHub labeled issue event for each labeled PR.\n3. Deduplicate on the label event ID so every label application queues exactly one review.\n4. Start an OpenHands conversation that clones the repo, checks out the exact PR head SHA, and inspects PR discussion, review comments, changed files, diff, and surrounding code.\n5. Post an acknowledgement with the conversation link, then post the final AI review comment only if the PR is still open and the head SHA has not changed."
|
|
13
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "github-repo-monitor",
|
|
3
|
-
"name": "GitHub repository monitor",
|
|
4
|
-
"category": "Developer tools",
|
|
5
|
-
"description": "Watch a repository for @OpenHands mentions in issues and PR comments, start a conversation, and post the agent's reply back to GitHub.",
|
|
6
|
-
"requiredIntegrationIds": ["github"],
|
|
7
|
-
"popularityRank": 98,
|
|
8
|
-
"estimatedSetupMinutes": 5,
|
|
9
|
-
"prompt": "/github-monitor:poll",
|
|
10
|
-
"exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes."
|
|
11
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
"requiredIntegrationIds": [
|
|
7
|
-
"slack",
|
|
8
|
-
"linear",
|
|
9
|
-
"notion"
|
|
10
|
-
],
|
|
11
|
-
"popularityRank": 78,
|
|
12
|
-
"estimatedSetupMinutes": 8,
|
|
13
|
-
"prompt": "/incident-retro:setup",
|
|
14
|
-
"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."
|
|
15
|
-
}
|