@openhands/extensions 0.8.0 → 0.10.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/deprecation-check.yml +25 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/catalog/jira-issue-to-pr.json +14 -0
- package/automations/index.js +2 -0
- package/integrations/README.md +25 -0
- package/integrations/catalog/atlassian.json +91 -3
- package/integrations/catalog/datadog.json +33 -2
- package/integrations/catalog/github.json +20 -0
- package/integrations/catalog/microsoft-outlook.json +13 -0
- package/integrations/catalog/microsoft-teams.json +13 -0
- package/integrations/catalog/notion.json +13 -0
- package/integrations/catalog/onedrive.json +13 -0
- package/integrations/catalog/sharepoint.json +13 -0
- package/integrations/catalog/slack.json +14 -0
- package/integrations/catalog/superhuman-mail.json +36 -0
- package/integrations/catalog/vanta.json +55 -0
- package/integrations/catalog-index.js +73 -69
- package/integrations/catalog.schema.json +70 -3
- package/integrations/index.d.ts +49 -3
- package/marketplaces/openhands-extensions.json +10 -0
- package/package.json +1 -1
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +1 -0
- package/plugins/cobol-modernization/skills/cobol-modernization/SKILL.md +9 -0
- package/plugins/migration-scoring/skills/migration-scoring/SKILL.md +11 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +2 -0
- package/plugins/openhands/SKILL.md +7 -0
- package/plugins/release-notes/SKILL.md +9 -0
- package/pyproject.toml +6 -1
- package/python/openhands_extensions/__init__.py +38 -0
- package/python/openhands_extensions/_version.py +2 -2
- package/python/openhands_extensions/deprecation.py +159 -0
- package/python/openhands_extensions/integration_models.py +233 -0
- package/python/openhands_extensions/integrations.py +59 -4
- package/scripts/build-skills-catalog.mjs +1 -1
- package/scripts/check_deprecations.py +488 -0
- package/skills/add-skill/SKILL.md +2 -0
- package/skills/azure-devops/SKILL.md +12 -1
- package/skills/bitbucket-cloud/SKILL.md +11 -0
- package/skills/code-review/SKILL.md +1 -0
- package/skills/datadog/SKILL.md +3 -1
- package/skills/datadog/references/windows.md +45 -0
- package/skills/discord/SKILL.md +2 -0
- package/skills/docker/SKILL.md +5 -1
- package/skills/github/SKILL.md +3 -1
- package/skills/github/references/windows.md +48 -0
- package/skills/github-pr-review/SKILL.md +1 -0
- package/skills/github-pr-review/references/windows.md +47 -0
- package/skills/github-pr-reviewer/SKILL.md +1 -0
- package/skills/github-pr-reviewer/references/windows.md +61 -0
- package/skills/github-repo-monitor/SKILL.md +1 -0
- package/skills/github-repo-monitor/references/windows.md +61 -0
- package/skills/gitlab/SKILL.md +3 -1
- package/skills/incident-retrospective/SKILL.md +2 -0
- package/skills/index.js +36 -30
- package/skills/iterate/SKILL.md +1 -0
- package/skills/iterate/references/windows.md +41 -0
- package/skills/jira-issue-to-pr/.plugin/plugin.json +19 -0
- package/skills/jira-issue-to-pr/README.md +56 -0
- package/skills/jira-issue-to-pr/SKILL.md +216 -0
- package/skills/jira-issue-to-pr/references/setup.md +170 -0
- package/skills/jira-issue-to-pr/scripts/main.py +341 -0
- package/skills/jupyter/SKILL.md +7 -1
- package/skills/kubernetes/SKILL.md +2 -1
- package/skills/kubernetes/references/windows.md +38 -0
- package/skills/linear/SKILL.md +2 -0
- package/skills/linear/references/windows.md +29 -0
- package/skills/linear-triage/SKILL.md +2 -0
- package/skills/notion/SKILL.md +2 -0
- package/skills/notion/references/windows.md +43 -0
- package/skills/openhands-api/SKILL.md +1 -0
- package/skills/openhands-api/references/windows.md +44 -0
- package/skills/openhands-automation/SKILL.md +1 -0
- package/skills/openhands-automation/references/custom-automation.md +20 -0
- package/skills/openhands-automation/references/windows.md +57 -0
- package/skills/pdflatex/SKILL.md +3 -1
- package/skills/research-brief/SKILL.md +2 -0
- package/skills/skill-creator/SKILL.md +1 -0
- package/skills/skill-creator/references/windows.md +30 -0
- package/skills/slack-channel-monitor/SKILL.md +1 -0
- package/skills/slack-channel-monitor/references/windows.md +64 -0
- package/skills/slack-standup-digest/SKILL.md +2 -0
- package/skills/spark-version-upgrade/SKILL.md +2 -1
- package/skills/ssh/SKILL.md +2 -1
- package/skills/ssh/references/windows.md +59 -0
- package/skills/swift-linux/SKILL.md +2 -1
- package/skills/vercel/SKILL.md +5 -0
- package/tests/test_catalog_schema.py +58 -0
- package/tests/test_catalogs.py +6 -1
- package/tests/test_deprecations.py +136 -0
- package/tests/test_integration_catalog_in_sync.py +118 -7
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jira issue-to-PR poller — reads all configuration from config.json in the same directory.
|
|
3
|
+
|
|
4
|
+
config.json fields:
|
|
5
|
+
jira_base_url e.g. "https://yourcompany.atlassian.net"
|
|
6
|
+
jira_email Atlassian account email used for Basic auth
|
|
7
|
+
jira_token_secret Name of the OpenHands secret holding the Jira API token
|
|
8
|
+
jira_label Label to watch for (default: "create-pr")
|
|
9
|
+
max_new_per_run Max conversations dispatched per run (default: 5)
|
|
10
|
+
|
|
11
|
+
The target GitHub repository is NOT configured here. Each Jira ticket body must include
|
|
12
|
+
the repo in "owner/repo" format; the spawned agent extracts it from the ticket text.
|
|
13
|
+
"""
|
|
14
|
+
import base64, json, os, re, sys, tempfile, urllib.error, urllib.request
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# ── Load config ───────────────────────────────────────────────────────────────
|
|
19
|
+
_HERE = Path(__file__).parent
|
|
20
|
+
with open(_HERE / "config.json") as _f:
|
|
21
|
+
_cfg = json.load(_f)
|
|
22
|
+
|
|
23
|
+
JIRA_BASE_URL = _cfg["jira_base_url"].rstrip("/")
|
|
24
|
+
JIRA_EMAIL = _cfg["jira_email"]
|
|
25
|
+
JIRA_TOKEN_SECRET = _cfg.get("jira_token_secret", "JIRA_CLOUD_KEY")
|
|
26
|
+
JIRA_LABEL = _cfg.get("jira_label", "create-pr")
|
|
27
|
+
MAX_NEW_PER_RUN = int(_cfg.get("max_new_per_run", 5))
|
|
28
|
+
|
|
29
|
+
# ── KV store helpers ──────────────────────────────────────────────────────────
|
|
30
|
+
_KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
|
|
31
|
+
_KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
|
|
32
|
+
_STATE_KEY = "state"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def kv_available():
|
|
36
|
+
return bool(_KV_TOKEN and _KV_BASE)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def kv_get(key):
|
|
40
|
+
req = urllib.request.Request(
|
|
41
|
+
f"{_KV_BASE}/v1/kv/{key}",
|
|
42
|
+
headers={"Authorization": f"Bearer {_KV_TOKEN}"},
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(req) as r:
|
|
46
|
+
return json.loads(r.read())["value"]
|
|
47
|
+
except urllib.error.HTTPError as exc:
|
|
48
|
+
if exc.code == 404:
|
|
49
|
+
return None
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def kv_set(key, value):
|
|
54
|
+
req = urllib.request.Request(
|
|
55
|
+
f"{_KV_BASE}/v1/kv/{key}",
|
|
56
|
+
data=json.dumps(value).encode(),
|
|
57
|
+
headers={
|
|
58
|
+
"Authorization": f"Bearer {_KV_TOKEN}",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
method="PUT",
|
|
62
|
+
)
|
|
63
|
+
with urllib.request.urlopen(req) as r:
|
|
64
|
+
r.read()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _state_file_path():
|
|
68
|
+
workspace_base = os.environ.get("WORKSPACE_BASE", "")
|
|
69
|
+
root = (Path(workspace_base).resolve().parent.parent if workspace_base
|
|
70
|
+
else Path.home() / ".openhands" / "workspaces")
|
|
71
|
+
state_dir = root / "automation-state"
|
|
72
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))
|
|
74
|
+
automation_id = payload.get("automation_id", "default")
|
|
75
|
+
return state_dir / f"jira_poller_{automation_id}.json"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_state():
|
|
79
|
+
if kv_available():
|
|
80
|
+
data = kv_get(_STATE_KEY)
|
|
81
|
+
if data is not None:
|
|
82
|
+
print("State loaded from KV store")
|
|
83
|
+
return data
|
|
84
|
+
return {"processed_keys": []}
|
|
85
|
+
path = _state_file_path()
|
|
86
|
+
if path.exists():
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(path.read_text())
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
print(f"Warning: state file unreadable ({exc}); starting fresh")
|
|
91
|
+
return {"processed_keys": []}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_state(state):
|
|
95
|
+
if kv_available():
|
|
96
|
+
kv_set(_STATE_KEY, state)
|
|
97
|
+
print("State saved to KV store")
|
|
98
|
+
return
|
|
99
|
+
path = _state_file_path()
|
|
100
|
+
tmp = path.with_suffix(".tmp")
|
|
101
|
+
tmp.write_text(json.dumps(state, indent=2))
|
|
102
|
+
tmp.replace(path)
|
|
103
|
+
print(f"State saved to {path}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Required stdlib helpers ───────────────────────────────────────────────────
|
|
107
|
+
def get_secret(name):
|
|
108
|
+
url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
109
|
+
key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
|
|
110
|
+
with urllib.request.urlopen(urllib.request.Request(
|
|
111
|
+
f"{url}/api/settings/secrets/{name}", headers={"X-Session-API-Key": key}
|
|
112
|
+
)) as r:
|
|
113
|
+
return r.read().decode().strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def fire_callback(status="COMPLETED", error=None):
|
|
117
|
+
url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
|
|
118
|
+
if not url:
|
|
119
|
+
return
|
|
120
|
+
body = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
|
|
121
|
+
if error:
|
|
122
|
+
body["error"] = error
|
|
123
|
+
try:
|
|
124
|
+
urllib.request.urlopen(urllib.request.Request(
|
|
125
|
+
url, data=json.dumps(body).encode(),
|
|
126
|
+
headers={
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
|
|
129
|
+
}
|
|
130
|
+
))
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Callback error (non-fatal): {e}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Jira helpers ──────────────────────────────────────────────────────────────
|
|
136
|
+
def extract_adf_text(adf):
|
|
137
|
+
"""Recursively extract plain text from Atlassian Document Format."""
|
|
138
|
+
if not isinstance(adf, dict):
|
|
139
|
+
return adf or ""
|
|
140
|
+
parts = []
|
|
141
|
+
for node in adf.get("content", []):
|
|
142
|
+
if node.get("type") == "text":
|
|
143
|
+
parts.append(node.get("text", ""))
|
|
144
|
+
elif "content" in node:
|
|
145
|
+
parts.append(extract_adf_text(node))
|
|
146
|
+
return " ".join(p for p in parts if p).strip()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def fetch_labeled_issues(auth_header):
|
|
150
|
+
"""Return open Jira issues with JIRA_LABEL using the current v3 search endpoint."""
|
|
151
|
+
url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
|
|
152
|
+
body = json.dumps({
|
|
153
|
+
"jql": f'labels = "{JIRA_LABEL}" AND statusCategory != Done',
|
|
154
|
+
"fields": ["key", "summary", "description", "status", "updated"],
|
|
155
|
+
"maxResults": 50,
|
|
156
|
+
}).encode()
|
|
157
|
+
req = urllib.request.Request(
|
|
158
|
+
url, data=body,
|
|
159
|
+
headers={
|
|
160
|
+
"Authorization": auth_header,
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
"Accept": "application/json",
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
with urllib.request.urlopen(req) as r:
|
|
167
|
+
return json.loads(r.read()).get("issues", [])
|
|
168
|
+
except urllib.error.HTTPError as exc:
|
|
169
|
+
body_text = exc.read().decode(errors="replace")
|
|
170
|
+
raise RuntimeError(f"Jira search failed {exc.code}: {body_text[:500]}") from exc
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def post_jira_comment(issue_key, auth_header, text):
|
|
174
|
+
"""Post a plain-text comment on a Jira issue using ADF."""
|
|
175
|
+
url = f"{JIRA_BASE_URL}/rest/api/3/issue/{issue_key}/comment"
|
|
176
|
+
body = json.dumps({
|
|
177
|
+
"body": {
|
|
178
|
+
"type": "doc",
|
|
179
|
+
"version": 1,
|
|
180
|
+
"content": [{
|
|
181
|
+
"type": "paragraph",
|
|
182
|
+
"content": [{"type": "text", "text": text}],
|
|
183
|
+
}],
|
|
184
|
+
}
|
|
185
|
+
}).encode()
|
|
186
|
+
req = urllib.request.Request(
|
|
187
|
+
url, data=body,
|
|
188
|
+
headers={
|
|
189
|
+
"Authorization": auth_header,
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
"Accept": "application/json",
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
try:
|
|
195
|
+
with urllib.request.urlopen(req) as r:
|
|
196
|
+
r.read()
|
|
197
|
+
except urllib.error.HTTPError as exc:
|
|
198
|
+
body_text = exc.read().decode(errors="replace")
|
|
199
|
+
print(f"Warning: failed to post Jira comment on {issue_key} ({exc.code}): {body_text[:200]}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ── Timestamp helpers ─────────────────────────────────────────────────────────
|
|
203
|
+
def _parse_ts(ts):
|
|
204
|
+
"""Parse an ISO-8601 timestamp, normalising +HHMM → +HH:MM for Python < 3.11."""
|
|
205
|
+
normalized = re.sub(r'([+-])(\d{2})(\d{2})$', r'\1\2:\3', ts)
|
|
206
|
+
return datetime.fromisoformat(normalized)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
210
|
+
try:
|
|
211
|
+
jira_token = get_secret(JIRA_TOKEN_SECRET)
|
|
212
|
+
auth_header = "Basic " + base64.b64encode(
|
|
213
|
+
f"{JIRA_EMAIL}:{jira_token}".encode()
|
|
214
|
+
).decode()
|
|
215
|
+
|
|
216
|
+
state = load_state()
|
|
217
|
+
processed_keys = set(state.get("processed_keys", []))
|
|
218
|
+
|
|
219
|
+
# On the very first run there is no baseline timestamp. Record one now so
|
|
220
|
+
# that all pre-existing issues (created before this moment) are silently
|
|
221
|
+
# skipped — preventing a thundering-herd of conversations on first deploy.
|
|
222
|
+
if "first_run_at" not in state:
|
|
223
|
+
state["first_run_at"] = datetime.now(timezone.utc).isoformat()
|
|
224
|
+
save_state(state)
|
|
225
|
+
print(f"First run — baseline recorded at {state['first_run_at']}; "
|
|
226
|
+
"issues created before this timestamp will be skipped.")
|
|
227
|
+
|
|
228
|
+
first_run_at = _parse_ts(state["first_run_at"])
|
|
229
|
+
|
|
230
|
+
print(f"Polling {JIRA_BASE_URL} for issues labeled '{JIRA_LABEL}'…")
|
|
231
|
+
issues = fetch_labeled_issues(auth_header)
|
|
232
|
+
print(f"Total matching issues : {len(issues)}")
|
|
233
|
+
|
|
234
|
+
new_issues = [
|
|
235
|
+
i for i in issues
|
|
236
|
+
if i["key"] not in processed_keys
|
|
237
|
+
and _parse_ts(i["fields"]["updated"]) >= first_run_at
|
|
238
|
+
]
|
|
239
|
+
print(f"New (unprocessed, after baseline) : {len(new_issues)}")
|
|
240
|
+
|
|
241
|
+
if len(new_issues) > MAX_NEW_PER_RUN:
|
|
242
|
+
print(f"Capping at {MAX_NEW_PER_RUN} per run "
|
|
243
|
+
f"({len(new_issues)} available); remainder picked up on next run.")
|
|
244
|
+
new_issues = new_issues[:MAX_NEW_PER_RUN]
|
|
245
|
+
|
|
246
|
+
if not new_issues:
|
|
247
|
+
save_state(state)
|
|
248
|
+
fire_callback("COMPLETED")
|
|
249
|
+
sys.exit(0)
|
|
250
|
+
|
|
251
|
+
# Start one agent conversation per new issue via the agent server HTTP API.
|
|
252
|
+
agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
253
|
+
session_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
|
|
254
|
+
|
|
255
|
+
# Fetch settings with encrypted secrets so llm.api_key is a Fernet token
|
|
256
|
+
# (starts with gAAAAA) rather than the masked "**********" placeholder.
|
|
257
|
+
# The conversation payload must include secrets_encrypted: True so the
|
|
258
|
+
# agent-server decrypts it server-side; we never handle the plaintext key.
|
|
259
|
+
with urllib.request.urlopen(urllib.request.Request(
|
|
260
|
+
f"{agent_url}/api/settings",
|
|
261
|
+
headers={"X-Session-API-Key": session_key, "X-Expose-Secrets": "encrypted"},
|
|
262
|
+
)) as r:
|
|
263
|
+
settings = json.loads(r.read())
|
|
264
|
+
|
|
265
|
+
agent_settings = settings.get("agent_settings", {})
|
|
266
|
+
agent_settings.pop("schema_version", None)
|
|
267
|
+
# Drop mcp_config to avoid MCP connection failures at conversation creation time.
|
|
268
|
+
agent_settings.pop("mcp_config", None)
|
|
269
|
+
ctx = agent_settings.setdefault("agent_context", {})
|
|
270
|
+
ctx.update({"load_public_skills": True, "load_user_skills": True, "load_project_skills": True})
|
|
271
|
+
max_iterations = (settings.get("conversation_settings") or {}).get("max_iterations") or 1000
|
|
272
|
+
|
|
273
|
+
for issue in new_issues:
|
|
274
|
+
key = issue["key"]
|
|
275
|
+
summary = issue["fields"]["summary"]
|
|
276
|
+
description = extract_adf_text(issue["fields"].get("description"))
|
|
277
|
+
branch = f"jira/{key.lower()}"
|
|
278
|
+
|
|
279
|
+
prompt = f"""Create a GitHub Pull Request for the following Jira issue.
|
|
280
|
+
|
|
281
|
+
Jira Issue : {key}
|
|
282
|
+
Summary : {summary}
|
|
283
|
+
Description: {description or "No description provided."}
|
|
284
|
+
|
|
285
|
+
Steps:
|
|
286
|
+
1. Find the target GitHub repository in the Description above. Look for a reference in
|
|
287
|
+
"owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL
|
|
288
|
+
(e.g. "https://github.com/acme-org/backend"). Use that repository.
|
|
289
|
+
If no repository is mentioned, create a file `jira/{key}/notes.md` with the issue
|
|
290
|
+
details and print a message explaining that no GitHub repo was found in the ticket.
|
|
291
|
+
2. Clone the repository (e.g. https://github.com/<owner>/<repo>).
|
|
292
|
+
3. Create branch `{branch}` from the default branch.
|
|
293
|
+
4. Implement the changes described in the issue.
|
|
294
|
+
If the description is vague or missing, create `jira/{key}/notes.md`
|
|
295
|
+
with the issue key, summary, and description as a placeholder.
|
|
296
|
+
5. Commit, push the branch, and open a Pull Request:
|
|
297
|
+
- Title : [{key}] {summary}
|
|
298
|
+
- Body : Reference the Jira issue key and describe the changes made.
|
|
299
|
+
6. Print the PR URL when done.
|
|
300
|
+
"""
|
|
301
|
+
workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-")
|
|
302
|
+
payload = {
|
|
303
|
+
"secrets_encrypted": True,
|
|
304
|
+
"agent_settings": agent_settings,
|
|
305
|
+
"workspace": {"kind": "LocalWorkspace", "working_dir": workdir},
|
|
306
|
+
"confirmation_policy": {"kind": "NeverConfirm"},
|
|
307
|
+
"max_iterations": max_iterations,
|
|
308
|
+
"stuck_detection": True,
|
|
309
|
+
"autotitle": True,
|
|
310
|
+
"worktree": False,
|
|
311
|
+
"initial_message": {
|
|
312
|
+
"role": "user",
|
|
313
|
+
"content": [{"type": "text", "text": prompt}],
|
|
314
|
+
"run": True,
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
conv_req = urllib.request.Request(
|
|
318
|
+
f"{agent_url}/api/conversations",
|
|
319
|
+
data=json.dumps(payload).encode(),
|
|
320
|
+
headers={"Content-Type": "application/json", "X-Session-API-Key": session_key},
|
|
321
|
+
)
|
|
322
|
+
with urllib.request.urlopen(conv_req) as r:
|
|
323
|
+
conv = json.loads(r.read())
|
|
324
|
+
|
|
325
|
+
conv_id = conv.get("id")
|
|
326
|
+
conv_url = f"{agent_url}/conversations/{conv_id}"
|
|
327
|
+
print(f"✓ Conversation started for {key}: id={conv_id}")
|
|
328
|
+
|
|
329
|
+
post_jira_comment(key, auth_header, f"I'm on it: {conv_url}")
|
|
330
|
+
|
|
331
|
+
processed_keys.add(key)
|
|
332
|
+
state["processed_keys"] = list(processed_keys)
|
|
333
|
+
save_state(state)
|
|
334
|
+
|
|
335
|
+
fire_callback("COMPLETED")
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
339
|
+
import traceback; traceback.print_exc()
|
|
340
|
+
fire_callback("FAILED", str(e))
|
|
341
|
+
sys.exit(1)
|
package/skills/jupyter/SKILL.md
CHANGED
|
@@ -33,6 +33,12 @@ jupyter nbconvert --to markdown notebook.ipynb # Convert to Markdown
|
|
|
33
33
|
grep -n "search_term" notebook.ipynb
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
PowerShell equivalent:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
Select-String -Path notebook.ipynb -Pattern "search_term"
|
|
40
|
+
```
|
|
41
|
+
|
|
36
42
|
## Cell Structure
|
|
37
43
|
```python
|
|
38
44
|
# Code cell
|
|
@@ -47,4 +53,4 @@ for cell in nb['cells']:
|
|
|
47
53
|
if cell['cell_type'] == 'code':
|
|
48
54
|
cell['outputs'] = []
|
|
49
55
|
cell['execution_count'] = None
|
|
50
|
-
```
|
|
56
|
+
```
|
|
@@ -14,6 +14,7 @@ triggers:
|
|
|
14
14
|
KIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.
|
|
15
15
|
|
|
16
16
|
IMPORTANT: Before you proceed with installation, make sure you have docker installed locally.
|
|
17
|
+
Windows PowerShell equivalents for installing KIND and kubectl are in `references/windows.md`.
|
|
17
18
|
|
|
18
19
|
### Installation
|
|
19
20
|
|
|
@@ -45,4 +46,4 @@ Create a basic KIND cluster:
|
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
kind create cluster
|
|
48
|
-
```
|
|
49
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Windows Notes for KIND and kubectl
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms instead of the Linux `curl`, `chmod`, and `sudo mv` commands.
|
|
4
|
+
|
|
5
|
+
## Install KIND
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
$binDir = Join-Path $env:USERPROFILE "bin"
|
|
9
|
+
New-Item -ItemType Directory -Force $binDir | Out-Null
|
|
10
|
+
|
|
11
|
+
Invoke-WebRequest `
|
|
12
|
+
-Uri "https://kind.sigs.k8s.io/dl/v0.22.0/kind-windows-amd64" `
|
|
13
|
+
-OutFile (Join-Path $binDir "kind.exe")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Ensure `$binDir` is on `PATH` for the current shell:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
$env:PATH = "$binDir;$env:PATH"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Install kubectl
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
$version = (Invoke-RestMethod "https://dl.k8s.io/release/stable.txt").Trim()
|
|
26
|
+
Invoke-WebRequest `
|
|
27
|
+
-Uri "https://dl.k8s.io/release/$version/bin/windows/amd64/kubectl.exe" `
|
|
28
|
+
-OutFile (Join-Path $binDir "kubectl.exe")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Create a Cluster
|
|
32
|
+
|
|
33
|
+
Docker Desktop must be running before creating a KIND cluster.
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
kind create cluster
|
|
37
|
+
kubectl cluster-info
|
|
38
|
+
```
|
package/skills/linear/SKILL.md
CHANGED
|
@@ -9,6 +9,8 @@ triggers:
|
|
|
9
9
|
|
|
10
10
|
# Linear
|
|
11
11
|
|
|
12
|
+
Windows PowerShell equivalents for the repeated Linear GraphQL `curl` and environment-variable snippets are in `references/windows.md`.
|
|
13
|
+
|
|
12
14
|
<IMPORTANT>
|
|
13
15
|
Before performing any Linear operations, check if the required environment variable is set:
|
|
14
16
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Windows Notes for Linear API Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when running the Linear GraphQL examples natively on Windows.
|
|
4
|
+
|
|
5
|
+
## Check the API Key
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
if ($env:LINEAR_API_KEY) { "LINEAR_API_KEY is set" } else { "LINEAR_API_KEY is NOT set" }
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Send a GraphQL Request
|
|
12
|
+
|
|
13
|
+
Use `curl.exe` for curl syntax, or use `Invoke-RestMethod` with a JSON body.
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
$body = @{
|
|
17
|
+
query = "query { viewer { assignedIssues(first: 50) { nodes { id identifier title } } } }"
|
|
18
|
+
} | ConvertTo-Json -Depth 10
|
|
19
|
+
|
|
20
|
+
Invoke-RestMethod `
|
|
21
|
+
-Uri "https://api.linear.app/graphql" `
|
|
22
|
+
-Method Post `
|
|
23
|
+
-Headers @{ "Content-Type" = "application/json"; Authorization = $env:LINEAR_API_KEY } `
|
|
24
|
+
-Body $body
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Translate Shell Variables
|
|
28
|
+
|
|
29
|
+
Replace `$LINEAR_API_KEY` with `$env:LINEAR_API_KEY`. Replace pipes to `jq` with `ConvertFrom-Json` if using `curl.exe`.
|
|
@@ -80,6 +80,8 @@ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
|
|
|
80
80
|
}'
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
PowerShell note: use `curl.exe` for this exact flag syntax, and replace `${OPENHANDS_HOST}` / `$OPENHANDS_AUTOMATION_API_KEY` with `$env:OPENHANDS_HOST` / `$env:OPENHANDS_AUTOMATION_API_KEY` if running it natively.
|
|
84
|
+
|
|
83
85
|
### Step 5 — Confirm
|
|
84
86
|
|
|
85
87
|
Tell the user:
|
package/skills/notion/SKILL.md
CHANGED
|
@@ -7,6 +7,8 @@ triggers:
|
|
|
7
7
|
|
|
8
8
|
# Notion
|
|
9
9
|
|
|
10
|
+
Windows PowerShell equivalents for the repeated Notion REST `curl`, environment-variable, and JSON-body snippets are in `references/windows.md`.
|
|
11
|
+
|
|
10
12
|
<IMPORTANT>
|
|
11
13
|
If authenticated Notion MCP tools are available in the environment, use them first. MCP tools do not require passing `NOTION_INTEGRATION_KEY` as a tool argument; authentication is handled by the configured MCP integration.
|
|
12
14
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Windows Notes for Notion REST Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when running the Notion direct-API examples natively on Windows.
|
|
4
|
+
|
|
5
|
+
## Check the Integration Key
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
if ($env:NOTION_INTEGRATION_KEY) { "NOTION_INTEGRATION_KEY is set" } else { "NOTION_INTEGRATION_KEY is NOT set" }
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Base Headers
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
$headers = @{
|
|
15
|
+
Authorization = "Bearer $env:NOTION_INTEGRATION_KEY"
|
|
16
|
+
"Notion-Version" = "2022-06-28"
|
|
17
|
+
"Content-Type" = "application/json"
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Search
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
$body = @{
|
|
25
|
+
query = "OpenHands Wiki"
|
|
26
|
+
page_size = 10
|
|
27
|
+
} | ConvertTo-Json -Depth 10
|
|
28
|
+
|
|
29
|
+
Invoke-RestMethod -Uri "https://api.notion.com/v1/search" -Method Post -Headers $headers -Body $body
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Variable Interpolation in JSON
|
|
33
|
+
|
|
34
|
+
For examples that embed IDs in JSON, build a PowerShell object and convert it rather than mixing shell quotes:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
$body = @{
|
|
38
|
+
parent = @{ type = "page_id"; page_id = $env:PARENT_PAGE_ID }
|
|
39
|
+
properties = @{
|
|
40
|
+
title = @{ title = @(@{ type = "text"; text = @{ content = "My new page" } }) }
|
|
41
|
+
}
|
|
42
|
+
} | ConvertTo-Json -Depth 20
|
|
43
|
+
```
|
|
@@ -11,6 +11,7 @@ triggers:
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
This skill documents the **OpenHands Cloud API** (V1), commonly used **agent-server APIs**, and small, easy-to-copy clients.
|
|
14
|
+
Windows PowerShell equivalents for the shell examples in this skill are in `references/windows.md`.
|
|
14
15
|
|
|
15
16
|
It is intentionally focused on common OpenHands API workflows:
|
|
16
17
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Windows Notes for OpenHands API Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when running this skill's shell examples natively on Windows.
|
|
4
|
+
|
|
5
|
+
## Environment Variables
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
$env:OPENHANDS_CLOUD_API_KEY = "..."
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Curl Examples
|
|
12
|
+
|
|
13
|
+
Call `curl.exe` and use PowerShell backticks for line continuation:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
curl.exe -X POST "https://app.all-hands.dev/api/v1/app-conversations" `
|
|
17
|
+
-H "Authorization: Bearer $env:OPENHANDS_CLOUD_API_KEY" `
|
|
18
|
+
-H "Content-Type: application/json" `
|
|
19
|
+
-d '{
|
|
20
|
+
"initial_message": {
|
|
21
|
+
"content": [{"type": "text", "text": "Investigate flaky tests in tests/test_api.py."}]
|
|
22
|
+
},
|
|
23
|
+
"selected_repository": "owner/repo"
|
|
24
|
+
}'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Python Here-doc Debugging Examples
|
|
28
|
+
|
|
29
|
+
PowerShell does not support Bash here-docs like `python3 - <<'PY'`. Save the script to a temp file and pipe the API response into it.
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
$script = Join-Path $env:TEMP "openhands-events.py"
|
|
33
|
+
@'
|
|
34
|
+
import json, sys
|
|
35
|
+
items = (json.load(sys.stdin) or {}).get("items", [])
|
|
36
|
+
for i, e in enumerate(items):
|
|
37
|
+
print(f"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}")
|
|
38
|
+
'@ | Set-Content -LiteralPath $script -Encoding utf8
|
|
39
|
+
|
|
40
|
+
curl.exe -s "https://app.all-hands.dev/api/v1/conversation/$env:APP_CONVERSATION_ID/events/search?limit=100" `
|
|
41
|
+
-H "Authorization: Bearer $env:OPENHANDS_CLOUD_API_KEY" `
|
|
42
|
+
-H "Accept: application/json" |
|
|
43
|
+
python $script
|
|
44
|
+
```
|
|
@@ -19,6 +19,7 @@ triggers:
|
|
|
19
19
|
# OpenHands Automations
|
|
20
20
|
|
|
21
21
|
Create and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).
|
|
22
|
+
Windows PowerShell equivalents for the automation API `curl` examples and shell-variable conventions are in `references/windows.md`.
|
|
22
23
|
|
|
23
24
|
## Automation Creation Process
|
|
24
25
|
The agent must follow these steps when creating an automation:
|
|
@@ -272,6 +272,26 @@ with OpenHandsCloudWorkspace(
|
|
|
272
272
|
# OpenHandsCloudWorkspace.__exit__ fires the completion callback here.
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
+
### Selecting an LLM profile
|
|
276
|
+
|
|
277
|
+
The automation service injects an `AUTOMATION_MODEL` environment variable holding the **LLM profile name** the automation should run with. This is the profile a user selects when editing the automation, or — when none is selected — a snapshot of the user's active profile taken when the automation was created. To honor it on every run, read `AUTOMATION_MODEL` and pass it to `workspace.get_llm(profile_name=...)`:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
# AUTOMATION_MODEL is the selected profile name (absent when none was chosen).
|
|
281
|
+
model_profile = os.environ.get("AUTOMATION_MODEL") or None
|
|
282
|
+
try:
|
|
283
|
+
llm = workspace.get_llm(profile_name=model_profile)
|
|
284
|
+
except FileNotFoundError:
|
|
285
|
+
# The profile was renamed or deleted after the automation was created;
|
|
286
|
+
# fall back to the user's default LLM rather than failing the run.
|
|
287
|
+
if not model_profile:
|
|
288
|
+
raise
|
|
289
|
+
print(f"profile {model_profile!r} not found; falling back to default profile")
|
|
290
|
+
llm = workspace.get_llm()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Calling `workspace.get_llm()` with no `profile_name` always uses the user's default LLM. The built-in prompt and plugin presets already follow the pattern above; custom scripts should too so the selected profile is honored regardless of trigger type or execution backend.
|
|
294
|
+
|
|
275
295
|
### Conversation Persistence
|
|
276
296
|
|
|
277
297
|
Conversations started during a run remain accessible in the OpenHands UI after the run completes — users can view the history and continue interacting. By default, `Conversation` does not delete the conversation on close:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Windows Notes for OpenHands Automation Commands
|
|
2
|
+
|
|
3
|
+
Use these PowerShell forms when running the automation API examples natively on Windows.
|
|
4
|
+
|
|
5
|
+
## Set the API Host
|
|
6
|
+
|
|
7
|
+
`OPENHANDS_HOST` is a shell convention in this skill. In PowerShell, set it as:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
$env:OPENHANDS_HOST = "https://app.all-hands.dev"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authentication Header
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
$headers = @{
|
|
17
|
+
Authorization = "Bearer $env:OPENHANDS_API_KEY"
|
|
18
|
+
"Content-Type" = "application/json"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Prompt Preset Request
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
$body = @{
|
|
26
|
+
name = "My Automation Name"
|
|
27
|
+
prompt = "What the automation should do"
|
|
28
|
+
trigger = @{
|
|
29
|
+
type = "cron"
|
|
30
|
+
schedule = "0 9 * * *"
|
|
31
|
+
timezone = "UTC"
|
|
32
|
+
}
|
|
33
|
+
} | ConvertTo-Json -Depth 20
|
|
34
|
+
|
|
35
|
+
Invoke-RestMethod `
|
|
36
|
+
-Uri "$env:OPENHANDS_HOST/api/automation/v1/preset/prompt" `
|
|
37
|
+
-Method Post `
|
|
38
|
+
-Headers $headers `
|
|
39
|
+
-Body $body
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Curl Examples
|
|
43
|
+
|
|
44
|
+
If copying an existing `curl` snippet, call `curl.exe`, replace `${OPENHANDS_HOST}` with `$env:OPENHANDS_HOST`, and replace `${OPENHANDS_API_KEY}` with `$env:OPENHANDS_API_KEY`.
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
curl.exe "$env:OPENHANDS_HOST/api/automation/v1?limit=20" `
|
|
48
|
+
-H "Authorization: Bearer $env:OPENHANDS_API_KEY"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Packaging Custom Automations
|
|
52
|
+
|
|
53
|
+
Modern Windows includes `tar.exe`, which can create the `.tar.gz` files expected by the upload API:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
tar.exe -czf $tarball -C $buildDir .
|
|
57
|
+
```
|
package/skills/pdflatex/SKILL.md
CHANGED
|
@@ -13,6 +13,8 @@ PdfLatex is a tool that converts Latex sources into PDF. This is specifically ve
|
|
|
13
13
|
apt-get install texlive-latex-base
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
On Windows, install MiKTeX or TeX Live with the native installer or a package manager such as `winget`. The `apt-get` commands only work in Linux or WSL.
|
|
17
|
+
|
|
16
18
|
* Also install the recommended and extra fonts to avoid running into errors, when trying to use pdflatex on latex files with more fonts.
|
|
17
19
|
|
|
18
20
|
```
|
|
@@ -31,4 +33,4 @@ Once installed as above, you may be able to create PDF files from latex sources
|
|
|
31
33
|
pdflatex latex_source_name.tex
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Ref: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html
|
|
36
|
+
Ref: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html
|