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,1104 @@
1
+ /**
2
+ * `/auth` dashboard — pure logic for the inline-keyboard auth surface.
3
+ *
4
+ * When a user sends `/auth` with no args, the gateway renders a mobile-
5
+ * native dashboard: slot list with health badges, utilization bars,
6
+ * and a button grid for the common actions (reauth, add, use, rm,
7
+ * fallback). Tapping a button fires a `callback_query` with a
8
+ * structured `auth:<action>:<agent>[:<slot>]` payload that the gateway
9
+ * routes back to the matching CLI handler.
10
+ *
11
+ * This module holds only the pure parts — dashboard text generator,
12
+ * keyboard builder, and the callback-data parser. Side effects (CLI
13
+ * execs, Telegram API calls) live in gateway.ts so tests run without
14
+ * a bot process or a live filesystem.
15
+ *
16
+ * JTBD rationale:
17
+ * - keep-my-subscription-honest: "user can state in one sentence
18
+ * what they're paying for" — dashboard header lists it in 2 lines
19
+ * (Plan + bank). "When the user hits a plan limit, the product
20
+ * says so honestly" — quota badges + [Fall back] button only
21
+ * visible when hot.
22
+ * - restart-and-know-what-im-running: "auth state is part of the
23
+ * picture" — the dashboard IS the auth picture, tappable.
24
+ */
25
+
26
+ import { InlineKeyboard } from "grammy";
27
+
28
+ /**
29
+ * Slot-health values emitted by `switchroom auth list --json`.
30
+ *
31
+ * The CLI distinguishes 'active' (the currently-active slot, which is
32
+ * also healthy) from 'healthy' (a non-active slot with a valid token).
33
+ * Dashboard treats both as healthy for the badge — 'active' is already
34
+ * surfaced via the ● marker and the '(active)' label; duplicating it
35
+ * in the health badge would be noisy.
36
+ *
37
+ * Source: src/auth/accounts.ts SlotHealth enum.
38
+ */
39
+ export type SlotHealth = "active" | "healthy" | "expired" | "quota-exhausted" | "missing";
40
+
41
+ export interface DashboardSlot {
42
+ slot: string;
43
+ active: boolean;
44
+ health: SlotHealth;
45
+ /** Epoch ms at which the quota window resets (for quota-exhausted). */
46
+ quotaExhaustedUntil?: number | null;
47
+ /** 5-hour utilization as a percentage 0-100, if known. */
48
+ fiveHourPct?: number | null;
49
+ /** 7-day utilization as a percentage 0-100, if known. */
50
+ sevenDayPct?: number | null;
51
+ }
52
+
53
+ export interface DashboardState {
54
+ agent: string;
55
+ bankId: string;
56
+ plan?: string | null;
57
+ /**
58
+ * Anthropic's `rateLimitTier` from the active slot's credentials
59
+ * — e.g. `default_claude_max_5x` vs `default_claude_max_20x`. The
60
+ * tier is the easiest human-visible signal that "the account I
61
+ * meant to authorize with got authorized". Without this, the
62
+ * dashboard just shows `Plan: max` for both tiers and an account
63
+ * mismatch is silent until the agent hits quota.
64
+ */
65
+ rateLimitTier?: string | null;
66
+ slots: DashboardSlot[];
67
+ /** True when at least one slot shows >= 90% utilization on either
68
+ * window. Toggles the [Fall back now] button's visibility. */
69
+ quotaHot: boolean;
70
+ /** ISO timestamp of the snapshot, shown in the header. */
71
+ generatedAt?: string;
72
+ /**
73
+ * Slot name of the currently-pending auth session, if any.
74
+ *
75
+ * Populated by the gateway from the agent's
76
+ * `.claude/.setup-token.session.json` when present. When non-null,
77
+ * the dashboard renders a `[♻️ Restart flow]` button so the user
78
+ * can explicitly kill + restart the flow if it's gone sideways
79
+ * (browser took too long, claude setup-token crashed, etc.).
80
+ *
81
+ * Complements the automatic stale-session detection in
82
+ * startAuthSession — catches the cases where the user wants to
83
+ * start over BEFORE the challenge actually drifts.
84
+ */
85
+ pendingSessionSlot?: string | null;
86
+ /**
87
+ * Per-account summaries derived from `switchroom auth account list
88
+ * --json`. Optional: undefined when the gateway can't reach the CLI
89
+ * or the CLI is older than v0.6.x (no --json flag). When present
90
+ * (even as an empty array), the dashboard renders the accounts
91
+ * section. The `enabledHere` flag drives the ✓/○ marker — `agents`
92
+ * field from the JSON, with `agents.includes(state.agent)` mapped
93
+ * into this struct by the gateway.
94
+ */
95
+ accounts?: ReadonlyArray<AccountSummary>;
96
+ /** True when more accounts exist than `ACCOUNTS_DISPLAY_CAP` — the
97
+ * render appends a noop "more accounts (use CLI)" row. */
98
+ accountsTruncated?: boolean;
99
+ /**
100
+ * True when this agent has slot credentials we could promote into a
101
+ * shared account via `auth share`. Drives the bootstrap "🌐 Share to
102
+ * fleet" button visibility — only useful when no accounts exist yet.
103
+ */
104
+ canBootstrapShare?: boolean;
105
+ }
106
+
107
+ /**
108
+ * Per-account summary for the inline-keyboard dashboard's accounts
109
+ * section. Mirrors the JSON shape `auth account list --json` emits,
110
+ * collapsed to the fields the renderer needs. Pure data — no behaviour.
111
+ */
112
+ export type AccountHealth =
113
+ | "healthy"
114
+ | "quota-exhausted"
115
+ | "expired"
116
+ | "missing-credentials"
117
+ | "missing-refresh-token";
118
+
119
+ export interface AccountSummary {
120
+ readonly label: string;
121
+ readonly health: AccountHealth;
122
+ /** True when this agent appears in the account's `agents` list. */
123
+ readonly enabledHere: boolean;
124
+ readonly subscriptionType?: string;
125
+ readonly expiresAt?: number;
126
+ /**
127
+ * Per-account 5h-window utilization, 0–100. Populated by the
128
+ * gateway's account-level quota probe — mirrored from the
129
+ * `anthropic-ratelimit-unified-5h-utilization` header on the
130
+ * Anthropic API response. Undefined means "not probed yet" (the
131
+ * dashboard renders a placeholder rather than 0%).
132
+ */
133
+ readonly fiveHourPct?: number;
134
+ /**
135
+ * Per-account 7d-window utilization. Same source as
136
+ * {@link fiveHourPct} — `anthropic-ratelimit-unified-7d-utilization`.
137
+ */
138
+ readonly sevenDayPct?: number;
139
+ /** Unix ms when the 5h cap resets, when known. */
140
+ readonly fiveHourResetAt?: number;
141
+ /** Unix ms when the 7d cap resets, when known. */
142
+ readonly sevenDayResetAt?: number;
143
+ /**
144
+ * Unix ms when the account is expected to come back from a
145
+ * quota-exhausted state. Populated when the cached probe says the
146
+ * account is exhausted (server-side `quota-exhausted` or local
147
+ * 5h utilization == 100%). Render shows "exhausted · resets in
148
+ * Nh Mm" rather than the percentage row.
149
+ */
150
+ readonly quotaExhaustedUntil?: number;
151
+ /**
152
+ * True when this account sits at index 0 of THIS agent's
153
+ * `auth.accounts:` list — i.e. it's the post-fanout active for this
154
+ * agent. Drives the `▶` glyph + "Active" framing in the dashboard
155
+ * render and suppresses the per-account `⤴ Promote` button (you
156
+ * can't promote what's already primary).
157
+ *
158
+ * Populated by the gateway from the new `primaryForAgents` field on
159
+ * `auth account list --json` (added v0.6.9). Optional: undefined
160
+ * means "old CLI without the field" — render falls back to the
161
+ * pre-v3 unmarked layout.
162
+ */
163
+ readonly activeForThisAgent?: boolean;
164
+ }
165
+
166
+ /**
167
+ * Thresholds that govern what counts as "quota hot" — the boundary at
168
+ * which we surface the [Fall back now] button without the user asking.
169
+ * Aligned with the auto-fallback poller's trigger point in
170
+ * telegram-plugin/auto-fallback.ts (DEFAULT_TRIGGER_UTILIZATION_PCT
171
+ * = 99.5) but relaxed a little for the "you might want to act" UX
172
+ * affordance on the dashboard — the button appearing at 90% gives the
173
+ * user agency before the automatic fallback takes over.
174
+ */
175
+ export const QUOTA_HOT_THRESHOLD_PCT = 90;
176
+
177
+ /** Max account rows rendered inline. Beyond this, the dashboard adds a
178
+ * truncated-noop row pointing the user to the CLI for the rest. Five
179
+ * is enough for typical fleets without overflowing a mobile screen. */
180
+ export const ACCOUNTS_DISPLAY_CAP = 5;
181
+
182
+ /** Telegram caps callback_data at 64 bytes. Render-time guard rejects
183
+ * encoded payloads beyond this and renders a noop fallback button. */
184
+ export const CALLBACK_BUDGET_BYTES = 64;
185
+
186
+ export type CallbackAction =
187
+ | { kind: "refresh"; agent: string }
188
+ | { kind: "reauth"; agent: string; slot?: string }
189
+ | { kind: "add"; agent: string }
190
+ | { kind: "use"; agent: string; slot: string }
191
+ | { kind: "rm"; agent: string; slot: string }
192
+ | { kind: "confirm-rm"; agent: string; slot: string }
193
+ | { kind: "fallback"; agent: string }
194
+ | { kind: "usage"; agent: string }
195
+ | { kind: "restart-flow"; agent: string; slot: string }
196
+ // Account-level (#per-agent-cards / #share-auth-across-the-fleet).
197
+ // Single-character verbs (ae/ad/cae/cad/sf) maximise label headroom
198
+ // inside the 64-byte callback_data cap.
199
+ | { kind: "account-enable"; agent: string; label: string }
200
+ | { kind: "account-disable"; agent: string; label: string }
201
+ | { kind: "confirm-account-enable"; agent: string; label: string }
202
+ | { kind: "confirm-account-disable"; agent: string; label: string }
203
+ | { kind: "share-fleet"; agent: string }
204
+ // v3a: per-account drill-down sub-view (accounts-first redesign).
205
+ // Short verbs (av/arm/armc/ara) preserve label headroom in 64-byte cap.
206
+ | { kind: "account-view"; agent: string; label: string }
207
+ | { kind: "account-rm"; agent: string; label: string }
208
+ | { kind: "account-rm-confirm"; agent: string; label: string }
209
+ | { kind: "account-reauth"; agent: string; label: string }
210
+ // v3b: in-place promote — moves a fallback to primary without leaving
211
+ // the dashboard. Two-stage confirm mirrors enable/disable. Verbs `apr`
212
+ // / `cpr` are 3 chars max so a 40-char agent + 64-char label still
213
+ // fits the 64-byte callback_data cap (auth:cpr:agent:label = 12 +
214
+ // agent + label ≤ 64).
215
+ | { kind: "account-promote"; agent: string; label: string }
216
+ | { kind: "confirm-account-promote"; agent: string; label: string }
217
+ // v3c: switch-primary picker. Replaces the per-fallback `⤴ Promote`
218
+ // buttons that flooded the main board with a single `🔀 Switch
219
+ // primary →` button. Tapping it edits the keyboard in-place to a
220
+ // picker view (one row per fallback → tap → confirm-account-promote).
221
+ // Cancel returns to the main dashboard via a refresh.
222
+ | { kind: "switch-primary-view"; agent: string }
223
+ | { kind: "noop" };
224
+
225
+ const CALLBACK_PREFIX = "auth:";
226
+
227
+ /** Encode an action into the <=64-byte callback_data string Telegram
228
+ * allows. Keep the shape `auth:<verb>:<agent>[:<slot>]` — single-level
229
+ * parser, no JSON, no escaping headaches. */
230
+ export function encodeCallbackData(action: CallbackAction): string {
231
+ switch (action.kind) {
232
+ case "refresh":
233
+ return `${CALLBACK_PREFIX}refresh:${action.agent}`;
234
+ case "reauth":
235
+ return action.slot
236
+ ? `${CALLBACK_PREFIX}reauth:${action.agent}:${action.slot}`
237
+ : `${CALLBACK_PREFIX}reauth:${action.agent}`;
238
+ case "add":
239
+ return `${CALLBACK_PREFIX}add:${action.agent}`;
240
+ case "use":
241
+ return `${CALLBACK_PREFIX}use:${action.agent}:${action.slot}`;
242
+ case "rm":
243
+ return `${CALLBACK_PREFIX}rm:${action.agent}:${action.slot}`;
244
+ case "confirm-rm":
245
+ return `${CALLBACK_PREFIX}confirm-rm:${action.agent}:${action.slot}`;
246
+ case "fallback":
247
+ return `${CALLBACK_PREFIX}fallback:${action.agent}`;
248
+ case "usage":
249
+ return `${CALLBACK_PREFIX}usage:${action.agent}`;
250
+ case "restart-flow":
251
+ return `${CALLBACK_PREFIX}restart-flow:${action.agent}:${action.slot}`;
252
+ case "account-enable":
253
+ return `${CALLBACK_PREFIX}ae:${action.agent}:${action.label}`;
254
+ case "account-disable":
255
+ return `${CALLBACK_PREFIX}ad:${action.agent}:${action.label}`;
256
+ case "confirm-account-enable":
257
+ return `${CALLBACK_PREFIX}cae:${action.agent}:${action.label}`;
258
+ case "confirm-account-disable":
259
+ return `${CALLBACK_PREFIX}cad:${action.agent}:${action.label}`;
260
+ case "share-fleet":
261
+ return `${CALLBACK_PREFIX}sf:${action.agent}`;
262
+ case "account-view":
263
+ return `${CALLBACK_PREFIX}av:${action.agent}:${action.label}`;
264
+ case "account-rm":
265
+ return `${CALLBACK_PREFIX}arm:${action.agent}:${action.label}`;
266
+ case "account-rm-confirm":
267
+ return `${CALLBACK_PREFIX}armc:${action.agent}:${action.label}`;
268
+ case "account-reauth":
269
+ return `${CALLBACK_PREFIX}ara:${action.agent}:${action.label}`;
270
+ case "account-promote":
271
+ return `${CALLBACK_PREFIX}apr:${action.agent}:${action.label}`;
272
+ case "confirm-account-promote":
273
+ return `${CALLBACK_PREFIX}cpr:${action.agent}:${action.label}`;
274
+ case "switch-primary-view":
275
+ return `${CALLBACK_PREFIX}spv:${action.agent}`;
276
+ case "noop":
277
+ return `${CALLBACK_PREFIX}noop`;
278
+ }
279
+ }
280
+
281
+ /** Parse the gateway's inbound callback_data into an action. Returns
282
+ * `{kind: 'noop'}` for anything that doesn't match our shape — the
283
+ * caller should still answerCallbackQuery() but otherwise drop. */
284
+ export function parseCallbackData(data: string): CallbackAction {
285
+ if (!data.startsWith(CALLBACK_PREFIX)) return { kind: "noop" };
286
+ // Reject payloads beyond Telegram's 64-byte cap. Telegram itself
287
+ // refuses to deliver those, but the parser stays defensive in case
288
+ // a test or fuzzer hands us one.
289
+ if (Buffer.byteLength(data, "utf8") > CALLBACK_BUDGET_BYTES) {
290
+ return { kind: "noop" };
291
+ }
292
+ const rest = data.slice(CALLBACK_PREFIX.length);
293
+ const parts = rest.split(":");
294
+ const [verb, agent, third] = parts;
295
+ // Account-level verbs (single-char) accept a label as the third
296
+ // segment instead of a slot. We branch on verb first so each segment
297
+ // is validated against its own regex.
298
+ if (verb === "ae" || verb === "ad" || verb === "cae" || verb === "cad" ||
299
+ verb === "av" || verb === "arm" || verb === "armc" || verb === "ara" ||
300
+ verb === "apr" || verb === "cpr") {
301
+ if (!isSafeAgentName(agent ?? "")) return { kind: "noop" };
302
+ if (!third || !isSafeAccountLabel(third)) return { kind: "noop" };
303
+ const label = third;
304
+ if (verb === "ae") return { kind: "account-enable", agent, label };
305
+ if (verb === "ad") return { kind: "account-disable", agent, label };
306
+ if (verb === "cae") return { kind: "confirm-account-enable", agent, label };
307
+ if (verb === "cad") return { kind: "confirm-account-disable", agent, label };
308
+ if (verb === "av") return { kind: "account-view", agent, label };
309
+ if (verb === "arm") return { kind: "account-rm", agent, label };
310
+ if (verb === "armc") return { kind: "account-rm-confirm", agent, label };
311
+ if (verb === "ara") return { kind: "account-reauth", agent, label };
312
+ if (verb === "apr") return { kind: "account-promote", agent, label };
313
+ // verb === "cpr"
314
+ return { kind: "confirm-account-promote", agent, label };
315
+ }
316
+ if (verb === "sf") {
317
+ if (!isSafeAgentName(agent ?? "")) return { kind: "noop" };
318
+ return { kind: "share-fleet", agent };
319
+ }
320
+ if (verb === "spv") {
321
+ if (!isSafeAgentName(agent ?? "")) return { kind: "noop" };
322
+ return { kind: "switch-primary-view", agent };
323
+ }
324
+ if (!isSafeAgentName(agent ?? "")) return { kind: "noop" };
325
+ const slot = third;
326
+ switch (verb) {
327
+ case "refresh":
328
+ return { kind: "refresh", agent };
329
+ case "reauth":
330
+ return slot && isSafeSlotName(slot)
331
+ ? { kind: "reauth", agent, slot }
332
+ : { kind: "reauth", agent };
333
+ case "add":
334
+ return { kind: "add", agent };
335
+ case "use":
336
+ if (!slot || !isSafeSlotName(slot)) return { kind: "noop" };
337
+ return { kind: "use", agent, slot };
338
+ case "rm":
339
+ if (!slot || !isSafeSlotName(slot)) return { kind: "noop" };
340
+ return { kind: "rm", agent, slot };
341
+ case "confirm-rm":
342
+ if (!slot || !isSafeSlotName(slot)) return { kind: "noop" };
343
+ return { kind: "confirm-rm", agent, slot };
344
+ case "fallback":
345
+ return { kind: "fallback", agent };
346
+ case "usage":
347
+ return { kind: "usage", agent };
348
+ case "restart-flow":
349
+ if (!slot || !isSafeSlotName(slot)) return { kind: "noop" };
350
+ return { kind: "restart-flow", agent, slot };
351
+ default:
352
+ return { kind: "noop" };
353
+ }
354
+ }
355
+
356
+ function isSafeAgentName(name: string): boolean {
357
+ return /^[a-zA-Z0-9_-]{1,64}$/.test(name);
358
+ }
359
+
360
+ function isSafeSlotName(name: string): boolean {
361
+ return /^[a-zA-Z0-9_-]{1,32}$/.test(name);
362
+ }
363
+
364
+ /**
365
+ * Account labels match the CLI's `validateAccountLabel` regex
366
+ * (`src/auth/account-store.ts`): `[A-Za-z0-9._@+-]{1,64}`. The label
367
+ * accepts email-shaped strings (`pixsoul@gmail.com`) and gmail-tag
368
+ * forms (`ken+work@example.com`) so operators can label accounts by
369
+ * the Anthropic email they signed up with — the JTBD's "the user
370
+ * manages accounts" works best when the labels read like the
371
+ * identities the user already knows.
372
+ *
373
+ * Dashboard-side validator so the parser doesn't need to import
374
+ * from `src/`. Keep in sync with `LABEL_RE` in account-store.ts and
375
+ * `ACCOUNT_LABEL_RE` in auth-slot-parser.ts.
376
+ *
377
+ * The `.` and `..` reservations match the CLI's defensive guards —
378
+ * those tokens are valid characters but reserved as filesystem
379
+ * lookalikes that would create ambiguous on-disk paths under
380
+ * `~/.switchroom/accounts/`. `:` is omitted on purpose because it
381
+ * would corrupt callback_data parsing in the Telegram dashboard.
382
+ */
383
+ export function isSafeAccountLabel(name: string): boolean {
384
+ if (name === "." || name === "..") return false;
385
+ return /^[A-Za-z0-9._@+-]{1,64}$/.test(name);
386
+ }
387
+
388
+ /**
389
+ * Build the dashboard message text + inline keyboard. Pure — no side
390
+ * effects. The gateway sends the result via ctx.reply or editMessageText.
391
+ */
392
+ export function buildDashboard(state: DashboardState): {
393
+ text: string;
394
+ keyboard: InlineKeyboard;
395
+ } {
396
+ return {
397
+ text: buildDashboardText(state),
398
+ keyboard: buildDashboardKeyboard(state),
399
+ };
400
+ }
401
+
402
+ export function buildDashboardText(state: DashboardState): string {
403
+ const lines: string[] = [];
404
+ lines.push(`━━━ <b>Auth • ${escapeHtml(state.agent)}</b> ━━━`);
405
+ // Show the full rate-limit tier when we have it — e.g. 'max_5x' vs
406
+ // 'max_20x' lets the user tell at a glance whether the correct
407
+ // Anthropic account got authorized during reauth. Otherwise fall
408
+ // back to the plain plan name.
409
+ const tierLabel = state.rateLimitTier
410
+ ? formatRateLimitTier(state.rateLimitTier)
411
+ : state.plan
412
+ ? state.plan
413
+ : null;
414
+ const planLine = tierLabel
415
+ ? `Bank: <code>${escapeHtml(state.bankId)}</code> · Plan: <b>${escapeHtml(tierLabel)}</b>`
416
+ : `Bank: <code>${escapeHtml(state.bankId)}</code>`;
417
+ lines.push(planLine);
418
+ lines.push("");
419
+
420
+ // v3a: accounts appear above slots — accounts are first-class, slots
421
+ // are an implementation detail of how credentials attach to a process.
422
+ // v3b: active account (the one at this agent's auth.accounts[0])
423
+ // floats to the top with a `▶` glyph; remaining rows render under a
424
+ // "Fallback:" subhead in agent-list order. When `activeForThisAgent`
425
+ // is unset on every entry (older CLI without primaryForAgents in
426
+ // --json), we fall back to the v3a layout — bullets only, no header.
427
+ if (state.accounts != null && state.accounts.length > 0) {
428
+ lines.push(`<b>Anthropic accounts (${state.accounts.length})</b>`);
429
+ const visible = state.accounts.slice(0, ACCOUNTS_DISPLAY_CAP);
430
+ const active = visible.find((a) => a.activeForThisAgent === true);
431
+ const fallbacks = visible.filter((a) => a !== active);
432
+ const haveActiveSignal = active != null;
433
+ if (haveActiveSignal && active != null) {
434
+ lines.push(renderActiveAccountRow(active));
435
+ }
436
+ if (fallbacks.length > 0) {
437
+ // Only emit the subhead when there's a distinguished active row;
438
+ // otherwise the list is just "all accounts, no opinion" and a
439
+ // header would be misleading.
440
+ if (haveActiveSignal) lines.push(` <i>Fallback ↓:</i>`);
441
+ for (const acc of fallbacks) {
442
+ lines.push(renderFallbackAccountRow(acc, haveActiveSignal));
443
+ const quotaLine = formatAccountQuotaLine(acc);
444
+ if (quotaLine) lines.push(` └ ${quotaLine}`);
445
+ }
446
+ }
447
+ if (state.accountsTruncated) {
448
+ lines.push(` … ${state.accounts.length - ACCOUNTS_DISPLAY_CAP} more (use CLI)`);
449
+ }
450
+ lines.push("");
451
+ }
452
+
453
+ // Slot ID lookup: under the new account model, slot IDs (`default`,
454
+ // etc.) are an internal mount-point identifier — not what the
455
+ // operator authorized. When we know which account is the post-fanout
456
+ // active for THIS agent, the active slot row would render as
457
+ // `● pixsoul@gmail.com (active) ✓ healthy` and the Pool line would
458
+ // say `Pool: pixsoul@gmail.com is active` — both 1:1 duplicates of
459
+ // the ▶ active-account row above. So we hide both sections when an
460
+ // active-account signal is present. Keep them visible only when:
461
+ // - No accounts data at all (older CLI without --json), OR
462
+ // - Accounts exist but no entry has activeForThisAgent set (older
463
+ // CLI without primaryForAgents), OR
464
+ // - Empty fleet (no accounts) — slots are still the bootstrap
465
+ // surface for the operator's first reauth/add taps.
466
+ const activeAccountLabel =
467
+ state.accounts?.find((a) => a.activeForThisAgent === true)?.label ?? null;
468
+ const slotsSectionRedundant =
469
+ activeAccountLabel != null &&
470
+ state.accounts != null &&
471
+ state.accounts.length > 0;
472
+
473
+ if (!slotsSectionRedundant) {
474
+ if (state.slots.length === 0) {
475
+ lines.push("<i>No account slots. Tap [➕ Add slot] to attach a subscription.</i>");
476
+ } else {
477
+ lines.push(`<b>Slots (${state.slots.length})</b>`);
478
+ for (const slot of state.slots) {
479
+ const marker = slot.active ? "●" : "○";
480
+ const badge = healthBadge(slot.health);
481
+ const label = healthLabel(slot.health);
482
+ lines.push(
483
+ ` ${marker} <code>${escapeHtml(slot.slot)}</code>${slot.active ? " (active)" : ""} ${badge} ${label}`,
484
+ );
485
+ const detail = slotDetailLine(slot);
486
+ if (detail) lines.push(` └ ${detail}`);
487
+ }
488
+ }
489
+
490
+ // Pool / fallback summary — show when accounts exist, so the user
491
+ // understands how slots and accounts relate. Suppressed alongside
492
+ // the slots section when the active-account row already says it.
493
+ if (state.accounts != null && state.accounts.length > 0 && state.slots.length > 0) {
494
+ const activeSlot = state.slots.find((s) => s.active);
495
+ if (activeSlot) {
496
+ lines.push(` Pool: slot <code>${escapeHtml(activeSlot.slot)}</code> is active`);
497
+ }
498
+ }
499
+ }
500
+
501
+ lines.push("");
502
+ if (state.pendingSessionSlot) {
503
+ lines.push(
504
+ `<i>⏳ Auth flow pending for slot <code>${escapeHtml(state.pendingSessionSlot)}</code>. If it's stuck, tap ♻️ below to restart.</i>`,
505
+ );
506
+ }
507
+ lines.push("━━━━━━━━━━━━━━━━━━━");
508
+ if (state.generatedAt) {
509
+ lines.push(`<i>Updated ${escapeHtml(state.generatedAt)}</i>`);
510
+ }
511
+
512
+ return lines.join("\n");
513
+ }
514
+
515
+ /**
516
+ * Render the active account row — the post-fanout primary for this
517
+ * agent. Uses the `▶` glyph + bold label + an inline quota summary
518
+ * carrying mini-bars when both percentages are known. Falls back to
519
+ * the plain `formatAccountQuotaLine` text on the next line if quota
520
+ * isn't probed yet — keeps the row honest about uncertainty.
521
+ */
522
+ function renderActiveAccountRow(acc: AccountSummary): string {
523
+ const badge = accountHealthBadge(acc.health);
524
+ const suffix = healthSuffix(acc.health);
525
+ const head = `▶ <b><code>${escapeHtml(acc.label)}</code></b> ${badge}${suffix}`;
526
+ const inline = formatActiveQuotaInline(acc);
527
+ return inline ? `${head}\n ${inline}` : head;
528
+ }
529
+
530
+ /**
531
+ * Render an indented fallback account row. `haveActiveSignal` controls
532
+ * the bullet vs. tree-prefix character — when there's a distinguished
533
+ * active row above, we use `↳` to imply ordering; without one we fall
534
+ * back to a plain `•` bullet so the layout matches v3a for older CLIs.
535
+ */
536
+ function renderFallbackAccountRow(
537
+ acc: AccountSummary,
538
+ haveActiveSignal: boolean,
539
+ ): string {
540
+ const badge = accountHealthBadge(acc.health);
541
+ const suffix = healthSuffix(acc.health);
542
+ const prefix = haveActiveSignal ? " ↳" : " •";
543
+ return `${prefix} <code>${escapeHtml(acc.label)}</code> ${badge}${suffix}`;
544
+ }
545
+
546
+ /**
547
+ * Inline quota summary for the active row. When BOTH 5h and 7d are
548
+ * known, emit the mini-bar form (`5h ████░░ 47% · 7d █░░░░░ 12%`).
549
+ * When the account is exhausted, defer to the existing
550
+ * `formatAccountQuotaLine` (it has the reset-time copy). Otherwise
551
+ * return null and let the caller skip the line.
552
+ */
553
+ function formatActiveQuotaInline(acc: AccountSummary): string | null {
554
+ if (acc.quotaExhaustedUntil != null && acc.quotaExhaustedUntil > Date.now()) {
555
+ return formatAccountQuotaLine(acc);
556
+ }
557
+ if (acc.fiveHourPct == null || acc.sevenDayPct == null) {
558
+ return formatAccountQuotaLine(acc);
559
+ }
560
+ const fiveBar = formatQuotaBar(acc.fiveHourPct);
561
+ const sevenBar = formatQuotaBar(acc.sevenDayPct);
562
+ return (
563
+ `<i>5h</i> <code>${fiveBar}</code> ${formatQuotaPct(acc.fiveHourPct)} ` +
564
+ `· <i>7d</i> <code>${sevenBar}</code> ${formatQuotaPct(acc.sevenDayPct)}`
565
+ );
566
+ }
567
+
568
+ /** Health badge for an account (not a slot). */
569
+ function accountHealthBadge(health: AccountHealth): string {
570
+ switch (health) {
571
+ case "healthy":
572
+ return "✓";
573
+ case "quota-exhausted":
574
+ return "⚠️";
575
+ case "expired":
576
+ case "missing-refresh-token":
577
+ return "⌛";
578
+ case "missing-credentials":
579
+ return "✗";
580
+ }
581
+ }
582
+
583
+ function slotDetailLine(slot: DashboardSlot): string | null {
584
+ const bits: string[] = [];
585
+ if (slot.fiveHourPct != null) bits.push(`5h: ${Math.round(slot.fiveHourPct)}%`);
586
+ if (slot.sevenDayPct != null) bits.push(`7d: ${Math.round(slot.sevenDayPct)}%`);
587
+ if (slot.health === "quota-exhausted" && slot.quotaExhaustedUntil) {
588
+ const mins = Math.max(0, Math.round((slot.quotaExhaustedUntil - Date.now()) / 60_000));
589
+ bits.push(`resets in ~${mins}m`);
590
+ } else if (slot.health === "expired") {
591
+ bits.push("run reauth");
592
+ }
593
+ return bits.length > 0 ? bits.join(" · ") : null;
594
+ }
595
+
596
+ function healthBadge(health: SlotHealth): string {
597
+ switch (health) {
598
+ case "active":
599
+ case "healthy":
600
+ return "✓";
601
+ case "quota-exhausted":
602
+ return "⚠️";
603
+ case "expired":
604
+ return "⌛";
605
+ case "missing":
606
+ return "✗";
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Human-readable label for a slot's health. 'active' collapses to
612
+ * 'healthy' — the ● + '(active)' markers already carry the active-slot
613
+ * signal; rendering 'active active' is redundant.
614
+ */
615
+ function healthLabel(health: SlotHealth): string {
616
+ return health === "active" ? "healthy" : health;
617
+ }
618
+
619
+ export function buildDashboardKeyboard(state: DashboardState): InlineKeyboard {
620
+ const kb = new InlineKeyboard();
621
+ const activeSlot = state.slots.find((s) => s.active);
622
+
623
+ // v3c: single `🔀 Switch primary →` entry replaces the v3b
624
+ // per-fallback `⤴ Promote` buttons + per-account drill-downs that
625
+ // flooded the main board. The text already names every account
626
+ // (`▶ active` + indented `↳ fallback` rows), so the keyboard's job
627
+ // is *actions*, not navigation. One button, one tap → picker.
628
+ //
629
+ // Visibility rules:
630
+ // - hidden when there are no fallbacks (single account = nothing
631
+ // to switch to)
632
+ // - hidden when no account claims active (older CLI without
633
+ // primaryForAgents — picker target would be ambiguous)
634
+ // - shown otherwise
635
+ if (state.accounts != null && state.accounts.length > 0) {
636
+ const visible = state.accounts.slice(0, ACCOUNTS_DISPLAY_CAP);
637
+ const active = visible.find((a) => a.activeForThisAgent === true);
638
+ const fallbacks = visible.filter((a) => a !== active);
639
+ if (active != null && fallbacks.length > 0) {
640
+ kb.text(
641
+ "🔀 Switch primary →",
642
+ encodeCallbackData({ kind: "switch-primary-view", agent: state.agent }),
643
+ );
644
+ kb.row();
645
+ }
646
+ if (state.accountsTruncated) {
647
+ kb.text(
648
+ `… ${state.accounts.length - ACCOUNTS_DISPLAY_CAP} more (use CLI)`,
649
+ encodeCallbackData({ kind: "noop" }),
650
+ );
651
+ kb.row();
652
+ }
653
+ } else if (state.accounts != null && state.accounts.length === 0 && state.canBootstrapShare) {
654
+ // Bootstrap one-tap: zero accounts exist, but this agent has
655
+ // healthy slot creds we could promote. Synthesises label="default"
656
+ // at the gateway so the user gets a reasonable starting state in
657
+ // one tap; rename via CLI later if "default" doesn't suit.
658
+ kb.text(
659
+ "🌐 Share to fleet",
660
+ encodeCallbackData({ kind: "share-fleet", agent: state.agent }),
661
+ );
662
+ kb.row();
663
+ }
664
+
665
+ // Slot rows — existing Reauth/Add/Use/Remove behavior, unchanged.
666
+ // Slots are still real and operators still need to manage them;
667
+ // they're just demoted below accounts in the v3a layout.
668
+
669
+ // Slot row A: primary auth actions. Reauth the active slot; add a new one.
670
+ if (activeSlot) {
671
+ kb.text(`🔄 Reauth ${activeSlot.slot}`, encodeCallbackData({ kind: "reauth", agent: state.agent, slot: activeSlot.slot }));
672
+ } else {
673
+ kb.text("🔄 Reauth", encodeCallbackData({ kind: "reauth", agent: state.agent }));
674
+ }
675
+ kb.text("➕ Add slot", encodeCallbackData({ kind: "add", agent: state.agent }));
676
+ kb.row();
677
+
678
+ // Slot row B: non-active slots — one "Use" button per, up to 3 to
679
+ // avoid runaway rows. Over 3 slots, user sees an overflow message.
680
+ const nonActiveSlots = state.slots.filter((s) => !s.active).slice(0, 3);
681
+ for (const slot of nonActiveSlots) {
682
+ kb.text(`Use: ${slot.slot}`, encodeCallbackData({ kind: "use", agent: state.agent, slot: slot.slot }));
683
+ }
684
+ if (nonActiveSlots.length > 0) kb.row();
685
+
686
+ // Slot row C: remove buttons (only for non-active slots; removing the
687
+ // active slot is blocked by auth-slot-parser's checkRemoveSafety).
688
+ const removableSlots = state.slots.filter((s) => !s.active).slice(0, 3);
689
+ for (const slot of removableSlots) {
690
+ kb.text(`🗑 Remove: ${slot.slot}`, encodeCallbackData({ kind: "rm", agent: state.agent, slot: slot.slot }));
691
+ }
692
+ if (removableSlots.length > 0) kb.row();
693
+
694
+ // Pending-flow recovery. Shown ONLY when an auth flow is
695
+ // pending (session meta file on disk). Lets the user explicitly
696
+ // kill + restart the flow.
697
+ if (state.pendingSessionSlot) {
698
+ kb.text(
699
+ `♻️ Restart ${state.pendingSessionSlot} flow`,
700
+ encodeCallbackData({ kind: "restart-flow", agent: state.agent, slot: state.pendingSessionSlot }),
701
+ );
702
+ kb.row();
703
+ }
704
+
705
+ // Quota row. [📊 Full quota] is the escape hatch when the
706
+ // operator wants the live numbers behind the cached mini-bars.
707
+ // The legacy `[⚠️ Fall back now]` button (manual auto-fallback at
708
+ // the slot level) was removed in v0.6.11 — the Switch primary
709
+ // picker is the operator-facing surface for "active is hot, swap
710
+ // to a fallback," and the auto-fallback poller still handles the
711
+ // automatic case when the active hits its quota wall. The
712
+ // `fallback` callback verb stays in the parser/dispatcher for
713
+ // legacy reachability of any pinned messages still bearing the
714
+ // pre-v0.6.11 button, but no new buttons emit it.
715
+ kb.text("📊 Full quota", encodeCallbackData({ kind: "usage", agent: state.agent }));
716
+ kb.row();
717
+
718
+ // Refresh
719
+ kb.text("🔁 Refresh", encodeCallbackData({ kind: "refresh", agent: state.agent }));
720
+
721
+ return kb;
722
+ }
723
+
724
+ /** Derive the `quotaHot` flag from a slot set. Used by the gateway
725
+ * at dashboard-build time and by tests. */
726
+ export function isQuotaHot(slots: DashboardSlot[]): boolean {
727
+ for (const s of slots) {
728
+ if (s.health === "quota-exhausted") return true;
729
+ if ((s.fiveHourPct ?? 0) >= QUOTA_HOT_THRESHOLD_PCT) return true;
730
+ if ((s.sevenDayPct ?? 0) >= QUOTA_HOT_THRESHOLD_PCT) return true;
731
+ }
732
+ return false;
733
+ }
734
+
735
+ /**
736
+ * Account-level analogue: derive the `quotaHot` flag from the
737
+ * accounts section of the dashboard. Under the new auth framework
738
+ * accounts (not slots) are the unit of quota, so the [Fall back now]
739
+ * affordance should fire when ANY account in the agent's list is
740
+ * approaching the cap — not just the slot that happens to be the
741
+ * active mirror.
742
+ *
743
+ * Combine with `isQuotaHot(slots)` via `||` at the call site so
744
+ * legacy slot setups still get the warning.
745
+ */
746
+ export function isAccountQuotaHot(
747
+ accounts: ReadonlyArray<AccountSummary> | undefined,
748
+ ): boolean {
749
+ if (!accounts) return false;
750
+ for (const a of accounts) {
751
+ if (a.health === "quota-exhausted") return true;
752
+ if ((a.fiveHourPct ?? 0) >= QUOTA_HOT_THRESHOLD_PCT) return true;
753
+ if ((a.sevenDayPct ?? 0) >= QUOTA_HOT_THRESHOLD_PCT) return true;
754
+ }
755
+ return false;
756
+ }
757
+
758
+ /**
759
+ * Render the per-account quota line shown under each account row in
760
+ * the dashboard. Returns null when no quota data is available — the
761
+ * caller skips the row entirely so a freshly-added (un-probed)
762
+ * account doesn't show a placeholder.
763
+ *
764
+ * Format priority:
765
+ * - quota-exhausted (server-side or 100% utilization) →
766
+ * "exhausted · resets in Nh Mm"
767
+ * - both percentages known → "5h: 47% · 7d: 12%"
768
+ * - one percentage known → that one
769
+ * - nothing → null
770
+ *
771
+ * Reset times come straight from the Anthropic response headers via
772
+ * `parseQuotaHeaders` (`fiveHourResetAt`, `sevenDayResetAt` epoch ms).
773
+ */
774
+ export function formatAccountQuotaLine(
775
+ acc: AccountSummary,
776
+ now: number = Date.now(),
777
+ ): string | null {
778
+ if (acc.quotaExhaustedUntil != null && acc.quotaExhaustedUntil > now) {
779
+ const reset = formatRelativeMs(acc.quotaExhaustedUntil - now);
780
+ return `<i>exhausted · resets in ${reset}</i>`;
781
+ }
782
+ const parts: string[] = [];
783
+ if (acc.fiveHourPct != null) {
784
+ parts.push(`<i>5h:</i> ${formatQuotaPct(acc.fiveHourPct)}`);
785
+ }
786
+ if (acc.sevenDayPct != null) {
787
+ parts.push(`<i>7d:</i> ${formatQuotaPct(acc.sevenDayPct)}`);
788
+ }
789
+ if (parts.length === 0) return null;
790
+ // Append a "<window> resets in <duration>" suffix when reset
791
+ // timestamps are available (issue #708). Picks whichever window
792
+ // resets sooner (5h preferred on tie). Hidden once the reset is in
793
+ // the past — the percent column already shows whether the cap is
794
+ // free again.
795
+ const resetSuffix = formatNearestAccountResetSuffix(acc, now);
796
+ if (resetSuffix) parts.push(resetSuffix);
797
+ return parts.join(" · ");
798
+ }
799
+
800
+ /**
801
+ * Render the "5h resets in 2h 14m" / "7d resets in 1d 3h" suffix
802
+ * appended to per-account quota lines (issue #708). Returns "" when
803
+ * neither timestamp is present or both are in the past — the parent
804
+ * caller decides whether to push it.
805
+ *
806
+ * Exported for the boot card so the two surfaces share one dialect.
807
+ */
808
+ export function formatNearestAccountResetSuffix(
809
+ acc: Pick<AccountSummary, "fiveHourResetAt" | "sevenDayResetAt">,
810
+ now: number = Date.now(),
811
+ ): string {
812
+ const five = acc.fiveHourResetAt;
813
+ const seven = acc.sevenDayResetAt;
814
+ let target: number | null = null;
815
+ let label: "5h" | "7d" | null = null;
816
+ if (five != null && (seven == null || five <= seven)) {
817
+ target = five;
818
+ label = "5h";
819
+ } else if (seven != null) {
820
+ target = seven;
821
+ label = "7d";
822
+ }
823
+ if (target == null || label == null) return "";
824
+ const delta = target - now;
825
+ if (delta <= 0) return "";
826
+ return `<i>${label} resets in</i> ${formatRelativeMs(delta)}`;
827
+ }
828
+
829
+ function formatQuotaPct(pct: number): string {
830
+ // Round to integer % for the dashboard. Show "<1%" when the value
831
+ // is positive but rounds to zero, so "0%" is reserved for genuine
832
+ // idle accounts.
833
+ const rounded = Math.round(pct);
834
+ if (pct > 0 && rounded === 0) return "&lt;1%";
835
+ return `${rounded}%`;
836
+ }
837
+
838
+ /**
839
+ * Render a Unicode mini-bar for a 0–100 percentage. Six cells wide —
840
+ * the active row carries two of these (5h + 7d) and they need to fit
841
+ * one mobile line alongside the labels and percentages.
842
+ *
843
+ * formatQuotaBar(0) → ░░░░░░
844
+ * formatQuotaBar(47) → ███░░░
845
+ * formatQuotaBar(99) → █████░ (clamps below full so 99% reads
846
+ * visibly different from 100%)
847
+ * formatQuotaBar(100) → ██████
848
+ *
849
+ * Used only on the active-account row (the one running quota right
850
+ * now). Fallback rows still render plain percentages because the bars
851
+ * eat horizontal space the indented "↳" rows don't have.
852
+ */
853
+ export function formatQuotaBar(pct: number, cells: number = 6): string {
854
+ if (cells <= 0) return "";
855
+ const clamped = Math.max(0, Math.min(100, pct));
856
+ // Math.floor for the filled cell count — 100% gets all cells, 99%
857
+ // gets cells-1, anything below the per-cell threshold gets 0.
858
+ const filled =
859
+ clamped >= 100 ? cells : Math.floor((clamped / 100) * cells);
860
+ return "█".repeat(filled) + "░".repeat(cells - filled);
861
+ }
862
+
863
+ function formatRelativeMs(ms: number): string {
864
+ const totalMin = Math.max(1, Math.floor(ms / 60_000));
865
+ if (totalMin < 60) return `${totalMin}m`;
866
+ const h = Math.floor(totalMin / 60);
867
+ const m = totalMin % 60;
868
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
869
+ }
870
+
871
+ /**
872
+ * Shorten Anthropic's verbose tier strings into something readable in a
873
+ * one-line dashboard header.
874
+ *
875
+ * default_claude_max_5x → max_5x
876
+ * default_claude_max_20x → max_20x
877
+ * default_claude_pro → pro
878
+ * anything else → passthrough (we don't pretend to
879
+ * understand every future tier string)
880
+ */
881
+ export function formatRateLimitTier(tier: string): string {
882
+ if (!tier) return tier;
883
+ return tier.replace(/^default_claude_/, "");
884
+ }
885
+
886
+ /** Tiny HTML escaper — same shape as welcome-text.ts's escapeHtml so
887
+ * this module stays dependency-free. */
888
+ export function escapeHtml(text: string): string {
889
+ return text
890
+ .replace(/&/g, "&amp;")
891
+ .replace(/</g, "&lt;")
892
+ .replace(/>/g, "&gt;")
893
+ .replace(/"/g, "&quot;");
894
+ }
895
+
896
+ /** Build the confirmation keyboard shown when the user taps Remove.
897
+ * Two-step confirm prevents accidental slot deletion on mobile. */
898
+ export function buildRemoveConfirmKeyboard(agent: string, slot: string): InlineKeyboard {
899
+ return new InlineKeyboard()
900
+ .text(`⚠️ Confirm remove: ${slot}`, encodeCallbackData({ kind: "confirm-rm", agent, slot }))
901
+ .row()
902
+ .text("↩️ Cancel", encodeCallbackData({ kind: "refresh", agent }));
903
+ }
904
+
905
+ /**
906
+ * Build the switch-primary picker keyboard. One row per non-active
907
+ * account (the candidates the user might promote). Each row is a
908
+ * direct `confirm-account-promote` — single tap fires the change, no
909
+ * second confirm screen, since the picker itself is already an
910
+ * intentional drill-down ("I tapped Switch primary, then I tapped
911
+ * the new primary").
912
+ *
913
+ * Why skip the two-stage confirm here when enable/disable have one:
914
+ * - The picker IS the confirmation surface. Showing a second
915
+ * "Confirm promote: foo?" screen on top of "tap the one you
916
+ * want" is mobile UX cruft.
917
+ * - The action is reversible — operators can re-promote at will.
918
+ *
919
+ * Cancel returns to the main dashboard via a refresh callback.
920
+ *
921
+ * Signature mirrors `buildAccountConfirmKeyboard` for consistency:
922
+ * `agent` first, then the picker-specific data (the candidates).
923
+ */
924
+ export function buildSwitchPrimaryKeyboard(
925
+ agent: string,
926
+ candidates: ReadonlyArray<{ label: string; health: AccountHealth }>,
927
+ ): InlineKeyboard {
928
+ const kb = new InlineKeyboard();
929
+ for (const cand of candidates) {
930
+ const action: CallbackAction = {
931
+ kind: "confirm-account-promote",
932
+ agent,
933
+ label: cand.label,
934
+ };
935
+ const encoded = encodeCallbackData(action);
936
+ if (Buffer.byteLength(encoded, "utf8") > CALLBACK_BUDGET_BYTES) {
937
+ // Pathological agent + label combo. Render the row inert so the
938
+ // operator falls back to the CLI rather than us silently
939
+ // dropping the candidate.
940
+ kb.text(
941
+ `⚠ ${truncateLabel(cand.label)} (use CLI)`,
942
+ encodeCallbackData({ kind: "noop" }),
943
+ );
944
+ } else {
945
+ kb.text(`⤴ ${cand.label}${healthSuffix(cand.health)}`, encoded);
946
+ }
947
+ kb.row();
948
+ }
949
+ kb.text("↩️ Cancel", encodeCallbackData({ kind: "refresh", agent }));
950
+ return kb;
951
+ }
952
+
953
+ /**
954
+ * Two-stage confirmation for the account promote action — mirrors
955
+ * `buildAccountConfirmKeyboard` but with the promote-specific verb so
956
+ * the confirm row's callback dispatches to `confirm-account-promote`.
957
+ *
958
+ * Why a separate helper instead of extending the existing one's `kind`
959
+ * parameter: the `enable | disable` discriminant is already in widely-
960
+ * used callsites; threading a third value through them would force
961
+ * cascading test updates. A dedicated helper is cleaner.
962
+ */
963
+ export function buildAccountPromoteConfirmKeyboard(
964
+ agent: string,
965
+ label: string,
966
+ ): InlineKeyboard {
967
+ const action: CallbackAction = { kind: "confirm-account-promote", agent, label };
968
+ return new InlineKeyboard()
969
+ .text(`⚠️ Confirm promote: ${label}`, encodeCallbackData(action))
970
+ .row()
971
+ .text("↩️ Cancel", encodeCallbackData({ kind: "refresh", agent }));
972
+ }
973
+
974
+ /**
975
+ * Two-stage confirmation for account toggles. Mirrors
976
+ * `buildRemoveConfirmKeyboard`'s shape — one confirm row + a cancel
977
+ * that re-renders the dashboard. `kind` selects enable vs disable so
978
+ * one helper covers both directions.
979
+ */
980
+ export function buildAccountConfirmKeyboard(
981
+ agent: string,
982
+ label: string,
983
+ kind: "enable" | "disable",
984
+ ): InlineKeyboard {
985
+ const action: CallbackAction = kind === "enable"
986
+ ? { kind: "confirm-account-enable", agent, label }
987
+ : { kind: "confirm-account-disable", agent, label };
988
+ const verb = kind === "enable" ? "enable" : "disable";
989
+ return new InlineKeyboard()
990
+ .text(`⚠️ Confirm ${verb}: ${label}`, encodeCallbackData(action))
991
+ .row()
992
+ .text("↩️ Cancel", encodeCallbackData({ kind: "refresh", agent }));
993
+ }
994
+
995
+ /**
996
+ * Health affix for the account button label. Keeps healthy accounts
997
+ * unadorned (the ✓/○ marker carries the enabled-here signal) and
998
+ * surfaces the failure modes that need operator attention. Quota and
999
+ * expiry use distinct icons so the user can tell which boundary the
1000
+ * account hit.
1001
+ */
1002
+ function healthSuffix(health: AccountHealth): string {
1003
+ switch (health) {
1004
+ case "quota-exhausted":
1005
+ return " ⚠️";
1006
+ case "expired":
1007
+ case "missing-refresh-token":
1008
+ return " ⌛";
1009
+ case "missing-credentials":
1010
+ return " ❌";
1011
+ case "healthy":
1012
+ default:
1013
+ return "";
1014
+ }
1015
+ }
1016
+
1017
+ /** Trim long labels in the noop fallback button so the row stays
1018
+ * readable on a narrow mobile screen. */
1019
+ function truncateLabel(label: string): string {
1020
+ if (label.length <= 32) return label;
1021
+ return label.slice(0, 31) + "…";
1022
+ }
1023
+
1024
+ // ─── v3a: Per-account sub-view ────────────────────────────────────────────
1025
+
1026
+ /**
1027
+ * Build the per-account drill-down sub-view text. Shown when the user
1028
+ * taps an account row on the main dashboard.
1029
+ */
1030
+ export function buildAccountSubViewText(agent: string, acc: AccountSummary): string {
1031
+ const lines: string[] = [];
1032
+ lines.push(`━━━ <b>Account • ${escapeHtml(acc.label)}</b> ━━━`);
1033
+ lines.push(`Agent: <code>${escapeHtml(agent)}</code>`);
1034
+ const badge = accountHealthBadge(acc.health);
1035
+ const suffix = healthSuffix(acc.health);
1036
+ lines.push(`Health: ${badge} ${acc.health}${suffix}`);
1037
+ if (acc.subscriptionType) {
1038
+ lines.push(`Type: <b>${escapeHtml(acc.subscriptionType)}</b>`);
1039
+ }
1040
+ if (acc.expiresAt) {
1041
+ const expiresDate = new Date(acc.expiresAt).toISOString().slice(0, 10);
1042
+ lines.push(`Expires: <code>${escapeHtml(expiresDate)}</code>`);
1043
+ }
1044
+ lines.push("━━━━━━━━━━━━━━━━━━━");
1045
+ return lines.join("\n");
1046
+ }
1047
+
1048
+ /**
1049
+ * Build the per-account drill-down keyboard.
1050
+ *
1051
+ * Reauth is visible-but-inert in v3a — no `auth account reauth` CLI
1052
+ * verb exists yet. The button is surfaced so the layout is complete;
1053
+ * the gateway handler returns a toast noting it'll land in v3b.
1054
+ */
1055
+ export function buildAccountSubViewKeyboard(agent: string, label: string): InlineKeyboard {
1056
+ const reauthAction: CallbackAction = { kind: "account-reauth", agent, label };
1057
+ const rmAction: CallbackAction = { kind: "account-rm", agent, label };
1058
+ const reauthEncoded = encodeCallbackData(reauthAction);
1059
+ const rmEncoded = encodeCallbackData(rmAction);
1060
+ const kb = new InlineKeyboard();
1061
+ // Reauth — inert in v3a (no CLI verb). Still wired so the layout is
1062
+ // complete; the gateway emits a "coming in v3b" toast.
1063
+ if (Buffer.byteLength(reauthEncoded, "utf8") <= CALLBACK_BUDGET_BYTES) {
1064
+ kb.text("🔁 Reauth", reauthEncoded);
1065
+ } else {
1066
+ kb.text("🔁 Reauth (use CLI)", encodeCallbackData({ kind: "noop" }));
1067
+ }
1068
+ kb.row();
1069
+ // Remove — triggers confirm sub-view.
1070
+ if (Buffer.byteLength(rmEncoded, "utf8") <= CALLBACK_BUDGET_BYTES) {
1071
+ kb.text("🗑 Remove", rmEncoded);
1072
+ } else {
1073
+ kb.text("🗑 Remove (use CLI)", encodeCallbackData({ kind: "noop" }));
1074
+ }
1075
+ kb.row();
1076
+ // Back to main dashboard.
1077
+ kb.text("← Accounts", encodeCallbackData({ kind: "refresh", agent }));
1078
+ return kb;
1079
+ }
1080
+
1081
+ /**
1082
+ * Build the remove-confirm sub-view for a per-account removal.
1083
+ * Ports the slot-remove confirm pattern.
1084
+ */
1085
+ export function buildAccountRemoveConfirmKeyboard(agent: string, label: string): InlineKeyboard {
1086
+ const confirmAction: CallbackAction = { kind: "account-rm-confirm", agent, label };
1087
+ const confirmEncoded = encodeCallbackData(confirmAction);
1088
+ return new InlineKeyboard()
1089
+ .text(
1090
+ `✓ Yes, remove`,
1091
+ Buffer.byteLength(confirmEncoded, "utf8") <= CALLBACK_BUDGET_BYTES
1092
+ ? confirmEncoded
1093
+ : encodeCallbackData({ kind: "noop" }),
1094
+ )
1095
+ .text(
1096
+ "✗ Cancel",
1097
+ (() => {
1098
+ const cancelEncoded = encodeCallbackData({ kind: "account-view", agent, label });
1099
+ return Buffer.byteLength(cancelEncoded, "utf8") <= CALLBACK_BUDGET_BYTES
1100
+ ? cancelEncoded
1101
+ : encodeCallbackData({ kind: "noop" });
1102
+ })(),
1103
+ );
1104
+ }