switchroom 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +447 -0
  3. package/bin/autoaccept.exp +81 -0
  4. package/bin/boot-self-test.sh +149 -0
  5. package/bin/bridge-watchdog.sh +967 -0
  6. package/bin/handoff-briefing.sh +206 -0
  7. package/bin/run-hook.sh +228 -0
  8. package/bin/switchroom.ts +4 -0
  9. package/bin/timezone-hook.sh +67 -0
  10. package/bin/user-profile-refresh-hook.sh +38 -0
  11. package/bin/workspace-dynamic-hook.sh +142 -0
  12. package/bin/workspace-stable-hook.sh +57 -0
  13. package/dist/cli/autoaccept-poll.js +118 -0
  14. package/dist/cli/switchroom.js +48557 -0
  15. package/package.json +95 -0
  16. package/profiles/_base/settings.json.hbs +15 -0
  17. package/profiles/_base/start.sh.hbs +383 -0
  18. package/profiles/_shared/telegram-style.md.hbs +140 -0
  19. package/profiles/coding/CLAUDE.md.hbs +57 -0
  20. package/profiles/coding/skills/architecture/SKILL.md +70 -0
  21. package/profiles/coding/skills/code-review/SKILL.md +58 -0
  22. package/profiles/coding/workspace/SOUL.md.hbs +25 -0
  23. package/profiles/default/CLAUDE.md +238 -0
  24. package/profiles/default/CLAUDE.md.hbs +113 -0
  25. package/profiles/default/workspace/CLAUDE.md.hbs +126 -0
  26. package/profiles/default/workspace/HEARTBEAT.md.hbs +40 -0
  27. package/profiles/default/workspace/IDENTITY.md.hbs +32 -0
  28. package/profiles/default/workspace/MEMORY.md.hbs +29 -0
  29. package/profiles/default/workspace/SOUL.md.hbs +61 -0
  30. package/profiles/default/workspace/TOOLS.md.hbs +29 -0
  31. package/profiles/default/workspace/USER.md.hbs +52 -0
  32. package/profiles/default/workspace/memory/.gitkeep +0 -0
  33. package/profiles/executive-assistant/CLAUDE.md.hbs +51 -0
  34. package/profiles/executive-assistant/skills/daily-briefing/SKILL.md +55 -0
  35. package/profiles/executive-assistant/skills/meeting-prep/SKILL.md +58 -0
  36. package/profiles/executive-assistant/workspace/SOUL.md.hbs +25 -0
  37. package/profiles/health-coach/CLAUDE.md.hbs +45 -0
  38. package/profiles/health-coach/skills/check-in/SKILL.md +41 -0
  39. package/profiles/health-coach/skills/weekly-review/SKILL.md +53 -0
  40. package/profiles/health-coach/workspace/SOUL.md.hbs +25 -0
  41. package/skills/buildkite-agent-infrastructure/SKILL.md +302 -0
  42. package/skills/buildkite-agent-infrastructure/agents/openai.yaml +6 -0
  43. package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
  44. package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
  45. package/skills/buildkite-agent-infrastructure/references/audit-logging.md +87 -0
  46. package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +690 -0
  47. package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +38 -0
  48. package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +73 -0
  49. package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +137 -0
  50. package/skills/buildkite-agent-infrastructure/references/sso-saml.md +92 -0
  51. package/skills/buildkite-agent-runtime/SKILL.md +476 -0
  52. package/skills/buildkite-agent-runtime/agents/openai.yaml +6 -0
  53. package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
  54. package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
  55. package/skills/buildkite-agent-runtime/references/flag-reference.md +417 -0
  56. package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +555 -0
  57. package/skills/buildkite-api/SKILL.md +285 -0
  58. package/skills/buildkite-api/agents/openai.yaml +6 -0
  59. package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
  60. package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
  61. package/skills/buildkite-api/references/graphql-reference.md +195 -0
  62. package/skills/buildkite-api/references/patterns.md +44 -0
  63. package/skills/buildkite-api/references/webhooks.md +161 -0
  64. package/skills/buildkite-cli/SKILL.md +379 -0
  65. package/skills/buildkite-cli/agents/openai.yaml +6 -0
  66. package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
  67. package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
  68. package/skills/buildkite-cli/references/command-reference.md +181 -0
  69. package/skills/buildkite-migration/SKILL.md +182 -0
  70. package/skills/buildkite-pipelines/SKILL.md +464 -0
  71. package/skills/buildkite-pipelines/agents/openai.yaml +6 -0
  72. package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
  73. package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
  74. package/skills/buildkite-pipelines/examples/basic-pipeline.yml +24 -0
  75. package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +100 -0
  76. package/skills/buildkite-pipelines/references/advanced-patterns.md +286 -0
  77. package/skills/buildkite-pipelines/references/retry-and-error-codes.md +131 -0
  78. package/skills/buildkite-pipelines/references/step-types-reference.md +225 -0
  79. package/skills/buildkite-secure-delivery/SKILL.md +168 -0
  80. package/skills/buildkite-secure-delivery/agents/openai.yaml +6 -0
  81. package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
  82. package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
  83. package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +83 -0
  84. package/skills/buildkite-secure-delivery/references/package-publishing.md +100 -0
  85. package/skills/buildkite-test-engine/SKILL.md +239 -0
  86. package/skills/buildkite-test-engine/agents/openai.yaml +6 -0
  87. package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
  88. package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
  89. package/skills/buildkite-test-engine/examples/bktec-splitting.yml +16 -0
  90. package/skills/buildkite-test-engine/examples/collector-pipeline.yml +11 -0
  91. package/skills/buildkite-test-engine/references/collectors.md +198 -0
  92. package/skills/buildkite-test-engine/references/splitting-examples.md +93 -0
  93. package/skills/docx/LICENSE.txt +30 -0
  94. package/skills/docx/SKILL.md +590 -0
  95. package/skills/docx/VENDORED.md +32 -0
  96. package/skills/docx/scripts/__init__.py +1 -0
  97. package/skills/docx/scripts/accept_changes.py +135 -0
  98. package/skills/docx/scripts/comment.py +318 -0
  99. package/skills/docx/scripts/office/helpers/__init__.py +0 -0
  100. package/skills/docx/scripts/office/helpers/merge_runs.py +199 -0
  101. package/skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
  102. package/skills/docx/scripts/office/pack.py +159 -0
  103. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  104. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  105. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  106. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  107. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  108. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  109. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  110. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  111. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  112. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  113. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  114. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  115. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  116. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  117. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  118. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  119. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  120. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  121. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  122. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  123. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  124. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  125. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  126. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  127. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  128. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  129. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  130. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  131. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  132. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  133. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  134. package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
  135. package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  136. package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  137. package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  138. package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  139. package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  140. package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  141. package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  142. package/skills/docx/scripts/office/soffice.py +183 -0
  143. package/skills/docx/scripts/office/unpack.py +132 -0
  144. package/skills/docx/scripts/office/validate.py +111 -0
  145. package/skills/docx/scripts/office/validators/__init__.py +15 -0
  146. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  147. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  148. package/skills/docx/scripts/office/validators/base.py +847 -0
  149. package/skills/docx/scripts/office/validators/docx.py +446 -0
  150. package/skills/docx/scripts/office/validators/pptx.py +275 -0
  151. package/skills/docx/scripts/office/validators/redlining.py +247 -0
  152. package/skills/docx/scripts/templates/comments.xml +3 -0
  153. package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  154. package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  155. package/skills/docx/scripts/templates/commentsIds.xml +3 -0
  156. package/skills/docx/scripts/templates/people.xml +3 -0
  157. package/skills/file-bug/SKILL.md +129 -0
  158. package/skills/humanizer/LICENSE +21 -0
  159. package/skills/humanizer/SKILL.md +559 -0
  160. package/skills/humanizer/VENDORED.md +38 -0
  161. package/skills/humanizer-calibrate/SKILL.md +144 -0
  162. package/skills/mcp-builder/LICENSE.txt +202 -0
  163. package/skills/mcp-builder/SKILL.md +236 -0
  164. package/skills/mcp-builder/VENDORED.md +32 -0
  165. package/skills/mcp-builder/reference/evaluation.md +602 -0
  166. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  167. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  168. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  169. package/skills/mcp-builder/scripts/connections.py +151 -0
  170. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  171. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  172. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  173. package/skills/pdf/LICENSE.txt +30 -0
  174. package/skills/pdf/SKILL.md +314 -0
  175. package/skills/pdf/VENDORED.md +32 -0
  176. package/skills/pdf/forms.md +294 -0
  177. package/skills/pdf/reference.md +612 -0
  178. package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
  179. package/skills/pdf/scripts/check_fillable_fields.py +11 -0
  180. package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
  181. package/skills/pdf/scripts/create_validation_image.py +37 -0
  182. package/skills/pdf/scripts/extract_form_field_info.py +122 -0
  183. package/skills/pdf/scripts/extract_form_structure.py +115 -0
  184. package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
  185. package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  186. package/skills/pptx/LICENSE.txt +30 -0
  187. package/skills/pptx/SKILL.md +232 -0
  188. package/skills/pptx/VENDORED.md +32 -0
  189. package/skills/pptx/editing.md +205 -0
  190. package/skills/pptx/pptxgenjs.md +420 -0
  191. package/skills/pptx/scripts/__init__.py +0 -0
  192. package/skills/pptx/scripts/add_slide.py +195 -0
  193. package/skills/pptx/scripts/clean.py +286 -0
  194. package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
  195. package/skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
  196. package/skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
  197. package/skills/pptx/scripts/office/pack.py +159 -0
  198. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  199. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  200. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  201. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  202. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  203. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  204. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  205. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  206. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  207. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  208. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  209. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  210. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  211. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  212. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  213. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  214. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  215. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  216. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  217. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  218. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  219. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  220. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  221. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  222. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  223. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  224. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  225. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  226. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  227. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  228. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  229. package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
  230. package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  231. package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  232. package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  233. package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  234. package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  235. package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  236. package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  237. package/skills/pptx/scripts/office/soffice.py +183 -0
  238. package/skills/pptx/scripts/office/unpack.py +132 -0
  239. package/skills/pptx/scripts/office/validate.py +111 -0
  240. package/skills/pptx/scripts/office/validators/__init__.py +15 -0
  241. package/skills/pptx/scripts/office/validators/base.py +847 -0
  242. package/skills/pptx/scripts/office/validators/docx.py +446 -0
  243. package/skills/pptx/scripts/office/validators/pptx.py +275 -0
  244. package/skills/pptx/scripts/office/validators/redlining.py +247 -0
  245. package/skills/pptx/scripts/thumbnail.py +289 -0
  246. package/skills/skill-creator/LICENSE.txt +202 -0
  247. package/skills/skill-creator/SKILL.md +485 -0
  248. package/skills/skill-creator/VENDORED.md +32 -0
  249. package/skills/skill-creator/agents/analyzer.md +274 -0
  250. package/skills/skill-creator/agents/comparator.md +202 -0
  251. package/skills/skill-creator/agents/grader.md +223 -0
  252. package/skills/skill-creator/assets/eval_review.html +146 -0
  253. package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  254. package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  255. package/skills/skill-creator/references/schemas.md +430 -0
  256. package/skills/skill-creator/scripts/__init__.py +0 -0
  257. package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  258. package/skills/skill-creator/scripts/generate_report.py +326 -0
  259. package/skills/skill-creator/scripts/improve_description.py +247 -0
  260. package/skills/skill-creator/scripts/package_skill.py +136 -0
  261. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  262. package/skills/skill-creator/scripts/run_eval.py +310 -0
  263. package/skills/skill-creator/scripts/run_loop.py +328 -0
  264. package/skills/skill-creator/scripts/utils.py +47 -0
  265. package/skills/switchroom-architecture/SKILL.md +60 -0
  266. package/skills/switchroom-architecture/cascade.md +112 -0
  267. package/skills/switchroom-architecture/sub-agents.md +87 -0
  268. package/skills/switchroom-architecture/telegram.md +94 -0
  269. package/skills/switchroom-cli/SKILL.md +274 -0
  270. package/skills/switchroom-health/SKILL.md +101 -0
  271. package/skills/switchroom-install/SKILL.md +116 -0
  272. package/skills/switchroom-manage/SKILL.md +90 -0
  273. package/skills/switchroom-status/SKILL.md +69 -0
  274. package/skills/switchroom-status/scripts/status.sh +69 -0
  275. package/skills/telegram-test-harness/SKILL.md +191 -0
  276. package/skills/token-helpers/SKILL.md +73 -0
  277. package/skills/token-helpers/scripts/google-cal-token.sh +62 -0
  278. package/skills/token-helpers/scripts/ms-graph-token.sh +70 -0
  279. package/skills/webapp-testing/LICENSE.txt +202 -0
  280. package/skills/webapp-testing/SKILL.md +96 -0
  281. package/skills/webapp-testing/VENDORED.md +32 -0
  282. package/skills/webapp-testing/examples/console_logging.py +35 -0
  283. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  284. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  285. package/skills/webapp-testing/scripts/with_server.py +106 -0
  286. package/skills/xlsx/LICENSE.txt +30 -0
  287. package/skills/xlsx/SKILL.md +292 -0
  288. package/skills/xlsx/VENDORED.md +32 -0
  289. package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
  290. package/skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
  291. package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  292. package/skills/xlsx/scripts/office/pack.py +159 -0
  293. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  294. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  295. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  296. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  297. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  298. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  299. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  300. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  301. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  302. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  303. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  304. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  305. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  306. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  307. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  308. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  309. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  310. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  311. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  312. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  313. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  314. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  315. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  316. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  317. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  318. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  319. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  320. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  321. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  322. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  323. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  324. package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  325. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  326. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  327. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  328. package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  329. package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  330. package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  331. package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  332. package/skills/xlsx/scripts/office/soffice.py +183 -0
  333. package/skills/xlsx/scripts/office/unpack.py +132 -0
  334. package/skills/xlsx/scripts/office/validate.py +111 -0
  335. package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
  336. package/skills/xlsx/scripts/office/validators/base.py +847 -0
  337. package/skills/xlsx/scripts/office/validators/docx.py +446 -0
  338. package/skills/xlsx/scripts/office/validators/pptx.py +275 -0
  339. package/skills/xlsx/scripts/office/validators/redlining.py +247 -0
  340. package/skills/xlsx/scripts/recalc.py +184 -0
  341. package/telegram-plugin/.claude-plugin/plugin.json +20 -0
  342. package/telegram-plugin/.mcp.json +14 -0
  343. package/telegram-plugin/LICENSE +21 -0
  344. package/telegram-plugin/README.md +352 -0
  345. package/telegram-plugin/active-pins-sweep.ts +204 -0
  346. package/telegram-plugin/active-pins.ts +146 -0
  347. package/telegram-plugin/active-reactions-sweep.ts +79 -0
  348. package/telegram-plugin/active-reactions.ts +134 -0
  349. package/telegram-plugin/admin-commands/dispatch.test.ts +149 -0
  350. package/telegram-plugin/admin-commands/index.ts +106 -0
  351. package/telegram-plugin/answer-stream.ts +565 -0
  352. package/telegram-plugin/ask-user.ts +179 -0
  353. package/telegram-plugin/attachment-path.ts +80 -0
  354. package/telegram-plugin/auth-code-redact.ts +83 -0
  355. package/telegram-plugin/auth-dashboard.ts +1104 -0
  356. package/telegram-plugin/auth-slot-parser.ts +497 -0
  357. package/telegram-plugin/auto-fallback-dispatcher.ts +68 -0
  358. package/telegram-plugin/auto-fallback.ts +348 -0
  359. package/telegram-plugin/bridge/bridge.ts +687 -0
  360. package/telegram-plugin/bridge/ipc-client.ts +326 -0
  361. package/telegram-plugin/bun.lock +218 -0
  362. package/telegram-plugin/card-format.ts +62 -0
  363. package/telegram-plugin/channel-envelope-safety.test.ts +56 -0
  364. package/telegram-plugin/channel-envelope-safety.ts +56 -0
  365. package/telegram-plugin/chat-lock.ts +65 -0
  366. package/telegram-plugin/context-exhaustion.ts +38 -0
  367. package/telegram-plugin/credits-watch.ts +220 -0
  368. package/telegram-plugin/dist/bridge/bridge.js +24758 -0
  369. package/telegram-plugin/dist/foreman/foreman.js +30723 -0
  370. package/telegram-plugin/dist/gateway/gateway.js +46497 -0
  371. package/telegram-plugin/dist/server.js +24551 -0
  372. package/telegram-plugin/dm-command-gate.ts +56 -0
  373. package/telegram-plugin/docs/gateway-server-split.md +133 -0
  374. package/telegram-plugin/docs/multi-agent-card-design.md +847 -0
  375. package/telegram-plugin/docs/pinned-progress-card-reliability.md +144 -0
  376. package/telegram-plugin/docs/stream-json-daemon-mode.md +477 -0
  377. package/telegram-plugin/docs/waiting-ux-spec.md +233 -0
  378. package/telegram-plugin/draft-stream.ts +442 -0
  379. package/telegram-plugin/draft-transport.ts +72 -0
  380. package/telegram-plugin/first-paint.ts +246 -0
  381. package/telegram-plugin/fleet-state.ts +246 -0
  382. package/telegram-plugin/foreman/foreman-create-flow.ts +202 -0
  383. package/telegram-plugin/foreman/foreman-handlers.ts +493 -0
  384. package/telegram-plugin/foreman/foreman.ts +1130 -0
  385. package/telegram-plugin/foreman/setup-flow.ts +345 -0
  386. package/telegram-plugin/foreman/setup-state.ts +239 -0
  387. package/telegram-plugin/foreman/state.ts +203 -0
  388. package/telegram-plugin/format.ts +685 -0
  389. package/telegram-plugin/gateway/access-validator.test.ts +95 -0
  390. package/telegram-plugin/gateway/access-validator.ts +37 -0
  391. package/telegram-plugin/gateway/boot-card.ts +582 -0
  392. package/telegram-plugin/gateway/boot-probes.ts +863 -0
  393. package/telegram-plugin/gateway/boot-reason.ts +51 -0
  394. package/telegram-plugin/gateway/boot-sweep-filter.test.ts +54 -0
  395. package/telegram-plugin/gateway/boot-sweep-filter.ts +32 -0
  396. package/telegram-plugin/gateway/clean-shutdown-marker.ts +183 -0
  397. package/telegram-plugin/gateway/disconnect-flush.ts +109 -0
  398. package/telegram-plugin/gateway/gateway.ts +10202 -0
  399. package/telegram-plugin/gateway/inbound-coalesce.ts +147 -0
  400. package/telegram-plugin/gateway/inject-handler.test.ts +221 -0
  401. package/telegram-plugin/gateway/inject-handler.ts +190 -0
  402. package/telegram-plugin/gateway/ipc-protocol.ts +151 -0
  403. package/telegram-plugin/gateway/ipc-server.ts +494 -0
  404. package/telegram-plugin/gateway/pid-file.ts +103 -0
  405. package/telegram-plugin/gateway/poll-health.ts +156 -0
  406. package/telegram-plugin/gateway/preamble-suppressor.ts +154 -0
  407. package/telegram-plugin/gateway/quota-cache.ts +125 -0
  408. package/telegram-plugin/gateway/resolve-calling-subagent.ts +78 -0
  409. package/telegram-plugin/gateway/restart-watchdog.ts +200 -0
  410. package/telegram-plugin/gateway/session-marker.ts +83 -0
  411. package/telegram-plugin/gateway/shutdown-drain.ts +162 -0
  412. package/telegram-plugin/gateway/startup-mutex.ts +285 -0
  413. package/telegram-plugin/gateway/startup-network-retry.ts +142 -0
  414. package/telegram-plugin/gateway/turn-active-marker.ts +176 -0
  415. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +78 -0
  416. package/telegram-plugin/handoff-continuity.ts +200 -0
  417. package/telegram-plugin/history.ts +468 -0
  418. package/telegram-plugin/hooks/hooks.json +58 -0
  419. package/telegram-plugin/hooks/secret-guard-pretool.mjs +208 -0
  420. package/telegram-plugin/hooks/secret-scrub-stop.mjs +98 -0
  421. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +111 -0
  422. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +296 -0
  423. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +261 -0
  424. package/telegram-plugin/html-sanitize.ts +244 -0
  425. package/telegram-plugin/idle-footer.ts +65 -0
  426. package/telegram-plugin/inline-keyboard-callbacks.ts +166 -0
  427. package/telegram-plugin/interrupt-marker.ts +66 -0
  428. package/telegram-plugin/issues-card.ts +371 -0
  429. package/telegram-plugin/issues-watcher.ts +125 -0
  430. package/telegram-plugin/model-unavailable.ts +325 -0
  431. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  432. package/telegram-plugin/operator-events-history.ts +94 -0
  433. package/telegram-plugin/operator-events.fixtures.json +161 -0
  434. package/telegram-plugin/operator-events.ts +421 -0
  435. package/telegram-plugin/package.json +55 -0
  436. package/telegram-plugin/permission-rule.ts +133 -0
  437. package/telegram-plugin/permission-title.ts +117 -0
  438. package/telegram-plugin/pin-event-log.ts +76 -0
  439. package/telegram-plugin/plugin-logger.ts +136 -0
  440. package/telegram-plugin/progress-card-driver.ts +2697 -0
  441. package/telegram-plugin/progress-card-pin-manager.ts +589 -0
  442. package/telegram-plugin/progress-card-pin-watchdog.ts +98 -0
  443. package/telegram-plugin/progress-card.ts +1409 -0
  444. package/telegram-plugin/pty-partial-handler.ts +247 -0
  445. package/telegram-plugin/pty-tail.ts +730 -0
  446. package/telegram-plugin/quota-check.ts +474 -0
  447. package/telegram-plugin/recent-outbound-dedup.ts +169 -0
  448. package/telegram-plugin/registry/api-registry.test.ts +201 -0
  449. package/telegram-plugin/registry/subagents-bugs.test.ts +454 -0
  450. package/telegram-plugin/registry/subagents-schema.ts +509 -0
  451. package/telegram-plugin/registry/subagents.test.ts +476 -0
  452. package/telegram-plugin/registry/turns-schema.test.ts +101 -0
  453. package/telegram-plugin/registry/turns-schema.ts +417 -0
  454. package/telegram-plugin/retry-api-call.ts +172 -0
  455. package/telegram-plugin/scripts/build.mjs +78 -0
  456. package/telegram-plugin/secret-detect/audit.ts +66 -0
  457. package/telegram-plugin/secret-detect/chunker.ts +37 -0
  458. package/telegram-plugin/secret-detect/entropy.ts +20 -0
  459. package/telegram-plugin/secret-detect/gitleaks-loader.ts +74 -0
  460. package/telegram-plugin/secret-detect/gitleaks.toml +27 -0
  461. package/telegram-plugin/secret-detect/index.ts +218 -0
  462. package/telegram-plugin/secret-detect/kv-scanner.ts +60 -0
  463. package/telegram-plugin/secret-detect/mask.ts +13 -0
  464. package/telegram-plugin/secret-detect/patterns.ts +115 -0
  465. package/telegram-plugin/secret-detect/pipeline.ts +144 -0
  466. package/telegram-plugin/secret-detect/rewrite.ts +26 -0
  467. package/telegram-plugin/secret-detect/secretlint-source.ts +95 -0
  468. package/telegram-plugin/secret-detect/slug.ts +44 -0
  469. package/telegram-plugin/secret-detect/staging.ts +85 -0
  470. package/telegram-plugin/secret-detect/suppressor.ts +34 -0
  471. package/telegram-plugin/secret-detect/url-redact.ts +60 -0
  472. package/telegram-plugin/secret-detect/vault-write.ts +56 -0
  473. package/telegram-plugin/server.js +41795 -0
  474. package/telegram-plugin/server.ts +171 -0
  475. package/telegram-plugin/session-tail.ts +884 -0
  476. package/telegram-plugin/shared/bot-runtime.ts +324 -0
  477. package/telegram-plugin/silent-reply.ts +58 -0
  478. package/telegram-plugin/slot-banner-driver.ts +147 -0
  479. package/telegram-plugin/slot-banner.ts +86 -0
  480. package/telegram-plugin/start.js +26 -0
  481. package/telegram-plugin/startup-reset.ts +45 -0
  482. package/telegram-plugin/status-reactions.ts +332 -0
  483. package/telegram-plugin/steering.ts +155 -0
  484. package/telegram-plugin/sticker-aliases.ts +249 -0
  485. package/telegram-plugin/stream-controller.ts +311 -0
  486. package/telegram-plugin/stream-reply-handler.ts +664 -0
  487. package/telegram-plugin/streaming-metrics.ts +134 -0
  488. package/telegram-plugin/streaming-report.ts +204 -0
  489. package/telegram-plugin/subagent-watcher.ts +880 -0
  490. package/telegram-plugin/telegram-button-constraints.ts +191 -0
  491. package/telegram-plugin/telegraph.ts +381 -0
  492. package/telegram-plugin/tests/HARNESS.md +340 -0
  493. package/telegram-plugin/tests/_progress-card-harness.ts +105 -0
  494. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +211 -0
  495. package/telegram-plugin/tests/active-pins-sweep.test.ts +309 -0
  496. package/telegram-plugin/tests/active-pins.test.ts +187 -0
  497. package/telegram-plugin/tests/active-reactions-sweep.test.ts +116 -0
  498. package/telegram-plugin/tests/active-reactions.test.ts +198 -0
  499. package/telegram-plugin/tests/answer-stream-dedup.test.ts +352 -0
  500. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +236 -0
  501. package/telegram-plugin/tests/answer-stream.test.ts +878 -0
  502. package/telegram-plugin/tests/ask-user.test.ts +203 -0
  503. package/telegram-plugin/tests/attachment-path.test.ts +199 -0
  504. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +118 -0
  505. package/telegram-plugin/tests/auth-code-auto-capture.test.ts +144 -0
  506. package/telegram-plugin/tests/auth-code-redact.test.ts +248 -0
  507. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +260 -0
  508. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +140 -0
  509. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +559 -0
  510. package/telegram-plugin/tests/auth-dashboard.test.ts +1045 -0
  511. package/telegram-plugin/tests/auth-login-url-button.test.ts +122 -0
  512. package/telegram-plugin/tests/auth-slot-commands.test.ts +640 -0
  513. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +183 -0
  514. package/telegram-plugin/tests/auto-fallback.test.ts +381 -0
  515. package/telegram-plugin/tests/boot-card-account-quota.test.ts +137 -0
  516. package/telegram-plugin/tests/boot-card-dedupe.test.ts +154 -0
  517. package/telegram-plugin/tests/boot-card-probe-target.test.ts +194 -0
  518. package/telegram-plugin/tests/boot-card-reason.test.ts +103 -0
  519. package/telegram-plugin/tests/boot-card-render.test.ts +219 -0
  520. package/telegram-plugin/tests/boot-probes.test.ts +451 -0
  521. package/telegram-plugin/tests/bot-api.harness.ts +116 -0
  522. package/telegram-plugin/tests/bot-runtime.test.ts +190 -0
  523. package/telegram-plugin/tests/bridge-anonymous-refuse.test.ts +60 -0
  524. package/telegram-plugin/tests/context-exhaustion.test.ts +114 -0
  525. package/telegram-plugin/tests/credits-watch.test.ts +221 -0
  526. package/telegram-plugin/tests/dm-command-gate.test.ts +176 -0
  527. package/telegram-plugin/tests/draft-stream.test.ts +752 -0
  528. package/telegram-plugin/tests/draft-transport.test.ts +141 -0
  529. package/telegram-plugin/tests/e2e.test.ts +436 -0
  530. package/telegram-plugin/tests/fake-bot-api.test.ts +213 -0
  531. package/telegram-plugin/tests/fake-bot-api.ts +617 -0
  532. package/telegram-plugin/tests/false-restart-banner.test.ts +253 -0
  533. package/telegram-plugin/tests/first-paint.test.ts +257 -0
  534. package/telegram-plugin/tests/fixtures/pty-tail-tmux-fragment.bin +6 -0
  535. package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +3624 -0
  536. package/telegram-plugin/tests/fleet-state-watcher.test.ts +101 -0
  537. package/telegram-plugin/tests/fleet-state.test.ts +185 -0
  538. package/telegram-plugin/tests/foreman-create-flow.test.ts +359 -0
  539. package/telegram-plugin/tests/foreman-handlers.test.ts +347 -0
  540. package/telegram-plugin/tests/foreman-state.test.ts +164 -0
  541. package/telegram-plugin/tests/foreman-write-ops.test.ts +214 -0
  542. package/telegram-plugin/tests/gateway-409-retry-banner.test.ts +173 -0
  543. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +72 -0
  544. package/telegram-plugin/tests/gateway-bridge.test.ts +811 -0
  545. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +414 -0
  546. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +144 -0
  547. package/telegram-plugin/tests/gateway-message-validator.test.ts +133 -0
  548. package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +103 -0
  549. package/telegram-plugin/tests/gateway-secret-detect.test.ts +127 -0
  550. package/telegram-plugin/tests/gateway-startup-mutex.test.ts +284 -0
  551. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +185 -0
  552. package/telegram-plugin/tests/gateway-startup-reset.test.ts +72 -0
  553. package/telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts +125 -0
  554. package/telegram-plugin/tests/handoff-continuity.test.ts +249 -0
  555. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +243 -0
  556. package/telegram-plugin/tests/harness-parse-mode-validation.test.ts +114 -0
  557. package/telegram-plugin/tests/history.test.ts +364 -0
  558. package/telegram-plugin/tests/html-balanced.ts +63 -0
  559. package/telegram-plugin/tests/html-sanitize.test.ts +146 -0
  560. package/telegram-plugin/tests/idle-footer-wiring.test.ts +88 -0
  561. package/telegram-plugin/tests/idle-footer.test.ts +66 -0
  562. package/telegram-plugin/tests/inbound-coalesce.test.ts +127 -0
  563. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +150 -0
  564. package/telegram-plugin/tests/interrupt-marker.test.ts +126 -0
  565. package/telegram-plugin/tests/ipc-protocol.test.ts +218 -0
  566. package/telegram-plugin/tests/ipc-server-anonymous-refuse.test.ts +82 -0
  567. package/telegram-plugin/tests/ipc-server-client.test.ts +323 -0
  568. package/telegram-plugin/tests/ipc-server-race.test.ts +183 -0
  569. package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +91 -0
  570. package/telegram-plugin/tests/ipc-server-validate-pty-partial.test.ts +64 -0
  571. package/telegram-plugin/tests/ipc-server-validate-update-placeholder.test.ts +77 -0
  572. package/telegram-plugin/tests/ipc-validator.test.ts +274 -0
  573. package/telegram-plugin/tests/issues-card.test.ts +495 -0
  574. package/telegram-plugin/tests/issues-watcher.test.ts +165 -0
  575. package/telegram-plugin/tests/model-unavailable.test.ts +303 -0
  576. package/telegram-plugin/tests/multi-turn-continuity.test.ts +159 -0
  577. package/telegram-plugin/tests/operator-events-history.test.ts +125 -0
  578. package/telegram-plugin/tests/operator-events-session-tail.test.ts +192 -0
  579. package/telegram-plugin/tests/operator-events.test.ts +331 -0
  580. package/telegram-plugin/tests/outbound-ordering.test.ts +293 -0
  581. package/telegram-plugin/tests/parse-mode-rotation.test.ts +164 -0
  582. package/telegram-plugin/tests/permission-rule.test.ts +121 -0
  583. package/telegram-plugin/tests/permission-title.test.ts +106 -0
  584. package/telegram-plugin/tests/pin-event-log.test.ts +124 -0
  585. package/telegram-plugin/tests/plugin-logger.test.ts +97 -0
  586. package/telegram-plugin/tests/poll-health.test.ts +86 -0
  587. package/telegram-plugin/tests/preamble-suppressor.test.ts +285 -0
  588. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +73 -0
  589. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +272 -0
  590. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +258 -0
  591. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +81 -0
  592. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +80 -0
  593. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +215 -0
  594. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +123 -0
  595. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +76 -0
  596. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +62 -0
  597. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +84 -0
  598. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +139 -0
  599. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +773 -0
  600. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +66 -0
  601. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +64 -0
  602. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +190 -0
  603. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +146 -0
  604. package/telegram-plugin/tests/progress-update.test.ts +236 -0
  605. package/telegram-plugin/tests/protocol-fixtures.test.ts +59 -0
  606. package/telegram-plugin/tests/protocol-fixtures.ts +198 -0
  607. package/telegram-plugin/tests/pty-partial-handler.test.ts +326 -0
  608. package/telegram-plugin/tests/pty-tail-real-fixture.test.ts +114 -0
  609. package/telegram-plugin/tests/pty-tail-tmux-fragment.test.ts +71 -0
  610. package/telegram-plugin/tests/pty-tail.test.ts +525 -0
  611. package/telegram-plugin/tests/quota-cache.test.ts +187 -0
  612. package/telegram-plugin/tests/quota-check.test.ts +622 -0
  613. package/telegram-plugin/tests/races.test.ts +842 -0
  614. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +123 -0
  615. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +82 -0
  616. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +114 -0
  617. package/telegram-plugin/tests/real-gateway-harness.ts +699 -0
  618. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +313 -0
  619. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +299 -0
  620. package/telegram-plugin/tests/real-gateway-spec.test.ts +487 -0
  621. package/telegram-plugin/tests/real-gateway.smoke.test.ts +101 -0
  622. package/telegram-plugin/tests/recent-outbound-dedup.test.ts +192 -0
  623. package/telegram-plugin/tests/registry-turns.test.ts +432 -0
  624. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +149 -0
  625. package/telegram-plugin/tests/resolve-calling-subagent.test.ts +269 -0
  626. package/telegram-plugin/tests/restart-watchdog.test.ts +224 -0
  627. package/telegram-plugin/tests/retry-api-call.test.ts +287 -0
  628. package/telegram-plugin/tests/secret-detect-audit.test.ts +58 -0
  629. package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +83 -0
  630. package/telegram-plugin/tests/secret-detect-gitleaks.test.ts +32 -0
  631. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +308 -0
  632. package/telegram-plugin/tests/secret-detect-pipeline.test.ts +123 -0
  633. package/telegram-plugin/tests/secret-detect-secretlint.test.ts +101 -0
  634. package/telegram-plugin/tests/secret-detect-staging.test.ts +45 -0
  635. package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +67 -0
  636. package/telegram-plugin/tests/secret-detect.test.ts +223 -0
  637. package/telegram-plugin/tests/secret-guard-pretool.test.ts +194 -0
  638. package/telegram-plugin/tests/send-typing-action-validation.test.ts +61 -0
  639. package/telegram-plugin/tests/session-tail-capped.test.ts +285 -0
  640. package/telegram-plugin/tests/session-tail.test.ts +524 -0
  641. package/telegram-plugin/tests/setup-flow.test.ts +510 -0
  642. package/telegram-plugin/tests/setup-state.test.ts +146 -0
  643. package/telegram-plugin/tests/silent-reply-guard.test.ts +122 -0
  644. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +350 -0
  645. package/telegram-plugin/tests/slot-banner.test.ts +74 -0
  646. package/telegram-plugin/tests/snapshot-serializer.ts +79 -0
  647. package/telegram-plugin/tests/spawn-detached-cgroup-escape.test.ts +51 -0
  648. package/telegram-plugin/tests/status-accent.test.ts +186 -0
  649. package/telegram-plugin/tests/status-reactions-allowed-filter.test.ts +132 -0
  650. package/telegram-plugin/tests/status-reactions.test.ts +314 -0
  651. package/telegram-plugin/tests/steering.test.ts +282 -0
  652. package/telegram-plugin/tests/sticker-aliases.test.ts +232 -0
  653. package/telegram-plugin/tests/stream-controller-html-fallback.test.ts +127 -0
  654. package/telegram-plugin/tests/stream-controller.test.ts +262 -0
  655. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +208 -0
  656. package/telegram-plugin/tests/stream-reply-handler.test.ts +1292 -0
  657. package/telegram-plugin/tests/streaming-e2e.test.ts +389 -0
  658. package/telegram-plugin/tests/streaming-metrics.test.ts +201 -0
  659. package/telegram-plugin/tests/streaming-orchestration.test.ts +756 -0
  660. package/telegram-plugin/tests/subagent-registry-bugs.test.ts +725 -0
  661. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +213 -0
  662. package/telegram-plugin/tests/subagent-watcher-parent-marker.test.ts +274 -0
  663. package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +243 -0
  664. package/telegram-plugin/tests/subagent-watcher.test.ts +877 -0
  665. package/telegram-plugin/tests/subagents-schema-init-order.test.ts +109 -0
  666. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +116 -0
  667. package/telegram-plugin/tests/telegram-button-constraints.test.ts +194 -0
  668. package/telegram-plugin/tests/telegram-format.test.ts +1093 -0
  669. package/telegram-plugin/tests/telegraph.test.ts +246 -0
  670. package/telegram-plugin/tests/tool-labels.test.ts +383 -0
  671. package/telegram-plugin/tests/turn-active-marker.test.ts +195 -0
  672. package/telegram-plugin/tests/turn-end-regressions.test.ts +489 -0
  673. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +218 -0
  674. package/telegram-plugin/tests/turn-flush-dedup-controller.test.ts +144 -0
  675. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +78 -0
  676. package/telegram-plugin/tests/turn-flush-safety.test.ts +189 -0
  677. package/telegram-plugin/tests/turn-signal-tracker.test.ts +107 -0
  678. package/telegram-plugin/tests/turns-writer.test.ts +323 -0
  679. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +131 -0
  680. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +120 -0
  681. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +114 -0
  682. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +87 -0
  683. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +211 -0
  684. package/telegram-plugin/tests/two-zone-card-cap.test.ts +62 -0
  685. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +101 -0
  686. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +68 -0
  687. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +110 -0
  688. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +128 -0
  689. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +58 -0
  690. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +133 -0
  691. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +155 -0
  692. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +117 -0
  693. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +143 -0
  694. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +149 -0
  695. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +101 -0
  696. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +114 -0
  697. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +105 -0
  698. package/telegram-plugin/tests/typing-wrap.test.ts +141 -0
  699. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +147 -0
  700. package/telegram-plugin/tests/update-factory-edited-and-reactions.test.ts +108 -0
  701. package/telegram-plugin/tests/update-factory.ts +305 -0
  702. package/telegram-plugin/tests/vault-grant-wizard.test.ts +84 -0
  703. package/telegram-plugin/tests/vault-grants-revoke.test.ts +265 -0
  704. package/telegram-plugin/tests/vault-subcommands.test.ts +234 -0
  705. package/telegram-plugin/tests/voice-transcribe.test.ts +196 -0
  706. package/telegram-plugin/tests/waiting-ux-harness.ts +381 -0
  707. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +233 -0
  708. package/telegram-plugin/tests/welcome-text.test.ts +407 -0
  709. package/telegram-plugin/tool-error-filter.ts +89 -0
  710. package/telegram-plugin/tool-labels.ts +330 -0
  711. package/telegram-plugin/tool-names.ts +53 -0
  712. package/telegram-plugin/turn-flush-prose-recovery.ts +40 -0
  713. package/telegram-plugin/turn-flush-safety.ts +109 -0
  714. package/telegram-plugin/turn-signal-tracker.ts +79 -0
  715. package/telegram-plugin/two-zone-card.ts +249 -0
  716. package/telegram-plugin/typing-wrap.ts +92 -0
  717. package/telegram-plugin/voice-transcribe.ts +166 -0
  718. package/telegram-plugin/welcome-text.ts +359 -0
@@ -0,0 +1,1093 @@
1
+ /**
2
+ * Tests for Telegram formatting utilities: markdownToHtml, splitHtmlChunks,
3
+ * file reference wrapping, and message coalescing.
4
+ */
5
+ import { describe, test, expect } from 'vitest'
6
+
7
+ // Import from the side-effect-free format module so tests don't trigger
8
+ // server.ts's startup (env load, token check, grammy init).
9
+ import { markdownToHtml, splitHtmlChunks, isLikelyTelegramHtml, repairEscapedWhitespace, sanitizeForTelegram } from '../format.js'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // markdownToHtml
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe('markdownToHtml', () => {
16
+ test('converts bold **text** to <b>text</b>', () => {
17
+ expect(markdownToHtml('Hello **world**')).toContain('<b>world</b>')
18
+ })
19
+
20
+ test('converts italic *text* to <i>text</i>', () => {
21
+ expect(markdownToHtml('Hello *world*')).toContain('<i>world</i>')
22
+ })
23
+
24
+ test('does not confuse bold and italic', () => {
25
+ const result = markdownToHtml('**bold** and *italic*')
26
+ expect(result).toContain('<b>bold</b>')
27
+ expect(result).toContain('<i>italic</i>')
28
+ })
29
+
30
+ // _..._ italic (underscore form) — 8 cases
31
+ test('converts _text_ to <i>text</i> (plain underscore italic)', () => {
32
+ expect(markdownToHtml('Hello _world_')).toContain('<i>world</i>')
33
+ })
34
+
35
+ test('converts emoji-leading _📥 queued as a new task_', () => {
36
+ expect(markdownToHtml('_📥 queued as a new task_')).toContain('<i>📥 queued as a new task</i>')
37
+ })
38
+
39
+ test('converts emoji-trailing _steer on the prior task 🔁_', () => {
40
+ expect(markdownToHtml('_steer on the prior task 🔁_')).toContain('<i>steer on the prior task 🔁</i>')
41
+ })
42
+
43
+ test('converts both-ends emoji _🔥 hot take 🔥_', () => {
44
+ expect(markdownToHtml('_🔥 hot take 🔥_')).toContain('<i>🔥 hot take 🔥</i>')
45
+ })
46
+
47
+ test('does NOT convert snake_case to italic', () => {
48
+ const result = markdownToHtml('my_snake_case_var')
49
+ expect(result).not.toContain('<i>')
50
+ expect(result).toContain('my_snake_case_var')
51
+ })
52
+
53
+ test('does NOT convert __double__ underscore to italic', () => {
54
+ const result = markdownToHtml('__double__')
55
+ expect(result).not.toContain('<i>')
56
+ })
57
+
58
+ test('does NOT convert word-internal underscores', () => {
59
+ const result = markdownToHtml('foo_bar')
60
+ expect(result).not.toContain('<i>')
61
+ expect(result).toContain('foo_bar')
62
+ })
63
+
64
+ test('_..._ and *...* italics coexist correctly', () => {
65
+ const result = markdownToHtml('*asterisk* and _underscore_')
66
+ expect(result).toContain('<i>asterisk</i>')
67
+ expect(result).toContain('<i>underscore</i>')
68
+ })
69
+
70
+ test('converts inline `code` to <code>code</code>', () => {
71
+ expect(markdownToHtml('Use `console.log`')).toContain('<code>console.log</code>')
72
+ })
73
+
74
+ test('converts code blocks with language', () => {
75
+ const input = '```typescript\nconst x = 1\n```'
76
+ const result = markdownToHtml(input)
77
+ expect(result).toContain('<pre><code class="language-typescript">')
78
+ expect(result).toContain('const x = 1')
79
+ expect(result).toContain('</code></pre>')
80
+ })
81
+
82
+ test('converts code blocks without language', () => {
83
+ const input = '```\nplain code\n```'
84
+ const result = markdownToHtml(input)
85
+ expect(result).toContain('<pre><code>')
86
+ expect(result).toContain('plain code')
87
+ })
88
+
89
+ test('converts strikethrough ~~text~~ to <s>text</s>', () => {
90
+ expect(markdownToHtml('~~deleted~~')).toContain('<s>deleted</s>')
91
+ })
92
+
93
+ test('converts [text](url) to <a href="url">text</a>', () => {
94
+ const result = markdownToHtml('Click [here](https://example.com)')
95
+ expect(result).toContain('<a href="https://example.com">here</a>')
96
+ })
97
+
98
+ test('escapes HTML entities in plain text', () => {
99
+ const result = markdownToHtml('x < y & z > w')
100
+ expect(result).toContain('&lt;')
101
+ expect(result).toContain('&amp;')
102
+ expect(result).toContain('&gt;')
103
+ })
104
+
105
+ test('does not escape HTML inside code blocks', () => {
106
+ const input = '```html\n<div>test</div>\n```'
107
+ const result = markdownToHtml(input)
108
+ expect(result).toContain('&lt;div&gt;test&lt;/div&gt;')
109
+ })
110
+
111
+ test('does not escape HTML inside inline code', () => {
112
+ const result = markdownToHtml('Use `<div>` element')
113
+ expect(result).toContain('<code>&lt;div&gt;</code>')
114
+ })
115
+
116
+ test('wraps file references in code tags', () => {
117
+ const result = markdownToHtml('Edit server.ts and package.json')
118
+ expect(result).toContain('<code>server.ts</code>')
119
+ expect(result).toContain('<code>package.json</code>')
120
+ })
121
+
122
+ test('does not double-wrap file references already in code', () => {
123
+ const result = markdownToHtml('Edit `server.ts` now')
124
+ // Should have exactly one <code>server.ts</code>, not nested
125
+ const matches = result.match(/<code>server\.ts<\/code>/g)
126
+ expect(matches).not.toBeNull()
127
+ expect(matches!.length).toBe(1)
128
+ // And crucially: NO nested <code><code>...</code></code>
129
+ expect(result).not.toContain('<code><code>')
130
+ expect(result).not.toContain('</code></code>')
131
+ })
132
+
133
+ test('inline code containing asterisks does not get re-matched by italic regex (#415)', () => {
134
+ // Regression for #415: inline-code spans containing `*` (e.g. C
135
+ // pointer syntax) used to be restored from their placeholder BEFORE
136
+ // the italic pass, so the italic regex would see `<code>size_t *p</code>`
137
+ // and try to wrap `p</code>...` in <i>...</i>, producing invalid HTML
138
+ // that Telegram rejected with 400 Bad Request, sending the caller into
139
+ // a `format: text` fallback for the rest of the chunk.
140
+ const result = markdownToHtml('Use `size_t *p` to declare a pointer.')
141
+ expect(result).toContain('<code>size_t *p</code>')
142
+ // No stray <i> wrapping the asterisk — pre-fix the buggy output was
143
+ // `<code>size_t <i>p</code> to declare a pointer.</i>`.
144
+ expect(result).not.toMatch(/<i>[^<]*<\/code>/)
145
+ expect(result).not.toMatch(/<code>[^<]*<i>/)
146
+ })
147
+
148
+ test('inline code containing double-asterisks does not get re-matched by bold regex (#415)', () => {
149
+ const result = markdownToHtml('Pattern is `**glob**` not regex.')
150
+ expect(result).toContain('<code>**glob**</code>')
151
+ // The bold regex must not have wrapped the literal asterisks inside <code>.
152
+ expect(result).not.toMatch(/<b>[^<]*<\/code>/)
153
+ expect(result).not.toMatch(/<code>[^<]*<b>/)
154
+ })
155
+
156
+ test('code block containing asterisks does not get re-matched by italic regex (#415)', () => {
157
+ const input = '```c\nsize_t *p = NULL;\n```'
158
+ const result = markdownToHtml(input)
159
+ expect(result).toContain('size_t *p = NULL;')
160
+ expect(result).not.toMatch(/<i>[^<]*<\/code>/)
161
+ expect(result).not.toMatch(/<i>[^<]*<\/pre>/)
162
+ })
163
+
164
+ test('does not double-wrap when inline code sits alongside prose with file refs', () => {
165
+ // Regression for the user-observed bug: messages that mixed inline code
166
+ // spans (backticks around filenames) with prose produced
167
+ // `<code><code>settings.json</code></code>` in the stored history. The
168
+ // file-reference regex ran AFTER inline-code placeholder restoration and
169
+ // re-wrapped the filename inside the just-restored <code> tag because
170
+ // its negative lookbehind did not exclude `>`.
171
+ const result = markdownToHtml(
172
+ 'I mixed raw `<a href="...">` HTML into messages whose `format` defaults ' +
173
+ 'to `html` — but the plugin runs a markdown→HTML converter which escapes ' +
174
+ 'literal `<` and `>`, so raw tags render as visible text in the rendered ' +
175
+ '`settings.json` output.'
176
+ )
177
+ expect(result).not.toContain('<code><code>')
178
+ expect(result).not.toContain('</code></code>')
179
+ // settings.json, format, html should each appear inside exactly one
180
+ // <code> tag — either from the backtick wrapping or the file-ref regex,
181
+ // but never both.
182
+ const settingsMatches = result.match(/<code>settings\.json<\/code>/g)
183
+ expect(settingsMatches).not.toBeNull()
184
+ expect(settingsMatches!.length).toBe(1)
185
+ })
186
+
187
+ test('file-reference wrap still runs on bare filenames in prose', () => {
188
+ // Confirm the fix doesn't break the normal case: bare filenames in
189
+ // plain prose still get auto-wrapped in <code> tags.
190
+ const result = markdownToHtml('Edit server.ts and then run tsc --noEmit')
191
+ expect(result).toContain('<code>server.ts</code>')
192
+ })
193
+
194
+ test('file-reference wrap does not match filenames adjacent to > (inside tag markup)', () => {
195
+ // A filename that sits right after a `>` (tag close) should not be
196
+ // re-wrapped — it's already inside some structured context.
197
+ const input = '<b>foo.ts</b>'
198
+ const result = markdownToHtml(input)
199
+ // Passes through as Telegram HTML (smart pass-through) — filename is
200
+ // not wrapped in <code> because it's inside a <b>.
201
+ expect(result).toBe(input)
202
+ })
203
+
204
+ test('handles nested bold and italic', () => {
205
+ const result = markdownToHtml('**bold *and italic* text**')
206
+ expect(result).toContain('<b>')
207
+ expect(result).toContain('</b>')
208
+ })
209
+
210
+ test('handles plain text with no formatting', () => {
211
+ const result = markdownToHtml('Just a plain message')
212
+ expect(result).toBe('Just a plain message')
213
+ })
214
+
215
+ test('handles empty string', () => {
216
+ expect(markdownToHtml('')).toBe('')
217
+ })
218
+
219
+ test('preserves multiple paragraphs', () => {
220
+ const result = markdownToHtml('First paragraph\n\nSecond paragraph')
221
+ expect(result).toContain('First paragraph')
222
+ expect(result).toContain('Second paragraph')
223
+ })
224
+
225
+ test('converts ## headings to bold (Telegram has no <h1>)', () => {
226
+ const result = markdownToHtml('## My Heading\n\nbody text')
227
+ expect(result).toContain('<b>My Heading</b>')
228
+ expect(result).not.toContain('## ')
229
+ })
230
+
231
+ test('converts # headings to bold', () => {
232
+ const result = markdownToHtml('# Top heading\n\nbody')
233
+ expect(result).toContain('<b>Top heading</b>')
234
+ expect(result).not.toMatch(/^# /m)
235
+ })
236
+
237
+ test('converts deep ### #### headings to bold without losing content', () => {
238
+ const result = markdownToHtml('### Section\n#### Subsection\nbody')
239
+ expect(result).toContain('<b>Section</b>')
240
+ expect(result).toContain('<b>Subsection</b>')
241
+ expect(result).not.toContain('###')
242
+ expect(result).not.toContain('####')
243
+ })
244
+
245
+ test('does not convert # inside code blocks', () => {
246
+ const input = '```bash\n# this is a comment\n```'
247
+ const result = markdownToHtml(input)
248
+ expect(result).toContain('# this is a comment')
249
+ expect(result).not.toContain('<b># this is a comment</b>')
250
+ })
251
+
252
+ // ─── HTML pass-through (the bug that made <b> tags render as text) ─────
253
+
254
+ test('passes through already-rendered Telegram HTML untouched', () => {
255
+ const input = '<b>Bold heading</b>\n<i>italic body</i>'
256
+ expect(markdownToHtml(input)).toBe(input)
257
+ })
258
+
259
+ test('passes through Telegram HTML with <code> blocks', () => {
260
+ const input = '<b>commit</b> <code>abc123</code>'
261
+ expect(markdownToHtml(input)).toBe(input)
262
+ })
263
+
264
+ test('passes through Telegram HTML with mixed tags and text', () => {
265
+ const input = '<b>What you should see</b>\n👀 immediately, then 🤔 after 2s'
266
+ expect(markdownToHtml(input)).toBe(input)
267
+ })
268
+
269
+ test('escapes when input has unsupported HTML tags (e.g. <div>)', () => {
270
+ const input = '<div>not telegram html</div>'
271
+ const out = markdownToHtml(input)
272
+ // Falls into the markdown path → escapes the angle brackets
273
+ expect(out).toContain('&lt;div&gt;')
274
+ })
275
+
276
+ test('escapes when input is plain markdown without HTML', () => {
277
+ const input = '**bold** text'
278
+ const out = markdownToHtml(input)
279
+ expect(out).toContain('<b>bold</b>')
280
+ })
281
+ })
282
+
283
+ describe('isLikelyTelegramHtml', () => {
284
+ test('returns true for simple <b>', () => {
285
+ expect(isLikelyTelegramHtml('<b>hello</b>')).toBe(true)
286
+ })
287
+
288
+ test('returns true for <code>', () => {
289
+ expect(isLikelyTelegramHtml('use <code>git status</code>')).toBe(true)
290
+ })
291
+
292
+ test('returns true for nested supported tags', () => {
293
+ expect(isLikelyTelegramHtml('<b><i>bold italic</i></b>')).toBe(true)
294
+ })
295
+
296
+ test('returns true for <a href>', () => {
297
+ expect(isLikelyTelegramHtml('see <a href="https://x.com">x</a>')).toBe(true)
298
+ })
299
+
300
+ test('returns false when ANY tag is unsupported', () => {
301
+ expect(isLikelyTelegramHtml('<b>fine</b> but <div>not</div>')).toBe(false)
302
+ })
303
+
304
+ test('returns false for plain text with no tags', () => {
305
+ expect(isLikelyTelegramHtml('just words here')).toBe(false)
306
+ })
307
+
308
+ test('returns false for plain markdown', () => {
309
+ expect(isLikelyTelegramHtml('**bold** and *italic*')).toBe(false)
310
+ })
311
+
312
+ test('returns false for code with angle brackets', () => {
313
+ expect(isLikelyTelegramHtml('the operator <-> means something')).toBe(false)
314
+ })
315
+
316
+ // ─── The bug: HTML tags inside markdown inline code spans ─────────────
317
+
318
+ test('ignores HTML tags inside backtick inline code', () => {
319
+ // The model writes `<b>tag</b>` (showing literal HTML in inline code).
320
+ // The text is markdown, NOT raw HTML — must return false.
321
+ expect(isLikelyTelegramHtml('Use `<b>tag</b>` to make text bold.')).toBe(false)
322
+ })
323
+
324
+ test('ignores HTML tags inside fenced code blocks', () => {
325
+ const input = 'Example:\n```html\n<div>hi</div>\n```\nThat\'s it.'
326
+ expect(isLikelyTelegramHtml(input)).toBe(false)
327
+ })
328
+
329
+ test('returns false when text mixes markdown bold with HTML examples in code', () => {
330
+ // The exact bug pattern from the user-facing screenshot regression
331
+ const input = '**1. Raw HTML rendering** — replies showed `<b>tag</b>` text instead of bold.'
332
+ expect(isLikelyTelegramHtml(input)).toBe(false)
333
+ })
334
+
335
+ test('returns false when text has markdown links', () => {
336
+ expect(isLikelyTelegramHtml('See [docs](https://example.com)')).toBe(false)
337
+ })
338
+
339
+ test('returns false when text has markdown headings', () => {
340
+ expect(isLikelyTelegramHtml('## Section\n\nbody')).toBe(false)
341
+ })
342
+
343
+ test('still returns true for pure HTML even with code spans', () => {
344
+ // Code spans can coexist with real HTML — as long as there are NO
345
+ // markdown bold/link/heading patterns and the tags outside code are
346
+ // all valid Telegram HTML, trust it.
347
+ expect(isLikelyTelegramHtml('<b>commit</b> <code>abc123</code>')).toBe(true)
348
+ })
349
+ })
350
+
351
+ describe('markdownToHtml regression: mixed markdown + raw Telegram HTML', () => {
352
+ // The exact bug pattern from the user-facing screenshot regression: model
353
+ // emits markdown bold AND raw <b>/<a> tags in the same message. The
354
+ // markdown path used to escape every `<` to `&lt;`, so the raw tags
355
+ // rendered as literal text. Now the converter preserves whitelisted
356
+ // Telegram HTML tags through the escape pass.
357
+
358
+ test('preserves embedded <b> when text also has markdown bold', () => {
359
+ const input = '**Pattern worth stealing:** the <b>verification subagent</b> is a validator.'
360
+ const out = markdownToHtml(input)
361
+ expect(out).toContain('<b>Pattern worth stealing:</b>')
362
+ expect(out).toContain('<b>verification subagent</b>')
363
+ expect(out).not.toContain('&lt;b&gt;')
364
+ })
365
+
366
+ test('preserves embedded <a href> when text also has markdown bold', () => {
367
+ const input = '**Sources:** see <a href="https://example.com/x">Example</a> for details.'
368
+ const out = markdownToHtml(input)
369
+ expect(out).toContain('<b>Sources:</b>')
370
+ expect(out).toContain('<a href="https://example.com/x">Example</a>')
371
+ expect(out).not.toContain('&lt;a ')
372
+ })
373
+
374
+ test('preserves embedded <i> when text also has markdown bold', () => {
375
+ const input = '**Rule:** group work by <i>what context it needs</i>.'
376
+ const out = markdownToHtml(input)
377
+ expect(out).toContain('<b>Rule:</b>')
378
+ expect(out).toContain('<i>what context it needs</i>')
379
+ expect(out).not.toContain('&lt;i&gt;')
380
+ })
381
+
382
+ test('preserves multiple embedded tags in one message', () => {
383
+ const input = '**Header**\n- <b>Context</b> matters\n- <i>Speed</i> too\n- See <a href="https://x.com">x</a>'
384
+ const out = markdownToHtml(input)
385
+ expect(out).toContain('<b>Header</b>')
386
+ expect(out).toContain('<b>Context</b>')
387
+ expect(out).toContain('<i>Speed</i>')
388
+ expect(out).toContain('<a href="https://x.com">x</a>')
389
+ })
390
+
391
+ test('still escapes unsupported tags even when whitelisted ones are present', () => {
392
+ const input = '**hi** <b>ok</b> and <div>bad</div>'
393
+ const out = markdownToHtml(input)
394
+ expect(out).toContain('<b>hi</b>')
395
+ expect(out).toContain('<b>ok</b>')
396
+ // <div> is not in the whitelist → escaped
397
+ expect(out).toContain('&lt;div&gt;')
398
+ })
399
+
400
+ test('preserves embedded <code> spans alongside markdown', () => {
401
+ const input = '**Run:** <code>git status</code> first.'
402
+ const out = markdownToHtml(input)
403
+ expect(out).toContain('<b>Run:</b>')
404
+ expect(out).toContain('<code>git status</code>')
405
+ })
406
+
407
+ test('preserves <a> with query-string href containing markdown-link-like text', () => {
408
+ const input = 'See <a href="https://example.com/path">the docs</a>.'
409
+ const out = markdownToHtml(input)
410
+ expect(out).toContain('<a href="https://example.com/path">the docs</a>')
411
+ })
412
+ })
413
+
414
+ describe('markdownToHtml regression: HTML in code spans', () => {
415
+ test('renders **bold** correctly when text also contains `<b>` in inline code', () => {
416
+ const input = '**1. Raw HTML rendering** — replies showed `<b>tag</b>` text instead of bold.'
417
+ const out = markdownToHtml(input)
418
+ expect(out).toContain('<b>1. Raw HTML rendering</b>')
419
+ expect(out).toContain('<code>&lt;b&gt;tag&lt;/b&gt;</code>')
420
+ expect(out).not.toContain('**1. Raw HTML rendering**')
421
+ })
422
+
423
+ test('renders fenced code blocks even when they contain HTML examples', () => {
424
+ const input = 'Example:\n```html\n<div>hi</div>\n```'
425
+ const out = markdownToHtml(input)
426
+ expect(out).toContain('<pre><code class="language-html">')
427
+ expect(out).toContain('&lt;div&gt;hi&lt;/div&gt;')
428
+ })
429
+ })
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // splitHtmlChunks
433
+ // ---------------------------------------------------------------------------
434
+
435
+ describe('splitHtmlChunks', () => {
436
+ test('returns single chunk for short text', () => {
437
+ const result = splitHtmlChunks('Hello world', 4000)
438
+ expect(result).toEqual(['Hello world'])
439
+ })
440
+
441
+ test('splits long text into multiple chunks', () => {
442
+ const longText = 'a'.repeat(5000)
443
+ const chunks = splitHtmlChunks(longText, 2000)
444
+ expect(chunks.length).toBeGreaterThan(1)
445
+ // All chunks should be <= maxLen (plus possible closing tags)
446
+ for (const c of chunks) {
447
+ expect(c.length).toBeLessThanOrEqual(2100) // small margin for closing tags
448
+ }
449
+ })
450
+
451
+ test('preserves open tags across chunk boundaries', () => {
452
+ const html = '<b>' + 'x'.repeat(5000) + '</b>'
453
+ const chunks = splitHtmlChunks(html, 2000)
454
+ expect(chunks.length).toBeGreaterThan(1)
455
+ // First chunk should have closing </b>
456
+ expect(chunks[0]).toContain('</b>')
457
+ // Second chunk should reopen <b>
458
+ expect(chunks[1]).toMatch(/^<b>/)
459
+ })
460
+
461
+ test('prefers splitting at paragraph boundaries', () => {
462
+ const html = 'First paragraph content here' + '\n\n' + 'Second paragraph content here'
463
+ // Set maxLen so it would split somewhere in the middle
464
+ const chunks = splitHtmlChunks(html, 35)
465
+ expect(chunks.length).toBe(2)
466
+ expect(chunks[0]).toContain('First paragraph')
467
+ expect(chunks[1]).toContain('Second paragraph')
468
+ })
469
+
470
+ test('handles nested tags', () => {
471
+ const html = '<b><i>' + 'x'.repeat(5000) + '</i></b>'
472
+ const chunks = splitHtmlChunks(html, 2000)
473
+ expect(chunks.length).toBeGreaterThan(1)
474
+ // First chunk should close both tags
475
+ expect(chunks[0]).toMatch(/<\/i><\/b>$/)
476
+ // Second chunk should reopen both tags
477
+ expect(chunks[1]).toMatch(/^<b><i>/)
478
+ })
479
+
480
+ test('handles empty string', () => {
481
+ expect(splitHtmlChunks('')).toEqual([''])
482
+ })
483
+
484
+ test('respects custom maxLen', () => {
485
+ const text = 'a'.repeat(100)
486
+ const chunks = splitHtmlChunks(text, 30)
487
+ expect(chunks.length).toBeGreaterThanOrEqual(3)
488
+ })
489
+
490
+ test('defaults to 4000 maxLen', () => {
491
+ const text = 'a'.repeat(3999)
492
+ const chunks = splitHtmlChunks(text)
493
+ expect(chunks).toEqual([text])
494
+ })
495
+
496
+ test('does not split inside an HTML entity (&amp;)', () => {
497
+ // Construct text where the natural cut would land inside &amp;
498
+ // Position the entity so that maxLen falls between & and ;
499
+ const filler = 'x'.repeat(20)
500
+ // Cut would be at position 22, mid-entity
501
+ const html = filler + ' &amp; more text after the entity'
502
+ const chunks = splitHtmlChunks(html, 22)
503
+ // The entity should not be broken — we should see the full &amp; in
504
+ // some chunk, never &am or amp;.
505
+ for (const c of chunks) {
506
+ expect(c).not.toMatch(/&am$/)
507
+ expect(c).not.toMatch(/^p;/)
508
+ expect(c).not.toMatch(/^amp;/)
509
+ }
510
+ // Recombined text should equal original (allowing for the chunker's
511
+ // tag-rebalancing trim of leading newlines)
512
+ expect(chunks.join('')).toContain('&amp;')
513
+ })
514
+
515
+ test('does not split inside a numeric HTML entity (&#x1F4A9;)', () => {
516
+ const filler = 'a'.repeat(15)
517
+ const html = filler + ' &#x1F4A9; more'
518
+ const chunks = splitHtmlChunks(html, 20)
519
+ for (const c of chunks) {
520
+ expect(c).not.toMatch(/&#x1F$/)
521
+ expect(c).not.toMatch(/^4A9;/)
522
+ }
523
+ })
524
+
525
+ // ─── Regression: tag-name parsing must allow `-` so `tg-spoiler` and
526
+ // `tg-emoji` survive chunk boundaries instead of being truncated to `tg`.
527
+ test('preserves <tg-spoiler> across chunk boundaries', () => {
528
+ const html = '<tg-spoiler>' + 'x'.repeat(5000) + '</tg-spoiler>'
529
+ const chunks = splitHtmlChunks(html, 2000)
530
+ expect(chunks.length).toBeGreaterThan(1)
531
+ // Chunk0 must close with the FULL tag name, not a truncated `</tg>`
532
+ expect(chunks[0]).toMatch(/<\/tg-spoiler>$/)
533
+ expect(chunks[0]).not.toMatch(/<\/tg>$/)
534
+ // Chunk1 must reopen with the full tag name
535
+ expect(chunks[1]).toMatch(/^<tg-spoiler>/)
536
+ expect(chunks[1]).not.toMatch(/^<tg>/)
537
+ })
538
+
539
+ test('preserves <tg-emoji> across chunk boundaries', () => {
540
+ const html = '<tg-emoji emoji-id="5368324170671202286">' + 'y'.repeat(5000) + '</tg-emoji>'
541
+ const chunks = splitHtmlChunks(html, 2000)
542
+ expect(chunks.length).toBeGreaterThan(1)
543
+ expect(chunks[0]).toMatch(/<\/tg-emoji>$/)
544
+ expect(chunks[1]).toMatch(/^<tg-emoji/)
545
+ })
546
+
547
+ // ─── Regression: reopening `<a href="...">` in the next chunk must
548
+ // preserve the href attribute. Previously the splitter emitted bare
549
+ // `<a>` which Telegram rejects.
550
+ test('preserves <a href="..."> attributes across chunk boundaries', () => {
551
+ const href = 'https://example.com/some/deep/path?x=1'
552
+ // Put a natural split point well into the link text so paragraph/space
553
+ // breaks don't land inside the opening tag itself.
554
+ const html = `<a href="${href}">` + 'word '.repeat(1000) + '</a>'
555
+ const chunks = splitHtmlChunks(html, 2000)
556
+ expect(chunks.length).toBeGreaterThan(1)
557
+ // First chunk must close the anchor
558
+ expect(chunks[0]).toMatch(/<\/a>$/)
559
+ // Second chunk must reopen with the FULL href attribute, not bare `<a>`
560
+ expect(chunks[1]).toMatch(new RegExp(`^<a href="${href.replace(/[.?/]/g, '\\$&')}">`))
561
+ expect(chunks[1]).not.toMatch(/^<a>/)
562
+ })
563
+
564
+ test('preserves <code class="language-ts"> attributes across boundaries', () => {
565
+ const html = '<pre><code class="language-ts">' + 'z '.repeat(2000) + '</code></pre>'
566
+ const chunks = splitHtmlChunks(html, 2000)
567
+ expect(chunks.length).toBeGreaterThan(1)
568
+ // Reopened chunk should carry the class attribute
569
+ expect(chunks[1]).toContain('<code class="language-ts">')
570
+ })
571
+
572
+ // ─── Regression: splitter must not cut INSIDE an open tag. Previously,
573
+ // `<a href="..."` followed by a long run of non-space text made the
574
+ // space-fallback pick position 2 (the space inside `<a href=`) and emit
575
+ // a chunk consisting of just `<a`, which Telegram rejects.
576
+ test('does not cut inside an open tag when tag contains the only nearby space', () => {
577
+ const html = '<a href="https://example.com/very/long/url">' + 'y'.repeat(5000) + '</a>'
578
+ const chunks = splitHtmlChunks(html, 2000)
579
+ // No chunk should end mid-tag (e.g. `<a` or `<a href="..`)
580
+ for (const c of chunks) {
581
+ // A chunk ending with `<` or `<tagname` with no closing `>` is malformed.
582
+ // Quick check: count unclosed `<`s by stripping complete tags.
583
+ const withoutTags = c.replace(/<[^>]*>/g, '')
584
+ expect(withoutTags).not.toContain('<')
585
+ }
586
+ })
587
+
588
+ test('backs off when the cut lands between < and > of an opening tag', () => {
589
+ // Construct a case where `cut` would naturally land inside `<b attr="...">`
590
+ const filler = 'a '.repeat(1000) // lots of spaces so splitter has choices
591
+ const html = filler + '<b class="very-long-classname-that-pushes-the-tag-past-cut">' + 'x'.repeat(5000) + '</b>'
592
+ const chunks = splitHtmlChunks(html, 2000)
593
+ // None of the chunks should contain a stray `<` without a matching `>`.
594
+ for (const c of chunks) {
595
+ const withoutTags = c.replace(/<[^>]*>/g, '')
596
+ expect(withoutTags).not.toContain('<')
597
+ expect(withoutTags).not.toContain('>')
598
+ }
599
+ })
600
+ })
601
+
602
+ // ---------------------------------------------------------------------------
603
+ // File reference wrapping
604
+ // ---------------------------------------------------------------------------
605
+
606
+ describe('file reference wrapping', () => {
607
+ test('wraps .ts files', () => {
608
+ expect(markdownToHtml('Look at server.ts')).toContain('<code>server.ts</code>')
609
+ })
610
+
611
+ test('wraps .json files', () => {
612
+ expect(markdownToHtml('Check package.json')).toContain('<code>package.json</code>')
613
+ })
614
+
615
+ test('wraps .py files', () => {
616
+ expect(markdownToHtml('Run main.py')).toContain('<code>main.py</code>')
617
+ })
618
+
619
+ test('wraps complex filenames', () => {
620
+ expect(markdownToHtml('Edit my-component.tsx')).toContain('<code>my-component.tsx</code>')
621
+ })
622
+
623
+ test('does not wrap non-file extensions', () => {
624
+ const result = markdownToHtml('This is sentence.ending with a period')
625
+ // "sentence.ending" shouldn't be wrapped since "ending" is not in the ext list
626
+ expect(result).not.toContain('<code>sentence.ending</code>')
627
+ })
628
+ })
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // Coalescing (unit-level: test the buffer/flush logic)
632
+ // ---------------------------------------------------------------------------
633
+
634
+ describe('coalescing logic', () => {
635
+ test('coalesceKey produces unique keys per chat+user', () => {
636
+ // We test the key format directly — the coalescing behavior is integration-level
637
+ const key1 = `chat1:user1`
638
+ const key2 = `chat1:user2`
639
+ const key3 = `chat2:user1`
640
+ expect(key1).not.toBe(key2)
641
+ expect(key1).not.toBe(key3)
642
+ })
643
+
644
+ test('messages combine with newline separator', () => {
645
+ // Simulate what the coalescing logic does: join texts with \n
646
+ const messages = ['Hello', 'How are you?', 'One more thing']
647
+ const combined = messages.join('\n')
648
+ expect(combined).toBe('Hello\nHow are you?\nOne more thing')
649
+ })
650
+
651
+ test('single message passes through unchanged', () => {
652
+ const messages = ['Hello']
653
+ const combined = messages.join('\n')
654
+ expect(combined).toBe('Hello')
655
+ })
656
+
657
+ test('empty messages produce empty combined text', () => {
658
+ const messages: string[] = []
659
+ const combined = messages.join('\n')
660
+ expect(combined).toBe('')
661
+ })
662
+
663
+ test('messages with newlines preserve internal structure', () => {
664
+ const messages = ['Line 1\nLine 2', 'Line 3']
665
+ const combined = messages.join('\n')
666
+ expect(combined).toBe('Line 1\nLine 2\nLine 3')
667
+ })
668
+ })
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // repairEscapedWhitespace — defends against LLM-side JSON escape bungles
672
+ // where real newlines come through as the literal two-char sequence `\n`.
673
+ // ---------------------------------------------------------------------------
674
+
675
+ describe('repairEscapedWhitespace', () => {
676
+ test('unescapes literal \\n when text has no real newlines', () => {
677
+ const input = 'Line one\\nLine two\\nLine three'
678
+ expect(repairEscapedWhitespace(input)).toBe('Line one\nLine two\nLine three')
679
+ })
680
+
681
+ test('unescapes literal \\n\\n paragraph breaks', () => {
682
+ const input = 'Paragraph one.\\n\\nParagraph two.'
683
+ expect(repairEscapedWhitespace(input)).toBe('Paragraph one.\n\nParagraph two.')
684
+ })
685
+
686
+ test('handles the exact observed bug: html tags mixed with literal \\n', () => {
687
+ // Reproduces the actual stream_reply failure: a model produced a message
688
+ // with <b>/<code> tags and literal `\n` escape sequences instead of real
689
+ // newlines, and Telegram rendered the `\n` as visible characters.
690
+ const input = 'Audit done:\\n\\n<b>README.md</b>\\n• Missing <code>switchroom update</code>\\n• Missing <code>switchroom agent grant</code>'
691
+ const repaired = repairEscapedWhitespace(input)
692
+ expect(repaired).toBe('Audit done:\n\n<b>README.md</b>\n• Missing <code>switchroom update</code>\n• Missing <code>switchroom agent grant</code>')
693
+ // And the repaired text should still be recognized as Telegram HTML
694
+ // so the markdownToHtml pass-through works correctly.
695
+ expect(isLikelyTelegramHtml(repaired)).toBe(true)
696
+ })
697
+
698
+ test('leaves text alone when it already contains real newlines', () => {
699
+ // If the caller provided real newlines, we trust them completely and
700
+ // don't touch literal `\n` that may appear inside their content (e.g.
701
+ // a regex or shell snippet).
702
+ const input = 'Real newline here\nand a literal \\n in a regex example'
703
+ expect(repairEscapedWhitespace(input)).toBe(input)
704
+ })
705
+
706
+ test('leaves single-line text alone when it has no escape sequences', () => {
707
+ const input = 'Just a plain single-line message.'
708
+ expect(repairEscapedWhitespace(input)).toBe(input)
709
+ })
710
+
711
+ test('unescapes \\t and \\r as well', () => {
712
+ const input = 'Col1\\tCol2\\tCol3'
713
+ expect(repairEscapedWhitespace(input)).toBe('Col1\tCol2\tCol3')
714
+ })
715
+
716
+ test('unescapes \\" (quote) when present alongside \\n', () => {
717
+ const input = 'Say \\"hello\\"\\nnext line'
718
+ expect(repairEscapedWhitespace(input)).toBe('Say "hello"\nnext line')
719
+ })
720
+
721
+ test('preserves literal backslash sequences via \\\\', () => {
722
+ // `\\n` in the source is `\\` followed by `n`, which means the user
723
+ // literally wanted a backslash followed by the letter n, NOT a newline.
724
+ // Our order-aware unescape must protect `\\` before touching `\n`.
725
+ const input = 'Windows path: C:\\\\temp\\\\file.txt\\nnext line'
726
+ const out = repairEscapedWhitespace(input)
727
+ expect(out).toBe('Windows path: C:\\temp\\file.txt\nnext line')
728
+ })
729
+
730
+ test('end-to-end with markdownToHtml: repaired text renders correctly', () => {
731
+ // Full pipeline: broken input → repair → markdownToHtml → Telegram HTML.
732
+ const broken = '**Bold line**\\n\\n- bullet one\\n- bullet two'
733
+ const repaired = repairEscapedWhitespace(broken)
734
+ const html = markdownToHtml(repaired)
735
+ expect(html).toContain('<b>Bold line</b>')
736
+ // Real newlines should be present in the HTML output (Telegram renders
737
+ // them as actual line breaks in HTML parse mode).
738
+ expect(html).toContain('\n\n')
739
+ expect(html).toContain('- bullet one')
740
+ // Literal \n must not survive anywhere.
741
+ expect(html).not.toContain('\\n')
742
+ })
743
+ })
744
+
745
+ // ---------------------------------------------------------------------------
746
+ // sanitizeForTelegram — output invariants enforced pre-send
747
+ // ---------------------------------------------------------------------------
748
+
749
+ describe('sanitizeForTelegram', () => {
750
+ // ── Rule 1: strip ## headings ────────────────────────────────────────────
751
+
752
+ test('strips ## heading and converts to bold', () => {
753
+ const result = sanitizeForTelegram('## My Heading\n\nbody text')
754
+ expect(result).toContain('<b>My Heading</b>')
755
+ expect(result).not.toContain('## ')
756
+ })
757
+
758
+ test('strips ### heading and converts to bold', () => {
759
+ const result = sanitizeForTelegram('### Section\n\nbody')
760
+ expect(result).toContain('<b>Section</b>')
761
+ expect(result).not.toContain('### ')
762
+ })
763
+
764
+ test('strips #### heading and converts to bold', () => {
765
+ const result = sanitizeForTelegram('#### Sub\n\nbody')
766
+ expect(result).toContain('<b>Sub</b>')
767
+ expect(result).not.toContain('#### ')
768
+ })
769
+
770
+ test('strips # (h1) heading and converts to bold', () => {
771
+ const result = sanitizeForTelegram('# Title\n\nbody')
772
+ expect(result).toContain('<b>Title</b>')
773
+ expect(result).not.toContain('# Title')
774
+ })
775
+
776
+ // ── Rule 2: flatten nested bullets ──────────────────────────────────────
777
+
778
+ test('flattens 2-space-indented bullets', () => {
779
+ const result = sanitizeForTelegram('- top\n - sub')
780
+ expect(result).toContain('· sub')
781
+ expect(result).not.toContain(' - sub')
782
+ })
783
+
784
+ test('flattens 4-space-indented bullets', () => {
785
+ const result = sanitizeForTelegram('- top\n - deeply nested')
786
+ expect(result).toContain('· deeply nested')
787
+ expect(result).not.toContain(' - deeply nested')
788
+ })
789
+
790
+ test('flattens tab-indented bullets', () => {
791
+ const result = sanitizeForTelegram('- top\n\t- tabbed sub')
792
+ expect(result).toContain('· tabbed sub')
793
+ expect(result).not.toContain('\t- tabbed sub')
794
+ })
795
+
796
+ test('preserves unindented bullets unchanged', () => {
797
+ const result = sanitizeForTelegram('- item one\n- item two')
798
+ expect(result).toContain('- item one')
799
+ expect(result).toContain('- item two')
800
+ // No middle-dot substitution on top-level bullets
801
+ expect(result).not.toContain('· item one')
802
+ })
803
+
804
+ // ── Rule 3: collapse blank lines ────────────────────────────────────────
805
+
806
+ test('collapses 4 blank lines to 2', () => {
807
+ const result = sanitizeForTelegram('before\n\n\n\nafter')
808
+ expect(result).toBe('before\n\nafter')
809
+ })
810
+
811
+ test('collapses 3 blank lines to 2', () => {
812
+ const result = sanitizeForTelegram('a\n\n\nb')
813
+ expect(result).toBe('a\n\nb')
814
+ })
815
+
816
+ test('leaves exactly 2 blank lines alone', () => {
817
+ const result = sanitizeForTelegram('a\n\nb')
818
+ expect(result).toBe('a\n\nb')
819
+ })
820
+
821
+ // ── Rule 4: trailing whitespace ──────────────────────────────────────────
822
+
823
+ test('strips trailing spaces from lines', () => {
824
+ const result = sanitizeForTelegram('hello \nworld ')
825
+ expect(result).toBe('hello\nworld')
826
+ })
827
+
828
+ test('strips trailing tabs from lines', () => {
829
+ const result = sanitizeForTelegram('hello\t\t\nworld')
830
+ expect(result).toBe('hello\nworld')
831
+ })
832
+
833
+ // ── Rule 5: HTML escape inside code/pre ─────────────────────────────────
834
+
835
+ test('HTML-escapes bare < and > inside <code> block', () => {
836
+ const result = sanitizeForTelegram('<code>a < b && c > d</code>')
837
+ expect(result).toContain('<code>a &lt; b')
838
+ expect(result).toContain('&gt; d</code>')
839
+ })
840
+
841
+ test('HTML-escapes bare & inside <code> block', () => {
842
+ const result = sanitizeForTelegram('<code>foo & bar</code>')
843
+ expect(result).toContain('<code>foo &amp; bar</code>')
844
+ })
845
+
846
+ test('HTML-escapes bare < and > inside <pre> block', () => {
847
+ const result = sanitizeForTelegram('<pre><code>if a < b</code></pre>')
848
+ expect(result).toContain('&lt; b')
849
+ })
850
+
851
+ test('does not double-escape already-escaped &amp; in <code>', () => {
852
+ const result = sanitizeForTelegram('<code>a &amp; b</code>')
853
+ // Must remain single-escaped, not become &amp;amp;
854
+ expect(result).toContain('<code>a &amp; b</code>')
855
+ expect(result).not.toContain('&amp;amp;')
856
+ })
857
+
858
+ test('does not double-escape &lt; in <code>', () => {
859
+ const result = sanitizeForTelegram('<code>&lt;div&gt;</code>')
860
+ expect(result).toContain('<code>&lt;div&gt;</code>')
861
+ expect(result).not.toContain('&amp;lt;')
862
+ })
863
+
864
+ test('does not double-escape &#123; numeric entity in <code>', () => {
865
+ const result = sanitizeForTelegram('<code>&#123; x &#125;</code>')
866
+ expect(result).toContain('&#123;')
867
+ expect(result).not.toContain('&amp;#123;')
868
+ })
869
+
870
+ // ── Code block exclusion from structural rules ───────────────────────────
871
+
872
+ test('does not strip ## heading inside <code> block', () => {
873
+ const result = sanitizeForTelegram('<code>## not a heading</code>')
874
+ // The ## stays; only < > & are touched inside code
875
+ expect(result).toContain('## not a heading')
876
+ })
877
+
878
+ test('does not flatten bullets inside <code> block', () => {
879
+ const result = sanitizeForTelegram('<code> - not flattened</code>')
880
+ // The indented bullet stays verbatim inside code
881
+ expect(result).toContain(' - not flattened')
882
+ })
883
+
884
+ test('does not strip ## heading inside <pre> block', () => {
885
+ const result = sanitizeForTelegram('<pre><code class="language-bash"># comment\n## heading\n</code></pre>')
886
+ expect(result).toContain('## heading')
887
+ })
888
+
889
+ // ── Idempotency ──────────────────────────────────────────────────────────
890
+
891
+ test('is idempotent for heading conversion', () => {
892
+ const once = sanitizeForTelegram('## Heading\n\nbody')
893
+ const twice = sanitizeForTelegram(once)
894
+ expect(twice).toBe(once)
895
+ })
896
+
897
+ test('is idempotent for bullet flattening', () => {
898
+ const once = sanitizeForTelegram('- top\n - sub\n - deep')
899
+ const twice = sanitizeForTelegram(once)
900
+ expect(twice).toBe(once)
901
+ })
902
+
903
+ test('is idempotent for blank-line collapse', () => {
904
+ const once = sanitizeForTelegram('a\n\n\n\nb')
905
+ const twice = sanitizeForTelegram(once)
906
+ expect(twice).toBe(once)
907
+ })
908
+
909
+ test('is idempotent for code-block escaping', () => {
910
+ const once = sanitizeForTelegram('<code>a < b & c > d</code>')
911
+ const twice = sanitizeForTelegram(once)
912
+ expect(twice).toBe(once)
913
+ })
914
+
915
+ test('is idempotent for a combined realistic message', () => {
916
+ const input = [
917
+ '## Status Report',
918
+ '',
919
+ '- top item',
920
+ ' - sub item one',
921
+ ' - sub item two',
922
+ '',
923
+ '',
924
+ '',
925
+ 'Here is some <code>a < b</code> inline code.',
926
+ ].join('\n')
927
+ const once = sanitizeForTelegram(input)
928
+ const twice = sanitizeForTelegram(once)
929
+ expect(twice).toBe(once)
930
+ })
931
+ })
932
+
933
+ // ---------------------------------------------------------------------------
934
+ // Markdown table rendering
935
+ // ---------------------------------------------------------------------------
936
+
937
+ describe('markdownToHtml — markdown table rendering', () => {
938
+ // 2-col 3-row → bullet list
939
+ test('2-col 3-row renders as bullet list', () => {
940
+ const input = [
941
+ '| Name | Value |',
942
+ '| --- | --- |',
943
+ '| Alpha | 1 |',
944
+ '| Beta | 2 |',
945
+ '| Gamma | 3 |',
946
+ ].join('\n')
947
+ const result = markdownToHtml(input)
948
+ // Header line present
949
+ expect(result).toContain('Name / Value')
950
+ // Each row is a bullet with <b> first column
951
+ expect(result).toContain('• <b>Alpha</b>')
952
+ expect(result).toContain('• <b>Beta</b>')
953
+ expect(result).toContain('• <b>Gamma</b>')
954
+ // Values appended after dash
955
+ expect(result).toContain('— 1')
956
+ expect(result).toContain('— 2')
957
+ expect(result).toContain('— 3')
958
+ // Must NOT contain any raw table markdown pipes
959
+ expect(result).not.toContain('| --- |')
960
+ expect(result).not.toContain('<table>')
961
+ })
962
+
963
+ // 3-col 4-row → bullet list (still within ≤3 cols AND ≤6 rows)
964
+ test('3-col 4-row renders as bullet list', () => {
965
+ const input = [
966
+ '| Tool | Status | Notes |',
967
+ '| ---- | ------ | ----- |',
968
+ '| bun | ok | fast |',
969
+ '| tsc | ok | strict |',
970
+ '| eslint | warn | fixable |',
971
+ '| vitest | skip | optional |',
972
+ ].join('\n')
973
+ const result = markdownToHtml(input)
974
+ expect(result).toContain('• <b>bun</b>')
975
+ expect(result).toContain('• <b>tsc</b>')
976
+ // Third column appended too
977
+ expect(result).toContain('fast')
978
+ expect(result).toContain('strict')
979
+ expect(result).not.toContain('| ---- |')
980
+ })
981
+
982
+ // 4-col 3-row → <pre> block (4 cols exceeds limit)
983
+ test('4-col 3-row renders as <pre> block', () => {
984
+ const input = [
985
+ '| A | B | C | D |',
986
+ '| - | - | - | - |',
987
+ '| 1 | 2 | 3 | 4 |',
988
+ '| 5 | 6 | 7 | 8 |',
989
+ '| 9 | 0 | 1 | 2 |',
990
+ ].join('\n')
991
+ const result = markdownToHtml(input)
992
+ expect(result).toContain('<pre>')
993
+ expect(result).toContain('</pre>')
994
+ // Column headers should appear in the pre block
995
+ expect(result).toContain('A')
996
+ expect(result).toContain('B')
997
+ // Must not produce a bullet list
998
+ expect(result).not.toContain('• <b>')
999
+ })
1000
+
1001
+ // 3-col 8-row → <pre> block (8 rows exceeds ≤6 limit)
1002
+ test('3-col 8-row renders as <pre> block', () => {
1003
+ const rows = Array.from({ length: 8 }, (_, i) => `| Row${i + 1} | X${i} | Y${i} |`)
1004
+ const input = [
1005
+ '| Name | ColX | ColY |',
1006
+ '| ---- | ---- | ---- |',
1007
+ ...rows,
1008
+ ].join('\n')
1009
+ const result = markdownToHtml(input)
1010
+ expect(result).toContain('<pre>')
1011
+ expect(result).toContain('</pre>')
1012
+ expect(result).not.toContain('• <b>')
1013
+ })
1014
+
1015
+ // Pipe in plain prose is NOT a table
1016
+ test('plain prose with a pipe is not converted to a table', () => {
1017
+ const input = 'Run echo foo | bar to see output'
1018
+ const result = markdownToHtml(input)
1019
+ expect(result).toContain('echo foo | bar')
1020
+ expect(result).not.toContain('• <b>')
1021
+ expect(result).not.toContain('<pre>')
1022
+ })
1023
+
1024
+ // Pipe in code block is not a table
1025
+ test('pipe inside fenced code block is left verbatim', () => {
1026
+ const input = [
1027
+ '```bash',
1028
+ '| Name | Value |',
1029
+ '| --- | --- |',
1030
+ '| foo | bar |',
1031
+ '```',
1032
+ ].join('\n')
1033
+ const result = markdownToHtml(input)
1034
+ // Should be inside <pre><code>, not a rendered table
1035
+ expect(result).toContain('<pre>')
1036
+ expect(result).toContain('| Name | Value |')
1037
+ expect(result).not.toContain('• <b>')
1038
+ })
1039
+
1040
+ // Table with empty cells
1041
+ test('table with empty cells is handled gracefully', () => {
1042
+ const input = [
1043
+ '| Key | Value |',
1044
+ '| --- | ----- |',
1045
+ '| present | |',
1046
+ '| | orphan |',
1047
+ ].join('\n')
1048
+ const result = markdownToHtml(input)
1049
+ // Should produce output without crashing; empty cells rendered as empty/—
1050
+ expect(result).toContain('• <b>present</b>')
1051
+ // No raw markdown pipes in output
1052
+ expect(result).not.toContain('| --- |')
1053
+ })
1054
+
1055
+ // Table preceded and followed by paragraph text — only the table transforms
1056
+ test('table inside paragraph text: only the table block transforms', () => {
1057
+ const input = [
1058
+ 'Before paragraph.',
1059
+ '',
1060
+ '| Name | Score |',
1061
+ '| ---- | ----- |',
1062
+ '| Alice | 95 |',
1063
+ '| Bob | 87 |',
1064
+ '',
1065
+ 'After paragraph.',
1066
+ ].join('\n')
1067
+ const result = markdownToHtml(input)
1068
+ // Prose preserved
1069
+ expect(result).toContain('Before paragraph.')
1070
+ expect(result).toContain('After paragraph.')
1071
+ // Table converted
1072
+ expect(result).toContain('• <b>Alice</b>')
1073
+ expect(result).toContain('• <b>Bob</b>')
1074
+ // No raw table markdown remains
1075
+ expect(result).not.toContain('| ---- |')
1076
+ })
1077
+
1078
+ // HTML entities in cell content are properly escaped
1079
+ test('cell content with ampersand is safely escaped', () => {
1080
+ const input = [
1081
+ '| Operator | Meaning |',
1082
+ '| -------- | ------- |',
1083
+ '| AND | a & b |',
1084
+ '| OR | x & y |',
1085
+ ].join('\n')
1086
+ const result = markdownToHtml(input)
1087
+ // & in cell content must be entity-escaped
1088
+ expect(result).toContain('&amp;')
1089
+ // Output is still a bullet list
1090
+ expect(result).toContain('• <b>AND</b>')
1091
+ expect(result).toContain('• <b>OR</b>')
1092
+ })
1093
+ })