@openhands/extensions 0.0.1-alpha → 0.2.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 (347) hide show
  1. package/.agents/skills/custom-codereview-guide.md +25 -0
  2. package/.github/pull_request_template.md +38 -0
  3. package/.github/release.yml +14 -0
  4. package/.github/workflows/check-extensions.yml +72 -0
  5. package/.github/workflows/npm-publish.yml +89 -0
  6. package/.github/workflows/pr.yml +30 -0
  7. package/.github/workflows/release.yml +24 -0
  8. package/.github/workflows/tests.yml +25 -0
  9. package/.github/workflows/vulnerability-scan.yml +87 -0
  10. package/.release-please-manifest.json +3 -0
  11. package/AGENTS.md +132 -0
  12. package/README.md +10 -0
  13. package/analysis_results.md +162 -0
  14. package/marketplaces/large-codebase.json +66 -0
  15. package/marketplaces/openhands-extensions.json +682 -0
  16. package/package.json +4 -10
  17. package/plugins/README.md +30 -0
  18. package/plugins/city-weather/.plugin/plugin.json +13 -0
  19. package/plugins/city-weather/README.md +145 -0
  20. package/plugins/city-weather/commands/now.md +56 -0
  21. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  22. package/plugins/cobol-modernization/README.md +201 -0
  23. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  24. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  25. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  26. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  27. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  28. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  29. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  30. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  31. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  33. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  34. package/plugins/issue-duplicate-checker/README.md +51 -0
  35. package/plugins/issue-duplicate-checker/action.yml +349 -0
  36. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  37. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  38. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  39. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  40. package/plugins/magic-test/.plugin/plugin.json +13 -0
  41. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  42. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  43. package/plugins/migration-scoring/README.md +244 -0
  44. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  45. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  46. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  47. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  48. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  49. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  50. package/plugins/onboarding/.plugin/plugin.json +20 -0
  51. package/plugins/onboarding/README.md +30 -0
  52. package/plugins/onboarding/references/criteria.md +144 -0
  53. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  54. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  60. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  61. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  62. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  63. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  64. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  65. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  66. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  67. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  68. package/plugins/openhands/.plugin/plugin.json +13 -0
  69. package/plugins/openhands/README.md +52 -0
  70. package/plugins/openhands/SKILL.md +61 -0
  71. package/plugins/openhands/commands/create.md +55 -0
  72. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  73. package/plugins/openhands/scripts/run.sh +69 -0
  74. package/plugins/pr-review/.plugin/plugin.json +13 -0
  75. package/plugins/pr-review/README.md +393 -0
  76. package/plugins/pr-review/action.yml +298 -0
  77. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  78. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  79. package/plugins/pr-review/scripts/prompt.py +260 -0
  80. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  81. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  82. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  83. package/plugins/qa-changes/README.md +185 -0
  84. package/plugins/qa-changes/action.yml +181 -0
  85. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  86. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  87. package/plugins/qa-changes/scripts/prompt.py +174 -0
  88. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  89. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  90. package/plugins/release-notes/.plugin/plugin.json +19 -0
  91. package/plugins/release-notes/README.md +283 -0
  92. package/plugins/release-notes/SKILL.md +83 -0
  93. package/plugins/release-notes/action.yml +117 -0
  94. package/plugins/release-notes/commands/release-notes.md +8 -0
  95. package/plugins/release-notes/scripts/agent_script.py +292 -0
  96. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  97. package/plugins/release-notes/scripts/prompt.py +90 -0
  98. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  99. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  100. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  101. package/plugins/vulnerability-remediation/README.md +217 -0
  102. package/plugins/vulnerability-remediation/action.yml +187 -0
  103. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  104. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  105. package/pyproject.toml +12 -0
  106. package/release-please-config.json +16 -0
  107. package/scripts/sync_extensions.py +494 -0
  108. package/scripts/sync_openhands_sdk_skill.py +264 -0
  109. package/skills/README.md +159 -0
  110. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/README.md +40 -0
  112. package/skills/add-javadoc/SKILL.md +35 -0
  113. package/skills/add-javadoc/references/example.md +32 -0
  114. package/skills/add-skill/.plugin/plugin.json +18 -0
  115. package/skills/add-skill/README.md +67 -0
  116. package/skills/add-skill/SKILL.md +47 -0
  117. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  118. package/skills/agent-creator/.plugin/plugin.json +20 -0
  119. package/skills/agent-creator/README.md +104 -0
  120. package/skills/agent-creator/SKILL.md +190 -0
  121. package/skills/agent-creator/commands/agent-creator.md +8 -0
  122. package/skills/agent-creator/references/fallback.md +117 -0
  123. package/skills/agent-memory/.plugin/plugin.json +18 -0
  124. package/skills/agent-memory/README.md +35 -0
  125. package/skills/agent-memory/SKILL.md +30 -0
  126. package/skills/agent-memory/commands/remember.md +8 -0
  127. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/README.md +40 -0
  129. package/skills/agent-sdk-builder/SKILL.md +37 -0
  130. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  131. package/skills/azure-devops/.plugin/plugin.json +18 -0
  132. package/skills/azure-devops/README.md +55 -0
  133. package/skills/azure-devops/SKILL.md +50 -0
  134. package/skills/bitbucket/.plugin/plugin.json +17 -0
  135. package/skills/bitbucket/README.md +50 -0
  136. package/skills/bitbucket/SKILL.md +45 -0
  137. package/skills/code-review/.plugin/plugin.json +19 -0
  138. package/skills/code-review/README.md +18 -0
  139. package/skills/code-review/SKILL.md +208 -0
  140. package/skills/code-review/commands/codereview-roasted.md +8 -0
  141. package/skills/code-review/commands/codereview.md +8 -0
  142. package/skills/code-review/references/risk-evaluation.md +41 -0
  143. package/skills/code-review/references/supply-chain-security.md +31 -0
  144. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  145. package/skills/code-simplifier/README.md +30 -0
  146. package/skills/code-simplifier/SKILL.md +91 -0
  147. package/skills/code-simplifier/commands/simplify.md +8 -0
  148. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  149. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  150. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  151. package/skills/datadog/.plugin/plugin.json +19 -0
  152. package/skills/datadog/README.md +100 -0
  153. package/skills/datadog/SKILL.md +95 -0
  154. package/skills/deno/.plugin/plugin.json +18 -0
  155. package/skills/deno/README.md +5 -0
  156. package/skills/deno/SKILL.md +99 -0
  157. package/skills/deno/references/README.md +6 -0
  158. package/skills/discord/.plugin/plugin.json +18 -0
  159. package/skills/discord/README.md +31 -0
  160. package/skills/discord/SKILL.md +109 -0
  161. package/skills/discord/__init__.py +0 -0
  162. package/skills/discord/references/REFERENCE.md +78 -0
  163. package/skills/discord/scripts/__init__.py +0 -0
  164. package/skills/discord/scripts/_http.py +127 -0
  165. package/skills/discord/scripts/post_webhook.py +106 -0
  166. package/skills/discord/scripts/send_message.py +102 -0
  167. package/skills/docker/.plugin/plugin.json +17 -0
  168. package/skills/docker/README.md +34 -0
  169. package/skills/docker/SKILL.md +29 -0
  170. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  171. package/skills/evidence-based-citations/README.md +31 -0
  172. package/skills/evidence-based-citations/SKILL.md +59 -0
  173. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  174. package/skills/flarglebargle/README.md +14 -0
  175. package/skills/flarglebargle/SKILL.md +9 -0
  176. package/skills/frontend-design/.plugin/plugin.json +21 -0
  177. package/skills/frontend-design/LICENSE.txt +177 -0
  178. package/skills/frontend-design/README.md +42 -0
  179. package/skills/frontend-design/SKILL.md +42 -0
  180. package/skills/github/.plugin/plugin.json +19 -0
  181. package/skills/github/README.md +42 -0
  182. package/skills/github/SKILL.md +106 -0
  183. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  184. package/skills/github-pr-review/README.md +145 -0
  185. package/skills/github-pr-review/SKILL.md +148 -0
  186. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  187. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  188. package/skills/github-pr-reviewer/README.md +34 -0
  189. package/skills/github-pr-reviewer/SKILL.md +89 -0
  190. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  191. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  192. package/skills/github-repo-monitor/README.md +70 -0
  193. package/skills/github-repo-monitor/SKILL.md +316 -0
  194. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  195. package/skills/github-repo-monitor/references/github-api.md +241 -0
  196. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  197. package/skills/github-repo-monitor/scripts/main.py +915 -0
  198. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  199. package/skills/gitlab/.plugin/plugin.json +17 -0
  200. package/skills/gitlab/README.md +37 -0
  201. package/skills/gitlab/SKILL.md +32 -0
  202. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  203. package/skills/incident-retrospective/README.md +34 -0
  204. package/skills/incident-retrospective/SKILL.md +98 -0
  205. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  206. package/skills/iterate/.plugin/plugin.json +13 -0
  207. package/skills/iterate/README.md +25 -0
  208. package/skills/iterate/SKILL.md +399 -0
  209. package/skills/iterate/commands/babysit.md +8 -0
  210. package/skills/iterate/commands/iterate.md +8 -0
  211. package/skills/iterate/commands/verify.md +8 -0
  212. package/skills/iterate/references/heuristics.md +58 -0
  213. package/skills/iterate/references/verification.md +96 -0
  214. package/skills/jupyter/.plugin/plugin.json +18 -0
  215. package/skills/jupyter/README.md +55 -0
  216. package/skills/jupyter/SKILL.md +50 -0
  217. package/skills/kubernetes/.plugin/plugin.json +18 -0
  218. package/skills/kubernetes/README.md +53 -0
  219. package/skills/kubernetes/SKILL.md +48 -0
  220. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  221. package/skills/learn-from-code-review/README.md +64 -0
  222. package/skills/learn-from-code-review/SKILL.md +186 -0
  223. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  224. package/skills/linear/.plugin/plugin.json +19 -0
  225. package/skills/linear/README.md +58 -0
  226. package/skills/linear/SKILL.md +213 -0
  227. package/skills/linear-triage/.plugin/plugin.json +21 -0
  228. package/skills/linear-triage/README.md +34 -0
  229. package/skills/linear-triage/SKILL.md +91 -0
  230. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  231. package/skills/notion/.plugin/plugin.json +17 -0
  232. package/skills/notion/README.md +114 -0
  233. package/skills/notion/SKILL.md +109 -0
  234. package/skills/npm/.plugin/plugin.json +17 -0
  235. package/skills/npm/README.md +14 -0
  236. package/skills/npm/SKILL.md +9 -0
  237. package/skills/openhands-api/.plugin/plugin.json +22 -0
  238. package/skills/openhands-api/README.md +48 -0
  239. package/skills/openhands-api/SKILL.md +399 -0
  240. package/skills/openhands-api/references/README.md +33 -0
  241. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  242. package/skills/openhands-api/references/example_prompt.md +12 -0
  243. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  244. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  245. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  246. package/skills/openhands-automation/README.md +89 -0
  247. package/skills/openhands-automation/SKILL.md +875 -0
  248. package/skills/openhands-automation/commands/automation:create.md +8 -0
  249. package/skills/openhands-automation/references/ab-testing.md +185 -0
  250. package/skills/openhands-automation/references/custom-automation.md +644 -0
  251. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  252. package/skills/openhands-sdk/README.md +22 -0
  253. package/skills/openhands-sdk/SKILL.md +229 -0
  254. package/skills/openhands-sdk/commands/sdk.md +8 -0
  255. package/skills/pdflatex/.plugin/plugin.json +18 -0
  256. package/skills/pdflatex/README.md +39 -0
  257. package/skills/pdflatex/SKILL.md +34 -0
  258. package/skills/prd/.plugin/plugin.json +19 -0
  259. package/skills/prd/README.md +28 -0
  260. package/skills/prd/SKILL.md +237 -0
  261. package/skills/prd/commands/prd.md +8 -0
  262. package/skills/qa-changes/README.md +18 -0
  263. package/skills/qa-changes/SKILL.md +229 -0
  264. package/skills/qa-changes/commands/qa-changes.md +8 -0
  265. package/skills/release-notes/README.md +24 -0
  266. package/skills/release-notes/SKILL.md +19 -0
  267. package/skills/release-notes/commands/release-notes.md +8 -0
  268. package/skills/research-brief/.plugin/plugin.json +20 -0
  269. package/skills/research-brief/README.md +34 -0
  270. package/skills/research-brief/SKILL.md +99 -0
  271. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  272. package/skills/security/.plugin/plugin.json +18 -0
  273. package/skills/security/README.md +38 -0
  274. package/skills/security/SKILL.md +33 -0
  275. package/skills/skill-creator/.plugin/plugin.json +17 -0
  276. package/skills/skill-creator/LICENSE.txt +202 -0
  277. package/skills/skill-creator/README.md +182 -0
  278. package/skills/skill-creator/SKILL.md +545 -0
  279. package/skills/skill-creator/references/output-patterns.md +82 -0
  280. package/skills/skill-creator/references/workflows.md +28 -0
  281. package/skills/skill-creator/scripts/init_skill.py +303 -0
  282. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  283. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  284. package/skills/slack-channel-monitor/README.md +91 -0
  285. package/skills/slack-channel-monitor/SKILL.md +276 -0
  286. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  287. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  288. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  289. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  290. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  291. package/skills/slack-standup-digest/README.md +34 -0
  292. package/skills/slack-standup-digest/SKILL.md +92 -0
  293. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  294. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  295. package/skills/spark-version-upgrade/README.md +54 -0
  296. package/skills/spark-version-upgrade/SKILL.md +233 -0
  297. package/skills/ssh/.plugin/plugin.json +18 -0
  298. package/skills/ssh/README.md +140 -0
  299. package/skills/ssh/SKILL.md +135 -0
  300. package/skills/swift-linux/.plugin/plugin.json +17 -0
  301. package/skills/swift-linux/README.md +86 -0
  302. package/skills/swift-linux/SKILL.md +81 -0
  303. package/skills/theme-factory/.plugin/plugin.json +19 -0
  304. package/skills/theme-factory/LICENSE.txt +202 -0
  305. package/skills/theme-factory/README.md +58 -0
  306. package/skills/theme-factory/SKILL.md +59 -0
  307. package/skills/theme-factory/theme-showcase.pdf +0 -0
  308. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  309. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  310. package/skills/theme-factory/themes/desert-rose.md +19 -0
  311. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  312. package/skills/theme-factory/themes/golden-hour.md +19 -0
  313. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  314. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  315. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  316. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  317. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  318. package/skills/uv/.plugin/plugin.json +18 -0
  319. package/skills/uv/README.md +5 -0
  320. package/skills/uv/SKILL.md +95 -0
  321. package/skills/uv/references/README.md +5 -0
  322. package/skills/vercel/.plugin/plugin.json +18 -0
  323. package/skills/vercel/README.md +108 -0
  324. package/skills/vercel/SKILL.md +103 -0
  325. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  326. package/tests/test_catalogs.py +109 -0
  327. package/tests/test_code_review_risk_evaluation.py +94 -0
  328. package/tests/test_issue_duplicate_checker.py +240 -0
  329. package/tests/test_openhands_api_python.py +152 -0
  330. package/tests/test_plugin_manifest.py +83 -0
  331. package/tests/test_pr_review_diff_payload.py +202 -0
  332. package/tests/test_pr_review_feedback.py +263 -0
  333. package/tests/test_pr_review_prompt.py +152 -0
  334. package/tests/test_pr_review_review_context.py +253 -0
  335. package/tests/test_qa_changes.py +232 -0
  336. package/tests/test_qa_changes_evaluation.py +259 -0
  337. package/tests/test_release_notes_generator.py +990 -0
  338. package/tests/test_sdk_loading.py +150 -0
  339. package/tests/test_skill_plugin_loading.py +149 -0
  340. package/tests/test_skills_have_readme.py +66 -0
  341. package/tests/test_sync_extensions.py +292 -0
  342. package/tests/test_workflow_sync.py +46 -0
  343. package/utils/analysis/README.md +7 -0
  344. package/utils/analysis/laminar_signals/README.md +211 -0
  345. package/utils/analysis/laminar_signals/analyze.py +780 -0
  346. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  347. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,606 @@
1
+ """OpenHands Cloud API (V1) minimal client.
2
+
3
+ This file is intentionally:
4
+ - small (easy to copy into other repos)
5
+ - dependency-light (only `httpx`)
6
+ - opinionated in a helpful way (defaults to OpenHands Cloud)
7
+
8
+ Audience: AI agents.
9
+
10
+ The V1 API is hosted on the OpenHands app server under:
11
+ {BASE_URL}/api/v1/...
12
+
13
+ Typical workflow for common operations:
14
+ 1) Discover: GET /api/v1/users/me
15
+ 2) List/search conversations: GET /api/v1/app-conversations/search
16
+ 3) Start a conversation (creates sandbox): POST /api/v1/app-conversations
17
+ 4) Monitor events for a conversation: GET /api/v1/conversation/{id}/events/search
18
+ 5) (Optional) download trajectory: GET /api/v1/app-conversations/{id}/download
19
+
20
+ Note: Some operations happen against the *agent server* running inside a sandbox
21
+ (not the app server). Those endpoints use X-Session-API-Key instead of Bearer auth.
22
+
23
+ This client purposefully keeps responses as raw dicts/lists so agents can quickly
24
+ adapt it without strict schema maintenance.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import time
33
+ import zipfile
34
+ from dataclasses import dataclass
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ import httpx
39
+
40
+
41
+ DEFAULT_BASE_URL = "https://app.all-hands.dev"
42
+ PREFERRED_API_KEY_ENV_VARS = ("OPENHANDS_CLOUD_API_KEY", "OPENHANDS_API_KEY")
43
+
44
+
45
+ # Start-task statuses observed in the wild. These may evolve, so keep this centralized.
46
+ START_TASK_TERMINAL_STATUSES = frozenset(
47
+ {"READY", "ERROR", "FAILED", "CANCELLED", "DONE", "COMPLETED"}
48
+ )
49
+
50
+
51
+ # Safety cap for paging calls. Keeps responses small and consistent across clients.
52
+ AGENT_EVENTS_SEARCH_MAX_LIMIT = 100
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class OpenHandsAPIConfig:
57
+ api_key: str
58
+ base_url: str = DEFAULT_BASE_URL
59
+
60
+ @property
61
+ def api_v1_url(self) -> str:
62
+ return f"{self.base_url.rstrip('/')}/api/v1"
63
+
64
+
65
+ class OpenHandsAPI:
66
+ """Minimal OpenHands Cloud API client for the supported V1 API."""
67
+
68
+ def __init__(self, api_key: str | None = None, base_url: str = DEFAULT_BASE_URL):
69
+ resolved_key = api_key
70
+ if not resolved_key:
71
+ for env_name in PREFERRED_API_KEY_ENV_VARS:
72
+ resolved_key = os.getenv(env_name)
73
+ if resolved_key:
74
+ break
75
+ if not resolved_key:
76
+ env_list = ", ".join(PREFERRED_API_KEY_ENV_VARS)
77
+ raise ValueError(f"Missing API key. Set one of: {env_list}, or pass api_key=...")
78
+
79
+ self._cfg = OpenHandsAPIConfig(api_key=resolved_key, base_url=base_url.rstrip("/"))
80
+ self._client = httpx.Client(
81
+ headers={
82
+ "Authorization": f"Bearer {self._cfg.api_key}",
83
+ "Content-Type": "application/json",
84
+ },
85
+ timeout=30,
86
+ )
87
+
88
+ @property
89
+ def base_url(self) -> str:
90
+ return self._cfg.base_url
91
+
92
+ @property
93
+ def api_v1_url(self) -> str:
94
+ return self._cfg.api_v1_url
95
+
96
+ def close(self) -> None:
97
+ self._client.close()
98
+
99
+ # -----------------------------
100
+ # App server endpoints (Bearer auth)
101
+ # -----------------------------
102
+
103
+ def users_me(self) -> dict[str, Any]:
104
+ r = self._client.get(f"{self.api_v1_url}/users/me")
105
+ r.raise_for_status()
106
+ return r.json()
107
+
108
+ def app_conversations_search(self, *, limit: int = 20) -> dict[str, Any]:
109
+ limit = max(1, int(limit))
110
+ r = self._client.get(
111
+ f"{self.api_v1_url}/app-conversations/search", params={"limit": limit}
112
+ )
113
+ r.raise_for_status()
114
+ return r.json()
115
+
116
+ def app_conversations_count(self) -> dict[str, Any]:
117
+ r = self._client.get(f"{self.api_v1_url}/app-conversations/count")
118
+ r.raise_for_status()
119
+ return r.json()
120
+
121
+ def app_conversations_get_batch(self, *, ids: list[str]) -> list[dict[str, Any]]:
122
+ if not ids:
123
+ return []
124
+ r = self._client.get(f"{self.api_v1_url}/app-conversations", params={"ids": ids})
125
+ r.raise_for_status()
126
+ return r.json()
127
+
128
+ def app_conversation_get(self, conversation_id: str) -> dict[str, Any] | None:
129
+ items = self.app_conversations_get_batch(ids=[conversation_id])
130
+ return items[0] if items else None
131
+
132
+ def sandboxes_search(self, *, limit: int = 20) -> dict[str, Any]:
133
+ limit = max(1, int(limit))
134
+ r = self._client.get(f"{self.api_v1_url}/sandboxes/search", params={"limit": limit})
135
+ r.raise_for_status()
136
+ return r.json()
137
+
138
+ def sandbox_specs_search(self, *, limit: int = 20) -> dict[str, Any]:
139
+ limit = max(1, int(limit))
140
+ r = self._client.get(
141
+ f"{self.api_v1_url}/sandbox-specs/search", params={"limit": limit}
142
+ )
143
+ r.raise_for_status()
144
+ return r.json()
145
+
146
+ def conversation_events_search(
147
+ self, conversation_id: str, *, limit: int = 50
148
+ ) -> dict[str, Any]:
149
+ limit = max(1, int(limit))
150
+ r = self._client.get(
151
+ f"{self.api_v1_url}/conversation/{conversation_id}/events/search",
152
+ params={"limit": limit},
153
+ )
154
+ r.raise_for_status()
155
+ return r.json()
156
+
157
+ def conversation_events_count(self, conversation_id: str) -> dict[str, Any]:
158
+ r = self._client.get(f"{self.api_v1_url}/conversation/{conversation_id}/events/count")
159
+ r.raise_for_status()
160
+ return r.json()
161
+
162
+ def app_conversation_start(
163
+ self,
164
+ *,
165
+ initial_message: str,
166
+ selected_repository: str | None = None,
167
+ selected_branch: str | None = None,
168
+ title: str | None = None,
169
+ run: bool = True,
170
+ ) -> dict[str, Any]:
171
+ """Start a new V1 app conversation.
172
+
173
+ WARNING: This typically creates a sandbox and may incur costs.
174
+
175
+ In many deployments this endpoint is **asynchronous** and returns a **start-task** dict.
176
+ Common fields:
177
+ - `id`: the *start_task_id*
178
+ - `app_conversation_id`: the id to use for `/download` and `/conversation/.../events/...`
179
+
180
+ If `app_conversation_id` is missing from the initial response, fetch it via:
181
+ - `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`
182
+ (see `app_conversation_start_task_get()` / `poll_start_task_until_ready()`).
183
+
184
+ The payload structure here mirrors what the V1 app server expects:
185
+ - initial_message.content is a list of content parts
186
+ """
187
+
188
+ payload: dict[str, Any] = {
189
+ "initial_message": {
190
+ "role": "user",
191
+ # V1 expects `content` as a list of parts, even for a single text message.
192
+ "content": [{"type": "text", "text": initial_message}],
193
+ "run": bool(run),
194
+ }
195
+ }
196
+ if selected_repository:
197
+ payload["selected_repository"] = selected_repository
198
+ if selected_branch:
199
+ payload["selected_branch"] = selected_branch
200
+ if title:
201
+ payload["title"] = title
202
+
203
+ r = self._client.post(f"{self.api_v1_url}/app-conversations", json=payload, timeout=120)
204
+ r.raise_for_status()
205
+ return r.json()
206
+
207
+ def app_conversations_start_tasks_get_batch(self, *, ids: list[str]) -> list[dict[str, Any]]:
208
+ if not ids:
209
+ return []
210
+ r = self._client.get(
211
+ f"{self.api_v1_url}/app-conversations/start-tasks", params={"ids": ids}
212
+ )
213
+ r.raise_for_status()
214
+ return r.json()
215
+
216
+ def app_conversation_start_task_get(self, task_id: str) -> dict[str, Any] | None:
217
+ items = self.app_conversations_start_tasks_get_batch(ids=[task_id])
218
+ return items[0] if items else None
219
+
220
+ def sandboxes_pause(self, sandbox_id: str) -> dict[str, Any]:
221
+ r = self._client.post(f"{self.api_v1_url}/sandboxes/{sandbox_id}/pause", timeout=60)
222
+ r.raise_for_status()
223
+ return r.json()
224
+
225
+ def sandboxes_resume(self, sandbox_id: str) -> dict[str, Any]:
226
+ r = self._client.post(f"{self.api_v1_url}/sandboxes/{sandbox_id}/resume", timeout=60)
227
+ r.raise_for_status()
228
+ return r.json()
229
+
230
+ def app_conversation_download_zip(
231
+ self, app_conversation_id: str, *, output_file: str | Path
232
+ ) -> dict[str, Any]:
233
+ """Download a conversation trajectory zip to disk.
234
+
235
+ Note: this endpoint expects the **app_conversation_id** (not the start-task id).
236
+ """
237
+ url = f"{self.api_v1_url}/app-conversations/{app_conversation_id}/download"
238
+ r = self._client.get(url, timeout=60)
239
+ r.raise_for_status()
240
+ out = Path(output_file)
241
+ out.write_bytes(r.content)
242
+ return {
243
+ "file": str(out),
244
+ "size": len(r.content),
245
+ "content_type": r.headers.get("content-type"),
246
+ }
247
+
248
+ def count_events_via_trajectory_zip(
249
+ self,
250
+ app_conversation_id: str,
251
+ *,
252
+ zip_file: str | Path,
253
+ extract_dir: str | Path,
254
+ ) -> dict[str, Any]:
255
+ """Fallback event counting: download trajectory zip, extract, count event files.
256
+
257
+ This is heavier than calling a count endpoint, but it is still a single API call and
258
+ also gives you the full exported event payloads.
259
+
260
+ Cleanup (optional): this helper writes a zip file and extracts JSON events. If you
261
+ want to clean up afterwards, you can remove them, e.g.:
262
+
263
+ - `zip_path.unlink(missing_ok=True)`
264
+ - `shutil.rmtree(extract_path, ignore_errors=True)`
265
+
266
+ Returns a small summary dict including `event_count`.
267
+ """
268
+
269
+ zip_path = Path(zip_file)
270
+ extract_path = Path(extract_dir)
271
+
272
+ download_meta = self.app_conversation_download_zip(app_conversation_id, output_file=zip_path)
273
+ extract_path.mkdir(parents=True, exist_ok=True)
274
+
275
+ with zipfile.ZipFile(zip_path, "r") as zf:
276
+ zf.extractall(extract_path)
277
+
278
+ event_count = len(list(extract_path.glob("event_*.json")))
279
+ has_meta = (extract_path / "meta.json").exists()
280
+
281
+ return {
282
+ "event_count": event_count,
283
+ "has_meta": has_meta,
284
+ "zip": download_meta,
285
+ "extract_dir": str(extract_path),
286
+ }
287
+
288
+ # -----------------------------
289
+ # Agent server endpoints (X-Session-API-Key)
290
+ # -----------------------------
291
+
292
+ @staticmethod
293
+ def agent_headers(session_api_key: str) -> dict[str, str]:
294
+ return {"X-Session-API-Key": session_api_key, "Content-Type": "application/json"}
295
+
296
+
297
+ @staticmethod
298
+ def _agent_event_filter_params(
299
+ *,
300
+ timestamp_gte: str | None = None,
301
+ timestamp_lt: str | None = None,
302
+ kind: str | None = None,
303
+ source: str | None = None,
304
+ body: str | None = None,
305
+ ) -> dict[str, Any]:
306
+ params: dict[str, Any] = {}
307
+ if timestamp_gte is not None:
308
+ params["timestamp__gte"] = timestamp_gte
309
+ if timestamp_lt is not None:
310
+ params["timestamp__lt"] = timestamp_lt
311
+ if kind is not None:
312
+ params["kind"] = kind
313
+ if source is not None:
314
+ params["source"] = source
315
+ if body is not None:
316
+ params["body"] = body
317
+ return params
318
+
319
+ def agent_events_search(
320
+ self,
321
+ *,
322
+ agent_server_url: str,
323
+ session_api_key: str,
324
+ conversation_id: str,
325
+ limit: int = 50,
326
+ sort_order: str | None = None,
327
+ timestamp_gte: str | None = None,
328
+ timestamp_lt: str | None = None,
329
+ kind: str | None = None,
330
+ source: str | None = None,
331
+ body: str | None = None,
332
+ ) -> dict[str, Any]:
333
+ """Search events via the sandbox agent-server.
334
+
335
+ Notes:
336
+ - `limit` is capped at AGENT_EVENTS_SEARCH_MAX_LIMIT to avoid huge responses.
337
+ - `sort_order` must be one of: "TIMESTAMP", "TIMESTAMP_DESC".
338
+ - timestamp filters are passed as ISO-8601 strings (e.g. "2026-02-14T21:54:00Z").
339
+ The server accepts both timezone-aware and naive datetimes.
340
+ """
341
+
342
+ url = f"{agent_server_url.rstrip('/')}/api/conversations/{conversation_id}/events/search"
343
+ capped_limit = min(AGENT_EVENTS_SEARCH_MAX_LIMIT, max(1, int(limit)))
344
+ params: dict[str, Any] = {"limit": capped_limit}
345
+ if sort_order is not None:
346
+ params["sort_order"] = sort_order
347
+ params.update(
348
+ self._agent_event_filter_params(
349
+ timestamp_gte=timestamp_gte,
350
+ timestamp_lt=timestamp_lt,
351
+ kind=kind,
352
+ source=source,
353
+ body=body,
354
+ )
355
+ )
356
+
357
+ r = httpx.get(
358
+ url,
359
+ headers=self.agent_headers(session_api_key),
360
+ params=params,
361
+ timeout=30,
362
+ )
363
+ r.raise_for_status()
364
+ return r.json()
365
+
366
+ def agent_events_count(
367
+ self,
368
+ *,
369
+ agent_server_url: str,
370
+ session_api_key: str,
371
+ conversation_id: str,
372
+ timestamp_gte: str | None = None,
373
+ timestamp_lt: str | None = None,
374
+ kind: str | None = None,
375
+ source: str | None = None,
376
+ body: str | None = None,
377
+ ) -> int:
378
+ """Count events via the sandbox agent-server.
379
+
380
+ Timestamp filters are passed as ISO-8601 strings (e.g. "2026-02-14T21:54:00Z").
381
+ """
382
+
383
+ url = f"{agent_server_url.rstrip('/')}/api/conversations/{conversation_id}/events/count"
384
+ params = self._agent_event_filter_params(
385
+ timestamp_gte=timestamp_gte,
386
+ timestamp_lt=timestamp_lt,
387
+ kind=kind,
388
+ source=source,
389
+ body=body,
390
+ )
391
+
392
+ r = httpx.get(
393
+ url,
394
+ headers=self.agent_headers(session_api_key),
395
+ params=params,
396
+ timeout=30,
397
+ )
398
+ r.raise_for_status()
399
+ return int(r.json())
400
+
401
+ def agent_execute_bash(
402
+ self,
403
+ *,
404
+ agent_server_url: str,
405
+ session_api_key: str,
406
+ command: str,
407
+ cwd: str | None = None,
408
+ timeout_s: int = 30,
409
+ ) -> dict[str, Any]:
410
+ url = f"{agent_server_url.rstrip('/')}/api/bash/execute_bash_command"
411
+ payload: dict[str, Any] = {"command": command, "timeout": int(timeout_s)}
412
+ if cwd:
413
+ payload["cwd"] = cwd
414
+ r = httpx.post(url, headers=self.agent_headers(session_api_key), json=payload, timeout=60)
415
+ r.raise_for_status()
416
+ return r.json()
417
+
418
+ def agent_download_file(
419
+ self,
420
+ *,
421
+ agent_server_url: str,
422
+ session_api_key: str,
423
+ path: str,
424
+ output_file: str | Path,
425
+ ) -> dict[str, Any]:
426
+ p = path if path.startswith("/") else f"/{path}"
427
+ url = f"{agent_server_url.rstrip('/')}/api/file/download{p}"
428
+ r = httpx.get(url, headers=self.agent_headers(session_api_key), timeout=30)
429
+ r.raise_for_status()
430
+ out = Path(output_file)
431
+ out.write_bytes(r.content)
432
+ return {"file": str(out), "size": len(r.content)}
433
+
434
+ def agent_upload_text_file(
435
+ self,
436
+ *,
437
+ agent_server_url: str,
438
+ session_api_key: str,
439
+ path: str,
440
+ content: str,
441
+ content_type: str = "text/plain",
442
+ ) -> dict[str, Any]:
443
+ p = path if path.startswith("/") else f"/{path}"
444
+ url = f"{agent_server_url.rstrip('/')}/api/file/upload{p}"
445
+ filename = os.path.basename(p)
446
+ headers = {"X-Session-API-Key": session_api_key}
447
+ files = {"file": (filename, content.encode("utf-8"), content_type)}
448
+ r = httpx.post(url, headers=headers, files=files, timeout=30)
449
+ r.raise_for_status()
450
+ return r.json() if r.text else {"success": True}
451
+
452
+ # -----------------------------
453
+ # Convenience helpers
454
+ # -----------------------------
455
+
456
+ def app_conversation_start_from_prompt_files(
457
+ self,
458
+ prompt_file: str | Path,
459
+ *,
460
+ selected_repository: str | None = None,
461
+ selected_branch: str | None = None,
462
+ title: str | None = None,
463
+ append_file: str | Path | None = None,
464
+ run: bool = True,
465
+ ) -> dict[str, Any]:
466
+ main_text = Path(prompt_file).read_text(encoding="utf-8")
467
+ if append_file and Path(append_file).exists():
468
+ tail = Path(append_file).read_text(encoding="utf-8")
469
+ initial = f"{main_text}\n\n{tail}"
470
+ else:
471
+ initial = main_text
472
+
473
+ return self.app_conversation_start(
474
+ initial_message=initial,
475
+ selected_repository=selected_repository,
476
+ selected_branch=selected_branch,
477
+ title=title,
478
+ run=run,
479
+ )
480
+
481
+
482
+ @staticmethod
483
+ def _start_task_status(task: dict[str, Any] | None) -> str:
484
+ return str((task or {}).get("status") or "").upper()
485
+
486
+ def poll_start_task_until_ready(
487
+ self,
488
+ task_id: str,
489
+ *,
490
+ timeout_s: int = 10 * 60,
491
+ poll_interval_s: float = 2.0,
492
+ backoff_factor: float = 1.5,
493
+ max_interval_s: float = 10.0,
494
+ max_polls: int | None = None,
495
+ ) -> dict[str, Any]:
496
+ """Poll a start-task until it reaches a terminal state.
497
+
498
+ This is the async companion to `POST /api/v1/app-conversations`.
499
+
500
+ It is intentionally *polite*:
501
+ - sleeps between requests
502
+ - uses exponential backoff (capped by `max_interval_s`)
503
+ - supports `max_polls` to cap the total number of API calls
504
+
505
+ Terminal statuses are defined in START_TASK_TERMINAL_STATUSES.
506
+
507
+ Raises:
508
+ TimeoutError: if the task doesn't reach a terminal state in time.
509
+ """
510
+
511
+ deadline = time.monotonic() + float(timeout_s)
512
+ interval = max(0.25, float(poll_interval_s))
513
+ factor = max(1.0, float(backoff_factor))
514
+ max_interval = max(interval, float(max_interval_s))
515
+
516
+ polls = 0
517
+ last: dict[str, Any] | None = None
518
+
519
+ while True:
520
+ if max_polls is not None and polls >= int(max_polls):
521
+ raise TimeoutError(
522
+ f"Start task {task_id} did not reach terminal state (max_polls={max_polls}, last={last})"
523
+ )
524
+
525
+ remaining = deadline - time.monotonic()
526
+ if remaining <= 0:
527
+ raise TimeoutError(
528
+ f"Start task {task_id} did not reach terminal state in {timeout_s}s (last={last})"
529
+ )
530
+
531
+ last = self.app_conversation_start_task_get(task_id)
532
+ polls += 1
533
+
534
+ status = self._start_task_status(last)
535
+ if status in START_TASK_TERMINAL_STATUSES:
536
+ return last or {}
537
+
538
+ sleep_s = min(interval, remaining)
539
+ if sleep_s > 0:
540
+ time.sleep(sleep_s)
541
+ interval = min(max_interval, interval * factor)
542
+
543
+
544
+ OpenHandsV1API = OpenHandsAPI
545
+
546
+ def _cmd_search_conversations(args: argparse.Namespace) -> int:
547
+ api = OpenHandsAPI(api_key=args.api_key, base_url=args.base_url)
548
+ try:
549
+ print(json.dumps(api.app_conversations_search(limit=args.limit), indent=2))
550
+ return 0
551
+ finally:
552
+ api.close()
553
+
554
+
555
+ def _cmd_start_conversation(args: argparse.Namespace) -> int:
556
+ api = OpenHandsAPI(api_key=args.api_key, base_url=args.base_url)
557
+ try:
558
+ resp = api.app_conversation_start_from_prompt_files(
559
+ args.prompt_file,
560
+ selected_repository=args.repo,
561
+ selected_branch=args.branch,
562
+ title=args.title,
563
+ append_file=args.append_file,
564
+ run=not args.no_run,
565
+ )
566
+ print(json.dumps(resp, indent=2))
567
+ return 0
568
+ finally:
569
+ api.close()
570
+
571
+
572
+ def main(argv: list[str] | None = None) -> int:
573
+ parser = argparse.ArgumentParser(prog="openhands_api.py")
574
+ sub = parser.add_subparsers(dest="cmd", required=True)
575
+
576
+ p_search = sub.add_parser("search-conversations", help="GET /api/v1/app-conversations/search")
577
+ p_search.add_argument(
578
+ "--api-key",
579
+ default=None,
580
+ help="Defaults to OPENHANDS_CLOUD_API_KEY, then OPENHANDS_API_KEY",
581
+ )
582
+ p_search.add_argument("--base-url", default=DEFAULT_BASE_URL)
583
+ p_search.add_argument("--limit", type=int, default=5)
584
+ p_search.set_defaults(func=_cmd_search_conversations)
585
+
586
+ p_start = sub.add_parser("start-conversation", help="POST /api/v1/app-conversations from a prompt file")
587
+ p_start.add_argument(
588
+ "--api-key",
589
+ default=None,
590
+ help="Defaults to OPENHANDS_CLOUD_API_KEY, then OPENHANDS_API_KEY",
591
+ )
592
+ p_start.add_argument("--base-url", default=DEFAULT_BASE_URL)
593
+ p_start.add_argument("--prompt-file", required=True)
594
+ p_start.add_argument("--append-file", default=None)
595
+ p_start.add_argument("--repo", default=None)
596
+ p_start.add_argument("--branch", default=None)
597
+ p_start.add_argument("--title", default=None)
598
+ p_start.add_argument("--no-run", action="store_true", help="If set, do not auto-run after sending initial message")
599
+ p_start.set_defaults(func=_cmd_start_conversation)
600
+
601
+ args = parser.parse_args(argv)
602
+ return int(args.func(args))
603
+
604
+
605
+ if __name__ == "__main__":
606
+ raise SystemExit(main())