@openhands/extensions 0.7.2 → 0.9.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 (75) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/README.md +4 -2
  3. package/automations/catalog/jira-issue-to-pr.json +14 -0
  4. package/automations/index.js +2 -0
  5. package/integrations/catalog/atlassian.json +78 -3
  6. package/integrations/catalog/datadog.json +33 -2
  7. package/integrations/catalog/notion.json +16 -16
  8. package/integrations/catalog/superhuman-mail.json +36 -0
  9. package/integrations/catalog-index.js +45 -43
  10. package/marketplaces/openhands-extensions.json +19 -0
  11. package/package.json +1 -1
  12. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +1 -0
  13. package/plugins/cobol-modernization/skills/cobol-modernization/SKILL.md +9 -0
  14. package/plugins/migration-scoring/skills/migration-scoring/SKILL.md +11 -0
  15. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +2 -0
  16. package/plugins/openhands/SKILL.md +7 -0
  17. package/plugins/release-notes/SKILL.md +9 -0
  18. package/pyproject.toml +1 -1
  19. package/python/openhands_extensions/_version.py +1 -1
  20. package/scripts/build-skills-catalog.mjs +1 -1
  21. package/skills/add-skill/SKILL.md +2 -0
  22. package/skills/azure-devops/SKILL.md +12 -1
  23. package/skills/bitbucket-cloud/SKILL.md +11 -0
  24. package/skills/code-review/SKILL.md +1 -0
  25. package/skills/datadog/SKILL.md +3 -1
  26. package/skills/datadog/references/windows.md +45 -0
  27. package/skills/discord/SKILL.md +2 -0
  28. package/skills/docker/SKILL.md +5 -1
  29. package/skills/github/SKILL.md +3 -1
  30. package/skills/github/references/windows.md +48 -0
  31. package/skills/github-pr-review/SKILL.md +1 -0
  32. package/skills/github-pr-review/references/windows.md +47 -0
  33. package/skills/github-pr-reviewer/SKILL.md +1 -0
  34. package/skills/github-pr-reviewer/references/windows.md +61 -0
  35. package/skills/github-repo-monitor/SKILL.md +1 -0
  36. package/skills/github-repo-monitor/references/windows.md +61 -0
  37. package/skills/gitlab/SKILL.md +3 -1
  38. package/skills/incident-retrospective/SKILL.md +2 -0
  39. package/skills/index.js +45 -30
  40. package/skills/iterate/SKILL.md +1 -0
  41. package/skills/iterate/references/windows.md +41 -0
  42. package/skills/jira-issue-to-pr/.plugin/plugin.json +19 -0
  43. package/skills/jira-issue-to-pr/README.md +56 -0
  44. package/skills/jira-issue-to-pr/SKILL.md +216 -0
  45. package/skills/jira-issue-to-pr/references/setup.md +170 -0
  46. package/skills/jira-issue-to-pr/scripts/main.py +341 -0
  47. package/skills/jupyter/SKILL.md +7 -1
  48. package/skills/kubernetes/SKILL.md +2 -1
  49. package/skills/kubernetes/references/windows.md +38 -0
  50. package/skills/linear/SKILL.md +2 -0
  51. package/skills/linear/references/windows.md +29 -0
  52. package/skills/linear-triage/SKILL.md +2 -0
  53. package/skills/notion/SKILL.md +2 -0
  54. package/skills/notion/references/windows.md +43 -0
  55. package/skills/openhands-api/SKILL.md +1 -0
  56. package/skills/openhands-api/references/windows.md +44 -0
  57. package/skills/openhands-automation/SKILL.md +1 -0
  58. package/skills/openhands-automation/references/custom-automation.md +20 -0
  59. package/skills/openhands-automation/references/windows.md +57 -0
  60. package/skills/pdflatex/SKILL.md +3 -1
  61. package/skills/plain-english-content/.plugin/plugin.json +15 -0
  62. package/skills/plain-english-content/README.md +12 -0
  63. package/skills/plain-english-content/SKILL.md +78 -0
  64. package/skills/research-brief/SKILL.md +2 -0
  65. package/skills/skill-creator/SKILL.md +1 -0
  66. package/skills/skill-creator/references/windows.md +30 -0
  67. package/skills/slack-channel-monitor/SKILL.md +1 -0
  68. package/skills/slack-channel-monitor/references/windows.md +64 -0
  69. package/skills/slack-standup-digest/SKILL.md +2 -0
  70. package/skills/spark-version-upgrade/SKILL.md +2 -1
  71. package/skills/ssh/SKILL.md +2 -1
  72. package/skills/ssh/references/windows.md +59 -0
  73. package/skills/swift-linux/SKILL.md +2 -1
  74. package/skills/vercel/SKILL.md +5 -0
  75. package/tests/test_catalogs.py +6 -1
@@ -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)
@@ -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
+ ```
@@ -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:
@@ -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
+ ```
@@ -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