@openhands/extensions 0.4.2 → 0.6.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 (134) hide show
  1. package/.github/workflows/pypi-publish.yml +104 -0
  2. package/.github/workflows/tests.yml +6 -0
  3. package/.release-please-manifest.json +1 -1
  4. package/AGENTS.md +1 -1
  5. package/README.md +31 -3
  6. package/automations/catalog/slack-channel-monitor.json +2 -2
  7. package/integrations/README.md +17 -7
  8. package/integrations/catalog/airtable.json +40 -10
  9. package/integrations/catalog/apify.json +0 -2
  10. package/integrations/catalog/asana.json +38 -0
  11. package/integrations/catalog/atlassian.json +5 -8
  12. package/integrations/catalog/bitbucket.json +39 -0
  13. package/integrations/catalog/box.json +39 -0
  14. package/integrations/catalog/brave-search.json +1 -2
  15. package/integrations/catalog/browser-mcp.json +0 -3
  16. package/integrations/catalog/canva.json +35 -0
  17. package/integrations/catalog/clickhouse.json +1 -2
  18. package/integrations/catalog/clickup.json +36 -0
  19. package/integrations/catalog/cloudflare-bindings.json +5 -8
  20. package/integrations/catalog/cloudflare-browser-rendering.json +5 -8
  21. package/integrations/catalog/cloudflare-builds.json +6 -9
  22. package/integrations/catalog/cloudflare-docs.json +4 -7
  23. package/integrations/catalog/cloudflare-observability.json +5 -8
  24. package/integrations/catalog/confluence.json +38 -0
  25. package/integrations/catalog/datadog.json +34 -0
  26. package/integrations/catalog/deepwiki.json +3 -7
  27. package/integrations/catalog/discord.json +39 -0
  28. package/integrations/catalog/dropbox.json +38 -0
  29. package/integrations/catalog/elevenlabs.json +27 -28
  30. package/integrations/catalog/everything.json +0 -2
  31. package/integrations/catalog/exa.json +0 -2
  32. package/integrations/catalog/fetch.json +0 -2
  33. package/integrations/catalog/figma.json +41 -9
  34. package/integrations/catalog/filesystem.json +0 -3
  35. package/integrations/catalog/firecrawl.json +0 -2
  36. package/integrations/catalog/freshdesk.json +38 -0
  37. package/integrations/catalog/git.json +0 -3
  38. package/integrations/catalog/github.json +51 -34
  39. package/integrations/catalog/gitlab.json +38 -0
  40. package/integrations/catalog/gmail.json +38 -0
  41. package/integrations/catalog/google-calendar.json +38 -0
  42. package/integrations/catalog/google-docs.json +38 -0
  43. package/integrations/catalog/google-drive.json +38 -0
  44. package/integrations/catalog/google-sheets.json +38 -0
  45. package/integrations/catalog/hubspot.json +34 -0
  46. package/integrations/catalog/huggingface.json +1 -2
  47. package/integrations/catalog/intercom.json +39 -0
  48. package/integrations/catalog/jira.json +38 -0
  49. package/integrations/catalog/kagi.json +2 -3
  50. package/integrations/catalog/linear.json +47 -16
  51. package/integrations/catalog/mailchimp.json +38 -0
  52. package/integrations/catalog/memory.json +0 -2
  53. package/integrations/catalog/microsoft-outlook.json +38 -0
  54. package/integrations/catalog/microsoft-teams.json +38 -0
  55. package/integrations/catalog/miro.json +38 -0
  56. package/integrations/catalog/monday.json +38 -0
  57. package/integrations/catalog/mongodb.json +1 -2
  58. package/integrations/catalog/neon.json +0 -2
  59. package/integrations/catalog/netlify.json +38 -0
  60. package/integrations/catalog/notion.json +37 -10
  61. package/integrations/catalog/obsidian.json +1 -3
  62. package/integrations/catalog/okta.json +38 -0
  63. package/integrations/catalog/onedrive.json +38 -0
  64. package/integrations/catalog/ordinal.json +33 -0
  65. package/integrations/catalog/paypal.json +5 -8
  66. package/integrations/catalog/pipedrive.json +39 -0
  67. package/integrations/catalog/plaid.json +31 -0
  68. package/integrations/catalog/playwright.json +0 -2
  69. package/integrations/catalog/posthog.json +38 -0
  70. package/integrations/catalog/quickbooks.json +38 -0
  71. package/integrations/catalog/redis.json +1 -3
  72. package/integrations/catalog/resend.json +1 -2
  73. package/integrations/catalog/salesforce.json +38 -0
  74. package/integrations/catalog/sentry.json +52 -14
  75. package/integrations/catalog/sequential-thinking.json +0 -2
  76. package/integrations/catalog/servicenow.json +39 -0
  77. package/integrations/catalog/sharepoint.json +38 -0
  78. package/integrations/catalog/shopify.json +38 -0
  79. package/integrations/catalog/slack.json +49 -10
  80. package/integrations/catalog/stripe.json +44 -14
  81. package/integrations/catalog/supabase.json +36 -9
  82. package/integrations/catalog/tavily.json +0 -2
  83. package/integrations/catalog/time.json +0 -2
  84. package/integrations/catalog/trello.json +31 -0
  85. package/integrations/catalog/vercel.json +38 -0
  86. package/integrations/catalog/webflow.json +34 -0
  87. package/integrations/catalog/xero.json +38 -0
  88. package/integrations/catalog/zendesk.json +38 -0
  89. package/integrations/catalog/zoom.json +38 -0
  90. package/integrations/catalog-index.js +171 -0
  91. package/integrations/index.d.ts +26 -60
  92. package/integrations/index.js +29 -207
  93. package/marketplaces/openhands-extensions.json +14 -1
  94. package/package.json +4 -7
  95. package/plugins/cobol-modernization/README.md +3 -3
  96. package/plugins/cobol-modernization/skills/{cobol-modernization-overview → cobol-modernization}/SKILL.md +1 -1
  97. package/plugins/magic-test/skills/magic-word/SKILL.md +1 -0
  98. package/plugins/migration-scoring/README.md +1 -1
  99. package/pyproject.toml +17 -3
  100. package/python/openhands_extensions/__init__.py +24 -0
  101. package/python/openhands_extensions/_version.py +24 -0
  102. package/python/openhands_extensions/integrations.py +92 -0
  103. package/release-please-config.json +2 -1
  104. package/scripts/build-integration-catalog.mjs +45 -0
  105. package/skills/agent-canvas-environment/.plugin/plugin.json +19 -0
  106. package/skills/agent-canvas-environment/README.md +5 -0
  107. package/skills/agent-canvas-environment/SKILL.md +141 -0
  108. package/skills/github-pr-reviewer/references/state-schema.md +18 -4
  109. package/skills/github-pr-reviewer/scripts/main.py +66 -12
  110. package/skills/github-repo-monitor/references/state-schema.md +11 -4
  111. package/skills/github-repo-monitor/scripts/main.py +67 -19
  112. package/skills/github-repo-monitor/tests/test_main.py +67 -8
  113. package/skills/index.js +19 -5
  114. package/skills/openhands-api/README.md +5 -1
  115. package/skills/openhands-api/SKILL.md +74 -5
  116. package/skills/openhands-automation/SKILL.md +3 -1
  117. package/skills/openhands-automation/references/custom-automation.md +166 -0
  118. package/skills/openhands-sdk/SKILL.md +8 -2
  119. package/skills/slack-channel-monitor/README.md +9 -5
  120. package/skills/slack-channel-monitor/SKILL.md +16 -6
  121. package/skills/slack-channel-monitor/references/slack-api.md +5 -3
  122. package/skills/slack-channel-monitor/references/state-schema.md +70 -36
  123. package/skills/slack-channel-monitor/scripts/main.py +335 -63
  124. package/tests/test_catalogs.py +21 -3
  125. package/tests/test_integration_catalog_in_sync.py +204 -0
  126. package/tests/test_slack_channel_monitor.py +142 -0
  127. package/tests/test_version_alignment.py +64 -0
  128. package/integrations/logos.d.ts +0 -5
  129. package/integrations/logos.js +0 -94
  130. package/integrations/oauth-provider-catalog.js +0 -553
  131. package/integrations/oauth-provider-registration-defaults.js +0 -610
  132. /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/SKILL.md +0 -0
  133. /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/references/cics-transformation-examples.md +0 -0
  134. /package/plugins/migration-scoring/skills/{migration-scoring-overview → migration-scoring}/SKILL.md +0 -0
@@ -0,0 +1,204 @@
1
+ """Assert integration catalog files drive the generated JS/Python assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ import openhands_extensions
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ CATALOG_DIR = ROOT / "integrations" / "catalog"
13
+ CATALOG_INDEX = ROOT / "integrations" / "catalog-index.js"
14
+ AGGREGATE_ASSETS = [
15
+ ROOT / "integrations" / "integration-catalog.json",
16
+ ROOT / "python" / "openhands_extensions" / "integration-catalog.json",
17
+ ]
18
+
19
+
20
+ def _catalog_files() -> dict[str, dict]:
21
+ return {p.stem: json.loads(p.read_text()) for p in CATALOG_DIR.glob("*.json")}
22
+
23
+
24
+ def _catalog_entries() -> list[dict]:
25
+ entries = list(_catalog_files().values())
26
+ return sorted(entries, key=lambda entry: (-(entry.get("popularityRank") if entry.get("popularityRank") is not None else -1), entry["id"]))
27
+
28
+
29
+ def _supports_mcp(entry: dict) -> bool:
30
+ return any(option.get("provider") == "mcp" for option in entry["connectionOptions"])
31
+
32
+
33
+ def _supports_oauth(entry: dict) -> bool:
34
+ return any(option.get("auth", {}).get("strategy") == "oauth2" for option in entry["connectionOptions"])
35
+
36
+
37
+
38
+ def test_catalog_directory_is_hand_authored_source_of_truth() -> None:
39
+ for integration_id, entry in _catalog_files().items():
40
+ assert entry["id"] == integration_id
41
+ assert "supportsMcp" not in entry
42
+ assert "supportsOauth" not in entry
43
+ assert "oauthProvider" not in entry
44
+ assert "registrationDefaults" not in entry
45
+ assert "managedConnectorSlug" not in entry
46
+ assert "authStrategy" not in entry
47
+ assert "defaultConnectionOptionId" not in entry
48
+ assert "kind" not in entry
49
+ assert "catalogStatus" not in entry
50
+ assert "availability" not in entry
51
+ assert "runtimeAvailability" not in entry
52
+
53
+
54
+ def test_no_aggregate_catalog_json_exists() -> None:
55
+ for asset in AGGREGATE_ASSETS:
56
+ assert not asset.exists()
57
+
58
+
59
+ def test_generated_js_index_references_catalog_directory() -> None:
60
+ body = CATALOG_INDEX.read_text()
61
+ for file in sorted(path.name for path in CATALOG_DIR.glob("*.json")):
62
+ assert f"./catalog/{file}" in body
63
+ assert "integration-catalog.json" not in body
64
+
65
+
66
+ def test_python_snapshot_is_built_from_catalog_directory() -> None:
67
+ assert openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT == {
68
+ "integrations": _catalog_entries()
69
+ }
70
+
71
+
72
+ def test_python_list_integration_catalog_returns_raw_entries() -> None:
73
+ entries = openhands_extensions.list_integration_catalog()
74
+ assert entries == _catalog_entries()
75
+ for entry in entries:
76
+ assert "supportsMcp" not in entry
77
+ assert "supportsOauth" not in entry
78
+ assert "registrationDefaults" not in entry
79
+ assert "managedConnectorSlug" not in entry
80
+ assert "authStrategy" not in entry
81
+ assert "defaultConnectionOptionId" not in entry
82
+ assert "catalogStatus" not in entry
83
+ assert "availability" not in entry
84
+ assert "runtimeAvailability" not in entry
85
+
86
+
87
+ def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
88
+ entries = openhands_extensions.list_integration_catalog()
89
+ with_logo = [entry for entry in entries if entry.get("logoUrl")]
90
+ assert with_logo
91
+ assert any(entry["id"].startswith("cloudflare-") for entry in with_logo)
92
+ for entry in with_logo:
93
+ assert isinstance(entry["logoUrl"], str)
94
+ assert entry["logoUrl"].startswith("https://")
95
+ assert "react" not in entry["logoUrl"].lower()
96
+
97
+
98
+ def test_get_integration_catalog_entry_round_trip() -> None:
99
+ github = openhands_extensions.get_integration_catalog_entry("github")
100
+ assert github is not None
101
+ assert github == next(
102
+ entry for entry in openhands_extensions.list_integration_catalog() if entry["id"] == "github"
103
+ )
104
+ assert openhands_extensions.get_integration_catalog_entry("nope") is None
105
+
106
+
107
+ def _js_call(expr: str) -> str:
108
+ result = subprocess.run(
109
+ ["node", "--input-type=module", "-e", expr],
110
+ capture_output=True,
111
+ text=True,
112
+ check=True,
113
+ cwd=ROOT,
114
+ )
115
+ return result.stdout
116
+
117
+
118
+ def test_js_reads_the_same_catalog_as_python() -> None:
119
+ integrations = _js_call(
120
+ "import { listIntegrationCatalog } from './integrations/index.js';\n"
121
+ "process.stdout.write(JSON.stringify(listIntegrationCatalog()));"
122
+ )
123
+ assert json.loads(integrations) == openhands_extensions.list_integration_catalog()
124
+
125
+ github = _js_call(
126
+ "import { getIntegrationCatalogEntry } from './integrations/index.js';\n"
127
+ "process.stdout.write(JSON.stringify(getIntegrationCatalogEntry('github')));"
128
+ )
129
+ assert json.loads(github) == openhands_extensions.get_integration_catalog_entry("github")
130
+
131
+
132
+ def _js_filter(mcp, oauth) -> list[str]:
133
+ expr = (
134
+ "import { listIntegrationCatalog } from './integrations/index.js';\n"
135
+ f"const f = listIntegrationCatalog({{ mcp: {mcp}, oauth: {oauth} }});\n"
136
+ "process.stdout.write(JSON.stringify(f.map((e) => e.id)));"
137
+ )
138
+ return json.loads(_js_call(expr))
139
+
140
+
141
+ def test_filter_mcp_only() -> None:
142
+ py_ids = {e["id"] for e in openhands_extensions.list_integration_catalog(mcp=True)}
143
+ js_ids = set(_js_filter("true", "undefined"))
144
+ all_mcp = {e["id"] for e in openhands_extensions.list_integration_catalog() if _supports_mcp(e)}
145
+ assert py_ids == js_ids == all_mcp
146
+ assert "filesystem" in py_ids
147
+
148
+
149
+ def test_filter_oauth_only() -> None:
150
+ py_ids = {e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True)}
151
+ js_ids = set(_js_filter("undefined", "true"))
152
+ all_oauth = {e["id"] for e in openhands_extensions.list_integration_catalog() if _supports_oauth(e)}
153
+ assert py_ids == js_ids == all_oauth
154
+ assert "github" in py_ids
155
+
156
+
157
+ def test_filter_oauth_not_mcp() -> None:
158
+ py_ids = {
159
+ e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True, mcp=False)
160
+ }
161
+ js_ids = set(_js_filter("false", "true"))
162
+ expected = {
163
+ e["id"]
164
+ for e in openhands_extensions.list_integration_catalog()
165
+ if _supports_oauth(e) and not _supports_mcp(e)
166
+ }
167
+ assert py_ids == js_ids == expected
168
+
169
+
170
+ def test_filter_none_returns_all() -> None:
171
+ assert len(openhands_extensions.list_integration_catalog()) == len(
172
+ openhands_extensions.list_integration_catalog(mcp=None, oauth=None)
173
+ )
174
+
175
+
176
+ def test_accessors_return_independent_copies() -> None:
177
+ catalog_a = openhands_extensions.list_integration_catalog()
178
+ catalog_b = openhands_extensions.list_integration_catalog()
179
+ assert catalog_a == catalog_b
180
+ assert catalog_a is not catalog_b
181
+ catalog_a[0]["__mutated"] = True
182
+ assert "__mutated" not in openhands_extensions.list_integration_catalog()[0]
183
+
184
+ github = openhands_extensions.get_integration_catalog_entry("github")
185
+ assert github is not None
186
+ github["__mutated"] = True
187
+ assert "__mutated" not in openhands_extensions.get_integration_catalog_entry("github")
188
+
189
+ snapshot = openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT
190
+ snapshot["integrations"][0]["__mutated"] = True
191
+ assert "__mutated" not in openhands_extensions.list_integration_catalog()[0]
192
+
193
+
194
+ def test_hubspot_oauth_config_lives_on_connection_option() -> None:
195
+ hubspot = openhands_extensions.get_integration_catalog_entry("hubspot")
196
+ assert hubspot is not None
197
+ assert "registrationDefaults" not in hubspot
198
+ option = hubspot["connectionOptions"][0]
199
+ assert option["provider"] == "mcp"
200
+ assert option["transport"]["url"] == "https://mcp.hubspot.com"
201
+ oauth = option["auth"]["oauth"]
202
+ assert oauth["authorizationUrl"] == "https://mcp.hubspot.com/oauth/authorize/user"
203
+ assert oauth["tokenUrl"] == "https://mcp.hubspot.com/oauth/v3/token"
204
+ assert oauth["pkce"] is True
@@ -0,0 +1,142 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ SCRIPT_PATH = (
6
+ Path(__file__).parent.parent
7
+ / "skills"
8
+ / "slack-channel-monitor"
9
+ / "scripts"
10
+ / "main.py"
11
+ )
12
+
13
+
14
+ def load_slack_monitor_helpers():
15
+ os.environ["WORKSPACE_BASE"] = "/tmp/openhands/workspaces/slack-monitor-test/run"
16
+ source = SCRIPT_PATH.read_text(encoding="utf-8")
17
+ helper_source = source.split("\nPOLL_ITERATIONS = 10", 1)[0]
18
+ namespace: dict = {}
19
+ exec(compile(helper_source, str(SCRIPT_PATH), "exec"), namespace)
20
+ return namespace
21
+
22
+
23
+ def test_post_message_sends_markdown_text(monkeypatch):
24
+ helpers = load_slack_monitor_helpers()
25
+ posted: dict = {}
26
+
27
+ def fake_slack_post(token: str, endpoint: str, body: dict) -> dict:
28
+ posted["token"] = token
29
+ posted["endpoint"] = endpoint
30
+ posted["body"] = body
31
+ return {"ts": "123.456"}
32
+
33
+ monkeypatch.setitem(helpers, "slack_post", fake_slack_post)
34
+ markdown_summary = "✅ Done!\n\n- **Bold:** [link](https://example.com)"
35
+
36
+ ts = helpers["post_message"](
37
+ "xoxb-test",
38
+ "C123",
39
+ markdown_summary,
40
+ thread_ts="111.222",
41
+ )
42
+
43
+ assert ts == "123.456"
44
+ assert posted["endpoint"] == "chat.postMessage"
45
+ assert posted["body"] == {
46
+ "channel": "C123",
47
+ "markdown_text": markdown_summary,
48
+ "unfurl_links": False,
49
+ "unfurl_media": False,
50
+ "thread_ts": "111.222",
51
+ }
52
+ assert "text" not in posted["body"]
53
+ assert "blocks" not in posted["body"]
54
+ assert "mrkdwn" not in posted["body"]
55
+
56
+
57
+ def test_followup_quiet_poll_is_capped_at_watch_expiry(monkeypatch):
58
+ helpers = load_slack_monitor_helpers()
59
+ monkeypatch.setattr(helpers["time"], "time", lambda: 950.0)
60
+ calls: list[tuple[str, str, str]] = []
61
+
62
+ def fake_thread_replies(
63
+ token: str,
64
+ channel: str,
65
+ thread_ts: str,
66
+ oldest: str,
67
+ ) -> list[dict]:
68
+ calls.append((channel, thread_ts, oldest))
69
+ return []
70
+
71
+ monkeypatch.setitem(helpers, "thread_replies", fake_thread_replies)
72
+ rec = {
73
+ "channel_id": "C123",
74
+ "thread_ts": "900.000000",
75
+ "status": "watching",
76
+ "last_seen_reply_ts": "900.000000",
77
+ "reply_poll_backoff_seconds": 80,
78
+ "next_reply_poll_at": 950.0,
79
+ "watch_until": 1000.0,
80
+ }
81
+
82
+ replies = helpers["_poll_due_thread_replies"](
83
+ "xoxb-test",
84
+ {"C123:900.000000": rec},
85
+ "UBOT",
86
+ [],
87
+ )
88
+
89
+ assert replies == []
90
+ assert calls == [("C123", "900.000000", "900.000000")]
91
+ assert rec["status"] == "watching"
92
+ assert rec["next_reply_poll_at"] == 1000.0
93
+
94
+
95
+ def test_expired_followup_watch_polls_once_before_closing(monkeypatch):
96
+ helpers = load_slack_monitor_helpers()
97
+ monkeypatch.setattr(helpers["time"], "time", lambda: 1001.0)
98
+ trigger = helpers["TRIGGER_PHRASE"]
99
+ reply = {
100
+ "ts": "999.500000",
101
+ "user": "U1",
102
+ "text": f"{trigger} please continue",
103
+ }
104
+ calls: list[tuple[str, str, str]] = []
105
+
106
+ def fake_thread_replies(
107
+ token: str,
108
+ channel: str,
109
+ thread_ts: str,
110
+ oldest: str,
111
+ ) -> list[dict]:
112
+ calls.append((channel, thread_ts, oldest))
113
+ return [reply]
114
+
115
+ monkeypatch.setitem(helpers, "thread_replies", fake_thread_replies)
116
+ rec = {
117
+ "channel_id": "C123",
118
+ "thread_ts": "900.000000",
119
+ "status": "watching",
120
+ "last_seen_reply_ts": "900.000000",
121
+ "reply_poll_backoff_seconds": 160,
122
+ "next_reply_poll_at": 1100.0,
123
+ "watch_until": 1000.0,
124
+ }
125
+
126
+ replies = helpers["_poll_due_thread_replies"](
127
+ "xoxb-test",
128
+ {"C123:900.000000": rec},
129
+ "UBOT",
130
+ [],
131
+ )
132
+
133
+ assert calls == [("C123", "900.000000", "900.000000")]
134
+ assert replies == [("C123", reply)]
135
+ assert rec["last_seen_reply_ts"] == "999.500000"
136
+ assert (
137
+ rec["reply_poll_backoff_seconds"]
138
+ == helpers["THREAD_REPLY_INITIAL_BACKOFF_SECONDS"]
139
+ )
140
+ assert rec["next_reply_poll_at"] == 1006.0
141
+ assert rec["watch_until"] == 1301.0
142
+
@@ -0,0 +1,64 @@
1
+ """Assert the JS (``package.json``) and Python (``pyproject.toml`` +
2
+ ``_version.py``) package versions never drift apart.
3
+
4
+ ``release-please`` is configured to bump both ``package.json`` and
5
+ ``pyproject.toml`` together (see ``release-please-config.json`` ->
6
+ ``extra-files``). This test is the guard that catches a manual edit that
7
+ bumps only one.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from pathlib import Path
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+
17
+
18
+ def _package_json_version() -> str:
19
+ text = (ROOT / "package.json").read_text()
20
+ match = re.search(r'"version"\s*:\s*"([^"]+)"', text)
21
+ assert match, "package.json has no version field"
22
+ return match.group(1)
23
+
24
+
25
+ def _pyproject_version() -> str:
26
+ text = (ROOT / "pyproject.toml").read_text()
27
+ match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
28
+ assert match, "pyproject.toml has no version field"
29
+ return match.group(1)
30
+
31
+
32
+ def _version_module_version() -> str:
33
+ import openhands_extensions._version as v
34
+
35
+ text = (ROOT / "python" / "openhands_extensions" / "_version.py").read_text()
36
+ match = re.search(r'_FALLBACK_VERSION\s*=\s*"([^"]+)"', text)
37
+ assert match, "_version.py has no _FALLBACK_VERSION"
38
+ fallback = match.group(1)
39
+ # Runtime __version__ is metadata-derived when installed, fallback otherwise;
40
+ # both must agree with the declared versions.
41
+ assert v.__version__ in (fallback, _package_json_version()), (
42
+ f"runtime __version__ {v.__version__!r} disagrees with fallback/package.json"
43
+ )
44
+ return fallback
45
+
46
+
47
+ def test_package_json_and_pyproject_versions_match() -> None:
48
+ assert _package_json_version() == _pyproject_version(), (
49
+ "package.json and pyproject.toml versions differ. release-please "
50
+ "should bump both together; check release-please-config.json."
51
+ )
52
+
53
+
54
+ def test_version_module_matches_package_json() -> None:
55
+ assert _version_module_version() == _package_json_version(), (
56
+ "python/openhands_extensions/_version.py is out of sync with "
57
+ "package.json."
58
+ )
59
+
60
+
61
+ def test_installed_package_version_matches_source() -> None:
62
+ import openhands_extensions
63
+
64
+ assert openhands_extensions.__version__ == _package_json_version()
@@ -1,5 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export const INTEGRATION_FALLBACK_LOGO: ReactNode;
4
- export const INTEGRATION_LOGOS: Record<string, ReactNode>;
5
- export const INTEGRATION_LOGO_IDS: Set<string>;
@@ -1,94 +0,0 @@
1
- import { createElement } from "react";
2
- import {
3
- BookOpen,
4
- Bot,
5
- Brain,
6
- Clock,
7
- Database,
8
- Flame,
9
- Folder,
10
- GitBranch,
11
- Globe,
12
- ListTree,
13
- MousePointerClick,
14
- Search,
15
- Sparkles,
16
- Telescope,
17
- TestTube,
18
- } from "lucide-react";
19
- import {
20
- SiAirtable,
21
- SiAtlassian,
22
- SiBrave,
23
- SiClickhouse,
24
- SiCloudflare,
25
- SiElevenlabs,
26
- SiFigma,
27
- SiGithub,
28
- SiHuggingface,
29
- SiKagi,
30
- SiLinear,
31
- SiMongodb,
32
- SiNotion,
33
- SiObsidian,
34
- SiPaypal,
35
- SiRedis,
36
- SiResend,
37
- SiSentry,
38
- SiSlack,
39
- SiStripe,
40
- SiSupabase,
41
- } from "react-icons/si";
42
-
43
- const LOGO = "h-5 w-5";
44
-
45
- const simpleIcon = (Icon) => createElement(Icon, { className: LOGO });
46
- const lucideIcon = (Icon) =>
47
- createElement(Icon, { className: LOGO, strokeWidth: 2.25 });
48
-
49
- export const INTEGRATION_FALLBACK_LOGO = lucideIcon(Bot);
50
-
51
- export const INTEGRATION_LOGOS = {
52
- github: simpleIcon(SiGithub),
53
- slack: simpleIcon(SiSlack),
54
- tavily: createElement(Search, { className: LOGO, strokeWidth: 2.5 }),
55
- linear: simpleIcon(SiLinear),
56
- notion: simpleIcon(SiNotion),
57
- atlassian: simpleIcon(SiAtlassian),
58
- sentry: simpleIcon(SiSentry),
59
- stripe: simpleIcon(SiStripe),
60
- paypal: simpleIcon(SiPaypal),
61
- "cloudflare-docs": simpleIcon(SiCloudflare),
62
- "cloudflare-bindings": simpleIcon(SiCloudflare),
63
- "cloudflare-observability": simpleIcon(SiCloudflare),
64
- huggingface: simpleIcon(SiHuggingface),
65
- deepwiki: simpleIcon(BookOpen),
66
- git: lucideIcon(GitBranch),
67
- "brave-search": simpleIcon(SiBrave),
68
- exa: lucideIcon(Telescope),
69
- firecrawl: lucideIcon(Flame),
70
- apify: lucideIcon(Bot),
71
- fetch: lucideIcon(Globe),
72
- "browser-mcp": lucideIcon(MousePointerClick),
73
- playwright: lucideIcon(TestTube),
74
- supabase: simpleIcon(SiSupabase),
75
- neon: lucideIcon(Database),
76
- mongodb: simpleIcon(SiMongodb),
77
- redis: simpleIcon(SiRedis),
78
- filesystem: lucideIcon(Folder),
79
- memory: lucideIcon(Brain),
80
- "sequential-thinking": lucideIcon(ListTree),
81
- time: lucideIcon(Clock),
82
- everything: lucideIcon(Sparkles),
83
- figma: simpleIcon(SiFigma),
84
- airtable: simpleIcon(SiAirtable),
85
- obsidian: simpleIcon(SiObsidian),
86
- elevenlabs: simpleIcon(SiElevenlabs),
87
- resend: simpleIcon(SiResend),
88
- "cloudflare-builds": simpleIcon(SiCloudflare),
89
- "cloudflare-browser-rendering": simpleIcon(SiCloudflare),
90
- kagi: simpleIcon(SiKagi),
91
- clickhouse: simpleIcon(SiClickhouse),
92
- };
93
-
94
- export const INTEGRATION_LOGO_IDS = new Set(Object.keys(INTEGRATION_LOGOS));