@openhands/extensions 0.24.0 → 0.26.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 (133) hide show
  1. package/.claude-plugin/marketplace.json +30 -0
  2. package/.codex-plugin/marketplace.json +30 -0
  3. package/.github/pull_request_template.md +17 -2
  4. package/.github/scripts/check_pr_description.py +64 -5
  5. package/.github/scripts/markdown_sections.py +95 -0
  6. package/.github/workflows/build-vscode-app.yml +30 -0
  7. package/.github/workflows/check-extensions.yml +3 -0
  8. package/.github/workflows/tests.yml +26 -0
  9. package/.plugin/marketplace.json +30 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +10 -3
  13. package/apps/README.md +20 -0
  14. package/apps/catalog.json +10 -0
  15. package/apps/index.d.ts +11 -0
  16. package/apps/index.js +5 -0
  17. package/apps/vscode/README.md +13 -0
  18. package/apps/vscode/canvas-extension.json +19 -0
  19. package/apps/vscode/extension.js +114 -0
  20. package/apps/vscode/scripts/package.mjs +84 -0
  21. package/automations/bundle-index.js +18 -14
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/automations/catalog-index.js +2 -0
  28. package/index.d.ts +2 -0
  29. package/index.js +1 -0
  30. package/marketplaces/openhands-extensions.json +30 -0
  31. package/package.json +9 -1
  32. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  33. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  34. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  35. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  36. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  37. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  38. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  39. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  40. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  41. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  42. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  43. package/plugins/runtime-api-configs/README.md +59 -0
  44. package/plugins/runtime-api-configs/SKILL.md +240 -0
  45. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  46. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  47. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  48. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +183 -15
  49. package/pyproject.toml +1 -1
  50. package/python/openhands_extensions/_version.py +1 -1
  51. package/scripts/check_plugin_resources.py +116 -0
  52. package/scripts/sync_extensions.py +32 -5
  53. package/skills/canvas-extension-api/SKILL.md +34 -3
  54. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  55. package/skills/canvas-extension-api/references/connections.md +24 -0
  56. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  57. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  58. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  59. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  60. package/skills/github/scripts/agent_conversation.py +223 -43
  61. package/skills/github/scripts/github_client.py +183 -15
  62. package/skills/github-agents-md-maintainer/README.md +7 -0
  63. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  64. package/skills/github-agents-md-maintainer/scripts/github_client.py +183 -15
  65. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  66. package/skills/github-delivery-watchdog/scripts/github_client.py +183 -15
  67. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  68. package/skills/github-issue-to-pr/scripts/github_client.py +183 -15
  69. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  70. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  71. package/skills/github-issue-triage/scripts/github_client.py +183 -15
  72. package/skills/github-pr-review/SKILL.md +12 -2
  73. package/skills/github-pr-reviewer/README.md +36 -0
  74. package/skills/github-pr-reviewer/SKILL.md +147 -4
  75. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  76. package/skills/github-pr-reviewer/scripts/github_client.py +183 -15
  77. package/skills/github-pr-reviewer/scripts/main.py +17 -1
  78. package/skills/github-pr-reviewer/scripts/worker.py +842 -57
  79. package/skills/github-repo-monitor/scripts/github_client.py +183 -15
  80. package/skills/github-repo-monitor/scripts/main.py +32 -1
  81. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  82. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  83. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  84. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  85. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  86. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  87. package/skills/github-stale-ci-pr-closer/scripts/worker.py +508 -0
  88. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +353 -0
  89. package/skills/gitlab-issue-to-mr/README.md +12 -0
  90. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  91. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  92. package/skills/incident-retrospective/SKILL.md +5 -5
  93. package/skills/index.js +34 -16
  94. package/skills/jira-issue-to-pr/README.md +12 -4
  95. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  96. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  97. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  98. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  99. package/skills/linear-triage/SKILL.md +5 -5
  100. package/skills/news-digest/README.md +6 -0
  101. package/skills/news-digest/SKILL.md +19 -9
  102. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  103. package/skills/news-digest/scripts/main.py +133 -15
  104. package/skills/openhands-api/README.md +1 -1
  105. package/skills/openhands-api/SKILL.md +3 -3
  106. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  107. package/skills/openhands-automation/SKILL.md +66 -4
  108. package/skills/openhands-automation/references/custom-automation.md +21 -7
  109. package/skills/openhands-sdk/SKILL.md +1 -0
  110. package/skills/research-brief/SKILL.md +5 -5
  111. package/skills/slack-standup-digest/SKILL.md +5 -5
  112. package/skills/ticket-to-code-change/SKILL.md +6 -3
  113. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  114. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  115. package/skills/use-jev/.plugin/plugin.json +8 -0
  116. package/skills/use-jev/README.md +66 -0
  117. package/skills/use-jev/SKILL.md +66 -0
  118. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  119. package/tests/test_agent_conversation_dispatch.py +212 -0
  120. package/tests/test_apps_catalog.py +26 -0
  121. package/tests/test_automation_setup.py +44 -0
  122. package/tests/test_check_pr_description.py +89 -6
  123. package/tests/test_github_automation_foundation.py +158 -0
  124. package/tests/test_github_repo_monitor.py +107 -0
  125. package/tests/test_github_reviewer_delivery.py +2247 -6
  126. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  127. package/tests/test_jira_issue_to_pr.py +312 -0
  128. package/tests/test_jira_setup_webhook.py +309 -0
  129. package/tests/test_openhands_api_python.py +37 -0
  130. package/tests/test_skill_plugin_loading.py +25 -15
  131. package/tests/test_sync_extensions.py +102 -0
  132. package/tests/test_vscode_app.py +33 -0
  133. package/tests/vscode-app.test.mjs +43 -0
@@ -71,3 +71,161 @@ def test_dependency_permission_errors_fail_closed():
71
71
  repository.gh = denied
72
72
  with pytest.raises(HTTPError):
73
73
  repository.dependencies_complete({"body": "Depends on: #12"})
74
+
75
+
76
+ def test_check_runs_reads_every_page_of_the_object_response():
77
+ """The endpoint answers with an object, not a list, so paging is manual."""
78
+ repository = object.__new__(github_client.GitHubRepository)
79
+ pages = {
80
+ 1: {"total_count": 3, "check_runs": [{"name": "a"}, {"name": "b"}]},
81
+ 2: {"total_count": 3, "check_runs": [{"name": "c"}]},
82
+ }
83
+ requests = []
84
+
85
+ def gh(method, path, body=None):
86
+ assert method == "GET"
87
+ page = int(path.rsplit("page=", 1)[1])
88
+ requests.append(page)
89
+ return pages[page]
90
+
91
+ repository.gh = gh
92
+
93
+ assert [run["name"] for run in repository.check_runs("abc")] == ["a", "b", "c"]
94
+ assert requests == [1, 2]
95
+
96
+
97
+ def test_check_runs_stops_on_an_empty_first_page():
98
+ repository = object.__new__(github_client.GitHubRepository)
99
+ repository.gh = lambda *args, **kwargs: {"total_count": 0, "check_runs": []}
100
+
101
+ assert repository.check_runs("abc") == []
102
+
103
+
104
+ def test_workflow_runs_reads_every_page_of_the_object_response():
105
+ """The runs endpoint answers with an object too, so paging is manual."""
106
+ repository = object.__new__(github_client.GitHubRepository)
107
+ pages = {
108
+ 1: {"total_count": 3, "workflow_runs": [{"name": "a"}, {"name": "b"}]},
109
+ 2: {"total_count": 3, "workflow_runs": [{"name": "c"}]},
110
+ }
111
+ requests = []
112
+
113
+ def gh(method, path, body=None):
114
+ assert method == "GET"
115
+ assert "head_sha=abc" in path
116
+ page = int(path.rsplit("page=", 1)[1])
117
+ requests.append(page)
118
+ return pages[page]
119
+
120
+ repository.gh = gh
121
+
122
+ assert [run["name"] for run in repository.workflow_runs("abc")] == [
123
+ "a",
124
+ "b",
125
+ "c",
126
+ ]
127
+ assert requests == [1, 2]
128
+
129
+
130
+ def test_required_check_contexts_keeps_only_required_nodes():
131
+ """`isRequired` is the merge-policy signal; optional contexts are dropped."""
132
+ repository = object.__new__(github_client.GitHubRepository)
133
+ repository.repository = "owner/repo"
134
+ calls = []
135
+
136
+ def api(method, path, params=None, body=None):
137
+ calls.append((method, path, body))
138
+ return {
139
+ "data": {
140
+ "repository": {
141
+ "pullRequest": {
142
+ "commits": {
143
+ "nodes": [
144
+ {
145
+ "commit": {
146
+ "statusCheckRollup": {
147
+ "contexts": {
148
+ "pageInfo": {"hasNextPage": False},
149
+ "nodes": [
150
+ {
151
+ "__typename": "CheckRun",
152
+ "name": "test-and-build (ubuntu)",
153
+ "isRequired": True,
154
+ },
155
+ {
156
+ "__typename": "CheckRun",
157
+ "name": "release ready",
158
+ "isRequired": False,
159
+ },
160
+ {
161
+ "__typename": "StatusContext",
162
+ "context": "ci/required",
163
+ "isRequired": True,
164
+ },
165
+ ],
166
+ }
167
+ }
168
+ }
169
+ }
170
+ ]
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ repository.api = api
178
+
179
+ contexts = repository.required_check_contexts(17200)
180
+
181
+ assert contexts == [
182
+ {"name": "test-and-build (ubuntu)", "kind": "CheckRun"},
183
+ {"name": "ci/required", "kind": "StatusContext"},
184
+ ]
185
+ method, path, body = calls[0]
186
+ assert (method, path) == ("POST", "/graphql")
187
+ assert body["variables"] == {"owner": "owner", "name": "repo", "number": 17200}
188
+ assert "isRequired(pullRequestNumber:$number)" in body["query"]
189
+
190
+
191
+ def test_required_check_contexts_rejects_an_over_long_rollup():
192
+ """A truncated rollup must fail loudly instead of reporting a partial set."""
193
+ repository = object.__new__(github_client.GitHubRepository)
194
+ repository.repository = "owner/repo"
195
+ repository.api = lambda *args, **kwargs: {
196
+ "data": {
197
+ "repository": {
198
+ "pullRequest": {
199
+ "commits": {
200
+ "nodes": [
201
+ {
202
+ "commit": {
203
+ "statusCheckRollup": {
204
+ "contexts": {
205
+ "pageInfo": {"hasNextPage": True},
206
+ "nodes": [],
207
+ }
208
+ }
209
+ }
210
+ }
211
+ ]
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ with pytest.raises(RuntimeError, match="one page"):
219
+ repository.required_check_contexts(2)
220
+
221
+
222
+ def test_required_check_contexts_raises_on_graphql_errors():
223
+ """A GraphQL error must surface, so the caller can fail closed."""
224
+ repository = object.__new__(github_client.GitHubRepository)
225
+ repository.repository = "owner/repo"
226
+ repository.api = lambda *args, **kwargs: {
227
+ "errors": [{"message": "A pull request ID or pull request number is required."}]
228
+ }
229
+
230
+ with pytest.raises(RuntimeError, match="required-check query failed"):
231
+ repository.required_check_contexts(2)
@@ -0,0 +1,107 @@
1
+ import io
2
+ import json
3
+ import urllib.error
4
+ from pathlib import Path
5
+
6
+
7
+ SCRIPT_PATH = (
8
+ Path(__file__).parent.parent
9
+ / "skills"
10
+ / "github-repo-monitor"
11
+ / "scripts"
12
+ / "main.py"
13
+ )
14
+
15
+
16
+ def load_repo_monitor_helpers():
17
+ source = SCRIPT_PATH.read_text(encoding="utf-8")
18
+ namespace: dict = {"__name__": "repo_monitor_main"}
19
+ exec(compile(source, str(SCRIPT_PATH), "exec"), namespace)
20
+ return namespace
21
+
22
+
23
+ class _FakeResponse:
24
+ def __init__(self, payload: dict):
25
+ self._payload = payload
26
+
27
+ def __enter__(self):
28
+ return self
29
+
30
+ def __exit__(self, *exc_info):
31
+ return False
32
+
33
+ def read(self):
34
+ return json.dumps(self._payload).encode()
35
+
36
+
37
+ def _http_error(url: str, code: int) -> urllib.error.HTTPError:
38
+ return urllib.error.HTTPError(url, code, "error", {}, io.BytesIO(b"{}"))
39
+
40
+
41
+ def _patch_urlopen(monkeypatch, helpers, handler):
42
+ monkeypatch.setattr(helpers["urllib"].request, "urlopen", handler)
43
+
44
+
45
+ ACTIVE_LLM = {"model": "openai/gpt-5.5", "api_key": "sk-active"}
46
+ PROFILE_LLM = {"model": "openai/gpt-5.6-sol", "api_key": "sk-profile"}
47
+
48
+
49
+ def test_automation_model_profile_resolves(monkeypatch):
50
+ helpers = load_repo_monitor_helpers()
51
+ monkeypatch.setenv("AUTOMATION_MODEL", "gpt-5.6-sol")
52
+ monkeypatch.setitem(
53
+ helpers, "_fetch_settings", lambda url, key: {"agent_settings": {"llm": ACTIVE_LLM}}
54
+ )
55
+ seen: dict = {}
56
+
57
+ def fake_urlopen(req, *args, **kwargs):
58
+ seen["url"] = req.full_url
59
+ seen["headers"] = {k.lower(): v for k, v in req.header_items()}
60
+ return _FakeResponse({"config": dict(PROFILE_LLM)})
61
+
62
+ _patch_urlopen(monkeypatch, helpers, fake_urlopen)
63
+
64
+ agent = helpers["_get_agent_dict"]("http://agent:8000", "key")
65
+
66
+ assert seen["url"] == "http://agent:8000/api/profiles/gpt-5.6-sol"
67
+ assert seen["headers"]["x-expose-secrets"] == "plaintext"
68
+ assert agent["llm"]["model"] == "openai/gpt-5.6-sol"
69
+ assert agent["llm"]["api_key"] == "sk-profile"
70
+ assert agent["llm"]["usage_id"] == "profile:gpt-5.6-sol"
71
+
72
+
73
+ def test_missing_profile_falls_back_to_active(monkeypatch):
74
+ helpers = load_repo_monitor_helpers()
75
+ monkeypatch.setenv("AUTOMATION_MODEL", "renamed-profile")
76
+ monkeypatch.setitem(
77
+ helpers, "_fetch_settings", lambda url, key: {"agent_settings": {"llm": ACTIVE_LLM}}
78
+ )
79
+
80
+ def fake_urlopen(req, *args, **kwargs):
81
+ raise _http_error(req.full_url, 404)
82
+
83
+ _patch_urlopen(monkeypatch, helpers, fake_urlopen)
84
+
85
+ agent = helpers["_get_agent_dict"]("http://agent:8000", "key")
86
+
87
+ assert agent["llm"] == ACTIVE_LLM
88
+
89
+
90
+ def test_no_automation_model_uses_active_profile(monkeypatch):
91
+ helpers = load_repo_monitor_helpers()
92
+ monkeypatch.delenv("AUTOMATION_MODEL", raising=False)
93
+ monkeypatch.setitem(
94
+ helpers, "_fetch_settings", lambda url, key: {"agent_settings": {"llm": ACTIVE_LLM}}
95
+ )
96
+ calls: list = []
97
+
98
+ def fake_urlopen(req, *args, **kwargs):
99
+ calls.append(req.full_url)
100
+ raise AssertionError("no profile request expected")
101
+
102
+ _patch_urlopen(monkeypatch, helpers, fake_urlopen)
103
+
104
+ agent = helpers["_get_agent_dict"]("http://agent:8000", "key")
105
+
106
+ assert agent["llm"] == ACTIVE_LLM
107
+ assert calls == []