skilldrop-cli 0.16.34 → 0.16.36

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 (212) hide show
  1. package/README.md +1 -0
  2. package/agents/README.md +4 -1
  3. package/agents/docs-reviewer.md +47 -0
  4. package/agents/finding-adjudicator.md +49 -0
  5. package/agents/test-reviewer.md +52 -0
  6. package/catalogue.json +17 -7
  7. package/model-routing.json +40 -0
  8. package/package.json +1 -1
  9. package/packs/ai-engineering/skills/agent-config-review/SKILL.md +116 -0
  10. package/packs/ai-engineering/skills/agent-config-review/evals/eval_queries.json +12 -0
  11. package/packs/ai-engineering/skills/agent-config-review/evals/evals.json +75 -0
  12. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/db-migrator.md +8 -0
  13. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/reviewer.md +9 -0
  14. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.json +48 -0
  15. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.local.json +5 -0
  16. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.mcp.json +22 -0
  17. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/package.json +12 -0
  18. package/packs/ai-engineering/skills/agent-config-review/evals/files/2/.cursor/mcp.json +17 -0
  19. package/packs/ai-engineering/skills/agent-config-review/evals/files/2/config.toml +21 -0
  20. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/my-user-settings.json +11 -0
  21. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/agents/release-notes.md +13 -0
  22. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/settings.json +39 -0
  23. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.mcp.json +18 -0
  24. package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/package.json +14 -0
  25. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/.github/workflows/codex-triage.yml +29 -0
  26. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/AGENTS.md +16 -0
  27. package/packs/ai-engineering/skills/agent-config-review/evals/files/5/codex/config.toml +34 -0
  28. package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.claude/settings.json +22 -0
  29. package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.mcp.json +9 -0
  30. package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +81 -0
  31. package/packs/ai-engineering/skills/agent-config-review/manifest.json +60 -0
  32. package/packs/ai-engineering/skills/agent-config-review/reference.md +205 -0
  33. package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +989 -0
  34. package/packs/ai-engineering/skills/agent-incident-response/SKILL.md +175 -0
  35. package/packs/ai-engineering/skills/agent-incident-response/evals/eval_queries.json +14 -0
  36. package/packs/ai-engineering/skills/agent-incident-response/evals/evals.json +107 -0
  37. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/agent-session-excerpt.txt +11 -0
  38. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/ci-build-log.txt +24 -0
  39. package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/incident-notes.md +19 -0
  40. package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/mcp-config.json +20 -0
  41. package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/session-transcript.txt +8 -0
  42. package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/agent-session.log +70 -0
  43. package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/infra/staging-db.tf +25 -0
  44. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/job.yaml +19 -0
  45. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/run.log +13 -0
  46. package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/usage-export-2026-10-20.csv +13 -0
  47. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/.mcp.json +20 -0
  48. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/git-log.txt +12 -0
  49. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync-excerpt.txt +30 -0
  50. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync.log +15 -0
  51. package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/ssh-config.txt +16 -0
  52. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/.github/workflows/ci.yml +28 -0
  53. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/SECURITY.md +8 -0
  54. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/issue-479.md +35 -0
  55. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/package.json +15 -0
  56. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/pr-482.diff +20 -0
  57. package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/run-log.txt +30 -0
  58. package/packs/ai-engineering/skills/agent-incident-response/examples/injected-issue-comment.md +57 -0
  59. package/packs/ai-engineering/skills/agent-incident-response/manifest.json +55 -0
  60. package/packs/ai-engineering/skills/agent-incident-response/reference.md +152 -0
  61. package/packs/ai-engineering/skills/agent-incident-response/scripts/find_secret_exposure.py +239 -0
  62. package/packs/ai-engineering/skills/agent-incident-response/scripts/usage_split.py +194 -0
  63. package/packs/converters/skills/file-to-markdown/SKILL.md +3 -2
  64. package/packs/converters/skills/file-to-markdown/evals/eval_queries.json +4 -0
  65. package/packs/converters/skills/file-to-markdown/manifest.json +2 -2
  66. package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +340 -7
  67. package/packs/converters/skills/md-to-confluence/SKILL.md +139 -0
  68. package/packs/converters/skills/md-to-confluence/evals/eval_queries.json +42 -0
  69. package/packs/converters/skills/md-to-confluence/evals/evals.json +89 -0
  70. package/packs/converters/skills/md-to-confluence/evals/files/1/img/latency-dashboard.png +0 -0
  71. package/packs/converters/skills/md-to-confluence/evals/files/1/payments-runbook.md +60 -0
  72. package/packs/converters/skills/md-to-confluence/evals/files/2/deploy-checklist.md +33 -0
  73. package/packs/converters/skills/md-to-confluence/evals/files/3/img/after/screen.png +0 -0
  74. package/packs/converters/skills/md-to-confluence/evals/files/3/img/before/screen.png +0 -0
  75. package/packs/converters/skills/md-to-confluence/evals/files/3/release-notes-4.2.md +49 -0
  76. package/packs/converters/skills/md-to-confluence/evals/files/4/docs/img/topology.png +0 -0
  77. package/packs/converters/skills/md-to-confluence/evals/files/4/docs/ingest-pipeline-design.md +65 -0
  78. package/packs/converters/skills/md-to-confluence/evals/files/5/support/troubleshooting-sso.md +57 -0
  79. package/packs/converters/skills/md-to-confluence/examples/inputs/img/queue-depth.png +0 -0
  80. package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.fixed.md +21 -0
  81. package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.md +21 -0
  82. package/packs/converters/skills/md-to-confluence/examples/queue-runbook.md +57 -0
  83. package/packs/converters/skills/md-to-confluence/manifest.json +52 -0
  84. package/packs/converters/skills/md-to-confluence/reference.md +92 -0
  85. package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +821 -0
  86. package/packs/converters/skills/md-to-pptx/SKILL.md +131 -0
  87. package/packs/converters/skills/md-to-pptx/evals/eval_queries.json +12 -0
  88. package/packs/converters/skills/md-to-pptx/evals/evals.json +84 -0
  89. package/packs/converters/skills/md-to-pptx/evals/files/1/img/defect-burndown.png +0 -0
  90. package/packs/converters/skills/md-to-pptx/evals/files/1/status-report.md +75 -0
  91. package/packs/converters/skills/md-to-pptx/evals/files/3/brand.json +12 -0
  92. package/packs/converters/skills/md-to-pptx/evals/files/3/night-shift-induction.md +53 -0
  93. package/packs/converters/skills/md-to-pptx/evals/files/4/charts/ontime.png +0 -0
  94. package/packs/converters/skills/md-to-pptx/evals/files/4/qbr-q3.md +85 -0
  95. package/packs/converters/skills/md-to-pptx/evals/files/5/vendor-sync-notes.md +28 -0
  96. package/packs/converters/skills/md-to-pptx/examples/support-weekly-input.md +48 -0
  97. package/packs/converters/skills/md-to-pptx/examples/support-weekly.md +75 -0
  98. package/packs/converters/skills/md-to-pptx/examples/tickets-by-week.png +0 -0
  99. package/packs/converters/skills/md-to-pptx/manifest.json +49 -0
  100. package/packs/converters/skills/md-to-pptx/reference.md +139 -0
  101. package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1841 -0
  102. package/packs/dev-team/loops/modernize/LOOP.md +124 -0
  103. package/packs/dev-team/loops/modernize/evals/eval_queries.json +9 -0
  104. package/packs/dev-team/loops/modernize/evals/evals.json +27 -0
  105. package/packs/dev-team/loops/modernize/loop.json +60 -0
  106. package/packs/dev-team/skills/behaviour-equivalence/SKILL.md +155 -0
  107. package/packs/dev-team/skills/behaviour-equivalence/evals/eval_queries.json +11 -0
  108. package/packs/dev-team/skills/behaviour-equivalence/evals/evals.json +76 -0
  109. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/cases.jsonl +12 -0
  110. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/new.jsonl +12 -0
  111. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/notes.md +15 -0
  112. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old.jsonl +12 -0
  113. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old_rerun.jsonl +12 -0
  114. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v1.py +45 -0
  115. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v2.py +65 -0
  116. package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/rules.json +10 -0
  117. package/packs/dev-team/skills/behaviour-equivalence/evals/files/3/diff-summary.txt +16 -0
  118. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/diff.txt +13 -0
  119. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py2.py +16 -0
  120. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py3.py +24 -0
  121. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/input.csv +13 -0
  122. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/new_output.csv +13 -0
  123. package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/old_output.csv +13 -0
  124. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/TEAM_NOTES.md +19 -0
  125. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/normalisation.yaml +12 -0
  126. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/new_run.jsonl +8 -0
  127. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run1.jsonl +8 -0
  128. package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run2.jsonl +8 -0
  129. package/packs/dev-team/skills/behaviour-equivalence/examples/delivery-date.md +78 -0
  130. package/packs/dev-team/skills/behaviour-equivalence/manifest.json +33 -0
  131. package/packs/dev-team/skills/behaviour-equivalence/reference.md +97 -0
  132. package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +524 -0
  133. package/packs/dev-team/skills/behaviour-equivalence/templates/accepted.json +10 -0
  134. package/packs/dev-team/skills/behaviour-equivalence/templates/rules.json +7 -0
  135. package/packs/frontend/pack.json +55 -0
  136. package/packs/frontend/skills/rendering-strategy/SKILL.md +149 -0
  137. package/packs/frontend/skills/rendering-strategy/evals/eval_queries.json +38 -0
  138. package/packs/frontend/skills/rendering-strategy/evals/evals.json +77 -0
  139. package/packs/frontend/skills/rendering-strategy/evals/files/1/fernhill-app.md +40 -0
  140. package/packs/frontend/skills/rendering-strategy/evals/files/3/routes.md +33 -0
  141. package/packs/frontend/skills/rendering-strategy/evals/files/4/build-notes.txt +5 -0
  142. package/packs/frontend/skills/rendering-strategy/evals/files/4/nuxt.config.ts +19 -0
  143. package/packs/frontend/skills/rendering-strategy/evals/files/4/pages/recipes/[slug].vue +37 -0
  144. package/packs/frontend/skills/rendering-strategy/evals/files/4/routes.md +15 -0
  145. package/packs/frontend/skills/rendering-strategy/evals/files/5/routes.txt +18 -0
  146. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/hooks.server.ts +14 -0
  147. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/(marketing)/pricing/+page.server.ts +10 -0
  148. package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/+layout.server.ts +10 -0
  149. package/packs/frontend/skills/rendering-strategy/evals/files/5/svelte.config.js +10 -0
  150. package/packs/frontend/skills/rendering-strategy/evals/files/5/traffic.md +18 -0
  151. package/packs/frontend/skills/rendering-strategy/examples/saas-site.md +42 -0
  152. package/packs/frontend/skills/rendering-strategy/manifest.json +50 -0
  153. package/packs/frontend/skills/rendering-strategy/reference.md +116 -0
  154. package/packs/frontend/skills/rendering-strategy/scripts/render_load.py +181 -0
  155. package/packs/frontend/skills/web-performance-review/SKILL.md +106 -0
  156. package/packs/frontend/skills/web-performance-review/evals/eval_queries.json +38 -0
  157. package/packs/frontend/skills/web-performance-review/evals/evals.json +76 -0
  158. package/packs/frontend/skills/web-performance-review/evals/files/1/psi.json +1065 -0
  159. package/packs/frontend/skills/web-performance-review/evals/files/2/after.json +359 -0
  160. package/packs/frontend/skills/web-performance-review/evals/files/2/before.json +375 -0
  161. package/packs/frontend/skills/web-performance-review/evals/files/4/psi-pdp-mobile-2026-10-19.json +171 -0
  162. package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-attribution-oct14-19.csv +6 -0
  163. package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-daily.csv +15 -0
  164. package/packs/frontend/skills/web-performance-review/evals/files/5/crux-home-phone.json +35 -0
  165. package/packs/frontend/skills/web-performance-review/evals/files/5/lighthouse-home-mobile.json +173 -0
  166. package/packs/frontend/skills/web-performance-review/examples/quayside-article-lhr.json +480 -0
  167. package/packs/frontend/skills/web-performance-review/examples/quayside-article.md +103 -0
  168. package/packs/frontend/skills/web-performance-review/manifest.json +48 -0
  169. package/packs/frontend/skills/web-performance-review/reference.md +105 -0
  170. package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +743 -0
  171. package/packs/product-manager/skills/lean-canvas/SKILL.md +162 -0
  172. package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +42 -0
  173. package/packs/product-manager/skills/lean-canvas/evals/evals.json +82 -0
  174. package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +54 -0
  175. package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +30 -0
  176. package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +44 -0
  177. package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +5 -0
  178. package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +40 -0
  179. package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +18 -0
  180. package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +72 -0
  181. package/packs/product-manager/skills/lean-canvas/manifest.json +34 -0
  182. package/packs/product-manager/skills/lean-canvas/reference.md +102 -0
  183. package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +211 -0
  184. package/packs/solution-architect/skills/api-contract-draft/SKILL.md +2 -2
  185. package/packs/solution-architect/skills/api-contract-draft/manifest.json +28 -6
  186. package/packs/solution-architect/skills/event-contract-draft/SKILL.md +167 -0
  187. package/packs/solution-architect/skills/event-contract-draft/evals/eval_queries.json +38 -0
  188. package/packs/solution-architect/skills/event-contract-draft/evals/evals.json +76 -0
  189. package/packs/solution-architect/skills/event-contract-draft/evals/files/1/order-events-brief.md +45 -0
  190. package/packs/solution-architect/skills/event-contract-draft/evals/files/4/NOTES.md +23 -0
  191. package/packs/solution-architect/skills/event-contract-draft/evals/files/4/schemas/shipment-update.json +40 -0
  192. package/packs/solution-architect/skills/event-contract-draft/evals/files/5/invoice.example.json +25 -0
  193. package/packs/solution-architect/skills/event-contract-draft/examples/order-service.asyncapi.json +246 -0
  194. package/packs/solution-architect/skills/event-contract-draft/examples/order-service.md +92 -0
  195. package/packs/solution-architect/skills/event-contract-draft/manifest.json +25 -0
  196. package/packs/solution-architect/skills/event-contract-draft/reference.md +113 -0
  197. package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +502 -0
  198. package/packs/solution-architect/skills/event-contract-draft/templates/asyncapi-skeleton.json +144 -0
  199. package/packs/sre-oncall/skills/slo-spec/SKILL.md +161 -0
  200. package/packs/sre-oncall/skills/slo-spec/evals/eval_queries.json +38 -0
  201. package/packs/sre-oncall/skills/slo-spec/evals/evals.json +75 -0
  202. package/packs/sre-oncall/skills/slo-spec/evals/files/1/order-service.md +51 -0
  203. package/packs/sre-oncall/skills/slo-spec/evals/files/3/search-slo-status.md +30 -0
  204. package/packs/sre-oncall/skills/slo-spec/evals/files/4/alerts/payments-api.rules.yaml +16 -0
  205. package/packs/sre-oncall/skills/slo-spec/evals/files/4/docs/dependencies.md +8 -0
  206. package/packs/sre-oncall/skills/slo-spec/evals/files/4/metrics/summary-90d.md +51 -0
  207. package/packs/sre-oncall/skills/slo-spec/evals/files/5/ops-notes.md +34 -0
  208. package/packs/sre-oncall/skills/slo-spec/examples/ticket-booking.md +88 -0
  209. package/packs/sre-oncall/skills/slo-spec/manifest.json +34 -0
  210. package/packs/sre-oncall/skills/slo-spec/reference.md +113 -0
  211. package/packs/sre-oncall/skills/slo-spec/scripts/slo_math.py +449 -0
  212. package/packs/sre-oncall/skills/slo-spec/templates/slo-spec.md +35 -0
@@ -2,7 +2,7 @@
2
2
  """Convert a document into clean Markdown that another skill can read.
3
3
 
4
4
  Stdlib only, Python 3.9+. Handles .docx, .pptx, .xlsx, .html/.htm, .csv/.tsv,
5
- .json, .txt/.md, and .pdf (through `pdftotext` when it is installed). A PDF page with no
5
+ .json, .txt/.md, .eml and Outlook .msg email, and .pdf (through `pdftotext` when it is installed). A PDF page with no
6
6
  text layer (a scan) is read by OCR when `tesseract` and `pdftoppm` are installed.
7
7
  Everything the conversion could not carry (images, charts, comments, formulas,
8
8
  tracked changes) is counted and reported on stderr, so nobody mistakes the
@@ -972,6 +972,256 @@ def html_to_md(path, rep):
972
972
  return parser.result()
973
973
 
974
974
 
975
+ def _html_text_to_md(text, rep):
976
+ """HTML already decoded to text (an email body) -> Markdown, through the same parser."""
977
+ skipped = len(re.findall(r"<(script|style)\b", text, re.I))
978
+ if skipped:
979
+ rep.drop("scripts/styles", skipped)
980
+ parser = HTMLToMD(rep)
981
+ parser.feed(text)
982
+ parser.close()
983
+ return parser.result()
984
+
985
+
986
+ # ---------------------------------------------------------------- email (.eml, .msg)
987
+
988
+ def _email_md(subject, rows, body_md, attachments, rep, note=None):
989
+ """One email as Markdown: subject as the title, the headers as a table, the body, and the
990
+ attachments listed by name and size, since their content is not converted."""
991
+ out = ["# " + md_escape(subject or "(no subject)"), "", "| | |", "|---|---|"]
992
+ for k, v in rows:
993
+ if v:
994
+ out.append("| %s | %s |" % (k, cell_text(md_escape(v))))
995
+ out += ["", body_md.strip() or "_(no text body)_", ""]
996
+ if attachments:
997
+ out += ["## Attachments", ""]
998
+ for name, size in attachments:
999
+ out.append("- %s (%s)" % (md_escape(name or "unnamed"), _human_size(size)))
1000
+ out.append("")
1001
+ rep.drop("attachments (listed, not converted)", len(attachments))
1002
+ if note:
1003
+ rep.notes.append(note)
1004
+ rep.keep("emails", 1)
1005
+ return "\n".join(out).rstrip() + "\n"
1006
+
1007
+
1008
+ def _email_text(text):
1009
+ """A plain-text email body as Markdown. Bullets and `> ` quoted replies already read as
1010
+ Markdown lists and quotes, so only a leading `#` (which would become a heading) and a
1011
+ line of `=` or `-` under text (which would turn that text into one) are escaped."""
1012
+ out = []
1013
+ for line in text.replace("\r\n", "\n").split("\n"):
1014
+ if line.lstrip().startswith("#"):
1015
+ line = line.replace("#", "\\#", 1)
1016
+ elif re.fullmatch(r"\s*(=+|-{2,})\s*", line) and out and out[-1].strip():
1017
+ line = "\\" + line.strip()
1018
+ out.append(line)
1019
+ return "\n".join(out)
1020
+
1021
+
1022
+ def _human_size(n):
1023
+ if n is None:
1024
+ return "size unknown"
1025
+ for unit in ("bytes", "KB", "MB", "GB"):
1026
+ if n < 1024 or unit == "GB":
1027
+ return ("%d %s" % (n, unit)) if unit == "bytes" else ("%.1f %s" % (n, unit))
1028
+ n /= 1024.0
1029
+
1030
+
1031
+ def eml_to_md(path, rep):
1032
+ """An RFC 5322 message (.eml): the plain-text part when there is one, else the HTML part."""
1033
+ import email
1034
+ from email import policy
1035
+ msg = email.message_from_bytes(path.read_bytes(), policy=policy.default)
1036
+ rows = [("From", str(msg.get("From", ""))), ("To", str(msg.get("To", ""))), ("Cc", str(msg.get("Cc", ""))),
1037
+ ("Date", str(msg.get("Date", "")))]
1038
+ plain = msg.get_body(preferencelist=("plain",))
1039
+ html = msg.get_body(preferencelist=("html",))
1040
+ if plain is not None:
1041
+ body = _email_text(plain.get_content())
1042
+ if html is not None:
1043
+ rep.drop("HTML alternative (the plain-text part was used)", 1)
1044
+ elif html is not None:
1045
+ body = _html_text_to_md(html.get_content(), rep)
1046
+ else:
1047
+ body = ""
1048
+ attachments = []
1049
+ for part in msg.iter_attachments():
1050
+ payload = part.get_payload(decode=True)
1051
+ name = part.get_filename() or ("forwarded message" if part.get_content_type() == "message/rfc822" else None)
1052
+ attachments.append((name, len(payload) if payload is not None else None))
1053
+ return _email_md(str(msg.get("Subject", "")), rows, body, attachments, rep)
1054
+
1055
+
1056
+ class _CFB:
1057
+ """A minimal, read-only reader for the Compound File Binary format Outlook saves .msg in.
1058
+ Bounded on every loop and offset, so a malformed file stops with an error, never a hang."""
1059
+ MAGIC = b"\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"
1060
+
1061
+ def __init__(self, data):
1062
+ import struct
1063
+ self.struct = struct
1064
+ if data[:8] != self.MAGIC or len(data) < 512:
1065
+ raise ValueError("not an Outlook .msg file (no compound-file header)")
1066
+ self.d = data
1067
+ self.ss = 1 << struct.unpack_from("<H", data, 30)[0]
1068
+ self.mss = 1 << struct.unpack_from("<H", data, 32)[0]
1069
+ if self.ss not in (512, 4096) or self.mss != 64:
1070
+ raise ValueError("unsupported compound-file sector size")
1071
+ nfat, dir_start = struct.unpack_from("<I", data, 44)[0], struct.unpack_from("<I", data, 48)[0]
1072
+ self.cutoff = struct.unpack_from("<I", data, 56)[0]
1073
+ minifat_start, nminifat, difat_start, ndifat = struct.unpack_from("<IIII", data, 60)
1074
+ difat = list(struct.unpack_from("<109I", data, 76))
1075
+ per = self.ss // 4
1076
+ s, guard = difat_start, 0
1077
+ while ndifat and s < 0xFFFFFFFA and guard < 10000:
1078
+ vals = struct.unpack("<%dI" % per, self._sector(s))
1079
+ difat += vals[:-1]
1080
+ s, guard, ndifat = vals[-1], guard + 1, ndifat - 1
1081
+ self.fat = []
1082
+ for fs in difat[:nfat]:
1083
+ if fs < 0xFFFFFFFA:
1084
+ self.fat += struct.unpack("<%dI" % per, self._sector(fs))
1085
+ raw = self._chain(dir_start)
1086
+ self.entries = []
1087
+ for i in range(0, len(raw) - 127, 128):
1088
+ e = raw[i:i + 128]
1089
+ nlen = struct.unpack_from("<H", e, 64)[0]
1090
+ name = e[:max(0, min(nlen, 64) - 2)].decode("utf-16-le", "replace")
1091
+ kind = e[66]
1092
+ left, right, child = struct.unpack_from("<III", e, 68)
1093
+ start = struct.unpack_from("<I", e, 116)[0]
1094
+ size = struct.unpack_from("<I", e, 120)[0] # the high half is zero for version 3 files
1095
+ self.entries.append({"name": name, "kind": kind, "left": left, "right": right, "child": child,
1096
+ "start": start, "size": size})
1097
+ if not self.entries or self.entries[0]["kind"] != 5:
1098
+ raise ValueError("compound file has no root entry")
1099
+ root = self.entries[0]
1100
+ self.ministream = self._chain(root["start"])[:root["size"]] if root["start"] < 0xFFFFFFFA else b""
1101
+ mf = self._chain(minifat_start) if nminifat else b""
1102
+ self.minifat = list(struct.unpack("<%dI" % (len(mf) // 4), mf[:len(mf) // 4 * 4]))
1103
+
1104
+ def _sector(self, n):
1105
+ off = (n + 1) * self.ss
1106
+ if off + self.ss > len(self.d):
1107
+ raise ValueError("compound file is truncated or corrupt")
1108
+ return self.d[off:off + self.ss]
1109
+
1110
+ def _chain(self, start, mini=False):
1111
+ fat, out, n, seen = (self.minifat if mini else self.fat), [], start, 0
1112
+ while n < 0xFFFFFFFA:
1113
+ if n >= len(fat) or seen > 1000000:
1114
+ raise ValueError("compound file has a broken sector chain")
1115
+ if mini:
1116
+ off = n * self.mss
1117
+ out.append(self.ministream[off:off + self.mss])
1118
+ else:
1119
+ out.append(self._sector(n))
1120
+ n, seen = fat[n], seen + 1
1121
+ return b"".join(out)
1122
+
1123
+ def children(self, idx):
1124
+ """The entries directly inside storage `idx`, by name."""
1125
+ out, stack, seen = {}, [self.entries[idx]["child"]], set()
1126
+ while stack:
1127
+ i = stack.pop()
1128
+ if i >= len(self.entries) or i in seen:
1129
+ continue
1130
+ seen.add(i)
1131
+ e = self.entries[i]
1132
+ out[e["name"]] = i
1133
+ stack += [e["left"], e["right"]]
1134
+ return out
1135
+
1136
+ def read(self, i):
1137
+ e = self.entries[i]
1138
+ if e["size"] < self.cutoff:
1139
+ return self._chain(e["start"], mini=True)[:e["size"]]
1140
+ return self._chain(e["start"])[:e["size"]]
1141
+
1142
+
1143
+ def _msg_props(cfb, idx):
1144
+ """The string and binary properties of one .msg storage, by four-hex-digit property id."""
1145
+ out = {}
1146
+ for name, i in cfb.children(idx).items():
1147
+ m = re.match(r"__substg1\.0_([0-9A-Fa-f]{4})([0-9A-Fa-f]{4})$", name)
1148
+ if not m or cfb.entries[i]["kind"] != 2:
1149
+ continue
1150
+ pid, ptype = m.group(1).upper(), m.group(2).upper()
1151
+ data = cfb.read(i)
1152
+ if ptype == "001F":
1153
+ out[pid] = data.decode("utf-16-le", "replace").rstrip("\x00")
1154
+ elif ptype == "001E":
1155
+ out[pid] = data.decode("cp1252", "replace").rstrip("\x00")
1156
+ elif ptype == "0102":
1157
+ out.setdefault(pid + ":bin", data)
1158
+ return out
1159
+
1160
+
1161
+ def _msg_fixed(cfb, idx, header):
1162
+ """Fixed-size properties (times, integers) from a storage's __properties_version1.0."""
1163
+ import struct
1164
+ from datetime import datetime as _dt
1165
+ stream = cfb.children(idx).get("__properties_version1.0")
1166
+ if stream is None:
1167
+ return {}
1168
+ data, out = cfb.read(stream), {}
1169
+ for off in range(header, len(data) - 15, 16):
1170
+ tag, = struct.unpack_from("<I", data, off)
1171
+ ptype, pid = tag & 0xFFFF, "%04X" % (tag >> 16)
1172
+ if ptype == 0x0040:
1173
+ ft, = struct.unpack_from("<Q", data, off + 8)
1174
+ if ft:
1175
+ out[pid] = _dt(1601, 1, 1) + timedelta(microseconds=ft // 10)
1176
+ elif ptype == 0x0003:
1177
+ out[pid] = struct.unpack_from("<i", data, off + 8)[0]
1178
+ return out
1179
+
1180
+
1181
+ def msg_to_md(path, rep):
1182
+ """An Outlook message (.msg): subject, sender, recipients, sent time, the text body (or the
1183
+ HTML body when there is no text one), and attachments listed by name and size."""
1184
+ try:
1185
+ cfb = _CFB(path.read_bytes())
1186
+ except ValueError as e:
1187
+ sys.exit("error: %s: %s" % (path, e))
1188
+ props = _msg_props(cfb, 0)
1189
+ fixed = _msg_fixed(cfb, 0, 32)
1190
+ to, cc = [], []
1191
+ for name, i in sorted(cfb.children(0).items()):
1192
+ if name.startswith("__recip_version1.0_"):
1193
+ rp, rf = _msg_props(cfb, i), _msg_fixed(cfb, i, 8)
1194
+ who = rp.get("3001", "")
1195
+ addr = rp.get("39FE") or rp.get("3003", "")
1196
+ label = ("%s <%s>" % (who, addr)) if who and addr and addr not in who else (who or addr)
1197
+ (cc if rf.get("0C15") == 2 else to).append(label)
1198
+ sender = props.get("0C1A", "")
1199
+ saddr = props.get("5D01") or props.get("0C1F") or props.get("0065", "")
1200
+ sent = fixed.get("0039") or fixed.get("0E06")
1201
+ rows = [("From", ("%s <%s>" % (sender, saddr)) if sender and saddr else (sender or saddr)),
1202
+ ("To", "; ".join(to) or props.get("0E04", "")), ("Cc", "; ".join(cc) or props.get("0E03", "")),
1203
+ ("Date", sent.strftime("%Y-%m-%d %H:%M UTC") if sent else "")]
1204
+ note = None
1205
+ if props.get("1000"):
1206
+ body = _email_text(props["1000"])
1207
+ elif props.get("1013") or props.get("1013:bin"):
1208
+ html = props.get("1013") or props["1013:bin"].decode("utf-8", "replace")
1209
+ body = _html_text_to_md(html, rep)
1210
+ else:
1211
+ body = ""
1212
+ if props.get("1009:bin"):
1213
+ note = "the message body is stored only as compressed RTF, which is not converted"
1214
+ rep.drop("RTF body", 1)
1215
+ attachments = []
1216
+ for name, i in sorted(cfb.children(0).items()):
1217
+ if name.startswith("__attach_version1.0_"):
1218
+ ap, kids = _msg_props(cfb, i), cfb.children(i)
1219
+ data = ap.get("3701:bin")
1220
+ fname = ap.get("3707") or ap.get("3704") or ("embedded message" if "__substg1.0_3701000D" in kids else None)
1221
+ attachments.append((fname, len(data) if data is not None else None))
1222
+ return _email_md(props.get("0037", ""), rows, body, attachments, rep, note)
1223
+
1224
+
975
1225
  # ---------------------------------------------------------------- csv / json / txt / pdf
976
1226
 
977
1227
  def read_text(path, rep):
@@ -1104,9 +1354,78 @@ def _pdf_page_plain(page):
1104
1354
  return out
1105
1355
 
1106
1356
 
1107
- def _pdf_page_md(page, rep):
1357
+ _BBOX_PAGE = re.compile(r"<page\b.*?</page>", re.S)
1358
+ _BBOX_WORD = re.compile(r'<word xMin="([\d.]+)" yMin="([\d.]+)" xMax="([\d.]+)" yMax="([\d.]+)">(.*?)</word>', re.S)
1359
+
1360
+
1361
+ def _word_rows(bbox_html):
1362
+ """Each physical line of every page as [(xMin, xMax, height, text)], keyed by its words.
1363
+ `pdftotext -bbox-layout` puts each table cell in a block of its own, so words are
1364
+ regrouped by their vertical position into the rows the layout reading shows."""
1365
+ import html as _html
1366
+ rows = {}
1367
+ for page in _BBOX_PAGE.findall(bbox_html):
1368
+ words = [(float(a), float(b), float(c), float(d), _html.unescape(t)) for a, b, c, d, t in _BBOX_WORD.findall(page)]
1369
+ words.sort(key=lambda w: (round(w[3]), w[0]))
1370
+ line, base = [], None
1371
+ for x0, y0, x1, y1, t in words + [(0, 0, 0, -99, "")]:
1372
+ if base is not None and abs(y1 - base) > 2:
1373
+ line.sort()
1374
+ key = " ".join(w[3] for w in line)
1375
+ if key and key not in rows:
1376
+ rows[key] = line
1377
+ line = []
1378
+ base = y1
1379
+ if t:
1380
+ line.append((x0, x1, y1 - y0, t))
1381
+ return rows
1382
+
1383
+
1384
+ def _finer_cells(region, boxes):
1385
+ """The region's rows split again from where each word sits on the page, or None.
1386
+
1387
+ `pdftotext -layout` turns positions into characters, so two columns whose text sits closer
1388
+ than about two characters (a right-aligned amount beside a left-aligned date) come out one
1389
+ space apart and merge into one cell. A gap between words is a column when it is at least
1390
+ 0.6 of the line's height (a word space is about a quarter of it) and, when the table has
1391
+ word spaces to compare with, at least twice the smallest of them, which keeps a monospace
1392
+ font's wide spaces from reading as columns. The new split is used only when every row then
1393
+ has the same number of cells, so a table that already came out right is never changed."""
1394
+ if not boxes:
1395
+ return None
1396
+ found = [boxes.get(" ".join(r.split())) for r in region]
1397
+ if not any(found):
1398
+ return None
1399
+ gaps = []
1400
+ for ws in found:
1401
+ if ws:
1402
+ h = sorted(w[2] for w in ws)[len(ws) // 2]
1403
+ gaps += [(b[0] - a[1], h) for a, b in zip(ws, ws[1:])]
1404
+ small = [g for g, h in gaps if g < 0.6 * h]
1405
+ floor = 2 * min(small) if small else 0
1406
+ rows = []
1407
+ for r, ws in zip(region, found):
1408
+ if not ws:
1409
+ rows.append(_cells(r))
1410
+ continue
1411
+ h = sorted(w[2] for w in ws)[len(ws) // 2]
1412
+ cells, cur = [], [ws[0][3]]
1413
+ for a, b in zip(ws, ws[1:]):
1414
+ g = b[0] - a[1]
1415
+ if g >= 0.6 * h and g >= floor:
1416
+ cells.append(" ".join(cur)); cur = []
1417
+ cur.append(b[3])
1418
+ cells.append(" ".join(cur))
1419
+ rows.append(cells)
1420
+ if len(set(len(c) for c in rows)) != 1 or rows == [_cells(r) for r in region]:
1421
+ return None
1422
+ return rows
1423
+
1424
+
1425
+ def _pdf_page_md(page, rep, fine=None):
1108
1426
  """One page of `pdftotext -layout` text -> Markdown. Layout mode keeps line breaks,
1109
- indentation and column alignment, which is what tables and lists are recovered from."""
1427
+ indentation and column alignment, which is what tables and lists are recovered from.
1428
+ `fine` maps a line's words to where each word sits on the page (see _finer_cells)."""
1110
1429
  lines = page.splitlines()
1111
1430
  out, i, n = [], 0, len(lines)
1112
1431
  while i < n:
@@ -1133,7 +1452,16 @@ def _pdf_page_md(page, rep):
1133
1452
  break
1134
1453
  if rows >= 2:
1135
1454
  widths = set(len(_cells(r)) for r in region)
1136
- if len(widths) == 1 and rows == len(region):
1455
+ finer = _finer_cells(region, fine) if rows == len(region) else None
1456
+ if finer and (len(widths) != 1 or len(finer[0]) > len(_cells(region[0]))):
1457
+ rep.keep("pdf table columns separated by position", 1)
1458
+ cells = [[cell_text(md_escape(c)) for c in r] for r in finer]
1459
+ width = len(cells[0])
1460
+ out.append("| " + " | ".join(cells[0]) + " |")
1461
+ out.append("|" + "---|" * width)
1462
+ out += ["| " + " | ".join(r) + " |" for r in cells[1:]]
1463
+ rep.keep("pdf tables", 1)
1464
+ elif len(widths) == 1 and rows == len(region):
1137
1465
  cells = [[cell_text(md_escape(c)) for c in _cells(r)] for r in region]
1138
1466
  width = len(cells[0])
1139
1467
  out.append("| " + " | ".join(cells[0]) + " |")
@@ -1318,6 +1646,11 @@ def pdf_to_md(path, rep, ocr=True, ocr_lang="eng"):
1318
1646
  elif blank and len(blank) == len(layout):
1319
1647
  sys.exit("error: %s has no text layer (probably scanned), and --no-ocr was given." % path)
1320
1648
  layout = _join_split_tables(layout, rep)
1649
+ # Where each word sits, to tell table columns apart that sit closer than two characters
1650
+ # in the layout reading (see _finer_cells).
1651
+ proc = subprocess.run([exe, "-bbox-layout", "-enc", "UTF-8", str(path), "-"], stdout=subprocess.PIPE,
1652
+ stderr=subprocess.PIPE, check=False)
1653
+ fine = _word_rows(proc.stdout.decode("utf-8", "replace")) if proc.returncode == 0 else {}
1321
1654
  out, plain_pages = [], []
1322
1655
  for n, page in enumerate(layout, 1):
1323
1656
  out += ["<!-- page %d -->" % n, ""]
@@ -1325,7 +1658,7 @@ def pdf_to_md(path, rep, ocr=True, ocr_lang="eng"):
1325
1658
  out += _pdf_page_plain(reading[n - 1])
1326
1659
  plain_pages.append(n)
1327
1660
  else:
1328
- out += _pdf_page_md(page, rep)
1661
+ out += _pdf_page_md(page, rep, fine)
1329
1662
  pages = layout
1330
1663
  if plain_pages:
1331
1664
  rep.notes.append("page(s) %s have side-by-side columns, so they were read in column order with "
@@ -1342,7 +1675,7 @@ def pdf_to_md(path, rep, ocr=True, ocr_lang="eng"):
1342
1675
  HANDLERS = {
1343
1676
  ".docx": docx_to_md, ".pptx": pptx_to_md, ".xlsx": xlsx_to_md, ".xlsm": xlsx_to_md,
1344
1677
  ".html": html_to_md, ".htm": html_to_md, ".csv": csv_to_md, ".tsv": csv_to_md,
1345
- ".json": json_to_md, ".pdf": pdf_to_md,
1678
+ ".json": json_to_md, ".pdf": pdf_to_md, ".eml": eml_to_md, ".msg": msg_to_md,
1346
1679
  }
1347
1680
  LEGACY = {".doc": ".docx", ".ppt": ".pptx", ".xls": ".xlsx", ".rtf": ".docx", ".odt": ".docx",
1348
1681
  ".pages": ".docx", ".key": ".pptx", ".numbers": ".xlsx"}
@@ -1350,7 +1683,7 @@ LEGACY = {".doc": ".docx", ".ppt": ".pptx", ".xls": ".xlsx", ".rtf": ".docx", ".
1350
1683
 
1351
1684
  def main(argv=None):
1352
1685
  ap = argparse.ArgumentParser(description="Convert a document to Markdown for another skill to read.")
1353
- ap.add_argument("input", help=".docx .pptx .xlsx .html .csv .tsv .json .txt .md .pdf")
1686
+ ap.add_argument("input", help=".docx .pptx .xlsx .html .csv .tsv .json .txt .md .eml .msg .pdf")
1354
1687
  ap.add_argument("-o", "--output", required=True, help="Markdown file to write, or - for stdout")
1355
1688
  ap.add_argument("--max-rows", type=int, default=0,
1356
1689
  help="xlsx only: keep at most this many data rows per sheet (0 = all)")
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: md-to-confluence
3
+ description: Turn a Markdown runbook, design note or guide into a Confluence page: storage-format XHTML with real code blocks, tables, tasks, info and warning panels, images as attachments and an optional contents macro, a warning for everything dropped, and an optional publish that first shows which page it will create or overwrite and at what version, refuses stale or wrong-space overwrites, and takes credentials only from the environment. Use when the user says "put this in Confluence", "publish this markdown to the wiki", "update the Confluence page with this", "convert to Confluence storage format", or names a Confluence space for a Markdown file.
4
+ ---
5
+
6
+ # md-to-confluence
7
+
8
+ You turn a Markdown file into a Confluence page that reads like one written in the editor:
9
+ code in the code macro, callouts as panels, checkboxes as Confluence tasks. A stdlib script
10
+ does the conversion and, when asked, the publish. Your job is the part a person gets wrong
11
+ under time pressure: fix what won't convert, list the images that must be attached, and make
12
+ sure nobody overwrites the wrong page. Runbooks often come from `runbook-generator`.
13
+
14
+ ## How to respond
15
+
16
+ **Four rules come before the steps and outrank them:**
17
+
18
+ - **Answer what was asked, first.** Open with the answer, the decision or the artifact, in plain words. Scores, matrices, frameworks and tags come after it, and anything that doesn't change the answer is cut.
19
+ - **Use only what you were given.** Don't add facts, names, numbers, incidents, history, steps or sections the input doesn't contain. What you need and don't have is left out of the artifact and listed once at the end under "To confirm".
20
+ - **Deliver from what you have.** When the request gives you something to work on, state your assumptions in a line and produce the result. When it gives you nothing to work on, ask for it in one or two plain sentences and say what you will do once you have it.
21
+ - **Write for someone who has never heard of this skill.** No skill names, no paths or scripts from this folder, no internal terms, and nothing about how the run was set up. A next step is one plain sentence at the end that describes the work.
22
+ - **For this skill:** Nothing reaches Confluence until the person has seen exactly which page will be created or overwritten (space, title, page id, current version, last editor) and the files to attach, and has said yes to that page. With nobody to ask, convert only and say what would be published.
23
+
24
+ 1. **Take the target from the request; ask once for the rest.** Publishing needs the site
25
+ (`https://<site>.atlassian.net/wiki`, or the Data Center address), the space key, and
26
+ either a parent page or the id of the page to replace. Ask for what's missing in one
27
+ message, at most two questions. Defaults: title from the first `#` heading; parent is
28
+ the space home; contents macro on when the page has four or more `##` sections.
29
+
30
+ 2. **Fix the Markdown in a copy before converting, and list each change.** These are the
31
+ losses the script can only warn about:
32
+ - **Raw HTML is dropped whole.** A `<details>` block often hides the rollback steps. Rewrite
33
+ it as Markdown (a `## Rollback` section with a numbered list).
34
+ ✅ *rollback steps survive as a list* ❌ *"1 HTML block dropped" in the reply, steps gone*
35
+ - **Code between numbered steps breaks the numbering.** Confluence restarts each list at 1.
36
+ Indent the code block three spaces under its step so steps read 1, 2, 3.
37
+ - **Two images with the same file name** (`before/screen.png`, `after/screen.png`) become one
38
+ attachment, so both show the same picture. Rename one (`screen-before.png`).
39
+ - **Footnotes are dropped.** Move the text into the sentence or a "Notes" section.
40
+ - **Mermaid shows as source.** Render it to PNG with `mermaid-render` and embed the PNG.
41
+ - **A front-matter `title:` wins over the `#` heading** for a new page. An update keeps the
42
+ page's current title, so when the Markdown's title differs (`Deploy checklist v2 (draft)`),
43
+ say so and rename only if asked.
44
+ - **Callouts need the label as the first words of the quote:** `> **Note:**` → info panel,
45
+ `> **Warning:**` → warning panel (full table in [`reference.md`](reference.md)).
46
+
47
+ 3. **Convert, then read every `WARN` line.**
48
+
49
+ ```bash
50
+ # Claude Code
51
+ python3 "${CLAUDE_SKILL_DIR}/scripts/md_to_confluence.py" runbook.md -o runbook.xhtml --toc
52
+ # Other IDEs (from the skill folder)
53
+ python3 scripts/md_to_confluence.py runbook.md -o runbook.xhtml --toc
54
+ ```
55
+
56
+ Fix the source and rerun for each warning you can fix. Report the rest in plain words:
57
+ ✅ *"The in-page link to Rollback is plain text now; the contents box at the top covers it."*
58
+ ❌ *"Converted with 3 warnings."* The script also lists every image to upload as an attachment.
59
+
60
+ 4. **Credentials come from the environment, never the chat.** The script reads
61
+ `CONFLUENCE_EMAIL` + `CONFLUENCE_API_TOKEN` (Cloud) or `CONFLUENCE_PAT` (Data Center).
62
+ If they're unset, convert, run `--publish --dry-run`, and stop: tell the person to export
63
+ the variables in their own terminal and restart the session. If they paste a token into the chat, don't put it in a command, a file or an
64
+ `export` you run: tell them to revoke it, because it now sits in the transcript.
65
+
66
+ 5. **Look at the target page before writing to it.** `--inspect` is read-only:
67
+
68
+ ```bash
69
+ python3 "${CLAUDE_SKILL_DIR}/scripts/md_to_confluence.py" runbook.md -o runbook.xhtml --inspect --base-url "https://<site>.atlassian.net/wiki" --space SRE --page-id 48213
70
+ ```
71
+
72
+ Without `--page-id` it says whether a page with that title already exists in the space.
73
+
74
+ 6. **Confirm with the facts, then publish.** One block, then wait for a yes:
75
+ ✅ *"This overwrites **Deploy checklist** (page 48213, PLAT), now at version 7, last edited by
76
+ Priya Nair on 1 Oct. Upload after: `flow.png`. Dropped: one footnote, moved
77
+ into Notes. Publish?"* ❌ *"Ready to push to Confluence, go ahead?"*
78
+
79
+ ```bash
80
+ python3 "${CLAUDE_SKILL_DIR}/scripts/md_to_confluence.py" runbook.md -o runbook.xhtml --publish --base-url "https://<site>.atlassian.net/wiki" --space PLAT --page-id 48213 --expected-version 7
81
+ # other IDEs: python3 scripts/md_to_confluence.py … with the same flags
82
+ ```
83
+
84
+ Always pass `--expected-version` with the number `--inspect` showed: if anyone saved the page
85
+ since, the script refuses instead of erasing their edit. Leave out `--page-id` to create;
86
+ it refuses when the title is taken. An update keeps the page's title unless `--title` is
87
+ passed. `--dry-run` prints the request and sends nothing.
88
+
89
+ 7. **Hand over in three or four lines, opening with the state.** ✅ *"Published: <link>, now
90
+ version 8."* ✅ *"Converted, not published yet: it becomes a new page **Payments API
91
+ runbook** under page 196612 in SRE."* ❌ *"Done!"* when nothing reached Confluence. Then the
92
+ files to upload in the page's Attachments with their names unchanged (images show broken
93
+ until then; the script never uploads them), what was dropped or changed, and what the
94
+ person does next.
95
+
96
+ **Non-interactive runs** (subagent, CI, headless): never publish. Convert, run `--publish
97
+ --dry-run` when a target was given, and reply with what would be published, to which space
98
+ and page, and the attachments, as the first thing to confirm. Defaults (title, contents
99
+ macro, parent) become `[assumption]` lines. With no Markdown, emit `BLOCKED: need the
100
+ Markdown file to convert` and write nothing.
101
+
102
+ ## Useful references in this skill
103
+
104
+ - [`reference.md`](reference.md): what maps to what, the callout convention, code languages, the API calls and every refusal
105
+ - [`scripts/md_to_confluence.py`](scripts/md_to_confluence.py): converter and publisher (stdlib only, Python 3.9+)
106
+ - [`examples/queue-runbook.md`](examples/queue-runbook.md): a runbook converted, with the fixes made and the confirmation shown
107
+
108
+ ## Quality bar
109
+
110
+ - **The right page, said out loud.** Every publish is preceded by the page id, title, space, current version and last editor from a live check, and a yes.
111
+ - **No silent overwrite.** Updates carry `--expected-version`; a newer version stops the run.
112
+ - **Nothing lost silently.** Each `WARN` is fixed in the source or named in the reply; rollback steps in HTML are rewritten, not dropped.
113
+ - **Images are listed by attachment name**, with any missing file or name clash called out before publishing.
114
+ - **Code survives byte for byte**, including a `]]>` inside it; the script checks the page is well-formed.
115
+ - **Tokens never appear** in a command, a file or the reply.
116
+ - **Facts untouched.** Restructuring Markdown never changes a command, threshold, name or date.
117
+
118
+ ## When to use this skill
119
+
120
+ - ✅ A Markdown runbook, design doc or how-to that belongs on a team's Confluence space
121
+ - ✅ Replacing an existing Confluence page with a newer Markdown version, without clobbering edits
122
+ - ✅ Storage-format XHTML to paste into the source editor or another tool's API call
123
+
124
+ ## When NOT to use this skill
125
+
126
+ - ❌ A Word file to edit with tracked changes; use `md-to-docx`
127
+ - ❌ A standalone web page or an HTML attachment for email; use `md-to-html`
128
+ - ❌ Jira epics and issues from a PRD; use `tracker-brief-sync`
129
+ - ❌ Writing the runbook itself; use `runbook-generator`, then convert
130
+
131
+ ## Anti-patterns to avoid
132
+
133
+ - ❌ **Publishing on "put it in Confluence".** That names a destination, not a page to overwrite. Inspect, show, ask.
134
+ - ❌ **Updating without `--expected-version`.** A teammate's edit from this morning disappears with no trace in the reply.
135
+ - ❌ **Hand-writing storage XHTML.** One unescaped `&` or a `]]>` in a code sample, and Confluence rejects the page or cuts the code.
136
+ - ❌ **Treating "published" as done while images are unattached.** The page renders with broken image boxes until the files are uploaded.
137
+ - ❌ **Putting a pasted token on a command line.** It lands in shell history and logs. Ask for environment variables and a revoke.
138
+ - ❌ **Showing the machinery.** No skill names, script paths or flags in the reply; describe the page and the steps.
139
+ - ❌ **A bare `BLOCKED` line.** Say in plain words what is missing and what you will produce once you have it.
@@ -0,0 +1,42 @@
1
+ [
2
+ {
3
+ "query": "put this runbook in Confluence under the SRE space",
4
+ "should_trigger": true
5
+ },
6
+ {
7
+ "query": "Convert README.md to Confluence storage format so I can paste it into the source editor",
8
+ "should_trigger": true
9
+ },
10
+ {
11
+ "query": "Publish design-notes.md as a wiki page under Architecture on our atlassian site",
12
+ "should_trigger": true
13
+ },
14
+ {
15
+ "query": "update Confluence page 48213 with the new deploy-checklist.md",
16
+ "should_trigger": true
17
+ },
18
+ {
19
+ "query": "our team docs live in Confluence, can you get this markdown onto a page with the code blocks looking right",
20
+ "should_trigger": true
21
+ },
22
+ {
23
+ "query": "Convert this markdown to a Word document legal can track changes in",
24
+ "should_trigger": false
25
+ },
26
+ {
27
+ "query": "Turn the markdown report into a single HTML file I can email",
28
+ "should_trigger": false
29
+ },
30
+ {
31
+ "query": "Create Jira epics and issues from this PRD",
32
+ "should_trigger": false
33
+ },
34
+ {
35
+ "query": "Write a runbook for the payments service before it goes to on-call",
36
+ "should_trigger": false
37
+ },
38
+ {
39
+ "query": "Render the Mermaid flowchart in arch.mmd to a PNG",
40
+ "should_trigger": false
41
+ }
42
+ ]