@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.
- package/.github/workflows/pypi-publish.yml +104 -0
- package/.github/workflows/tests.yml +6 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +1 -1
- package/README.md +31 -3
- package/automations/catalog/slack-channel-monitor.json +2 -2
- package/integrations/README.md +17 -7
- package/integrations/catalog/airtable.json +40 -10
- package/integrations/catalog/apify.json +0 -2
- package/integrations/catalog/asana.json +38 -0
- package/integrations/catalog/atlassian.json +5 -8
- package/integrations/catalog/bitbucket.json +39 -0
- package/integrations/catalog/box.json +39 -0
- package/integrations/catalog/brave-search.json +1 -2
- package/integrations/catalog/browser-mcp.json +0 -3
- package/integrations/catalog/canva.json +35 -0
- package/integrations/catalog/clickhouse.json +1 -2
- package/integrations/catalog/clickup.json +36 -0
- package/integrations/catalog/cloudflare-bindings.json +5 -8
- package/integrations/catalog/cloudflare-browser-rendering.json +5 -8
- package/integrations/catalog/cloudflare-builds.json +6 -9
- package/integrations/catalog/cloudflare-docs.json +4 -7
- package/integrations/catalog/cloudflare-observability.json +5 -8
- package/integrations/catalog/confluence.json +38 -0
- package/integrations/catalog/datadog.json +34 -0
- package/integrations/catalog/deepwiki.json +3 -7
- package/integrations/catalog/discord.json +39 -0
- package/integrations/catalog/dropbox.json +38 -0
- package/integrations/catalog/elevenlabs.json +27 -28
- package/integrations/catalog/everything.json +0 -2
- package/integrations/catalog/exa.json +0 -2
- package/integrations/catalog/fetch.json +0 -2
- package/integrations/catalog/figma.json +41 -9
- package/integrations/catalog/filesystem.json +0 -3
- package/integrations/catalog/firecrawl.json +0 -2
- package/integrations/catalog/freshdesk.json +38 -0
- package/integrations/catalog/git.json +0 -3
- package/integrations/catalog/github.json +51 -34
- package/integrations/catalog/gitlab.json +38 -0
- package/integrations/catalog/gmail.json +38 -0
- package/integrations/catalog/google-calendar.json +38 -0
- package/integrations/catalog/google-docs.json +38 -0
- package/integrations/catalog/google-drive.json +38 -0
- package/integrations/catalog/google-sheets.json +38 -0
- package/integrations/catalog/hubspot.json +34 -0
- package/integrations/catalog/huggingface.json +1 -2
- package/integrations/catalog/intercom.json +39 -0
- package/integrations/catalog/jira.json +38 -0
- package/integrations/catalog/kagi.json +2 -3
- package/integrations/catalog/linear.json +47 -16
- package/integrations/catalog/mailchimp.json +38 -0
- package/integrations/catalog/memory.json +0 -2
- package/integrations/catalog/microsoft-outlook.json +38 -0
- package/integrations/catalog/microsoft-teams.json +38 -0
- package/integrations/catalog/miro.json +38 -0
- package/integrations/catalog/monday.json +38 -0
- package/integrations/catalog/mongodb.json +1 -2
- package/integrations/catalog/neon.json +0 -2
- package/integrations/catalog/netlify.json +38 -0
- package/integrations/catalog/notion.json +37 -10
- package/integrations/catalog/obsidian.json +1 -3
- package/integrations/catalog/okta.json +38 -0
- package/integrations/catalog/onedrive.json +38 -0
- package/integrations/catalog/ordinal.json +33 -0
- package/integrations/catalog/paypal.json +5 -8
- package/integrations/catalog/pipedrive.json +39 -0
- package/integrations/catalog/plaid.json +31 -0
- package/integrations/catalog/playwright.json +0 -2
- package/integrations/catalog/posthog.json +38 -0
- package/integrations/catalog/quickbooks.json +38 -0
- package/integrations/catalog/redis.json +1 -3
- package/integrations/catalog/resend.json +1 -2
- package/integrations/catalog/salesforce.json +38 -0
- package/integrations/catalog/sentry.json +52 -14
- package/integrations/catalog/sequential-thinking.json +0 -2
- package/integrations/catalog/servicenow.json +39 -0
- package/integrations/catalog/sharepoint.json +38 -0
- package/integrations/catalog/shopify.json +38 -0
- package/integrations/catalog/slack.json +49 -10
- package/integrations/catalog/stripe.json +44 -14
- package/integrations/catalog/supabase.json +36 -9
- package/integrations/catalog/tavily.json +0 -2
- package/integrations/catalog/time.json +0 -2
- package/integrations/catalog/trello.json +31 -0
- package/integrations/catalog/vercel.json +38 -0
- package/integrations/catalog/webflow.json +34 -0
- package/integrations/catalog/xero.json +38 -0
- package/integrations/catalog/zendesk.json +38 -0
- package/integrations/catalog/zoom.json +38 -0
- package/integrations/catalog-index.js +171 -0
- package/integrations/index.d.ts +26 -60
- package/integrations/index.js +29 -207
- package/marketplaces/openhands-extensions.json +14 -1
- package/package.json +4 -7
- package/plugins/cobol-modernization/README.md +3 -3
- package/plugins/cobol-modernization/skills/{cobol-modernization-overview → cobol-modernization}/SKILL.md +1 -1
- package/plugins/magic-test/skills/magic-word/SKILL.md +1 -0
- package/plugins/migration-scoring/README.md +1 -1
- package/pyproject.toml +17 -3
- package/python/openhands_extensions/__init__.py +24 -0
- package/python/openhands_extensions/_version.py +24 -0
- package/python/openhands_extensions/integrations.py +92 -0
- package/release-please-config.json +2 -1
- package/scripts/build-integration-catalog.mjs +45 -0
- package/skills/agent-canvas-environment/.plugin/plugin.json +19 -0
- package/skills/agent-canvas-environment/README.md +5 -0
- package/skills/agent-canvas-environment/SKILL.md +141 -0
- package/skills/github-pr-reviewer/references/state-schema.md +18 -4
- package/skills/github-pr-reviewer/scripts/main.py +66 -12
- package/skills/github-repo-monitor/references/state-schema.md +11 -4
- package/skills/github-repo-monitor/scripts/main.py +67 -19
- package/skills/github-repo-monitor/tests/test_main.py +67 -8
- package/skills/index.js +19 -5
- package/skills/openhands-api/README.md +5 -1
- package/skills/openhands-api/SKILL.md +74 -5
- package/skills/openhands-automation/SKILL.md +3 -1
- package/skills/openhands-automation/references/custom-automation.md +166 -0
- package/skills/openhands-sdk/SKILL.md +8 -2
- package/skills/slack-channel-monitor/README.md +9 -5
- package/skills/slack-channel-monitor/SKILL.md +16 -6
- package/skills/slack-channel-monitor/references/slack-api.md +5 -3
- package/skills/slack-channel-monitor/references/state-schema.md +70 -36
- package/skills/slack-channel-monitor/scripts/main.py +335 -63
- package/tests/test_catalogs.py +21 -3
- package/tests/test_integration_catalog_in_sync.py +204 -0
- package/tests/test_slack_channel_monitor.py +142 -0
- package/tests/test_version_alignment.py +64 -0
- package/integrations/logos.d.ts +0 -5
- package/integrations/logos.js +0 -94
- package/integrations/oauth-provider-catalog.js +0 -553
- package/integrations/oauth-provider-registration-defaults.js +0 -610
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/SKILL.md +0 -0
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/references/cics-transformation-examples.md +0 -0
- /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()
|
package/integrations/logos.d.ts
DELETED
package/integrations/logos.js
DELETED
|
@@ -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));
|