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
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env python3
2
+ """Find where a leaked key appears in a repository's git history and in log files.
3
+
4
+ Read-only. Never prints the key: every match is shown masked (first 8 characters,
5
+ an ellipsis, the last 4, and the length). Identify the key by a prefix you already
6
+ know (--prefix, optionally with --suffix for the last characters) or by piping the
7
+ full key on stdin (--key-stdin) so it never lands in shell history.
8
+
9
+ python3 find_secret_exposure.py --prefix sk-live-AbCdEf12 --repo . --log ci.txt
10
+ printf '%s' "$LEAKED" | python3 find_secret_exposure.py --key-stdin --repo .
11
+
12
+ Reports: commits that added or removed it (author and commit time, UTC), whether the
13
+ current tree and working copy still hold it, which branches and tags still reach the
14
+ adding commits, commits reachable only from the local reflog, and every log line that
15
+ holds it with the line's own timestamp. Writes only the -o path when given.
16
+ """
17
+ import argparse
18
+ import datetime as dt
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+
24
+ TOKEN_CHARS = r"[A-Za-z0-9_\-+/=.]"
25
+ STAMP = re.compile(
26
+ r"(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?"
27
+ )
28
+ MIN_PREFIX = 10
29
+
30
+
31
+ def mask(token):
32
+ if len(token) <= 12:
33
+ return token[:4] + "…" + " (len %d)" % len(token)
34
+ return "%s…%s (len %d)" % (token[:8], token[-4:], len(token))
35
+
36
+
37
+ class Needle:
38
+ def __init__(self, prefix=None, suffix=None, full=None):
39
+ self.full = full
40
+ self.prefix = full[:12] if full else prefix
41
+ self.suffix = suffix
42
+ self.rx = re.compile(re.escape(self.prefix) + TOKEN_CHARS + "*")
43
+ self.seen = set()
44
+
45
+ def tokens(self, text):
46
+ """Masked tokens in text that match the key."""
47
+ out = []
48
+ for m in self.rx.finditer(text):
49
+ tok = m.group(0).rstrip(".=") # trailing punctuation from prose or logs
50
+ if self.full and self.full not in m.group(0):
51
+ continue
52
+ if self.suffix and not tok.endswith(self.suffix):
53
+ continue
54
+ out.append(mask(self.full if self.full else tok))
55
+ self.seen.update(out)
56
+ return out
57
+
58
+
59
+ def utc(epoch):
60
+ return dt.datetime.fromtimestamp(int(epoch), dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
61
+
62
+
63
+ def line_stamp(line):
64
+ m = STAMP.search(line)
65
+ if not m:
66
+ return "no timestamp on the line"
67
+ day, clock, tz = m.groups()
68
+ if not tz:
69
+ return "%s %s (no timezone on the line)" % (day, clock)
70
+ tz = "+00:00" if tz == "Z" else (tz if ":" in tz else tz[:3] + ":" + tz[3:])
71
+ try:
72
+ when = dt.datetime.fromisoformat("%sT%s%s" % (day, clock, tz))
73
+ except ValueError:
74
+ return "%s %s%s (unparsed)" % (day, clock, tz)
75
+ return when.astimezone(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
76
+
77
+
78
+ def git(repo, *args):
79
+ r = subprocess.run(["git", "-C", repo] + list(args), capture_output=True)
80
+ return r.returncode, r.stdout.decode("utf-8", "replace"), r.stderr.decode("utf-8", "replace")
81
+
82
+
83
+ def scan_repo(repo, needle):
84
+ rc, _, err = git(repo, "rev-parse", "--git-dir")
85
+ if rc != 0:
86
+ return None, "not a git repository: %s (%s)" % (repo, err.strip())
87
+ rc, out, err = git(
88
+ repo, "log", "--all", "--reflog", "--no-color", "--no-ext-diff", "-p",
89
+ "-S" + needle.prefix, "--format=%x00COMMIT %H %at %ct",
90
+ )
91
+ if rc != 0:
92
+ return None, "git log failed: %s" % err.strip()
93
+ commits, cur, path = [], None, None
94
+ for line in out.splitlines():
95
+ if line.startswith("\x00COMMIT "):
96
+ sha, at, ct = line.split()[1:4]
97
+ cur = {"sha": sha, "authored": utc(at), "committed": utc(ct), "added": [], "removed": []}
98
+ commits.append(cur)
99
+ elif cur is None:
100
+ continue
101
+ elif line.startswith("diff --git "):
102
+ path = line.split(" b/", 1)[-1]
103
+ elif line.startswith("+") and not line.startswith("+++"):
104
+ if needle.tokens(line):
105
+ cur["added"].append(path)
106
+ elif line.startswith("-") and not line.startswith("---"):
107
+ if needle.tokens(line):
108
+ cur["removed"].append(path)
109
+ commits = [c for c in commits if c["added"] or c["removed"]]
110
+ for c in commits:
111
+ c["added"], c["removed"] = sorted(set(c["added"])), sorted(set(c["removed"]))
112
+ if c["added"]:
113
+ _, b, _ = git(repo, "branch", "-a", "--contains", c["sha"])
114
+ _, t, _ = git(repo, "tag", "--contains", c["sha"])
115
+ c["refs"] = [x.strip("* ").strip() for x in b.splitlines() if x.strip()]
116
+ c["refs"] += ["tag " + x.strip() for x in t.splitlines() if x.strip()]
117
+ present = {}
118
+ for label, args in (("HEAD", ["grep", "-n", "-I", "-F", "-e", needle.prefix, "HEAD", "--"]),
119
+ ("working copy", ["grep", "-n", "-I", "-F", "--untracked", "-e", needle.prefix])):
120
+ _, g, _ = git(repo, *args)
121
+ hits = []
122
+ for gl in g.splitlines():
123
+ parts = gl.split(":", 3 if label == "HEAD" else 2)
124
+ loc = ":".join(parts[1:3]) if label == "HEAD" else ":".join(parts[0:2])
125
+ if needle.tokens(gl):
126
+ hits.append(loc)
127
+ present[label] = hits
128
+ return {"commits": commits, "present": present}, None
129
+
130
+
131
+ def scan_log(path, needle):
132
+ hits = []
133
+ with open(path, "r", encoding="utf-8", errors="replace") as fh:
134
+ for n, line in enumerate(fh, 1):
135
+ toks = needle.tokens(line)
136
+ if toks:
137
+ hits.append((n, line_stamp(line), toks[0]))
138
+ return hits
139
+
140
+
141
+ def report(args, needle, repo_res, repo_err, logs):
142
+ o = ["# Where the key appears", ""]
143
+ if len(needle.seen) > 1:
144
+ o += ["%d different keys match this prefix: %s. Pass --suffix with the leaked key's last "
145
+ "characters to keep only that one." % (len(needle.seen), ", ".join(sorted(needle.seen))), ""]
146
+ if args.repo:
147
+ o.append("## Git history: %s" % args.repo)
148
+ if repo_err:
149
+ o += ["", "Not scanned: " + repo_err]
150
+ else:
151
+ cs = repo_res["commits"]
152
+ adds = [c for c in cs if c["added"]]
153
+ if not cs:
154
+ o += ["", "No commit adds or removes it (searched every branch, tag, stash and the local reflog)."]
155
+ if adds:
156
+ first = min(adds, key=lambda c: c["authored"])
157
+ o += ["", "First commit that adds it: `%s`, authored %s, committed %s. Commit time is "
158
+ "when it was committed locally; when it reached a remote is in the host's audit or push log."
159
+ % (first["sha"][:12], first["authored"], first["committed"])]
160
+ for c in cs:
161
+ o.append("")
162
+ o.append("- `%s` authored %s, committed %s" % (c["sha"][:12], c["authored"], c["committed"]))
163
+ if c["added"]:
164
+ o.append(" - adds it in: %s" % ", ".join(c["added"]))
165
+ o.append(" - still reachable from: %s" % (", ".join(c["refs"]) if c["refs"]
166
+ else "no branch or tag; only the local reflog keeps it"))
167
+ if c["removed"]:
168
+ o.append(" - removes it from: %s (the earlier commits still hold it)" % ", ".join(c["removed"]))
169
+ for label, hits in repo_res["present"].items():
170
+ o.append("")
171
+ o.append("In %s now: %s" % (label, ", ".join(hits) if hits else "not present"))
172
+ o.append("")
173
+ for path, hits, err in logs:
174
+ o.append("## Log: %s" % path)
175
+ o.append("")
176
+ if err:
177
+ o.append("Not scanned: " + err)
178
+ elif not hits:
179
+ o.append("Not found.")
180
+ else:
181
+ for n, stamp, tok in hits:
182
+ o.append("- line %d, %s, %s" % (n, stamp, tok))
183
+ o.append("")
184
+ o += ["## Not searched", "",
185
+ "Forks, other clones, pull-request refs that were never fetched, CI caches and artifacts, "
186
+ "the hosting provider's own copies, chat and ticket systems. A clean result here does not "
187
+ "mean the key is safe: revoke it regardless.", ""]
188
+ return "\n".join(o)
189
+
190
+
191
+ def main(argv=None):
192
+ p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
193
+ who = p.add_mutually_exclusive_group(required=True)
194
+ who.add_argument("--prefix", help="the first characters of the key (at least %d)" % MIN_PREFIX)
195
+ who.add_argument("--key-stdin", action="store_true", help="read the full key from stdin")
196
+ p.add_argument("--suffix", help="the last characters of the key, to tell it apart from keys with the same prefix")
197
+ p.add_argument("--repo", help="path to a git repository to search")
198
+ p.add_argument("--log", action="append", default=[], help="a log file to search (repeatable)")
199
+ p.add_argument("-o", "--out", help="write the report here instead of stdout")
200
+ a = p.parse_args(argv)
201
+ if a.key_stdin:
202
+ full = sys.stdin.read().strip()
203
+ if len(full) < MIN_PREFIX:
204
+ p.error("the key on stdin is shorter than %d characters" % MIN_PREFIX)
205
+ needle = Needle(full=full)
206
+ else:
207
+ if len(a.prefix) < MIN_PREFIX:
208
+ p.error("--prefix needs at least %d characters: a short prefix such as a provider's "
209
+ "key-type marker matches every key of that type" % MIN_PREFIX)
210
+ needle = Needle(prefix=a.prefix, suffix=a.suffix)
211
+ if not a.repo and not a.log:
212
+ p.error("give --repo, --log, or both")
213
+ repo_res, repo_err = (None, None)
214
+ if a.repo:
215
+ if not os.path.isdir(a.repo):
216
+ repo_err = "no such directory: %s" % a.repo
217
+ else:
218
+ repo_res, repo_err = scan_repo(a.repo, needle)
219
+ logs = []
220
+ for path in a.log:
221
+ if not os.path.isfile(path):
222
+ logs.append((path, [], "no such file"))
223
+ continue
224
+ try:
225
+ logs.append((path, scan_log(path, needle), None))
226
+ except OSError as e:
227
+ logs.append((path, [], str(e)))
228
+ text = report(a, needle, repo_res, repo_err, logs)
229
+ if a.out:
230
+ with open(a.out, "w", encoding="utf-8") as fh:
231
+ fh.write(text)
232
+ print("wrote %s" % a.out)
233
+ else:
234
+ print(text)
235
+ return 0
236
+
237
+
238
+ if __name__ == "__main__":
239
+ sys.exit(main())
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """Split a provider usage or billing export (CSV) by who spent it.
3
+
4
+ Read-only. Sums cost exactly (decimal, not float) per group of columns, so a bill
5
+ is split from the export rather than estimated. Groups by the key, model and
6
+ user-agent columns it finds (override with --by). Each group shows its total,
7
+ its share of the whole, its row and request counts, and the time of its first and
8
+ last row as written in the export.
9
+
10
+ python3 usage_split.py usage.csv
11
+ python3 usage_split.py usage.csv --since 2026-10-19T20:00 --expect model=model-medium
12
+ python3 usage_split.py usage.csv --by api_key_name,user_agent -o split.txt
13
+ cat usage.csv | python3 usage_split.py -
14
+
15
+ --expect col=value (repeatable) marks every group whose value in that column
16
+ differs as UNEXPECTED: traffic the known job does not explain. --since/--until
17
+ compare the time column as text, so give them in the export's own format.
18
+ Writes only the -o path when given.
19
+ """
20
+ import argparse
21
+ import csv
22
+ import io
23
+ import sys
24
+ from decimal import Decimal, InvalidOperation
25
+
26
+ COST_HINTS = ("cost_usd", "cost", "amount_usd", "amount", "spend", "usd", "price")
27
+ TIME_HINTS = ("date_hour_utc", "timestamp", "time", "hour", "date", "start_time", "period")
28
+ REQ_HINTS = ("requests", "request_count", "num_requests", "calls")
29
+ GROUP_HINTS = (
30
+ ("api_key_name", "api_key", "key_name", "key_id", "key"),
31
+ ("model", "model_name", "model_id"),
32
+ ("user_agent", "useragent", "ua", "client"),
33
+ )
34
+
35
+
36
+ def find_col(headers, hints, contains=False):
37
+ low = {h.lower().strip(): h for h in headers}
38
+ for hint in hints:
39
+ if hint in low:
40
+ return low[hint]
41
+ if contains:
42
+ for hint in hints:
43
+ for key, orig in low.items():
44
+ if hint in key:
45
+ return orig
46
+ return None
47
+
48
+
49
+ def to_decimal(text):
50
+ cleaned = (text or "").strip().replace("$", "").replace(",", "")
51
+ if cleaned == "":
52
+ return Decimal("0")
53
+ return Decimal(cleaned)
54
+
55
+
56
+ def money(value):
57
+ return "${:,.2f}".format(value)
58
+
59
+
60
+ def main(argv=None):
61
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
62
+ ap.add_argument("export", help="CSV usage export path, or - for stdin")
63
+ ap.add_argument("--by", help="comma-separated columns to group by (default: key, model, user agent)")
64
+ ap.add_argument("--cost", help="cost column name (default: auto-detect)")
65
+ ap.add_argument("--time", help="time column name (default: auto-detect)")
66
+ ap.add_argument("--since", help="keep rows whose time is >= this text")
67
+ ap.add_argument("--until", help="keep rows whose time is < this text")
68
+ ap.add_argument("--expect", action="append", default=[], metavar="COL=VALUE",
69
+ help="mark groups whose COL differs from VALUE as UNEXPECTED")
70
+ ap.add_argument("-o", "--output", help="write the report to this path instead of stdout")
71
+ args = ap.parse_args(argv)
72
+
73
+ if args.export == "-":
74
+ text = sys.stdin.read()
75
+ else:
76
+ try:
77
+ with open(args.export, encoding="utf-8", newline="") as fh:
78
+ text = fh.read()
79
+ except OSError as exc:
80
+ print("error: cannot read {}: {}".format(args.export, exc), file=sys.stderr)
81
+ return 2
82
+ text = text.lstrip("")
83
+ reader = csv.DictReader(io.StringIO(text))
84
+ headers = reader.fieldnames or []
85
+ if not headers:
86
+ print("error: the export has no header row", file=sys.stderr)
87
+ return 2
88
+
89
+ cost_col = args.cost or find_col(headers, COST_HINTS, contains=True)
90
+ time_col = args.time or find_col(headers, TIME_HINTS, contains=True)
91
+ req_col = find_col(headers, REQ_HINTS)
92
+ if not cost_col or cost_col not in headers:
93
+ print("error: no cost column found; columns are: {}; pass --cost".format(", ".join(headers)),
94
+ file=sys.stderr)
95
+ return 2
96
+ if args.by:
97
+ group_cols = [c.strip() for c in args.by.split(",") if c.strip()]
98
+ else:
99
+ group_cols = [c for c in (find_col(headers, h) for h in GROUP_HINTS) if c]
100
+ missing = [c for c in group_cols if c not in headers]
101
+ if missing or not group_cols:
102
+ print("error: group columns not in the export: {}; columns are: {}".format(
103
+ ", ".join(missing) or "(none found)", ", ".join(headers)), file=sys.stderr)
104
+ return 2
105
+ if (args.since or args.until) and not time_col:
106
+ print("error: --since/--until need a time column; pass --time", file=sys.stderr)
107
+ return 2
108
+
109
+ expects = []
110
+ for item in args.expect:
111
+ if "=" not in item:
112
+ print("error: --expect takes COL=VALUE, got {!r}".format(item), file=sys.stderr)
113
+ return 2
114
+ col, val = item.split("=", 1)
115
+ if col not in group_cols:
116
+ print("error: --expect column {!r} must be one of the group columns: {}".format(
117
+ col, ", ".join(group_cols)), file=sys.stderr)
118
+ return 2
119
+ expects.append((col, val))
120
+
121
+ groups = {}
122
+ total = Decimal("0")
123
+ kept = 0
124
+ bad = []
125
+ for line_no, row in enumerate(reader, start=2):
126
+ when = (row.get(time_col) or "").strip() if time_col else ""
127
+ if args.since and when < args.since:
128
+ continue
129
+ if args.until and when >= args.until:
130
+ continue
131
+ try:
132
+ cost = to_decimal(row.get(cost_col))
133
+ except InvalidOperation:
134
+ bad.append(line_no)
135
+ continue
136
+ key = tuple((row.get(c) or "").strip() for c in group_cols)
137
+ g = groups.setdefault(key, {"cost": Decimal("0"), "rows": 0, "requests": 0,
138
+ "first": None, "last": None, "times": []})
139
+ g["cost"] += cost
140
+ g["rows"] += 1
141
+ if req_col:
142
+ try:
143
+ g["requests"] += int(float((row.get(req_col) or "0").replace(",", "")))
144
+ except ValueError:
145
+ pass
146
+ if when:
147
+ g["times"].append(when)
148
+ g["first"] = when if g["first"] is None or when < g["first"] else g["first"]
149
+ g["last"] = when if g["last"] is None or when > g["last"] else g["last"]
150
+ total += cost
151
+ kept += 1
152
+
153
+ out = []
154
+ window = ""
155
+ if args.since or args.until:
156
+ window = " ({} to {})".format(args.since or "start", args.until or "end")
157
+ out.append("Rows: {}{} Total: {}".format(kept, window, money(total)))
158
+ out.append("Grouped by: {} Cost column: {}{}".format(
159
+ ", ".join(group_cols), cost_col, " Time column: " + time_col if time_col else ""))
160
+ out.append("")
161
+ for key, g in sorted(groups.items(), key=lambda kv: kv[1]["cost"], reverse=True):
162
+ share = (g["cost"] / total * 100) if total else Decimal("0")
163
+ label = " | ".join("{}={}".format(c, v) for c, v in zip(group_cols, key))
164
+ flag = ""
165
+ if expects:
166
+ off = [c for c, v in expects if key[group_cols.index(c)] != v]
167
+ if off:
168
+ flag = " UNEXPECTED ({} differs)".format(", ".join(off))
169
+ out.append("{} {} ({:.1f}%){}".format(money(g["cost"]), label, share, flag))
170
+ detail = " rows={}".format(g["rows"])
171
+ if req_col:
172
+ detail += " requests={:,}".format(g["requests"])
173
+ if g["first"]:
174
+ detail += " first={} last={}".format(g["first"], g["last"])
175
+ out.append(detail)
176
+ if g["times"] and len(g["times"]) <= 12:
177
+ out.append(" times: " + ", ".join(sorted(g["times"])))
178
+ if bad:
179
+ out.append("")
180
+ out.append("Skipped rows with an unreadable cost (CSV line numbers): " +
181
+ ", ".join(str(n) for n in bad))
182
+ report = "\n".join(out) + "\n"
183
+
184
+ if args.output:
185
+ with open(args.output, "w", encoding="utf-8") as fh:
186
+ fh.write(report)
187
+ print("wrote {}".format(args.output))
188
+ else:
189
+ sys.stdout.write(report)
190
+ return 0
191
+
192
+
193
+ if __name__ == "__main__":
194
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: file-to-markdown
3
- description: Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json or .pdf and says "read this", "turn this into markdown", "extract the text", or wants it critiqued or briefed.
3
+ description: Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file, email (.eml or Outlook .msg) or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json, .eml, .msg or .pdf and says "read this", "turn this into markdown", "extract the text", or wants it critiqued or briefed.
4
4
  ---
5
5
 
6
6
  # file-to-markdown
@@ -46,8 +46,9 @@ or a rewrite that goes back out through `md-to-docx` or `md-to-html`.
46
46
  | `.html` | Headings, lists, tables, links, code blocks, quotes; scripts, styles and nav menus dropped |
47
47
  | `.csv` / `.tsv` | One table, delimiter detected |
48
48
  | `.json` | An array of objects becomes a table; an object becomes a key/value table plus a table per array |
49
+ | `.eml` / `.msg` | Subject as the title; From, To, Cc and the sent time as a table; the plain-text body (the HTML body only when there is no text one); attachments listed by name and size, not converted. Outlook `.msg` is read directly, with nothing to install |
49
50
  | `.txt` / `.md` | Passed through |
50
- | `.pdf` | Through `pdftotext`. Tables from aligned columns, lists from indentation, short standalone lines as `###` headings (guessed, and said so); a table that runs onto the next page is joined into one, without the header the PDF repeats; pages with side-by-side columns are read in column order with nothing inferred. Scanned pages are read by OCR when `tesseract` is installed. No images |
51
+ | `.pdf` | Through `pdftotext`. Tables from aligned columns (told apart by where each word sits on the page, so two columns set close together don't merge), lists from indentation, short standalone lines as `###` headings (guessed, and said so); a table that runs onto the next page is joined into one, without the header the PDF repeats; pages with side-by-side columns are read in column order with nothing inferred. Scanned pages are read by OCR when `tesseract` is installed. No images |
51
52
 
52
53
  3. **PDF without `pdftotext`.** The script stops with install steps (`brew install poppler`
53
54
  on macOS, `sudo apt install poppler-utils` on Debian/Ubuntu). Pass those on; don't try to
@@ -1,4 +1,8 @@
1
1
  [
2
+ {
3
+ "query": "turn this Outlook .msg email into markdown so I can paste it into the ticket",
4
+ "should_trigger": true
5
+ },
2
6
  {
3
7
  "query": "Convert this Word doc to markdown",
4
8
  "should_trigger": true
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "file-to-markdown",
3
- "version": "0.2.0",
4
- "description": "Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json or .pdf and says \"read this\", \"turn this into markdown\", \"extract the text\", or wants it critiqued or briefed.",
3
+ "version": "0.3.0",
4
+ "description": "Convert a Word document, PowerPoint deck, Excel workbook, web page, CSV, JSON, text file, email (.eml or Outlook .msg) or PDF into clean Markdown that keeps headings, lists, tables, links, bold and italic, slide titles and speaker notes, and reports everything it dropped (images, charts, comments, formulas, tracked changes), so the content can be read, reviewed or fed to another skill. Use when the user hands over a .docx, .pptx, .xlsx, .html, .csv, .json, .eml, .msg or .pdf and says \"read this\", \"turn this into markdown\", \"extract the text\", or wants it critiqued or briefed.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
7
7
  "npm": [],