@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,780 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Laminar Signal Analysis Script
4
+
5
+ Downloads signal events from Laminar and uses an LLM to analyze patterns.
6
+ Supports customizable prompts via Jinja templates for different use cases.
7
+ Uses function calling to get structured output with trace IDs.
8
+
9
+ Usage:
10
+ python analyze.py --signal "pr review suggestion and analysis"
11
+ python analyze.py --signal "my-signal" --prompt-file custom_prompt.j2
12
+ python analyze.py --signal "my-signal" --days 30 --format json
13
+
14
+ Environment Variables:
15
+ LMNR_PROJECT_API_KEY: Laminar project API key (required)
16
+ LLM_API_KEY: API key for the LLM (required)
17
+ LLM_MODEL: Model to use (default: gemini-3-pro-preview)
18
+ LLM_BASE_URL: Base URL for LLM API (default: https://llm-proxy.app.all-hands.dev)
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import urllib.request
26
+ import urllib.error
27
+ from pathlib import Path
28
+
29
+ from jinja2 import Template
30
+
31
+
32
+ LAMINAR_API_URL = "https://api.lmnr.ai/v1/sql/query"
33
+ LAMINAR_APP_URL = "https://laminar.sh"
34
+ DEFAULT_LLM_BASE_URL = "https://llm-proxy.app.all-hands.dev"
35
+ DEFAULT_LLM_MODEL = "gemini-3-pro-preview"
36
+ DEFAULT_DAYS_LOOKBACK = 90
37
+
38
+ # Directory containing this script
39
+ SCRIPT_DIR = Path(__file__).parent
40
+ TEMPLATES_DIR = SCRIPT_DIR / "templates"
41
+
42
+ # Mapping of signal names to their template files
43
+ BUILTIN_TEMPLATES = {
44
+ "pr review suggestion and analysis": "pr_review.j2",
45
+ }
46
+
47
+
48
+ def load_skill_content(skill_dir: str) -> str:
49
+ """Load skill/plugin content from a directory.
50
+
51
+ Looks for SKILL.md files in the directory and any subdirectories.
52
+ For plugins, also looks in skills/ subdirectory and scripts/prompt.py.
53
+
54
+ Args:
55
+ skill_dir: Path to skill or plugin directory
56
+
57
+ Returns:
58
+ Combined content of all skill files found
59
+ """
60
+ skill_path = Path(skill_dir)
61
+ if not skill_path.exists():
62
+ raise FileNotFoundError(f"Skill directory not found: {skill_dir}")
63
+
64
+ content_parts = []
65
+
66
+ # Load main SKILL.md if present
67
+ main_skill = skill_path / "SKILL.md"
68
+ if main_skill.exists():
69
+ content_parts.append(f"## Skill: {skill_path.name}\n\n{main_skill.read_text()}")
70
+
71
+ # For plugins, check for prompt.py
72
+ prompt_py = skill_path / "scripts" / "prompt.py"
73
+ if prompt_py.exists():
74
+ content_parts.append(f"## Prompt Template ({prompt_py.name})\n\n```python\n{prompt_py.read_text()}\n```")
75
+
76
+ # Check for nested skills (common in plugins)
77
+ skills_subdir = skill_path / "skills"
78
+ if skills_subdir.exists():
79
+ for nested_skill_dir in skills_subdir.iterdir():
80
+ if nested_skill_dir.is_dir():
81
+ nested_skill_md = nested_skill_dir / "SKILL.md"
82
+ if nested_skill_md.exists():
83
+ content_parts.append(
84
+ f"## Nested Skill: {nested_skill_dir.name}\n\n{nested_skill_md.read_text()}"
85
+ )
86
+
87
+ if not content_parts:
88
+ raise FileNotFoundError(f"No SKILL.md or prompt.py found in: {skill_dir}")
89
+
90
+ return "\n\n---\n\n".join(content_parts)
91
+
92
+ # JSON Schema for the analysis function call output
93
+ ANALYSIS_FUNCTION = {
94
+ "name": "report_analysis",
95
+ "description": "Report the analysis of signal events, focusing on issues and areas for improvement",
96
+ "parameters": {
97
+ "type": "object",
98
+ "properties": {
99
+ "issues": {
100
+ "type": "array",
101
+ "description": "List of issues, problems, and areas needing improvement (THIS IS THE PRIMARY FOCUS)",
102
+ "items": {
103
+ "type": "object",
104
+ "properties": {
105
+ "title": {
106
+ "type": "string",
107
+ "description": "Short title for this issue"
108
+ },
109
+ "description": {
110
+ "type": "string",
111
+ "description": "Detailed description of the issue, why it's problematic, and its impact"
112
+ },
113
+ "severity": {
114
+ "type": "string",
115
+ "enum": ["critical", "high", "medium", "low"],
116
+ "description": "How severe/impactful this issue is"
117
+ },
118
+ "frequency": {
119
+ "type": "string",
120
+ "description": "How often this issue occurs (e.g., '15% of traces', 'frequent', 'occasional')"
121
+ },
122
+ "trace_urls": {
123
+ "type": "array",
124
+ "description": "Up to 5 representative trace URLs demonstrating this issue",
125
+ "items": {"type": "string"},
126
+ "maxItems": 5
127
+ }
128
+ },
129
+ "required": ["title", "description", "severity", "trace_urls"]
130
+ }
131
+ },
132
+ "recommendations": {
133
+ "type": "array",
134
+ "description": "Specific, actionable recommendations with CONCRETE implementation details (e.g., exact prompt changes, code snippets)",
135
+ "items": {
136
+ "type": "object",
137
+ "properties": {
138
+ "title": {
139
+ "type": "string",
140
+ "description": "Short title for this recommendation"
141
+ },
142
+ "description": {
143
+ "type": "string",
144
+ "description": "Detailed description with SPECIFIC implementation. Include exact prompt text changes, code modifications, or configuration updates. Use 'Change X to Y' format where possible."
145
+ },
146
+ "prompt_changes": {
147
+ "type": "array",
148
+ "description": "Specific prompt/instruction changes. Each item should have 'before' (current text) and 'after' (proposed text)",
149
+ "items": {
150
+ "type": "object",
151
+ "properties": {
152
+ "section": {"type": "string", "description": "Which section of the prompt to modify"},
153
+ "before": {"type": "string", "description": "Current text (or 'N/A' if adding new)"},
154
+ "after": {"type": "string", "description": "Proposed new text"}
155
+ },
156
+ "required": ["section", "after"]
157
+ }
158
+ },
159
+ "addresses": {
160
+ "type": "array",
161
+ "description": "Which issues this recommendation fixes",
162
+ "items": {"type": "string"}
163
+ },
164
+ "priority": {
165
+ "type": "string",
166
+ "enum": ["high", "medium", "low"],
167
+ "description": "Priority for implementing this recommendation"
168
+ }
169
+ },
170
+ "required": ["title", "description", "addresses", "priority"]
171
+ }
172
+ },
173
+ "strengths": {
174
+ "type": "array",
175
+ "description": "Brief list of things working well (keep this short, focus is on improvements)",
176
+ "items": {
177
+ "type": "object",
178
+ "properties": {
179
+ "title": {
180
+ "type": "string",
181
+ "description": "Short title"
182
+ },
183
+ "description": {
184
+ "type": "string",
185
+ "description": "Brief description of what's working well"
186
+ }
187
+ },
188
+ "required": ["title", "description"]
189
+ }
190
+ },
191
+ "metrics": {
192
+ "type": "object",
193
+ "description": "Quantitative metrics from the analysis",
194
+ "properties": {
195
+ "total_signals": {
196
+ "type": "integer",
197
+ "description": "Total number of signals analyzed"
198
+ },
199
+ "issue_rate": {
200
+ "type": "string",
201
+ "description": "Percentage of signals showing issues"
202
+ },
203
+ "key_statistics": {
204
+ "type": "array",
205
+ "description": "Other relevant statistics",
206
+ "items": {
207
+ "type": "object",
208
+ "properties": {
209
+ "name": {"type": "string"},
210
+ "value": {"type": "string"}
211
+ },
212
+ "required": ["name", "value"]
213
+ }
214
+ }
215
+ },
216
+ "required": ["total_signals"]
217
+ },
218
+ "summary": {
219
+ "type": "string",
220
+ "description": "Executive summary focusing on the most critical improvements needed"
221
+ }
222
+ },
223
+ "required": ["issues", "recommendations", "metrics", "summary"]
224
+ }
225
+ }
226
+
227
+
228
+ def get_env_var(name: str, required: bool = True) -> str | None:
229
+ """Get an environment variable.
230
+
231
+ Args:
232
+ name: Environment variable name
233
+ required: If True, raise error if not set. If False, return None.
234
+ """
235
+ value = os.getenv(name)
236
+ if not value and required:
237
+ raise ValueError(f"{name} environment variable is required")
238
+ return value
239
+
240
+
241
+ def get_llm_config() -> tuple[str, str, str]:
242
+ """Get LLM configuration from environment variables.
243
+
244
+ Returns:
245
+ Tuple of (api_key, model, base_url)
246
+ """
247
+ api_key = get_env_var("LLM_API_KEY")
248
+ model = os.getenv("LLM_MODEL", DEFAULT_LLM_MODEL)
249
+ base_url = os.getenv("LLM_BASE_URL", DEFAULT_LLM_BASE_URL)
250
+ return api_key, model, base_url
251
+
252
+
253
+ def query_laminar_signals(api_key: str, signal_name: str, days: int) -> list[dict]:
254
+ """Query Laminar SQL API to fetch signal events.
255
+
256
+ Note: days is validated as int by argparse, so no SQL injection risk there.
257
+ """
258
+ # Escape single quotes to prevent SQL injection
259
+ escaped_signal = signal_name.replace("'", "''")
260
+ query = f"""
261
+ SELECT id, trace_id, name, payload, timestamp
262
+ FROM signal_events
263
+ WHERE name = '{escaped_signal}'
264
+ AND timestamp > now() - INTERVAL {days} DAY
265
+ ORDER BY timestamp DESC
266
+ """
267
+
268
+ request_data = json.dumps({"query": query}).encode("utf-8")
269
+ request = urllib.request.Request(
270
+ LAMINAR_API_URL,
271
+ data=request_data,
272
+ headers={
273
+ "Authorization": f"Bearer {api_key}",
274
+ "Content-Type": "application/json",
275
+ },
276
+ )
277
+
278
+ try:
279
+ with urllib.request.urlopen(request, timeout=60) as response:
280
+ result = json.loads(response.read().decode("utf-8"))
281
+ return result.get("data", [])
282
+ except urllib.error.HTTPError as e:
283
+ print(f"Error querying Laminar API: HTTP {e.code}", file=sys.stderr)
284
+ print(f"Response: {e.read().decode('utf-8')}", file=sys.stderr)
285
+ sys.exit(1)
286
+
287
+
288
+ def list_available_signals(api_key: str, days: int) -> list[dict]:
289
+ """List all available signal names and their counts.
290
+
291
+ Note: days is validated as int by argparse, so no SQL injection risk.
292
+ """
293
+ query = f"""
294
+ SELECT name, COUNT(*) as count
295
+ FROM signal_events
296
+ WHERE timestamp > now() - INTERVAL {days} DAY
297
+ GROUP BY name
298
+ ORDER BY count DESC
299
+ """
300
+
301
+ request_data = json.dumps({"query": query}).encode("utf-8")
302
+ request = urllib.request.Request(
303
+ LAMINAR_API_URL,
304
+ data=request_data,
305
+ headers={
306
+ "Authorization": f"Bearer {api_key}",
307
+ "Content-Type": "application/json",
308
+ },
309
+ )
310
+
311
+ try:
312
+ with urllib.request.urlopen(request, timeout=60) as response:
313
+ result = json.loads(response.read().decode("utf-8"))
314
+ return result.get("data", [])
315
+ except urllib.error.HTTPError as e:
316
+ print(f"Error querying Laminar API: HTTP {e.code}", file=sys.stderr)
317
+ return []
318
+
319
+
320
+ def parse_signal(signal: dict, project_id: str | None = None) -> dict:
321
+ """Parse signal and extract payload as both dict and formatted JSON."""
322
+ payload_str = signal.get("payload", "{}")
323
+ try:
324
+ payload = json.loads(payload_str)
325
+ except json.JSONDecodeError:
326
+ payload = {}
327
+
328
+ signal_id = signal.get("id")
329
+ trace_id = signal.get("trace_id")
330
+
331
+ # Construct the Laminar trace URL
332
+ # Format: https://laminar.sh/project/{project_id}/traces?traceId={trace_id}
333
+ if project_id and trace_id:
334
+ trace_url = f"{LAMINAR_APP_URL}/project/{project_id}/traces?traceId={trace_id}"
335
+ else:
336
+ trace_url = None
337
+
338
+ # Return signal data with both parsed payload fields and formatted JSON
339
+ result = {
340
+ "id": signal_id,
341
+ "trace_id": trace_id,
342
+ "trace_url": trace_url,
343
+ "timestamp": signal.get("timestamp"),
344
+ "payload": payload,
345
+ "payload_json": json.dumps(payload, indent=2),
346
+ }
347
+
348
+ # Also flatten payload fields to top level for easy template access
349
+ for key, value in payload.items():
350
+ if key not in result:
351
+ result[key] = value
352
+
353
+ return result
354
+
355
+
356
+ def load_prompt_template(
357
+ signal_name: str,
358
+ prompt_file: str | None = None,
359
+ ) -> str:
360
+ """Load the appropriate prompt template.
361
+
362
+ Priority:
363
+ 1. Custom prompt file if provided
364
+ 2. Built-in template for the signal type
365
+ 3. Default template
366
+ """
367
+ # If a custom prompt file is provided, use it
368
+ if prompt_file:
369
+ prompt_path = Path(prompt_file)
370
+ if not prompt_path.exists():
371
+ print(f"Error: Prompt file not found: {prompt_file}", file=sys.stderr)
372
+ sys.exit(1)
373
+ return prompt_path.read_text()
374
+
375
+ # Check for built-in template for this signal type
376
+ if signal_name in BUILTIN_TEMPLATES:
377
+ template_file = TEMPLATES_DIR / BUILTIN_TEMPLATES[signal_name]
378
+ if template_file.exists():
379
+ return template_file.read_text()
380
+
381
+ # Fall back to default template
382
+ default_template = TEMPLATES_DIR / "default.j2"
383
+ if default_template.exists():
384
+ return default_template.read_text()
385
+
386
+ # Fallback if templates directory doesn't exist
387
+ raise FileNotFoundError(
388
+ f"No template found. Expected templates in: {TEMPLATES_DIR}"
389
+ )
390
+
391
+
392
+ def build_analysis_prompt(
393
+ signals: list[dict],
394
+ signal_name: str,
395
+ template_str: str,
396
+ skill_content: str | None = None,
397
+ ) -> str:
398
+ """Build the analysis prompt using Jinja template."""
399
+ template = Template(template_str)
400
+ prompt = template.render(
401
+ signals=signals,
402
+ num_signals=len(signals),
403
+ signal_name=signal_name,
404
+ )
405
+
406
+ # Append skill content if provided
407
+ if skill_content:
408
+ prompt += f"""
409
+
410
+ ---
411
+
412
+ ## Current Agent Skill/Prompt Configuration
413
+
414
+ The following is the current skill/prompt configuration that the agent uses.
415
+ Your recommendations MUST be grounded in this configuration with SPECIFIC, ACTIONABLE changes.
416
+
417
+ {skill_content}
418
+
419
+ ---
420
+
421
+ ## CRITICAL REQUIREMENTS FOR RECOMMENDATIONS
422
+
423
+ Your recommendations MUST include SPECIFIC, ACTIONABLE prompt changes:
424
+
425
+ 1. **Quote the exact text** from the skill/prompt that needs to change
426
+ 2. **Provide the replacement text** - not vague suggestions, but actual wording
427
+ 3. **Specify the section** where the change should be made
428
+ 4. **Use the prompt_changes field** to provide before/after diffs
429
+
430
+ ❌ BAD (vague): "Update the prompt to be more strict about blocking PRs"
431
+ ✅ GOOD (specific):
432
+ - Section: VERDICT
433
+ - Before: "❌ Needs rework: Fundamental design issues must be addressed first"
434
+ - After: "❌ Needs rework: BLOCK the PR if any of these are found: 1. Security vulnerabilities 2. Race conditions 3. Missing tests for new behavior. Do NOT approve with suggestions for these categories."
435
+
436
+ ❌ BAD: "Add instructions about deduplication"
437
+ ✅ GOOD:
438
+ - Section: CRITICAL REVIEW OUTPUT FORMAT (new subsection)
439
+ - After: "**NOISE CONTROL**: If the same issue appears in multiple locations, post ONE comment on the first occurrence that summarizes the pattern (e.g., 'This None-check issue appears on lines X, Y, Z') rather than commenting on each line."
440
+
441
+ Be as specific as possible. The goal is for someone to copy-paste your suggested changes directly into the skill file.
442
+ """
443
+
444
+ return prompt
445
+
446
+
447
+ def query_llm(api_key: str, prompt: str, model: str, base_url: str) -> dict:
448
+ """Query the LLM with the analysis prompt using function calling.
449
+
450
+ Returns:
451
+ Parsed JSON object from the function call response.
452
+ """
453
+ url = f"{base_url.rstrip('/')}/v1/chat/completions"
454
+
455
+ request_data = json.dumps({
456
+ "model": model,
457
+ "messages": [
458
+ {
459
+ "role": "user",
460
+ "content": prompt,
461
+ }
462
+ ],
463
+ "tools": [
464
+ {
465
+ "type": "function",
466
+ "function": ANALYSIS_FUNCTION,
467
+ }
468
+ ],
469
+ "tool_choice": {
470
+ "type": "function",
471
+ "function": {"name": "report_analysis"}
472
+ },
473
+ "max_tokens": 8192,
474
+ "temperature": 0.7,
475
+ }).encode("utf-8")
476
+
477
+ request = urllib.request.Request(
478
+ url,
479
+ data=request_data,
480
+ headers={
481
+ "Authorization": f"Bearer {api_key}",
482
+ "Content-Type": "application/json",
483
+ },
484
+ )
485
+
486
+ try:
487
+ with urllib.request.urlopen(request, timeout=300) as response:
488
+ result = json.loads(response.read().decode("utf-8"))
489
+
490
+ # Extract the function call arguments
491
+ message = result["choices"][0]["message"]
492
+ if "tool_calls" in message and message["tool_calls"]:
493
+ tool_call = message["tool_calls"][0]
494
+ if tool_call["type"] == "function":
495
+ return json.loads(tool_call["function"]["arguments"])
496
+
497
+ # Fallback: try to parse content as JSON if no tool call
498
+ if message.get("content"):
499
+ content = message["content"]
500
+ # Try to extract JSON from markdown code blocks
501
+ if "```json" in content:
502
+ start = content.find("```json") + 7
503
+ end = content.find("```", start)
504
+ if end > start:
505
+ content = content[start:end].strip()
506
+ elif "```" in content:
507
+ start = content.find("```") + 3
508
+ end = content.find("```", start)
509
+ if end > start:
510
+ content = content[start:end].strip()
511
+ try:
512
+ return json.loads(content)
513
+ except json.JSONDecodeError:
514
+ print(f"Warning: Could not parse LLM response as JSON", file=sys.stderr)
515
+ print(f"Response content: {message['content'][:500]}...", file=sys.stderr)
516
+
517
+ raise ValueError("No function call response received from LLM")
518
+
519
+ except urllib.error.HTTPError as e:
520
+ print(f"Error querying LLM: HTTP {e.code}", file=sys.stderr)
521
+ print(f"Response: {e.read().decode('utf-8')}", file=sys.stderr)
522
+ sys.exit(1)
523
+
524
+
525
+ def format_analysis_as_markdown(analysis: dict, signal_name: str) -> str:
526
+ """Convert the structured analysis output to markdown format."""
527
+ lines = [
528
+ "# Agent Improvement Report",
529
+ "",
530
+ f"**Signal:** `{signal_name}`",
531
+ "",
532
+ ]
533
+
534
+ # Executive Summary
535
+ if analysis.get("summary"):
536
+ lines.append("## Executive Summary")
537
+ lines.append("")
538
+ lines.append(analysis["summary"])
539
+ lines.append("")
540
+
541
+ # Issues (Primary Focus)
542
+ lines.append("## Issues Requiring Attention")
543
+ lines.append("")
544
+ for i, issue in enumerate(analysis.get("issues", []), 1):
545
+ severity = issue.get("severity", "medium").upper()
546
+ lines.append(f"### {i}. [{severity}] {issue['title']}")
547
+ lines.append("")
548
+ lines.append(issue["description"])
549
+ lines.append("")
550
+ if issue.get("frequency"):
551
+ lines.append(f"**Frequency:** {issue['frequency']}")
552
+ lines.append("")
553
+ if issue.get("trace_urls"):
554
+ lines.append("**Example traces:**")
555
+ for url in issue["trace_urls"]:
556
+ lines.append(f"- {url}")
557
+ lines.append("")
558
+
559
+ # Recommendations
560
+ lines.append("## Recommended Fixes")
561
+ lines.append("")
562
+ for i, rec in enumerate(analysis.get("recommendations", []), 1):
563
+ priority = rec.get("priority", "medium").upper()
564
+ lines.append(f"### {i}. [{priority} PRIORITY] {rec['title']}")
565
+ lines.append("")
566
+ lines.append(rec["description"])
567
+ lines.append("")
568
+
569
+ # Display specific prompt changes if provided
570
+ if rec.get("prompt_changes"):
571
+ lines.append("**Suggested Prompt Changes:**")
572
+ lines.append("")
573
+ for change in rec["prompt_changes"]:
574
+ section = change.get("section", "Unknown section")
575
+ before = change.get("before", "N/A")
576
+ after = change.get("after", "")
577
+ lines.append(f"📍 **Section:** {section}")
578
+ lines.append("")
579
+ if before and before != "N/A":
580
+ lines.append("```diff")
581
+ lines.append(f"- {before}")
582
+ lines.append(f"+ {after}")
583
+ lines.append("```")
584
+ else:
585
+ lines.append("```")
586
+ lines.append(f"+ {after}")
587
+ lines.append("```")
588
+ lines.append("")
589
+
590
+ if rec.get("addresses"):
591
+ lines.append(f"*Fixes: {', '.join(rec['addresses'])}*")
592
+ lines.append("")
593
+
594
+ # Metrics
595
+ metrics = analysis.get("metrics", {})
596
+ lines.append("## Metrics")
597
+ lines.append("")
598
+ lines.append(f"**Total signals analyzed:** {metrics.get('total_signals', 'N/A')}")
599
+ if metrics.get("issue_rate"):
600
+ lines.append(f"**Issue rate:** {metrics['issue_rate']}")
601
+ lines.append("")
602
+
603
+ if metrics.get("key_statistics"):
604
+ lines.append("| Metric | Value |")
605
+ lines.append("|--------|-------|")
606
+ for stat in metrics["key_statistics"]:
607
+ lines.append(f"| {stat['name']} | {stat['value']} |")
608
+ lines.append("")
609
+
610
+ # Strengths (Brief)
611
+ if analysis.get("strengths"):
612
+ lines.append("## What's Working Well")
613
+ lines.append("")
614
+ for strength in analysis["strengths"]:
615
+ lines.append(f"- **{strength['title']}**: {strength['description']}")
616
+ lines.append("")
617
+
618
+ return "\n".join(lines)
619
+
620
+
621
+ def main():
622
+ """Main entry point."""
623
+ parser = argparse.ArgumentParser(
624
+ description="Analyze Laminar signal events using an LLM",
625
+ formatter_class=argparse.RawDescriptionHelpFormatter,
626
+ epilog="""
627
+ Examples:
628
+ # Analyze PR review signals with built-in template
629
+ python analyze.py --signal "pr review suggestion and analysis"
630
+
631
+ # Analyze with skill context for grounded recommendations
632
+ python analyze.py --signal "pr review suggestion and analysis" \\
633
+ --skill-dir ../../../plugins/pr-review
634
+
635
+ # List available signals
636
+ python analyze.py --list-signals
637
+
638
+ # Use a custom prompt template
639
+ python analyze.py --signal "my-signal" --prompt-file my_prompt.j2
640
+
641
+ # Analyze last 30 days with JSON output
642
+ python analyze.py --signal "my-signal" --days 30 --format json
643
+
644
+ Environment Variables:
645
+ LMNR_PROJECT_API_KEY Laminar project API key (required)
646
+ LLM_API_KEY API key for the LLM (required)
647
+ LLM_MODEL Model to use (default: gemini-3-pro-preview)
648
+ LLM_BASE_URL Base URL for LLM API (default: https://llm-proxy.app.all-hands.dev)
649
+ """,
650
+ )
651
+ parser.add_argument(
652
+ "--signal",
653
+ help="Name of the signal to analyze",
654
+ )
655
+ parser.add_argument(
656
+ "--list-signals",
657
+ action="store_true",
658
+ help="List available signal names and exit",
659
+ )
660
+ parser.add_argument(
661
+ "--prompt-file",
662
+ help="Path to custom Jinja2 prompt template file",
663
+ )
664
+ parser.add_argument(
665
+ "--skill-dir",
666
+ help="Path to skill or plugin directory to ground recommendations in current prompts",
667
+ )
668
+ parser.add_argument(
669
+ "--days",
670
+ type=int,
671
+ default=DEFAULT_DAYS_LOOKBACK,
672
+ help=f"Number of days to look back (default: {DEFAULT_DAYS_LOOKBACK})",
673
+ )
674
+ parser.add_argument(
675
+ "--format",
676
+ choices=["md", "json"],
677
+ default="md",
678
+ help="Output format: 'md' for markdown (default), 'json' for raw JSON",
679
+ )
680
+ parser.add_argument(
681
+ "--output",
682
+ help="Output file path (default: stdout)",
683
+ )
684
+ args = parser.parse_args()
685
+
686
+ # Get API keys and config
687
+ laminar_key = get_env_var("LMNR_PROJECT_API_KEY")
688
+ laminar_project_id = get_env_var("LMNR_PROJECT_ID", required=False)
689
+ llm_key, llm_model, llm_base_url = get_llm_config()
690
+
691
+ # Handle --list-signals
692
+ if args.list_signals:
693
+ print(f"Available signals (last {args.days} days):")
694
+ print()
695
+ signals = list_available_signals(laminar_key, args.days)
696
+ if not signals:
697
+ print(" No signals found")
698
+ else:
699
+ for s in signals:
700
+ builtin = " [has built-in template]" if s["name"] in BUILTIN_TEMPLATES else ""
701
+ print(f" {s['name']}: {s['count']} events{builtin}")
702
+ return
703
+
704
+ # Require --signal if not listing
705
+ if not args.signal:
706
+ parser.error("--signal is required (or use --list-signals)")
707
+
708
+ print("=" * 60, file=sys.stderr)
709
+ print("Laminar Signal Analysis", file=sys.stderr)
710
+ print("=" * 60, file=sys.stderr)
711
+ print(file=sys.stderr)
712
+
713
+ # Fetch signals from Laminar
714
+ print(f"Signal: {args.signal}", file=sys.stderr)
715
+ print(f"Fetching signals from Laminar (last {args.days} days)...", file=sys.stderr)
716
+ raw_signals = query_laminar_signals(laminar_key, args.signal, args.days)
717
+ print(f"Found {len(raw_signals)} signal events", file=sys.stderr)
718
+ print(file=sys.stderr)
719
+
720
+ if not raw_signals:
721
+ print("No signals found. Exiting.", file=sys.stderr)
722
+ return
723
+
724
+ # Parse signals
725
+ signals = [parse_signal(s, laminar_project_id) for s in raw_signals]
726
+
727
+ # Warn if no project ID (trace URLs won't be generated)
728
+ if not laminar_project_id:
729
+ print("Warning: LMNR_PROJECT_ID not set, trace URLs will not be generated", file=sys.stderr)
730
+ print("Set LMNR_PROJECT_ID to enable clickable trace links", file=sys.stderr)
731
+ print(file=sys.stderr)
732
+
733
+ # Load prompt template
734
+ template_str = load_prompt_template(args.signal, args.prompt_file)
735
+ if args.prompt_file:
736
+ template_source = f"custom ({args.prompt_file})"
737
+ elif args.signal in BUILTIN_TEMPLATES:
738
+ template_source = f"built-in ({BUILTIN_TEMPLATES[args.signal]})"
739
+ else:
740
+ template_source = "default (default.j2)"
741
+ print(f"Using {template_source} prompt template", file=sys.stderr)
742
+
743
+ # Load skill content if provided
744
+ skill_content = None
745
+ if args.skill_dir:
746
+ print(f"Loading skill content from: {args.skill_dir}", file=sys.stderr)
747
+ try:
748
+ skill_content = load_skill_content(args.skill_dir)
749
+ print(f"Loaded {len(skill_content)} characters of skill content", file=sys.stderr)
750
+ except FileNotFoundError as e:
751
+ print(f"Warning: {e}", file=sys.stderr)
752
+
753
+ # Build prompt and query LLM
754
+ print("Building analysis prompt...", file=sys.stderr)
755
+ prompt = build_analysis_prompt(signals, args.signal, template_str, skill_content)
756
+ print(f"Prompt length: {len(prompt)} characters", file=sys.stderr)
757
+ print(file=sys.stderr)
758
+
759
+ print(f"Querying LLM ({llm_model}) for analysis...", file=sys.stderr)
760
+ print("This may take a minute...", file=sys.stderr)
761
+ print(file=sys.stderr)
762
+
763
+ analysis = query_llm(llm_key, prompt, llm_model, llm_base_url)
764
+
765
+ # Format output based on requested format
766
+ if args.format == "json":
767
+ output_text = json.dumps(analysis, indent=2)
768
+ else:
769
+ output_text = format_analysis_as_markdown(analysis, args.signal)
770
+
771
+ # Write output
772
+ if args.output:
773
+ Path(args.output).write_text(output_text)
774
+ print(f"Analysis written to: {args.output}", file=sys.stderr)
775
+ else:
776
+ print(output_text)
777
+
778
+
779
+ if __name__ == "__main__":
780
+ main()