opencode-skills-collection 3.0.51 → 3.1.1

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 (287) hide show
  1. package/README.md +44 -12
  2. package/bundled-skills/.antigravity-install-manifest.json +84 -1
  3. package/bundled-skills/android-ui-journey-testing/SKILL.md +191 -0
  4. package/bundled-skills/ask-matt/SKILL.md +92 -0
  5. package/bundled-skills/bugs-are-annoying/SKILL.md +137 -0
  6. package/bundled-skills/codebase-design/DEEPENING.md +37 -0
  7. package/bundled-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  8. package/bundled-skills/codebase-design/SKILL.md +145 -0
  9. package/bundled-skills/competitor-analysis/LICENSE.txt +21 -0
  10. package/bundled-skills/competitor-analysis/SKILL.md +434 -0
  11. package/bundled-skills/competitor-analysis/references/battle-card-subagent.md +127 -0
  12. package/bundled-skills/competitor-analysis/references/battle-card.md +91 -0
  13. package/bundled-skills/competitor-analysis/references/example-research.md +130 -0
  14. package/bundled-skills/competitor-analysis/references/report-template.html +127 -0
  15. package/bundled-skills/competitor-analysis/references/research-patterns.md +217 -0
  16. package/bundled-skills/competitor-analysis/references/workflow.md +434 -0
  17. package/bundled-skills/competitor-analysis/scripts/capture_screenshots.mjs +142 -0
  18. package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +929 -0
  19. package/bundled-skills/competitor-analysis/scripts/extract_vs_names.mjs +140 -0
  20. package/bundled-skills/competitor-analysis/scripts/gate_candidates.mjs +194 -0
  21. package/bundled-skills/competitor-analysis/scripts/list_urls.mjs +90 -0
  22. package/bundled-skills/competitor-analysis/scripts/md_utils.mjs +50 -0
  23. package/bundled-skills/competitor-analysis/scripts/merge_partials.mjs +291 -0
  24. package/bundled-skills/competitor-analysis/scripts/package.json +6 -0
  25. package/bundled-skills/design-it/3d-ui/SKILL.md +259 -0
  26. package/bundled-skills/design-it/SKILL.md +170 -0
  27. package/bundled-skills/design-it/ai-native-ui/SKILL.md +295 -0
  28. package/bundled-skills/design-it/aurora-ui/SKILL.md +307 -0
  29. package/bundled-skills/design-it/bento-ui/SKILL.md +314 -0
  30. package/bundled-skills/design-it/brutalism/SKILL.md +270 -0
  31. package/bundled-skills/design-it/brutalist-typography/SKILL.md +287 -0
  32. package/bundled-skills/design-it/card-based-design/SKILL.md +262 -0
  33. package/bundled-skills/design-it/claymorphism/SKILL.md +287 -0
  34. package/bundled-skills/design-it/color-blocking/SKILL.md +278 -0
  35. package/bundled-skills/design-it/command-center-ui/SKILL.md +345 -0
  36. package/bundled-skills/design-it/cyber-y2k/SKILL.md +312 -0
  37. package/bundled-skills/design-it/cyberpunk-ui/SKILL.md +262 -0
  38. package/bundled-skills/design-it/dark-mode/SKILL.md +289 -0
  39. package/bundled-skills/design-it/dashboard-design/SKILL.md +331 -0
  40. package/bundled-skills/design-it/data-dense-design/SKILL.md +322 -0
  41. package/bundled-skills/design-it/duotone-design/SKILL.md +248 -0
  42. package/bundled-skills/design-it/editorial-design/SKILL.md +328 -0
  43. package/bundled-skills/design-it/flat-design/SKILL.md +221 -0
  44. package/bundled-skills/design-it/flat-design-2/SKILL.md +240 -0
  45. package/bundled-skills/design-it/floating-ui/SKILL.md +299 -0
  46. package/bundled-skills/design-it/frutiger-aero/SKILL.md +274 -0
  47. package/bundled-skills/design-it/glassmorphism/SKILL.md +272 -0
  48. package/bundled-skills/design-it/gradient-design/SKILL.md +309 -0
  49. package/bundled-skills/design-it/high-contrast/SKILL.md +288 -0
  50. package/bundled-skills/design-it/holographic-ui/SKILL.md +310 -0
  51. package/bundled-skills/design-it/isometric-design/SKILL.md +228 -0
  52. package/bundled-skills/design-it/layered-design/SKILL.md +247 -0
  53. package/bundled-skills/design-it/material-design/SKILL.md +275 -0
  54. package/bundled-skills/design-it/maximalism/SKILL.md +297 -0
  55. package/bundled-skills/design-it/minimalism/SKILL.md +267 -0
  56. package/bundled-skills/design-it/monochromatic-ui/SKILL.md +296 -0
  57. package/bundled-skills/design-it/neo-brutalism/SKILL.md +270 -0
  58. package/bundled-skills/design-it/neumorphism/SKILL.md +248 -0
  59. package/bundled-skills/design-it/retro-design/SKILL.md +283 -0
  60. package/bundled-skills/design-it/retro-futurism/SKILL.md +259 -0
  61. package/bundled-skills/design-it/sci-fi-interface/SKILL.md +309 -0
  62. package/bundled-skills/design-it/skeuomorphism/SKILL.md +280 -0
  63. package/bundled-skills/design-it/soft-pastel/SKILL.md +307 -0
  64. package/bundled-skills/design-it/spatial-computing-ui/SKILL.md +300 -0
  65. package/bundled-skills/design-it/spatial-design/SKILL.md +268 -0
  66. package/bundled-skills/design-it/swiss-design/SKILL.md +293 -0
  67. package/bundled-skills/design-it/synthwave/SKILL.md +257 -0
  68. package/bundled-skills/design-it/tile-design/SKILL.md +297 -0
  69. package/bundled-skills/design-it/typography-first/SKILL.md +247 -0
  70. package/bundled-skills/design-it/vaporwave/SKILL.md +331 -0
  71. package/bundled-skills/design-it/vibrant-maximalism/SKILL.md +291 -0
  72. package/bundled-skills/design-it/widget-based-design/SKILL.md +274 -0
  73. package/bundled-skills/design-it/y2k-design/SKILL.md +268 -0
  74. package/bundled-skills/diagnosing-bugs/SKILL.md +165 -0
  75. package/bundled-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  76. package/bundled-skills/docs/contributors/skill-scoring.md +235 -0
  77. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  78. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  79. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  80. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  81. package/bundled-skills/docs/users/bundles.md +1 -1
  82. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  83. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  84. package/bundled-skills/docs/users/getting-started.md +1 -1
  85. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  86. package/bundled-skills/docs/users/usage.md +4 -4
  87. package/bundled-skills/docs/users/visual-guide.md +4 -4
  88. package/bundled-skills/domain-modeling/ADR-FORMAT.md +47 -0
  89. package/bundled-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  90. package/bundled-skills/domain-modeling/SKILL.md +105 -0
  91. package/bundled-skills/grill-me/SKILL.md +36 -0
  92. package/bundled-skills/grill-with-docs/SKILL.md +36 -0
  93. package/bundled-skills/grilling/SKILL.md +39 -0
  94. package/bundled-skills/handoff/SKILL.md +45 -0
  95. package/bundled-skills/image-generator/.env.example +7 -0
  96. package/bundled-skills/image-generator/SKILL.md +509 -0
  97. package/bundled-skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  98. package/bundled-skills/improve-codebase-architecture/SKILL.md +97 -0
  99. package/bundled-skills/learn/SKILL.md +156 -0
  100. package/bundled-skills/lesson-generator/SKILL.md +90 -0
  101. package/bundled-skills/llm-council/.env.example +7 -0
  102. package/bundled-skills/llm-council/SKILL.md +602 -0
  103. package/bundled-skills/loop-library/SKILL.md +205 -0
  104. package/bundled-skills/loop-library/agents/openai.yaml +4 -0
  105. package/bundled-skills/loop-library/references/catalog.md +270 -0
  106. package/bundled-skills/mailtrap-managing-contacts/SKILL.md +112 -0
  107. package/bundled-skills/mailtrap-sending-emails/SKILL.md +167 -0
  108. package/bundled-skills/mailtrap-setting-up-sending-domain/SKILL.md +77 -0
  109. package/bundled-skills/mailtrap-testing-with-sandbox/SKILL.md +110 -0
  110. package/bundled-skills/prototype/LOGIC.md +79 -0
  111. package/bundled-skills/prototype/SKILL.md +62 -0
  112. package/bundled-skills/prototype/UI.md +112 -0
  113. package/bundled-skills/setup-matt-pocock-skills/SKILL.md +158 -0
  114. package/bundled-skills/setup-matt-pocock-skills/domain.md +51 -0
  115. package/bundled-skills/setup-matt-pocock-skills/issue-tracker-github.md +34 -0
  116. package/bundled-skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +35 -0
  117. package/bundled-skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
  118. package/bundled-skills/setup-matt-pocock-skills/triage-labels.md +15 -0
  119. package/bundled-skills/survey-generator/LICENSE +21 -0
  120. package/bundled-skills/survey-generator/SKILL.md +143 -0
  121. package/bundled-skills/survey-generator/build_artifact.py +208 -0
  122. package/bundled-skills/survey-generator/examples/agentic-engineering/research_bundle.json +1196 -0
  123. package/bundled-skills/survey-generator/examples/agentic-engineering/survey.html +706 -0
  124. package/bundled-skills/survey-generator/style_spec.json +85 -0
  125. package/bundled-skills/survey-generator/templates/research_bundle_template.json +69 -0
  126. package/bundled-skills/tdd/SKILL.md +139 -0
  127. package/bundled-skills/tdd/mocking.md +59 -0
  128. package/bundled-skills/tdd/refactoring.md +10 -0
  129. package/bundled-skills/tdd/tests.md +61 -0
  130. package/bundled-skills/teach/GLOSSARY-FORMAT.md +35 -0
  131. package/bundled-skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  132. package/bundled-skills/teach/MISSION-FORMAT.md +31 -0
  133. package/bundled-skills/teach/RESOURCES-FORMAT.md +32 -0
  134. package/bundled-skills/teach/SKILL.md +169 -0
  135. package/bundled-skills/to-issues/SKILL.md +115 -0
  136. package/bundled-skills/to-prd/SKILL.md +104 -0
  137. package/bundled-skills/tools-page-seo-optimizer/SKILL.md +616 -0
  138. package/bundled-skills/triage/AGENT-BRIEF.md +207 -0
  139. package/bundled-skills/triage/OUT-OF-SCOPE.md +105 -0
  140. package/bundled-skills/triage/SKILL.md +143 -0
  141. package/bundled-skills/vibecode-production-qa-validator/SKILL.md +371 -141
  142. package/bundled-skills/wiki-builder/SKILL.md +157 -0
  143. package/bundled-skills/wiki-builder/agents/openai.yaml +5 -0
  144. package/bundled-skills/wiki-builder/references/wiki-flavors.md +98 -0
  145. package/bundled-skills/wiki-builder/scripts/init_wiki.sh +105 -0
  146. package/bundled-skills/wiki-builder/templates/index.md +20 -0
  147. package/bundled-skills/wiki-builder/templates/maintenance-log.md +7 -0
  148. package/bundled-skills/wiki-builder/templates/prompts/compile-concept-page.md +12 -0
  149. package/bundled-skills/wiki-builder/templates/prompts/compile-index.md +11 -0
  150. package/bundled-skills/wiki-builder/templates/prompts/compile-source-page.md +12 -0
  151. package/bundled-skills/wiki-builder/templates/prompts/lint-wiki.md +10 -0
  152. package/bundled-skills/wiki-builder/templates/prompts/query-and-file.md +11 -0
  153. package/bundled-skills/wiki-builder/templates/sources.md +9 -0
  154. package/bundled-skills/wiki-builder/templates/wiki.config.md +53 -0
  155. package/bundled-skills/writing-great-skills/GLOSSARY.md +181 -0
  156. package/bundled-skills/writing-great-skills/SKILL.md +111 -0
  157. package/bundled-skills/yao-meta-skill/SKILL.md +86 -0
  158. package/bundled-skills/yao-meta-skill/agents/interface.yaml +26 -0
  159. package/bundled-skills/yao-meta-skill/manifest.json +24 -0
  160. package/bundled-skills/yao-meta-skill/references/artifact-design-doctrine.md +49 -0
  161. package/bundled-skills/yao-meta-skill/references/authoring-discipline.md +78 -0
  162. package/bundled-skills/yao-meta-skill/references/autonomous-adaptation.md +65 -0
  163. package/bundled-skills/yao-meta-skill/references/distribution-registry-method.md +60 -0
  164. package/bundled-skills/yao-meta-skill/references/eval-playbook.md +69 -0
  165. package/bundled-skills/yao-meta-skill/references/gate-selection.md +68 -0
  166. package/bundled-skills/yao-meta-skill/references/governance.md +134 -0
  167. package/bundled-skills/yao-meta-skill/references/human-review-template.md +54 -0
  168. package/bundled-skills/yao-meta-skill/references/intent-dialogue.md +138 -0
  169. package/bundled-skills/yao-meta-skill/references/iteration-philosophy.md +30 -0
  170. package/bundled-skills/yao-meta-skill/references/non-skill-decision-tree.md +39 -0
  171. package/bundled-skills/yao-meta-skill/references/operating-modes.md +107 -0
  172. package/bundled-skills/yao-meta-skill/references/output-eval-method.md +113 -0
  173. package/bundled-skills/yao-meta-skill/references/output-quality-risk.md +41 -0
  174. package/bundled-skills/yao-meta-skill/references/output-visual-quality.md +53 -0
  175. package/bundled-skills/yao-meta-skill/references/packaging-contracts.md +70 -0
  176. package/bundled-skills/yao-meta-skill/references/pattern-extraction-doctrine.md +76 -0
  177. package/bundled-skills/yao-meta-skill/references/platform-capability-matrix.md +49 -0
  178. package/bundled-skills/yao-meta-skill/references/prompt-engineering-doctrine.md +76 -0
  179. package/bundled-skills/yao-meta-skill/references/qa-ladder.md +57 -0
  180. package/bundled-skills/yao-meta-skill/references/reference-scan.md +126 -0
  181. package/bundled-skills/yao-meta-skill/references/regression-cause-taxonomy.md +80 -0
  182. package/bundled-skills/yao-meta-skill/references/resource-boundaries.md +120 -0
  183. package/bundled-skills/yao-meta-skill/references/review-studio-method.md +87 -0
  184. package/bundled-skills/yao-meta-skill/references/review-waiver-method.md +76 -0
  185. package/bundled-skills/yao-meta-skill/references/runtime-conformance-method.md +21 -0
  186. package/bundled-skills/yao-meta-skill/references/skill-archetypes.md +86 -0
  187. package/bundled-skills/yao-meta-skill/references/skill-atlas-method.md +35 -0
  188. package/bundled-skills/yao-meta-skill/references/skill-engineering-method.md +210 -0
  189. package/bundled-skills/yao-meta-skill/references/skill-ir-method.md +41 -0
  190. package/bundled-skills/yao-meta-skill/references/skillops-decision-policy.md +53 -0
  191. package/bundled-skills/yao-meta-skill/references/systems-thinking-doctrine.md +75 -0
  192. package/bundled-skills/yao-meta-skill/references/telemetry-drift-method.md +182 -0
  193. package/bundled-skills/yao-meta-skill/references/trust-security-method.md +79 -0
  194. package/bundled-skills/yao-meta-skill/references/user-memory-policy.md +35 -0
  195. package/bundled-skills/youtube-notetaker/SKILL.md +209 -0
  196. package/bundled-skills/youtube-notetaker/reference/artifact.html +269 -0
  197. package/bundled-skills/youtube-notetaker/scripts/contact_sheet.py +53 -0
  198. package/bundled-skills/youtube-notetaker/scripts/detect_slides.sh +19 -0
  199. package/bundled-skills/youtube-notetaker/scripts/download.sh +24 -0
  200. package/bundled-skills/youtube-notetaker/scripts/extract_slides.py +43 -0
  201. package/bundled-skills/youtube-notetaker/scripts/serve.py +173 -0
  202. package/bundled-skills/youtube-notetaker/scripts/setup.sh +27 -0
  203. package/bundled-skills/youtube-notetaker/scripts/verify.sh +31 -0
  204. package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +59 -0
  205. package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +69 -0
  206. package/dist/skill-pointer/config-loader.d.ts +14 -0
  207. package/dist/skill-pointer/config-loader.js +30 -3
  208. package/dist/skill-pointer/content-scanner.d.ts +38 -0
  209. package/dist/skill-pointer/content-scanner.js +118 -0
  210. package/dist/skill-pointer/index.d.ts +7 -2
  211. package/dist/skill-pointer/index.js +14 -4
  212. package/dist/skill-pointer/pointer-generator.js +2 -0
  213. package/dist/skill-pointer/skill-patcher.d.ts +13 -0
  214. package/dist/skill-pointer/skill-patcher.js +99 -0
  215. package/package.json +1 -1
  216. package/skills_index.json +1956 -286
  217. package/bundled-skills/ai-md/SKILL.md +0 -523
  218. package/bundled-skills/atlas-contract/SKILL.md +0 -650
  219. package/bundled-skills/busybox-on-windows/SKILL.md +0 -40
  220. package/bundled-skills/monte-carlo-prevent/SKILL.md +0 -257
  221. package/bundled-skills/monte-carlo-prevent/references/TROUBLESHOOTING.md +0 -23
  222. package/bundled-skills/monte-carlo-prevent/references/parameters.md +0 -32
  223. package/bundled-skills/monte-carlo-prevent/references/workflows.md +0 -478
  224. package/bundled-skills/monte-carlo-push-ingestion/SKILL.md +0 -372
  225. package/bundled-skills/monte-carlo-push-ingestion/references/anomaly-detection.md +0 -87
  226. package/bundled-skills/monte-carlo-push-ingestion/references/custom-lineage.md +0 -203
  227. package/bundled-skills/monte-carlo-push-ingestion/references/direct-http-api.md +0 -207
  228. package/bundled-skills/monte-carlo-push-ingestion/references/prerequisites.md +0 -150
  229. package/bundled-skills/monte-carlo-push-ingestion/references/push-lineage.md +0 -160
  230. package/bundled-skills/monte-carlo-push-ingestion/references/push-metadata.md +0 -158
  231. package/bundled-skills/monte-carlo-push-ingestion/references/push-query-logs.md +0 -219
  232. package/bundled-skills/monte-carlo-push-ingestion/references/validation.md +0 -257
  233. package/bundled-skills/monte-carlo-push-ingestion/scripts/sample_verify.py +0 -357
  234. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_lineage.py +0 -70
  235. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_metadata.py +0 -65
  236. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_and_push_query_logs.py +0 -70
  237. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_lineage.py +0 -214
  238. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_metadata.py +0 -160
  239. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/collect_query_logs.py +0 -164
  240. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_lineage.py +0 -198
  241. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_metadata.py +0 -193
  242. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery/push_query_logs.py +0 -207
  243. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_metadata.py +0 -71
  244. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_and_push_query_logs.py +0 -64
  245. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_metadata.py +0 -253
  246. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/collect_query_logs.py +0 -149
  247. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_metadata.py +0 -190
  248. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/bigquery-iceberg/push_query_logs.py +0 -208
  249. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_lineage.py +0 -83
  250. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_metadata.py +0 -77
  251. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_and_push_query_logs.py +0 -83
  252. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_lineage.py +0 -240
  253. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_metadata.py +0 -212
  254. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/collect_query_logs.py +0 -204
  255. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_lineage.py +0 -192
  256. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_metadata.py +0 -178
  257. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/databricks/push_query_logs.py +0 -200
  258. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_lineage.py +0 -119
  259. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_metadata.py +0 -119
  260. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_and_push_query_logs.py +0 -117
  261. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_lineage.py +0 -265
  262. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_metadata.py +0 -313
  263. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/collect_query_logs.py +0 -284
  264. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_lineage.py +0 -309
  265. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_metadata.py +0 -245
  266. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/hive/push_query_logs.py +0 -255
  267. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_lineage.py +0 -78
  268. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_metadata.py +0 -80
  269. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_and_push_query_logs.py +0 -88
  270. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_lineage.py +0 -235
  271. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_metadata.py +0 -219
  272. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/collect_query_logs.py +0 -239
  273. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_lineage.py +0 -178
  274. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_metadata.py +0 -178
  275. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/redshift/push_query_logs.py +0 -196
  276. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_lineage.py +0 -154
  277. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_metadata.py +0 -137
  278. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_and_push_query_logs.py +0 -137
  279. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_lineage.py +0 -349
  280. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_metadata.py +0 -329
  281. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/collect_query_logs.py +0 -254
  282. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_lineage.py +0 -307
  283. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_metadata.py +0 -228
  284. package/bundled-skills/monte-carlo-push-ingestion/scripts/templates/snowflake/push_query_logs.py +0 -248
  285. package/bundled-skills/monte-carlo-push-ingestion/scripts/test_template_sdk_usage.py +0 -340
  286. package/bundled-skills/skill-optimizer/SKILL.md +0 -271
  287. package/bundled-skills/using-superpowers/SKILL.md +0 -98
@@ -0,0 +1,929 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Compiles per-competitor markdown files into an HTML report + CSV.
4
+ // Produces four views: index.html (overview), competitors/*.html (deep dive),
5
+ // matrix.html (side-by-side feature/pricing grid), mentions.html (chronological feed).
6
+ //
7
+ // Usage: node compile_report.mjs <research-dir> [--user-company "Acme"] [--template <path>] [--open]
8
+
9
+ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
20
+ console.error(`Usage: node compile_report.mjs <research-dir> [--user-company "<name>"] [--template <path>] [--open]
21
+
22
+ Reads all .md files from <research-dir>, generates:
23
+ - index.html — overview: competitor table with tagline, pricing, features, strategic diff
24
+ - competitors/<slug>.html — per-competitor deep dive pages
25
+ - matrix.html — side-by-side feature/pricing grid across competitors
26
+ - mentions.html — chronological feed of all external mentions with source-type filter
27
+ - results.csv — flat spreadsheet
28
+
29
+ Options:
30
+ --user-company <name> Name of the user's company (used in comparison sections)
31
+ --template <path> Path to report-template.html (default: auto-detect)
32
+ --open Open index.html in the default browser after generation
33
+ --help, -h Show this help message`);
34
+ process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1);
35
+ }
36
+
37
+ const dir = args[0];
38
+ const shouldOpen = args.includes('--open');
39
+ const userCompanyIdx = args.indexOf('--user-company');
40
+ const userCompany = userCompanyIdx !== -1 ? args[userCompanyIdx + 1] : '';
41
+ const templateIdx = args.indexOf('--template');
42
+ let templatePath = templateIdx !== -1 ? args[templateIdx + 1] : null;
43
+
44
+ if (!templatePath) {
45
+ const candidates = [
46
+ join(__dirname, '..', 'references', 'report-template.html'),
47
+ join(__dirname, 'report-template.html'),
48
+ ];
49
+ templatePath = candidates.find(p => existsSync(p));
50
+ if (!templatePath) {
51
+ console.error('Error: Could not find report-template.html. Use --template to specify path.');
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ const template = readFileSync(templatePath, 'utf-8');
57
+
58
+ let files;
59
+ try {
60
+ files = readdirSync(dir).filter(f => f.endsWith('.md')).sort();
61
+ } catch (err) {
62
+ console.error(`Error reading directory ${dir}: ${err.message}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ if (files.length === 0) {
67
+ console.error(`No .md files found in ${dir}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ // ---------- Parsing ----------
72
+
73
+ // parseFrontmatter, parseBody, parseSections imported from md_utils.mjs
74
+
75
+ // Normalize subagent-invented source types onto the canonical taxonomy so the mentions
76
+ // feed CSS has a pill class for every entry. Observed drift: HackerNews→HN, VendorBlog→Blog,
77
+ // CompetitorBlog→Blog, GitHubIssue→Blog, Twitter→X. Unknown types fall back to "Blog" to
78
+ // guarantee styled rendering (catch-all). Also handles free-text leaking into the bracket
79
+ // slot (e.g. "Browsaur Blog — ..." — sourceType becomes "Blog" if we can find that token).
80
+ function normalizeSourceType(raw) {
81
+ if (!raw) return 'Blog';
82
+ const t = raw.trim();
83
+ const canonical = new Set([
84
+ 'Benchmark','Comparison','News','Reddit','HN','LinkedIn','YouTube',
85
+ 'Review','Podcast','X','DevTo','Hashnode','Substack','Blog'
86
+ ]);
87
+ if (canonical.has(t)) return t;
88
+ // Alias table for common drifts
89
+ const aliases = {
90
+ 'Hacker News': 'HN', 'HackerNews': 'HN', 'Show HN': 'HN', 'Ask HN': 'HN',
91
+ 'Twitter': 'X',
92
+ 'Vendor Blog': 'Blog', 'VendorBlog': 'Blog',
93
+ 'Competitor Blog': 'Blog', 'CompetitorBlog': 'Blog',
94
+ 'GitHub Issue': 'Blog', 'GitHubIssue': 'Blog', 'GitHub': 'Blog',
95
+ 'Documentation': 'Blog', 'Docs': 'Blog',
96
+ 'Medium': 'Blog', 'Substack Post': 'Substack',
97
+ };
98
+ if (aliases[t]) return aliases[t];
99
+ // Keyword scan — if the raw contains a canonical token anywhere, use that.
100
+ for (const c of canonical) {
101
+ if (new RegExp(`\\b${c}\\b`, 'i').test(t)) return c;
102
+ }
103
+ return 'Blog'; // catch-all for fully unknown types (styled via .src-Blog)
104
+ }
105
+
106
+ // Parse Mentions section into structured entries.
107
+ // Format: `- **[SourceType]** Title | Snippet (source: URL, YYYY-MM-DD)`
108
+ function parseMentions(sectionText) {
109
+ if (!sectionText) return [];
110
+ const out = [];
111
+ for (const raw of sectionText.split('\n')) {
112
+ const line = raw.trim();
113
+ if (!line.startsWith('- ')) continue;
114
+ const typeM = line.match(/^-\s*\*\*\[([^\]]+)\]\*\*\s*(.*)$/);
115
+ if (!typeM) continue;
116
+ const sourceType = normalizeSourceType(typeM[1].trim());
117
+ let rest = typeM[2];
118
+
119
+ let url = '';
120
+ let date = '';
121
+ const sourceM = rest.match(/\(source:\s*([^)]+)\)\s*$/);
122
+ if (sourceM) {
123
+ const sourceBlock = sourceM[1];
124
+ const parts = sourceBlock.split(',').map(s => s.trim()).filter(Boolean);
125
+ url = parts[0] || '';
126
+ const dateCandidate = parts.slice(1).join(', ');
127
+ if (dateCandidate && /\d{4}-\d{2}-\d{2}/.test(dateCandidate)) date = dateCandidate.match(/\d{4}-\d{2}-\d{2}/)[0];
128
+ rest = rest.slice(0, sourceM.index).trim();
129
+ }
130
+
131
+ let title = rest;
132
+ let snippet = '';
133
+ const pipeIdx = rest.indexOf('|');
134
+ if (pipeIdx !== -1) {
135
+ title = rest.slice(0, pipeIdx).trim();
136
+ snippet = rest.slice(pipeIdx + 1).trim();
137
+ }
138
+
139
+ out.push({ sourceType, title, snippet, url, date });
140
+ }
141
+ return out;
142
+ }
143
+
144
+ // Parse Benchmarks section into structured entries.
145
+ // Format: `- Title | Source | URL | Key finding` or `- **Title** — Source (URL): finding`
146
+ function parseBenchmarks(sectionText) {
147
+ if (!sectionText) return [];
148
+ const out = [];
149
+ for (const raw of sectionText.split('\n')) {
150
+ const line = raw.trim();
151
+ if (!line.startsWith('- ')) continue;
152
+ const rest = line.slice(2).trim();
153
+ const parts = rest.split('|').map(s => s.trim()).filter(Boolean);
154
+ let title = '', source = '', url = '', finding = '';
155
+ if (parts.length >= 4) {
156
+ [title, source, url, finding] = parts;
157
+ } else if (parts.length === 3) {
158
+ [title, url, finding] = parts;
159
+ } else {
160
+ title = rest;
161
+ const urlM = rest.match(/https?:\/\/\S+/);
162
+ if (urlM) url = urlM[0];
163
+ }
164
+ out.push({ title, source, url, finding });
165
+ }
166
+ return out;
167
+ }
168
+
169
+ function splitPipes(s) {
170
+ return (s || '').split('|').map(x => x.trim()).filter(Boolean);
171
+ }
172
+
173
+ function escapeHtml(str) {
174
+ return (str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
175
+ }
176
+
177
+ function mdToHtml(md) {
178
+ const lines = md.split('\n');
179
+ const out = [];
180
+ let inList = false;
181
+ let paraLines = [];
182
+
183
+ function flushPara() {
184
+ if (paraLines.length > 0) {
185
+ let text = escapeHtml(paraLines.join(' ').trim());
186
+ text = text.replace(/\*\*\[(\w+)\]\*\*/g, '<span class="confidence $1">[$1]</span>');
187
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
188
+ if (text) out.push(`<p>${text}</p>`);
189
+ paraLines = [];
190
+ }
191
+ }
192
+ function closeList() { if (inList) { out.push('</ul>'); inList = false; } }
193
+
194
+ for (const line of lines) {
195
+ const trimmed = line.trim();
196
+ if (!trimmed) { flushPara(); closeList(); continue; }
197
+ if (trimmed.startsWith('## ')) { flushPara(); closeList(); out.push(`<h2>${escapeHtml(trimmed.slice(3))}</h2>`); continue; }
198
+ if (trimmed.startsWith('### ')) { flushPara(); closeList(); out.push(`<h3>${escapeHtml(trimmed.slice(4))}</h3>`); continue; }
199
+ if (trimmed.startsWith('- ')) {
200
+ flushPara();
201
+ if (!inList) { out.push('<ul>'); inList = true; }
202
+ let text = escapeHtml(trimmed.slice(2));
203
+ text = text.replace(/\*\*\[(\w+)\]\*\*/g, '<span class="confidence $1">[$1]</span>');
204
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
205
+ text = text.replace(/(https?:\/\/\S+)/g, (_, raw) => {
206
+ let url = raw;
207
+ let trail = '';
208
+ while (url && /[)\],.;:!?]$/.test(url)) {
209
+ trail = url.slice(-1) + trail;
210
+ url = url.slice(0, -1);
211
+ }
212
+ if (!url) return raw;
213
+ return `<a href="${url}" target="_blank">${url}</a>${trail}`;
214
+ });
215
+ out.push(`<li>${text}</li>`);
216
+ continue;
217
+ }
218
+ closeList();
219
+ paraLines.push(trimmed);
220
+ }
221
+ flushPara(); closeList();
222
+ return out.join('\n');
223
+ }
224
+
225
+ // ---------- Load all competitor records ----------
226
+
227
+ const competitors = [];
228
+ for (const file of files) {
229
+ const content = readFileSync(join(dir, file), 'utf-8');
230
+ const fields = parseFrontmatter(content);
231
+ if (!fields) continue;
232
+ const body = parseBody(content);
233
+ const sections = parseSections(body);
234
+ const mentions = parseMentions(sections['Mentions']);
235
+ const benchmarks = parseBenchmarks(sections['Benchmarks']);
236
+ const slug = file.replace('.md', '');
237
+ competitors.push({ ...fields, body, sections, mentions, benchmarks, slug, file });
238
+ }
239
+
240
+ // Deduplicate by normalized competitor name (keep first occurrence — richer data tends to come first alphabetically)
241
+ // The `\b` word boundary before the suffix group is load-bearing: without it the regex would
242
+ // strip "co" from inside names like "Cisco" or "Costco" (`\s*` matches zero chars), corrupting
243
+ // the dedup key and silently dropping legit competitors.
244
+ const seen = new Map();
245
+ for (const c of competitors) {
246
+ const name = (c.competitor_name || '').toLowerCase().replace(/\s*\b(inc|llc|ltd|corp|co)\b\s*\.?$/i, '').trim();
247
+ if (!seen.has(name)) seen.set(name, c);
248
+ }
249
+ const deduped = [...seen.values()].sort((a, b) => (a.competitor_name || '').localeCompare(b.competitor_name || ''));
250
+
251
+ // Load the curated matrix EARLY — the overview table needs userCompany.name to filter the
252
+ // user's own company out of the competitor list, and the strategic summary card needs the
253
+ // whole matrix. Keep this block above the first use site to avoid temporal dead zones.
254
+ let curatedMatrix = null;
255
+ try {
256
+ const p = join(dir, 'matrix.json');
257
+ if (existsSync(p)) curatedMatrix = JSON.parse(readFileSync(p, 'utf-8'));
258
+ } catch (err) {
259
+ console.error(`Warning: matrix.json present but unreadable — falling back to pipe split. ${err.message}`);
260
+ }
261
+
262
+ // Filter the user's own company out before computing any "competitor" totals or rendering
263
+ // any view. matrix.json's userCompany.name wins; fall back to the --user-company CLI arg.
264
+ // Match case-insensitively against competitor_name AND slug. EVERY downstream loop that
265
+ // represents "the competitor set" (matrix.html columns, mentions feed, totals, strategic
266
+ // summary, per-competitor pages, CSV) must iterate `competitorRows`, not `deduped` —
267
+ // otherwise the user appears as a phantom column with all-false features.
268
+ const userCompanyName = (curatedMatrix && curatedMatrix.userCompany && curatedMatrix.userCompany.name) || userCompany || '';
269
+ // Normalize before comparing so legal/DBA drift ("Exa, Inc." in matrix.json vs slug `exa`)
270
+ // still excludes the user's own file. Strip a trailing corporate suffix then all non-alphanumerics.
271
+ // We deliberately do NOT fuzzy-match — exclusion still requires an exact normalized-equality, so a
272
+ // real competitor is only dropped if its normalized name/slug is identical to the user's.
273
+ const normKey = s => (s || '').toLowerCase().replace(/\s*\b(inc|llc|ltd|corp|co)\b\s*\.?$/i, '').replace(/[^a-z0-9]/g, '');
274
+ const userKey = normKey(userCompanyName);
275
+ // Slug compare strips punctuation only (no suffix strip) so `rival-co` isn't reduced to `rival`.
276
+ const slugKey = s => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
277
+ const userSlugKey = slugKey(userCompanyName);
278
+ const competitorRows = deduped.filter(c => {
279
+ if (!userKey) return true;
280
+ const nameKey = normKey(c.competitor_name);
281
+ const sKey = slugKey(c.slug);
282
+ return nameKey !== userKey && sKey !== userKey && sKey !== userSlugKey;
283
+ });
284
+
285
+ // ---------- Aggregates ----------
286
+
287
+ const totalMentions = competitorRows.reduce((sum, c) => sum + c.mentions.length, 0);
288
+ const totalBenchmarks = competitorRows.reduce((sum, c) => sum + c.benchmarks.length, 0);
289
+ const withPricing = competitorRows.filter(c => c.pricing_tiers).length;
290
+
291
+ const dirName = dir.split('/').pop();
292
+ const title = dirName.replace(/_/g, ' ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
293
+ const genDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
294
+ const metaLine = `${competitorRows.length} competitors · ${totalMentions} mentions · ${totalBenchmarks} benchmarks · ${genDate}`;
295
+
296
+ // ---------- index.html (overview) ----------
297
+
298
+ function featurePills(featuresStr, max = 4) {
299
+ // key_features is supposed to be pipe-separated but subagents drift into prose.
300
+ // If no pipes are present, split on commas as a fallback so we still show something
301
+ // and cap item length to avoid bleeding wall-of-text into the table.
302
+ let feats = splitPipes(featuresStr);
303
+ if (feats.length <= 1 && featuresStr) {
304
+ feats = featuresStr.split(/[;,]/).map(s => s.trim()).filter(Boolean);
305
+ }
306
+ return feats.slice(0, max).map(f => {
307
+ const short = f.length > 42 ? f.slice(0, 40).replace(/\s+\S*$/, '') + '…' : f;
308
+ return `<span class="pill pill-feature">${escapeHtml(short)}</span>`;
309
+ }).join('');
310
+ }
311
+
312
+ function truncate(str, n) {
313
+ if (!str) return '';
314
+ if (str.length <= n) return str;
315
+ return str.slice(0, n - 1).replace(/\s+\S*$/, '') + '…';
316
+ }
317
+
318
+ const tableRows = competitorRows.map(c => {
319
+ const hasDetail = c.body && c.body.length > 50;
320
+ const nameHtml = hasDetail
321
+ ? `<a href="competitors/${c.slug}.html">${escapeHtml(c.competitor_name)}</a>`
322
+ : escapeHtml(c.competitor_name);
323
+ const websiteHtml = c.website
324
+ ? `<span class="muted-line"><a href="${escapeHtml(c.website)}" target="_blank" style="color:var(--muted);">${escapeHtml(c.website.replace(/^https?:\/\/(www\.)?/, ''))}</a></span>`
325
+ : '';
326
+ // Pricing: prefer pipe-split summary; if there are no pipes (prose drift), truncate hard.
327
+ let pricingShort = splitPipes(c.pricing_tiers).slice(0, 3).join(' · ');
328
+ if (!pricingShort) pricingShort = truncate(c.pricing_tiers || '', 140) || '—';
329
+ return ` <tr>
330
+ <td><strong>${nameHtml}</strong>${websiteHtml}</td>
331
+ <td style="max-width:260px;">${escapeHtml(truncate(c.tagline || c.positioning || c.product_description || '', 140))}</td>
332
+ <td style="max-width:180px;">${escapeHtml(pricingShort)}</td>
333
+ <td style="max-width:260px;">${featurePills(c.key_features)}</td>
334
+ <td class="muted-line" style="max-width:260px;color:var(--muted);font-size:0.8125rem;">${escapeHtml(truncate(c.strategic_diff || '', 160))}</td>
335
+ </tr>`;
336
+ }).join('\n');
337
+
338
+ // curatedMatrix was loaded earlier (before the overview table renderer needed userCompany.name).
339
+ // Keeping this comment as a marker for the matrix-axis functions below.
340
+
341
+ // Strategic summary — "Where are you winning?" / "Where are you losing?"
342
+ // Requires matrix.json to carry a `userCompany` entry with feature flags. We then
343
+ // compare the user's flag per feature against how many competitors also have it.
344
+ // - Winning: user has the feature + at most 1 competitor has it (differentiated).
345
+ // - Losing: user LACKS the feature + 3 or more competitors have it (common gap).
346
+ // If userCompany is absent we render nothing — a skill run that skipped Step 5's
347
+ // matrix synthesis shouldn't get a broken/empty block here.
348
+ function buildStrategicSummary() {
349
+ if (!curatedMatrix || !curatedMatrix.userCompany) return '';
350
+ const user = curatedMatrix.userCompany;
351
+ const userName = user.name || userCompany || 'You';
352
+ const userEsc = escapeHtml(userName);
353
+
354
+ function analyze(kind) {
355
+ const axis = curatedMatrix[kind] || [];
356
+ const compMap = curatedMatrix.competitors || {};
357
+ const userFlags = user[kind] || {};
358
+ const wins = [];
359
+ const losses = [];
360
+ for (const entry of axis) {
361
+ const label = entry.name;
362
+ const userHas = !!userFlags[label];
363
+ const whoElseHas = [];
364
+ for (const c of competitorRows) {
365
+ const compEntry = compMap[c.slug];
366
+ if (compEntry && compEntry[kind] && compEntry[kind][label]) whoElseHas.push(c.competitor_name);
367
+ }
368
+ const competitorCount = whoElseHas.length;
369
+ if (userHas && competitorCount <= 1) {
370
+ wins.push({ label, whoElseHas });
371
+ } else if (!userHas && competitorCount >= 3) {
372
+ losses.push({ label, whoElseHas });
373
+ }
374
+ }
375
+ // Order wins by rarity (fewest competitors have it first → most differentiated).
376
+ wins.sort((a, b) => a.whoElseHas.length - b.whoElseHas.length);
377
+ // Order losses by how many competitors have it (more = bigger gap).
378
+ losses.sort((a, b) => b.whoElseHas.length - a.whoElseHas.length);
379
+ return { wins, losses };
380
+ }
381
+
382
+ const featureAnalysis = analyze('features');
383
+ const integrationAnalysis = analyze('integrations');
384
+ const allWins = [...featureAnalysis.wins, ...integrationAnalysis.wins];
385
+ const allLosses = [...featureAnalysis.losses, ...integrationAnalysis.losses];
386
+
387
+ function renderList(items, emptyMessage) {
388
+ if (!items.length) return `<div class="empty">${escapeHtml(emptyMessage)}</div>`;
389
+ return `<ul>${items.slice(0, 10).map(it => {
390
+ const n = it.whoElseHas.length;
391
+ const who = n === 0 ? 'only you' : (n <= 3 ? it.whoElseHas.join(', ') : `${n} competitors`);
392
+ return `<li><span class="label">${escapeHtml(it.label)}</span><span class="who">${escapeHtml(who)}</span></li>`;
393
+ }).join('')}</ul>`;
394
+ }
395
+
396
+ // Prefer the analyst-written prose from matrix.json when present — reads as narrative,
397
+ // not a spreadsheet. Falls back to the bulleted list when no prose is provided so a
398
+ // skill run that skipped the prose step still surfaces the boolean comparison.
399
+ function renderBody(prose, items, emptyMessage) {
400
+ if (prose && prose.trim()) return `<p class="prose">${escapeHtml(prose)}</p>`;
401
+ return renderList(items, emptyMessage);
402
+ }
403
+
404
+ // The badge counts the boolean-heuristic list. When analyst prose is shown instead of that
405
+ // list, the count can be 0 or unrelated to the prose — so only show the badge when the list
406
+ // is what's actually rendered.
407
+ const winBadge = (user.winningSummary && user.winningSummary.trim()) ? '' : ` <span class="badge win">${allWins.length}</span>`;
408
+ const lossBadge = (user.losingSummary && user.losingSummary.trim()) ? '' : ` <span class="badge loss">${allLosses.length}</span>`;
409
+
410
+ return `<div class="strategic">
411
+ <div class="card win">
412
+ <h3>Where ${userEsc} is winning${winBadge}</h3>
413
+ ${user.winningSummary ? '' : `<div class="sub">Features and integrations ${userEsc} has that 0–1 competitors match.</div>`}
414
+ ${renderBody(user.winningSummary, allWins, 'No clear differentiators found — user has no unique features in the current taxonomy.')}
415
+ </div>
416
+ <div class="card loss">
417
+ <h3>Where ${userEsc} is losing${lossBadge}</h3>
418
+ ${user.losingSummary ? '' : `<div class="sub">Features and integrations ${userEsc} lacks that 3+ competitors have.</div>`}
419
+ ${renderBody(user.losingSummary, allLosses, 'No major gaps found — user keeps up on table-stakes features.')}
420
+ </div>
421
+ </div>`;
422
+ }
423
+
424
+ const strategicSummary = buildStrategicSummary();
425
+
426
+ let indexHtml = template
427
+ .replace(/\{\{TITLE\}\}/g, escapeHtml(`${title}`))
428
+ .replace(/\{\{META\}\}/g, escapeHtml(metaLine))
429
+ .replace(/\{\{TOTAL\}\}/g, String(competitorRows.length))
430
+ .replace(/\{\{MENTION_COUNT\}\}/g, String(totalMentions))
431
+ .replace(/\{\{BENCHMARK_COUNT\}\}/g, String(totalBenchmarks))
432
+ .replace(/\{\{WITH_PRICING\}\}/g, String(withPricing))
433
+ .replace(/\{\{STRATEGIC_SUMMARY\}\}/g, strategicSummary)
434
+ .replace(/\{\{TABLE_ROWS\}\}/g, tableRows);
435
+
436
+ writeFileSync(join(dir, 'index.html'), indexHtml);
437
+
438
+ // ---------- competitors/{slug}.html ----------
439
+
440
+ try { mkdirSync(join(dir, 'competitors'), { recursive: true }); } catch {}
441
+
442
+ const perCompetitorCss = `
443
+ :root { --brand:#F03603; --blue:#4DA9E4; --black:#100D0D; --gray:#514F4F; --border:#edebeb; --bg:#F9F6F4; --card:#ffffff; --text:#100D0D; --muted:#514F4F; }
444
+ * { margin:0; padding:0; box-sizing:border-box; }
445
+ body { font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif; background:var(--bg); color:var(--text); line-height:1.6; font-size:16px; }
446
+ .container { max-width:880px; margin:0 auto; padding:2rem 1.5rem; }
447
+ a { color:var(--brand); text-decoration:none; }
448
+ a:hover { text-decoration:underline; }
449
+ .back { font-size:0.875rem; color:var(--muted); margin-bottom:1.5rem; display:inline-block; }
450
+ .back:hover { color:var(--brand); }
451
+ header { margin-bottom:2rem; }
452
+ header h1 { font-size:1.5rem; font-weight:600; margin-bottom:0.25rem; }
453
+ header .meta { color:var(--muted); font-size:0.875rem; }
454
+ .fields { background:var(--card); border:1px solid var(--border); border-radius:4px; padding:1.25rem; margin-bottom:2rem; display:grid; grid-template-columns:auto 1fr; gap:0.375rem 1rem; font-size:0.875rem; }
455
+ .fields dt { color:var(--muted); font-weight:500; }
456
+ .fields dd { color:var(--text); }
457
+ .research { background:var(--card); border:1px solid var(--border); border-radius:4px; padding:1.5rem; margin-bottom:1.25rem; }
458
+ .research h2 { font-size:1.125rem; font-weight:600; margin:1.5rem 0 0.5rem 0; color:var(--black); }
459
+ .research h2:first-child { margin-top:0; }
460
+ .research h3 { font-size:0.9375rem; font-weight:600; margin:1rem 0 0.375rem 0; color:var(--black); }
461
+ .research p { margin-bottom:0.75rem; }
462
+ .research ul { margin:0.5rem 0 1rem 1.25rem; }
463
+ .research li { margin-bottom:0.375rem; font-size:0.875rem; }
464
+ .research.battle { border-left:3px solid var(--brand); }
465
+ .research.battle h2 { color:var(--brand); }
466
+ .research.battle h3 { text-transform:uppercase; letter-spacing:0.04em; font-size:0.75rem; color:var(--muted); margin-top:1.25rem; }
467
+ .confidence { font-size:0.75rem; font-weight:600; padding:1px 6px; border-radius:2px; }
468
+ .confidence.high { background:rgba(144,201,77,0.12); color:#5a8a1a; }
469
+ .confidence.medium { background:rgba(244,186,65,0.12); color:#9a7520; }
470
+ .confidence.low { background:rgba(240,54,3,0.08); color:var(--brand); }
471
+ .mention-item { display:flex; gap:0.5rem; align-items:flex-start; padding:0.5rem 0; border-bottom:1px solid var(--border); font-size:0.875rem; }
472
+ .mention-item:last-child { border-bottom:none; }
473
+ .src-pill { font-size:0.6875rem; font-weight:600; padding:2px 8px; border-radius:999px; white-space:nowrap; border:1px solid; }
474
+ .src-Benchmark { background:rgba(77,169,228,0.12); color:#2172a3; border-color:rgba(77,169,228,0.4); }
475
+ .src-Comparison { background:rgba(240,54,3,0.10); color:var(--brand); border-color:rgba(240,54,3,0.4); }
476
+ .src-News { background:#f2f2f2; color:var(--black); border-color:#ddd; }
477
+ .src-Reddit { background:#fff2eb; color:#d84300; border-color:#ffd4b7; }
478
+ .src-HN { background:#fff4e5; color:#c95500; border-color:#ffcc99; }
479
+ .src-LinkedIn { background:#e7f1fa; color:#0a66c2; border-color:#b3d4ee; }
480
+ .src-YouTube { background:#ffebee; color:#c4302b; border-color:#f7b2ae; }
481
+ .src-Review { background:rgba(144,201,77,0.12); color:#5a8a1a; border-color:rgba(144,201,77,0.4); }
482
+ .src-Podcast { background:#efe7fa; color:#6236c2; border-color:#d1bde9; }
483
+ .src-X { background:#eef2f7; color:#111; border-color:#cfd9e5; }
484
+ .src-Twitter { background:#eef2f7; color:#111; border-color:#cfd9e5; }
485
+ .src-DevTo { background:#f3f3f6; color:#0a0a0a; border-color:#dcdce0; }
486
+ .src-Hashnode { background:#eef4ff; color:#2962ff; border-color:#c6d8ff; }
487
+ .src-Substack { background:#fff4e5; color:#ff6719; border-color:#ffd4b7; }
488
+ .src-Blog { background:#f6f3ee; color:#6a5d45; border-color:#e1dbcc; }
489
+ .shots { margin-bottom:1.5rem; }
490
+ .shot { background:var(--card); border:1px solid var(--border); border-radius:4px; overflow:hidden; }
491
+ .shot-label { font-size:0.6875rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--muted); font-weight:600; padding:0.5rem 0.75rem; border-bottom:1px solid var(--border); background:#fafafa; }
492
+ .shot img { display:block; width:100%; height:auto; }
493
+ footer { margin-top:3rem; padding-top:1.5rem; border-top:1px solid var(--border); text-align:center; font-size:0.75rem; color:var(--muted); }
494
+ footer a { color:var(--brand); text-decoration:none; font-weight:500; }
495
+ `;
496
+
497
+ for (const c of competitorRows) {
498
+ if (!c.body || c.body.length < 50) continue;
499
+
500
+ const mentionsHtml = c.mentions.length
501
+ ? c.mentions.map(m => {
502
+ const dateStr = m.date ? `<span class="muted-line" style="color:var(--muted);font-size:0.75rem;margin-left:auto;">${escapeHtml(m.date)}</span>` : '';
503
+ const linkText = m.url ? `<a href="${escapeHtml(m.url)}" target="_blank">${escapeHtml(m.title || m.url)}</a>` : escapeHtml(m.title);
504
+ const snippet = m.snippet ? ` — <span style="color:var(--muted);">${escapeHtml(m.snippet)}</span>` : '';
505
+ return `<div class="mention-item"><span class="src-pill src-${escapeHtml(m.sourceType)}">${escapeHtml(m.sourceType)}</span><div style="flex:1;">${linkText}${snippet}</div>${dateStr}</div>`;
506
+ }).join('\n')
507
+ : '<p style="color:var(--muted);font-size:0.875rem;">No mentions collected.</p>';
508
+
509
+ const benchmarksHtml = c.benchmarks.length
510
+ ? `<ul>${c.benchmarks.map(b => {
511
+ const link = b.url ? `<a href="${escapeHtml(b.url)}" target="_blank">${escapeHtml(b.title || b.url)}</a>` : escapeHtml(b.title);
512
+ const src = b.source ? ` <span style="color:var(--muted);">(${escapeHtml(b.source)})</span>` : '';
513
+ const finding = b.finding ? ` — ${escapeHtml(b.finding)}` : '';
514
+ return `<li>${link}${src}${finding}</li>`;
515
+ }).join('')}</ul>`
516
+ : '';
517
+
518
+ const productHtml = c.sections['Product'] ? `<h2>Product</h2>${mdToHtml(c.sections['Product'])}` : '';
519
+ const pricingHtml = c.sections['Pricing'] ? `<h2>Pricing</h2>${mdToHtml(c.sections['Pricing'])}` : '';
520
+ const featuresHtml = c.sections['Features'] ? `<h2>Features</h2>${mdToHtml(c.sections['Features'])}` : '';
521
+ const positioningHtml = c.sections['Positioning'] ? `<h2>Positioning</h2>${mdToHtml(c.sections['Positioning'])}` : '';
522
+ const comparisonKey = Object.keys(c.sections).find(k => k.startsWith('Comparison'));
523
+ const comparisonHtml = comparisonKey ? `<h2>${escapeHtml(comparisonKey)}</h2>${mdToHtml(c.sections[comparisonKey])}` : '';
524
+ // Battle Card — synthesized by the Battle lane subagent (Step 5d) after fact-check completes.
525
+ // Contains Landmines / Objection Handlers / Talk Tracks — sales-enablement-grade output.
526
+ const battleCardKey = Object.keys(c.sections).find(k => k === 'Battle Card' || k.startsWith('Battle'));
527
+ const battleCardHtml = battleCardKey ? `<h2>${escapeHtml(battleCardKey)}</h2>${mdToHtml(c.sections[battleCardKey])}` : '';
528
+ const findingsHtml = c.sections['Research Findings'] ? `<h2>Research Findings</h2>${mdToHtml(c.sections['Research Findings'])}` : '';
529
+
530
+ // Screenshot — filename matches capture_screenshots.mjs output.
531
+ const heroShot = existsSync(join(dir, 'screenshots', `${c.slug}-hero.png`));
532
+ const screenshotsHtml = heroShot ? `
533
+ <div class="shots">
534
+ <div class="shot shot-hero"><div class="shot-label">Homepage</div><img src="../screenshots/${escapeHtml(c.slug)}-hero.png" alt="${escapeHtml(c.competitor_name)} homepage hero" loading="lazy"></div>
535
+ </div>` : '';
536
+
537
+ const companyHtml = `<!DOCTYPE html>
538
+ <html lang="en">
539
+ <head>
540
+ <meta charset="UTF-8">
541
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
542
+ <title>${escapeHtml(c.competitor_name)} — Competitor Analysis</title>
543
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
544
+ <style>${perCompetitorCss}</style>
545
+ </head>
546
+ <body>
547
+ <div class="container">
548
+ <a href="../index.html" class="back">&larr; Back to overview</a>
549
+ <header>
550
+ <h1>${escapeHtml(c.competitor_name)}</h1>
551
+ <div class="meta">
552
+ ${c.website ? `<a href="${escapeHtml(c.website)}" target="_blank">${escapeHtml(c.website)}</a>` : ''}
553
+ ${c.tagline ? ` · ${escapeHtml(c.tagline)}` : ''}
554
+ </div>
555
+ </header>${screenshotsHtml}
556
+ <dl class="fields">
557
+ ${c.positioning ? `<dt>Positioning</dt><dd>${escapeHtml(c.positioning)}</dd>` : ''}
558
+ ${c.product_description ? `<dt>Product</dt><dd>${escapeHtml(c.product_description)}</dd>` : ''}
559
+ ${c.target_customer ? `<dt>Target Customer</dt><dd>${escapeHtml(c.target_customer)}</dd>` : ''}
560
+ ${c.pricing_model ? `<dt>Pricing Model</dt><dd>${escapeHtml(c.pricing_model)}</dd>` : ''}
561
+ ${c.pricing_tiers ? `<dt>Pricing Tiers</dt><dd>${escapeHtml(c.pricing_tiers)}</dd>` : ''}
562
+ ${c.key_features ? `<dt>Key Features</dt><dd>${escapeHtml(c.key_features)}</dd>` : ''}
563
+ ${c.integrations ? `<dt>Integrations</dt><dd>${escapeHtml(c.integrations)}</dd>` : ''}
564
+ ${c.headquarters ? `<dt>HQ</dt><dd>${escapeHtml(c.headquarters)}</dd>` : ''}
565
+ ${c.founded ? `<dt>Founded</dt><dd>${escapeHtml(c.founded)}</dd>` : ''}
566
+ ${c.employee_estimate ? `<dt>Employees</dt><dd>${escapeHtml(c.employee_estimate)}</dd>` : ''}
567
+ ${c.funding_info ? `<dt>Funding</dt><dd>${escapeHtml(c.funding_info)}</dd>` : ''}
568
+ ${c.strategic_diff ? `<dt>Strategic Diff</dt><dd>${escapeHtml(c.strategic_diff)}</dd>` : ''}
569
+ </dl>
570
+ <div class="research">
571
+ ${productHtml}
572
+ ${pricingHtml}
573
+ ${featuresHtml}
574
+ ${positioningHtml}
575
+ ${comparisonHtml}
576
+ </div>
577
+ ${battleCardHtml ? `<div class="research battle">${battleCardHtml}</div>` : ''}
578
+ <div class="research">
579
+ <h2>Mentions</h2>
580
+ ${mentionsHtml}
581
+ </div>
582
+ ${c.benchmarks.length ? `<div class="research"><h2>Benchmarks</h2>${benchmarksHtml}</div>` : ''}
583
+ ${findingsHtml ? `<div class="research">${findingsHtml}</div>` : ''}
584
+ </div>
585
+ <footer>Generated by <a href="https://github.com/anthropics/skills">competitor-analysis</a> · Powered by <a href="https://browserbase.com">Browserbase</a></footer>
586
+ </body>
587
+ </html>`;
588
+
589
+ writeFileSync(join(dir, 'competitors', `${c.slug}.html`), companyHtml);
590
+ }
591
+
592
+ // ---------- matrix.html (side-by-side) ----------
593
+
594
+ // curatedMatrix is loaded earlier (before the index.html section) because the
595
+ // strategic summary on the overview page reads userCompany from it.
596
+
597
+ function buildMatrixAxisFromCurated(kind) {
598
+ if (!curatedMatrix || !curatedMatrix[kind]) return [];
599
+ const compMap = curatedMatrix.competitors || {};
600
+ return curatedMatrix[kind].map(entry => {
601
+ const label = entry.name;
602
+ let count = 0;
603
+ for (const c of competitorRows) {
604
+ const compKey = compMap[c.slug];
605
+ if (compKey && compKey[kind] && compKey[kind][label]) count += 1;
606
+ }
607
+ return { label, count, description: entry.description || '' };
608
+ });
609
+ }
610
+
611
+ function buildMatrixAxisFromPipes(field) {
612
+ const counts = new Map();
613
+ for (const c of competitorRows) {
614
+ for (const item of splitPipes(c[field])) {
615
+ const key = item.toLowerCase();
616
+ if (!counts.has(key)) counts.set(key, { label: item, count: 0 });
617
+ counts.get(key).count += 1;
618
+ }
619
+ }
620
+ return [...counts.values()].sort((a, b) => b.count - a.count).slice(0, 18);
621
+ }
622
+
623
+ const featureAxis = curatedMatrix
624
+ ? buildMatrixAxisFromCurated('features')
625
+ : buildMatrixAxisFromPipes('key_features');
626
+ const integrationAxis = curatedMatrix
627
+ ? buildMatrixAxisFromCurated('integrations')
628
+ : buildMatrixAxisFromPipes('integrations');
629
+
630
+ function competitorHas(c, field, label) {
631
+ // Curated mode: look up in matrix.json (field is 'features' or 'integrations').
632
+ if (curatedMatrix) {
633
+ const compMap = curatedMatrix.competitors || {};
634
+ const compEntry = compMap[c.slug];
635
+ return !!(compEntry && compEntry[field] && compEntry[field][label]);
636
+ }
637
+ // Fallback: raw pipe-split match.
638
+ const rawField = field === 'features' ? 'key_features' : field;
639
+ return splitPipes(c[rawField]).some(x => x.toLowerCase() === label.toLowerCase());
640
+ }
641
+
642
+ function matrixSection(heading, axis, field) {
643
+ if (!axis.length) return '';
644
+ // Horizontal competitor-name headers — simpler to read than rotated. Row label (feature name) is
645
+ // the sticky left column so users can scroll horizontally without losing context on wide tables.
646
+ const header = `<tr>
647
+ <th class="mx-feature-h">${escapeHtml(heading)}</th>
648
+ ${competitorRows.map(c => `<th class="mx-comp-h"><a href="competitors/${escapeHtml(c.slug)}.html">${escapeHtml(c.competitor_name)}</a></th>`).join('')}
649
+ </tr>`;
650
+ const rows = axis.map(a => {
651
+ const cells = competitorRows.map(c => competitorHas(c, field, a.label)
652
+ ? `<td class="mx-cell mx-yes" title="${escapeHtml(c.competitor_name)} has ${escapeHtml(a.label)}">●</td>`
653
+ : `<td class="mx-cell mx-no">·</td>`).join('');
654
+ return `<tr>
655
+ <td class="mx-feature"><span class="mx-feature-label">${escapeHtml(a.label)}</span><span class="mx-count">${a.count}</span></td>
656
+ ${cells}
657
+ </tr>`;
658
+ }).join('\n');
659
+ return `<section class="mx-section">
660
+ <h2 class="mx-heading">${escapeHtml(heading)}</h2>
661
+ <div class="mx-scroll">
662
+ <table class="mx-table">${header}${rows}</table>
663
+ </div>
664
+ </section>`;
665
+ }
666
+
667
+ const pricingRows = competitorRows.map(c => `<tr><td style="font-weight:500;">${escapeHtml(c.competitor_name)}</td><td style="color:var(--muted);font-size:0.8125rem;">${escapeHtml(c.pricing_model || '')}</td><td style="font-size:0.8125rem;">${escapeHtml(c.pricing_tiers || '—')}</td><td style="font-size:0.8125rem;">${escapeHtml(c.target_customer || '')}</td></tr>`).join('');
668
+
669
+ const matrixHtml = `<!DOCTYPE html>
670
+ <html lang="en">
671
+ <head>
672
+ <meta charset="UTF-8">
673
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
674
+ <title>Feature Matrix — ${escapeHtml(title)}</title>
675
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
676
+ <style>
677
+ :root { --brand:#F03603; --black:#100D0D; --border:#edebeb; --bg:#F9F6F4; --card:#ffffff; --text:#100D0D; --muted:#514F4F; }
678
+ * { margin:0; padding:0; box-sizing:border-box; }
679
+ body { font-family:Inter,system-ui,sans-serif; background:var(--bg); color:var(--text); line-height:1.5; font-size:15px; }
680
+ .container { max-width:1400px; margin:0 auto; padding:2rem 1.5rem; }
681
+ header { margin-bottom:1.5rem; }
682
+ header h1 { font-size:1.5rem; font-weight:600; margin-bottom:0.25rem; }
683
+ header .meta { color:var(--muted); font-size:0.875rem; }
684
+ nav.views { display:flex; gap:0.5rem; margin-bottom:2rem; }
685
+ nav.views a { background:var(--card); border:1px solid var(--border); border-radius:4px; padding:0.5rem 0.875rem; font-size:0.8125rem; color:var(--muted); text-decoration:none; font-weight:500; }
686
+ nav.views a:hover { border-color:var(--brand); color:var(--brand); }
687
+ nav.views a.active { background:var(--brand); color:#fff; border-color:var(--brand); }
688
+ table { border-collapse:collapse; background:var(--card); border:1px solid var(--border); border-radius:4px; overflow:hidden; margin-bottom:1.5rem; }
689
+ th, td { border:1px solid var(--border); padding:0.5rem 0.625rem; }
690
+ th { background:#fafafa; font-size:0.75rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.04em; }
691
+
692
+ /* Feature matrix — sticky first column + tilted competitor headers */
693
+ .mx-section { margin:1.5rem 0; }
694
+ .mx-heading { font-size:1rem; font-weight:600; margin:0 0 0.5rem; color:var(--black); }
695
+ .mx-scroll { background:var(--card); border:1px solid var(--border); border-radius:4px; overflow-x:auto; }
696
+ .mx-table { border-collapse:collapse; width:auto; margin:0; background:var(--card); border:none; border-radius:0; }
697
+ .mx-table th, .mx-table td { border:1px solid var(--border); padding:0; }
698
+ .mx-table tr:hover td:not(.mx-feature) { background:#fdf7f5; }
699
+ .mx-table tr:hover .mx-feature { background:#fdfcfb; }
700
+ .mx-feature-h { position:sticky; left:0; z-index:3; background:#fafafa; text-align:left; min-width:240px; padding:0.75rem !important; border-bottom:1px solid var(--border); font-size:0.6875rem; text-transform:uppercase; letter-spacing:0.05em; color:var(--muted); font-weight:600; }
701
+ .mx-comp-h { padding:0.75rem 0.5rem !important; background:#fafafa; min-width:110px; max-width:140px; border-bottom:1px solid var(--border); text-align:center; font-size:0.8125rem; font-weight:600; text-transform:none; letter-spacing:0; color:var(--text); white-space:nowrap; }
702
+ .mx-comp-h a { color:var(--text); text-decoration:none; }
703
+ .mx-comp-h a:hover { color:var(--brand); }
704
+ .mx-feature { position:sticky; left:0; z-index:2; background:var(--card); min-width:240px; font-size:0.8125rem; padding:0.45rem 0.75rem !important; display:flex; align-items:center; justify-content:space-between; gap:0.5rem; }
705
+ .mx-feature-label { flex:1; }
706
+ .mx-count { color:var(--muted); font-size:0.7rem; font-weight:600; background:#f4f1ee; padding:0 6px; border-radius:999px; }
707
+ .mx-cell { text-align:center; font-weight:700; min-width:110px; max-width:140px; padding:0.5rem 0 !important; font-size:0.95rem; }
708
+ .mx-yes { color:#5a8a1a; background:rgba(144,201,77,0.06); }
709
+ .mx-no { color:#e0dcd7; }
710
+
711
+ footer { margin-top:3rem; padding-top:1.5rem; border-top:1px solid var(--border); text-align:center; font-size:0.75rem; color:var(--muted); }
712
+ footer a { color:var(--brand); text-decoration:none; }
713
+ </style>
714
+ </head>
715
+ <body>
716
+ <div class="container">
717
+ <header>
718
+ <h1>Feature & Pricing Matrix</h1>
719
+ <div class="meta">${escapeHtml(metaLine)}</div>
720
+ </header>
721
+ <nav class="views">
722
+ <a href="index.html">Overview</a>
723
+ <a href="matrix.html" class="active">Matrix</a>
724
+ <a href="mentions.html">Mentions</a>
725
+ </nav>
726
+
727
+ <section>
728
+ <h2 style="font-size:1rem;font-weight:600;margin:0 0 0.5rem;">Pricing</h2>
729
+ <table style="width:100%;">
730
+ <thead><tr><th style="text-align:left;">Competitor</th><th style="text-align:left;">Model</th><th style="text-align:left;">Tiers</th><th style="text-align:left;">Target Customer</th></tr></thead>
731
+ <tbody>${pricingRows}</tbody>
732
+ </table>
733
+ </section>
734
+
735
+ ${matrixSection('Features', featureAxis, 'features')}
736
+ ${matrixSection('Integrations', integrationAxis, 'integrations')}
737
+ </div>
738
+ <footer>Generated by <a href="https://github.com/anthropics/skills">competitor-analysis</a> · Powered by <a href="https://browserbase.com">Browserbase</a></footer>
739
+ </body>
740
+ </html>`;
741
+
742
+ writeFileSync(join(dir, 'matrix.html'), matrixHtml);
743
+
744
+ // ---------- mentions.html (feed + filter) ----------
745
+
746
+ // Mentions feed: iterate `competitorRows` (user's own company already filtered out earlier)
747
+ // so the chronological feed doesn't mix the user's own mentions with competitors'.
748
+ const allMentions = [];
749
+ for (const c of competitorRows) {
750
+ for (const m of c.mentions) {
751
+ allMentions.push({ ...m, competitor: c.competitor_name || c.slug, slug: c.slug });
752
+ }
753
+ }
754
+ // Sort by date desc (empty dates last)
755
+ allMentions.sort((a, b) => {
756
+ if (a.date && b.date) return b.date.localeCompare(a.date);
757
+ if (a.date) return -1;
758
+ if (b.date) return 1;
759
+ return 0;
760
+ });
761
+
762
+ const sourceTypes = [...new Set(allMentions.map(m => m.sourceType))].sort();
763
+ const sourceFilterButtons = ['All', ...sourceTypes].map(t =>
764
+ `<button class="filter-btn${t === 'All' ? ' active' : ''}" data-filter="${escapeHtml(t)}">${escapeHtml(t)}</button>`
765
+ ).join('');
766
+
767
+ const mentionItems = allMentions.map(m => {
768
+ const link = m.url ? `<a href="${escapeHtml(m.url)}" target="_blank">${escapeHtml(m.title || m.url)}</a>` : escapeHtml(m.title);
769
+ const snippet = m.snippet ? `<div class="snippet">${escapeHtml(m.snippet)}</div>` : '';
770
+ const date = m.date ? `<span class="date">${escapeHtml(m.date)}</span>` : '';
771
+ return `<div class="mention" data-type="${escapeHtml(m.sourceType)}">
772
+ <span class="src-pill src-${escapeHtml(m.sourceType)}">${escapeHtml(m.sourceType)}</span>
773
+ <div class="body">
774
+ <div class="header-line">
775
+ <a href="competitors/${escapeHtml(m.slug)}.html" class="competitor-chip">${escapeHtml(m.competitor)}</a>
776
+ ${date}
777
+ </div>
778
+ <div class="title">${link}</div>
779
+ ${snippet}
780
+ </div>
781
+ </div>`;
782
+ }).join('\n');
783
+
784
+ const mentionsHtml = `<!DOCTYPE html>
785
+ <html lang="en">
786
+ <head>
787
+ <meta charset="UTF-8">
788
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
789
+ <title>Mentions Feed — ${escapeHtml(title)}</title>
790
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
791
+ <style>
792
+ :root { --brand:#F03603; --blue:#4DA9E4; --black:#100D0D; --border:#edebeb; --bg:#F9F6F4; --card:#ffffff; --text:#100D0D; --muted:#514F4F; }
793
+ * { margin:0; padding:0; box-sizing:border-box; }
794
+ body { font-family:Inter,system-ui,sans-serif; background:var(--bg); color:var(--text); line-height:1.5; font-size:15px; }
795
+ .container { max-width:900px; margin:0 auto; padding:2rem 1.5rem; }
796
+ header { margin-bottom:1.5rem; }
797
+ header h1 { font-size:1.5rem; font-weight:600; margin-bottom:0.25rem; }
798
+ header .meta { color:var(--muted); font-size:0.875rem; }
799
+ nav.views { display:flex; gap:0.5rem; margin-bottom:1.5rem; }
800
+ nav.views a { background:var(--card); border:1px solid var(--border); border-radius:4px; padding:0.5rem 0.875rem; font-size:0.8125rem; color:var(--muted); text-decoration:none; font-weight:500; }
801
+ nav.views a:hover { border-color:var(--brand); color:var(--brand); }
802
+ nav.views a.active { background:var(--brand); color:#fff; border-color:var(--brand); }
803
+ .filters { display:flex; gap:0.375rem; margin-bottom:1rem; flex-wrap:wrap; }
804
+ .filter-btn { background:var(--card); border:1px solid var(--border); border-radius:999px; padding:0.25rem 0.75rem; font-size:0.75rem; color:var(--muted); cursor:pointer; font-weight:500; font-family:inherit; }
805
+ .filter-btn:hover { border-color:var(--brand); color:var(--brand); }
806
+ .filter-btn.active { background:var(--brand); color:#fff; border-color:var(--brand); }
807
+ .mention { display:flex; gap:0.75rem; align-items:flex-start; padding:0.875rem; background:var(--card); border:1px solid var(--border); border-radius:4px; margin-bottom:0.5rem; }
808
+ .mention.hidden { display:none; }
809
+ .mention .body { flex:1; min-width:0; }
810
+ .header-line { display:flex; gap:0.75rem; align-items:center; margin-bottom:0.25rem; font-size:0.8125rem; }
811
+ .competitor-chip { color:var(--muted); font-weight:500; text-decoration:none; }
812
+ .competitor-chip:hover { color:var(--brand); }
813
+ .date { color:var(--muted); font-size:0.75rem; margin-left:auto; }
814
+ .title { font-size:0.9375rem; margin-bottom:0.25rem; }
815
+ .title a { color:var(--text); text-decoration:none; font-weight:500; }
816
+ .title a:hover { color:var(--brand); text-decoration:underline; }
817
+ .snippet { color:var(--muted); font-size:0.8125rem; }
818
+ .src-pill { font-size:0.6875rem; font-weight:600; padding:3px 9px; border-radius:999px; white-space:nowrap; border:1px solid; flex-shrink:0; align-self:flex-start; }
819
+ .src-Benchmark { background:rgba(77,169,228,0.12); color:#2172a3; border-color:rgba(77,169,228,0.4); }
820
+ .src-Comparison { background:rgba(240,54,3,0.10); color:var(--brand); border-color:rgba(240,54,3,0.4); }
821
+ .src-News { background:#f2f2f2; color:var(--black); border-color:#ddd; }
822
+ .src-Reddit { background:#fff2eb; color:#d84300; border-color:#ffd4b7; }
823
+ .src-HN { background:#fff4e5; color:#c95500; border-color:#ffcc99; }
824
+ .src-LinkedIn { background:#e7f1fa; color:#0a66c2; border-color:#b3d4ee; }
825
+ .src-YouTube { background:#ffebee; color:#c4302b; border-color:#f7b2ae; }
826
+ .src-Review { background:rgba(144,201,77,0.12); color:#5a8a1a; border-color:rgba(144,201,77,0.4); }
827
+ .src-Podcast { background:#efe7fa; color:#6236c2; border-color:#d1bde9; }
828
+ .src-X { background:#eef2f7; color:#111; border-color:#cfd9e5; }
829
+ .src-Twitter { background:#eef2f7; color:#111; border-color:#cfd9e5; }
830
+ .src-DevTo { background:#f3f3f6; color:#0a0a0a; border-color:#dcdce0; }
831
+ .src-Hashnode { background:#eef4ff; color:#2962ff; border-color:#c6d8ff; }
832
+ .src-Substack { background:#fff4e5; color:#ff6719; border-color:#ffd4b7; }
833
+ .src-Blog { background:#f6f3ee; color:#6a5d45; border-color:#e1dbcc; }
834
+ .empty { text-align:center; color:var(--muted); padding:3rem 1rem; font-size:0.875rem; }
835
+ footer { margin-top:3rem; padding-top:1.5rem; border-top:1px solid var(--border); text-align:center; font-size:0.75rem; color:var(--muted); }
836
+ footer a { color:var(--brand); text-decoration:none; }
837
+ </style>
838
+ </head>
839
+ <body>
840
+ <div class="container">
841
+ <header>
842
+ <h1>Mentions Feed</h1>
843
+ <div class="meta">${allMentions.length} mentions across ${competitorRows.length} competitors · ${escapeHtml(genDate)}</div>
844
+ </header>
845
+ <nav class="views">
846
+ <a href="index.html">Overview</a>
847
+ <a href="matrix.html">Matrix</a>
848
+ <a href="mentions.html" class="active">Mentions</a>
849
+ </nav>
850
+ <div class="filters">${sourceFilterButtons}</div>
851
+ <div id="mentions-list">
852
+ ${mentionItems || '<div class="empty">No mentions collected — try running in deep or deeper mode.</div>'}
853
+ </div>
854
+ </div>
855
+ <footer>Generated by <a href="https://github.com/anthropics/skills">competitor-analysis</a> · Powered by <a href="https://browserbase.com">Browserbase</a></footer>
856
+ <script>
857
+ (function () {
858
+ const buttons = document.querySelectorAll('.filter-btn');
859
+ const items = document.querySelectorAll('.mention');
860
+ buttons.forEach(btn => btn.addEventListener('click', () => {
861
+ buttons.forEach(b => b.classList.remove('active'));
862
+ btn.classList.add('active');
863
+ const f = btn.dataset.filter;
864
+ items.forEach(el => {
865
+ el.classList.toggle('hidden', f !== 'All' && el.dataset.type !== f);
866
+ });
867
+ }));
868
+ })();
869
+ </script>
870
+ </body>
871
+ </html>`;
872
+
873
+ writeFileSync(join(dir, 'mentions.html'), mentionsHtml);
874
+
875
+ // ---------- CSV ----------
876
+
877
+ const priority = [
878
+ 'competitor_name', 'website', 'tagline', 'positioning', 'product_description',
879
+ 'target_customer', 'pricing_model', 'pricing_tiers', 'key_features', 'integrations',
880
+ 'headquarters', 'founded', 'employee_estimate', 'funding_info', 'strategic_diff'
881
+ ];
882
+ const flatRows = competitorRows.map(c => {
883
+ const row = {};
884
+ for (const k of Object.keys(c)) {
885
+ if (['body', 'sections', 'mentions', 'benchmarks', 'slug', 'file'].includes(k)) continue;
886
+ row[k] = c[k];
887
+ }
888
+ row.mention_count = String(c.mentions.length);
889
+ row.benchmark_count = String(c.benchmarks.length);
890
+ return row;
891
+ });
892
+ const allCols = [...new Set(flatRows.flatMap(r => Object.keys(r)))];
893
+ const cols = [...priority.filter(c => allCols.includes(c)), ...allCols.filter(c => !priority.includes(c)).sort()];
894
+
895
+ function csvEscape(v) {
896
+ v = String(v || '');
897
+ if (v.includes(',') || v.includes('"') || v.includes('\n')) return '"' + v.replace(/"/g, '""') + '"';
898
+ return v;
899
+ }
900
+
901
+ const csvLines = [cols.join(',')];
902
+ for (const row of flatRows) csvLines.push(cols.map(c => csvEscape(row[c] || '')).join(','));
903
+ writeFileSync(join(dir, 'results.csv'), csvLines.join('\n') + '\n');
904
+
905
+ // ---------- Summary ----------
906
+
907
+ console.error(JSON.stringify({
908
+ total: competitorRows.length,
909
+ mentions: totalMentions,
910
+ benchmarks: totalBenchmarks,
911
+ with_pricing: withPricing,
912
+ user_company: userCompany,
913
+ files_generated: {
914
+ index: join(dir, 'index.html'),
915
+ matrix: join(dir, 'matrix.html'),
916
+ mentions: join(dir, 'mentions.html'),
917
+ competitors: competitorRows.filter(c => c.body && c.body.length > 50).length,
918
+ csv: join(dir, 'results.csv')
919
+ }
920
+ }, null, 2));
921
+
922
+ console.log(join(dir, 'index.html'));
923
+
924
+ if (shouldOpen) {
925
+ const { execFileSync } = await import('child_process');
926
+ // Use execFileSync (not execSync with string interpolation) so a `dir` containing
927
+ // shell metacharacters like `"`, `$`, or backticks can't break out into command exec.
928
+ try { execFileSync('open', [join(dir, 'index.html')]); } catch {}
929
+ }