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.
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/autoaccept.exp +81 -0
- package/bin/boot-self-test.sh +149 -0
- package/bin/bridge-watchdog.sh +967 -0
- package/bin/handoff-briefing.sh +206 -0
- package/bin/run-hook.sh +228 -0
- package/bin/switchroom.ts +4 -0
- package/bin/timezone-hook.sh +67 -0
- package/bin/user-profile-refresh-hook.sh +38 -0
- package/bin/workspace-dynamic-hook.sh +142 -0
- package/bin/workspace-stable-hook.sh +57 -0
- package/dist/cli/autoaccept-poll.js +118 -0
- package/dist/cli/switchroom.js +48557 -0
- package/package.json +95 -0
- package/profiles/_base/settings.json.hbs +15 -0
- package/profiles/_base/start.sh.hbs +383 -0
- package/profiles/_shared/telegram-style.md.hbs +140 -0
- package/profiles/coding/CLAUDE.md.hbs +57 -0
- package/profiles/coding/skills/architecture/SKILL.md +70 -0
- package/profiles/coding/skills/code-review/SKILL.md +58 -0
- package/profiles/coding/workspace/SOUL.md.hbs +25 -0
- package/profiles/default/CLAUDE.md +238 -0
- package/profiles/default/CLAUDE.md.hbs +113 -0
- package/profiles/default/workspace/CLAUDE.md.hbs +126 -0
- package/profiles/default/workspace/HEARTBEAT.md.hbs +40 -0
- package/profiles/default/workspace/IDENTITY.md.hbs +32 -0
- package/profiles/default/workspace/MEMORY.md.hbs +29 -0
- package/profiles/default/workspace/SOUL.md.hbs +61 -0
- package/profiles/default/workspace/TOOLS.md.hbs +29 -0
- package/profiles/default/workspace/USER.md.hbs +52 -0
- package/profiles/default/workspace/memory/.gitkeep +0 -0
- package/profiles/executive-assistant/CLAUDE.md.hbs +51 -0
- package/profiles/executive-assistant/skills/daily-briefing/SKILL.md +55 -0
- package/profiles/executive-assistant/skills/meeting-prep/SKILL.md +58 -0
- package/profiles/executive-assistant/workspace/SOUL.md.hbs +25 -0
- package/profiles/health-coach/CLAUDE.md.hbs +45 -0
- package/profiles/health-coach/skills/check-in/SKILL.md +41 -0
- package/profiles/health-coach/skills/weekly-review/SKILL.md +53 -0
- package/profiles/health-coach/workspace/SOUL.md.hbs +25 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +302 -0
- package/skills/buildkite-agent-infrastructure/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-infrastructure/references/audit-logging.md +87 -0
- package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +690 -0
- package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +38 -0
- package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +73 -0
- package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +137 -0
- package/skills/buildkite-agent-infrastructure/references/sso-saml.md +92 -0
- package/skills/buildkite-agent-runtime/SKILL.md +476 -0
- package/skills/buildkite-agent-runtime/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-runtime/references/flag-reference.md +417 -0
- package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +555 -0
- package/skills/buildkite-api/SKILL.md +285 -0
- package/skills/buildkite-api/agents/openai.yaml +6 -0
- package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-api/references/graphql-reference.md +195 -0
- package/skills/buildkite-api/references/patterns.md +44 -0
- package/skills/buildkite-api/references/webhooks.md +161 -0
- package/skills/buildkite-cli/SKILL.md +379 -0
- package/skills/buildkite-cli/agents/openai.yaml +6 -0
- package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-cli/references/command-reference.md +181 -0
- package/skills/buildkite-migration/SKILL.md +182 -0
- package/skills/buildkite-pipelines/SKILL.md +464 -0
- package/skills/buildkite-pipelines/agents/openai.yaml +6 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-pipelines/examples/basic-pipeline.yml +24 -0
- package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +100 -0
- package/skills/buildkite-pipelines/references/advanced-patterns.md +286 -0
- package/skills/buildkite-pipelines/references/retry-and-error-codes.md +131 -0
- package/skills/buildkite-pipelines/references/step-types-reference.md +225 -0
- package/skills/buildkite-secure-delivery/SKILL.md +168 -0
- package/skills/buildkite-secure-delivery/agents/openai.yaml +6 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +83 -0
- package/skills/buildkite-secure-delivery/references/package-publishing.md +100 -0
- package/skills/buildkite-test-engine/SKILL.md +239 -0
- package/skills/buildkite-test-engine/agents/openai.yaml +6 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-test-engine/examples/bktec-splitting.yml +16 -0
- package/skills/buildkite-test-engine/examples/collector-pipeline.yml +11 -0
- package/skills/buildkite-test-engine/references/collectors.md +198 -0
- package/skills/buildkite-test-engine/references/splitting-examples.md +93 -0
- package/skills/docx/LICENSE.txt +30 -0
- package/skills/docx/SKILL.md +590 -0
- package/skills/docx/VENDORED.md +32 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/accept_changes.py +135 -0
- package/skills/docx/scripts/comment.py +318 -0
- package/skills/docx/scripts/office/helpers/__init__.py +0 -0
- package/skills/docx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/docx/scripts/office/pack.py +159 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/scripts/office/soffice.py +183 -0
- package/skills/docx/scripts/office/unpack.py +132 -0
- package/skills/docx/scripts/office/validate.py +111 -0
- package/skills/docx/scripts/office/validators/__init__.py +15 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/base.py +847 -0
- package/skills/docx/scripts/office/validators/docx.py +446 -0
- package/skills/docx/scripts/office/validators/pptx.py +275 -0
- package/skills/docx/scripts/office/validators/redlining.py +247 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/file-bug/SKILL.md +129 -0
- package/skills/humanizer/LICENSE +21 -0
- package/skills/humanizer/SKILL.md +559 -0
- package/skills/humanizer/VENDORED.md +38 -0
- package/skills/humanizer-calibrate/SKILL.md +144 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +236 -0
- package/skills/mcp-builder/VENDORED.md +32 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/pdf/LICENSE.txt +30 -0
- package/skills/pdf/SKILL.md +314 -0
- package/skills/pdf/VENDORED.md +32 -0
- package/skills/pdf/forms.md +294 -0
- package/skills/pdf/reference.md +612 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
- package/skills/pdf/scripts/check_fillable_fields.py +11 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- package/skills/pdf/scripts/create_validation_image.py +37 -0
- package/skills/pdf/scripts/extract_form_field_info.py +122 -0
- package/skills/pdf/scripts/extract_form_structure.py +115 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/pptx/LICENSE.txt +30 -0
- package/skills/pptx/SKILL.md +232 -0
- package/skills/pptx/VENDORED.md +32 -0
- package/skills/pptx/editing.md +205 -0
- package/skills/pptx/pptxgenjs.md +420 -0
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/add_slide.py +195 -0
- package/skills/pptx/scripts/clean.py +286 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- package/skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/pptx/scripts/office/pack.py +159 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/scripts/office/soffice.py +183 -0
- package/skills/pptx/scripts/office/unpack.py +132 -0
- package/skills/pptx/scripts/office/validate.py +111 -0
- package/skills/pptx/scripts/office/validators/__init__.py +15 -0
- package/skills/pptx/scripts/office/validators/base.py +847 -0
- package/skills/pptx/scripts/office/validators/docx.py +446 -0
- package/skills/pptx/scripts/office/validators/pptx.py +275 -0
- package/skills/pptx/scripts/office/validators/redlining.py +247 -0
- package/skills/pptx/scripts/thumbnail.py +289 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/VENDORED.md +32 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/skills/switchroom-architecture/SKILL.md +60 -0
- package/skills/switchroom-architecture/cascade.md +112 -0
- package/skills/switchroom-architecture/sub-agents.md +87 -0
- package/skills/switchroom-architecture/telegram.md +94 -0
- package/skills/switchroom-cli/SKILL.md +274 -0
- package/skills/switchroom-health/SKILL.md +101 -0
- package/skills/switchroom-install/SKILL.md +116 -0
- package/skills/switchroom-manage/SKILL.md +90 -0
- package/skills/switchroom-status/SKILL.md +69 -0
- package/skills/switchroom-status/scripts/status.sh +69 -0
- package/skills/telegram-test-harness/SKILL.md +191 -0
- package/skills/token-helpers/SKILL.md +73 -0
- package/skills/token-helpers/scripts/google-cal-token.sh +62 -0
- package/skills/token-helpers/scripts/ms-graph-token.sh +70 -0
- package/skills/webapp-testing/LICENSE.txt +202 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/VENDORED.md +32 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/skills/xlsx/LICENSE.txt +30 -0
- package/skills/xlsx/SKILL.md +292 -0
- package/skills/xlsx/VENDORED.md +32 -0
- package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- package/skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/xlsx/scripts/office/pack.py +159 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/xlsx/scripts/office/soffice.py +183 -0
- package/skills/xlsx/scripts/office/unpack.py +132 -0
- package/skills/xlsx/scripts/office/validate.py +111 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
- package/skills/xlsx/scripts/office/validators/base.py +847 -0
- package/skills/xlsx/scripts/office/validators/docx.py +446 -0
- package/skills/xlsx/scripts/office/validators/pptx.py +275 -0
- package/skills/xlsx/scripts/office/validators/redlining.py +247 -0
- package/skills/xlsx/scripts/recalc.py +184 -0
- package/telegram-plugin/.claude-plugin/plugin.json +20 -0
- package/telegram-plugin/.mcp.json +14 -0
- package/telegram-plugin/LICENSE +21 -0
- package/telegram-plugin/README.md +352 -0
- package/telegram-plugin/active-pins-sweep.ts +204 -0
- package/telegram-plugin/active-pins.ts +146 -0
- package/telegram-plugin/active-reactions-sweep.ts +79 -0
- package/telegram-plugin/active-reactions.ts +134 -0
- package/telegram-plugin/admin-commands/dispatch.test.ts +149 -0
- package/telegram-plugin/admin-commands/index.ts +106 -0
- package/telegram-plugin/answer-stream.ts +565 -0
- package/telegram-plugin/ask-user.ts +179 -0
- package/telegram-plugin/attachment-path.ts +80 -0
- package/telegram-plugin/auth-code-redact.ts +83 -0
- package/telegram-plugin/auth-dashboard.ts +1104 -0
- package/telegram-plugin/auth-slot-parser.ts +497 -0
- package/telegram-plugin/auto-fallback-dispatcher.ts +68 -0
- package/telegram-plugin/auto-fallback.ts +348 -0
- package/telegram-plugin/bridge/bridge.ts +687 -0
- package/telegram-plugin/bridge/ipc-client.ts +326 -0
- package/telegram-plugin/bun.lock +218 -0
- package/telegram-plugin/card-format.ts +62 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +56 -0
- package/telegram-plugin/channel-envelope-safety.ts +56 -0
- package/telegram-plugin/chat-lock.ts +65 -0
- package/telegram-plugin/context-exhaustion.ts +38 -0
- package/telegram-plugin/credits-watch.ts +220 -0
- package/telegram-plugin/dist/bridge/bridge.js +24758 -0
- package/telegram-plugin/dist/foreman/foreman.js +30723 -0
- package/telegram-plugin/dist/gateway/gateway.js +46497 -0
- package/telegram-plugin/dist/server.js +24551 -0
- package/telegram-plugin/dm-command-gate.ts +56 -0
- package/telegram-plugin/docs/gateway-server-split.md +133 -0
- package/telegram-plugin/docs/multi-agent-card-design.md +847 -0
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +144 -0
- package/telegram-plugin/docs/stream-json-daemon-mode.md +477 -0
- package/telegram-plugin/docs/waiting-ux-spec.md +233 -0
- package/telegram-plugin/draft-stream.ts +442 -0
- package/telegram-plugin/draft-transport.ts +72 -0
- package/telegram-plugin/first-paint.ts +246 -0
- package/telegram-plugin/fleet-state.ts +246 -0
- package/telegram-plugin/foreman/foreman-create-flow.ts +202 -0
- package/telegram-plugin/foreman/foreman-handlers.ts +493 -0
- package/telegram-plugin/foreman/foreman.ts +1130 -0
- package/telegram-plugin/foreman/setup-flow.ts +345 -0
- package/telegram-plugin/foreman/setup-state.ts +239 -0
- package/telegram-plugin/foreman/state.ts +203 -0
- package/telegram-plugin/format.ts +685 -0
- package/telegram-plugin/gateway/access-validator.test.ts +95 -0
- package/telegram-plugin/gateway/access-validator.ts +37 -0
- package/telegram-plugin/gateway/boot-card.ts +582 -0
- package/telegram-plugin/gateway/boot-probes.ts +863 -0
- package/telegram-plugin/gateway/boot-reason.ts +51 -0
- package/telegram-plugin/gateway/boot-sweep-filter.test.ts +54 -0
- package/telegram-plugin/gateway/boot-sweep-filter.ts +32 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +183 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +109 -0
- package/telegram-plugin/gateway/gateway.ts +10202 -0
- package/telegram-plugin/gateway/inbound-coalesce.ts +147 -0
- package/telegram-plugin/gateway/inject-handler.test.ts +221 -0
- package/telegram-plugin/gateway/inject-handler.ts +190 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +151 -0
- package/telegram-plugin/gateway/ipc-server.ts +494 -0
- package/telegram-plugin/gateway/pid-file.ts +103 -0
- package/telegram-plugin/gateway/poll-health.ts +156 -0
- package/telegram-plugin/gateway/preamble-suppressor.ts +154 -0
- package/telegram-plugin/gateway/quota-cache.ts +125 -0
- package/telegram-plugin/gateway/resolve-calling-subagent.ts +78 -0
- package/telegram-plugin/gateway/restart-watchdog.ts +200 -0
- package/telegram-plugin/gateway/session-marker.ts +83 -0
- package/telegram-plugin/gateway/shutdown-drain.ts +162 -0
- package/telegram-plugin/gateway/startup-mutex.ts +285 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +142 -0
- package/telegram-plugin/gateway/turn-active-marker.ts +176 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +78 -0
- package/telegram-plugin/handoff-continuity.ts +200 -0
- package/telegram-plugin/history.ts +468 -0
- package/telegram-plugin/hooks/hooks.json +58 -0
- package/telegram-plugin/hooks/secret-guard-pretool.mjs +208 -0
- package/telegram-plugin/hooks/secret-scrub-stop.mjs +98 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +111 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +296 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +261 -0
- package/telegram-plugin/html-sanitize.ts +244 -0
- package/telegram-plugin/idle-footer.ts +65 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +166 -0
- package/telegram-plugin/interrupt-marker.ts +66 -0
- package/telegram-plugin/issues-card.ts +371 -0
- package/telegram-plugin/issues-watcher.ts +125 -0
- package/telegram-plugin/model-unavailable.ts +325 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/operator-events-history.ts +94 -0
- package/telegram-plugin/operator-events.fixtures.json +161 -0
- package/telegram-plugin/operator-events.ts +421 -0
- package/telegram-plugin/package.json +55 -0
- package/telegram-plugin/permission-rule.ts +133 -0
- package/telegram-plugin/permission-title.ts +117 -0
- package/telegram-plugin/pin-event-log.ts +76 -0
- package/telegram-plugin/plugin-logger.ts +136 -0
- package/telegram-plugin/progress-card-driver.ts +2697 -0
- package/telegram-plugin/progress-card-pin-manager.ts +589 -0
- package/telegram-plugin/progress-card-pin-watchdog.ts +98 -0
- package/telegram-plugin/progress-card.ts +1409 -0
- package/telegram-plugin/pty-partial-handler.ts +247 -0
- package/telegram-plugin/pty-tail.ts +730 -0
- package/telegram-plugin/quota-check.ts +474 -0
- package/telegram-plugin/recent-outbound-dedup.ts +169 -0
- package/telegram-plugin/registry/api-registry.test.ts +201 -0
- package/telegram-plugin/registry/subagents-bugs.test.ts +454 -0
- package/telegram-plugin/registry/subagents-schema.ts +509 -0
- package/telegram-plugin/registry/subagents.test.ts +476 -0
- package/telegram-plugin/registry/turns-schema.test.ts +101 -0
- package/telegram-plugin/registry/turns-schema.ts +417 -0
- package/telegram-plugin/retry-api-call.ts +172 -0
- package/telegram-plugin/scripts/build.mjs +78 -0
- package/telegram-plugin/secret-detect/audit.ts +66 -0
- package/telegram-plugin/secret-detect/chunker.ts +37 -0
- package/telegram-plugin/secret-detect/entropy.ts +20 -0
- package/telegram-plugin/secret-detect/gitleaks-loader.ts +74 -0
- package/telegram-plugin/secret-detect/gitleaks.toml +27 -0
- package/telegram-plugin/secret-detect/index.ts +218 -0
- package/telegram-plugin/secret-detect/kv-scanner.ts +60 -0
- package/telegram-plugin/secret-detect/mask.ts +13 -0
- package/telegram-plugin/secret-detect/patterns.ts +115 -0
- package/telegram-plugin/secret-detect/pipeline.ts +144 -0
- package/telegram-plugin/secret-detect/rewrite.ts +26 -0
- package/telegram-plugin/secret-detect/secretlint-source.ts +95 -0
- package/telegram-plugin/secret-detect/slug.ts +44 -0
- package/telegram-plugin/secret-detect/staging.ts +85 -0
- package/telegram-plugin/secret-detect/suppressor.ts +34 -0
- package/telegram-plugin/secret-detect/url-redact.ts +60 -0
- package/telegram-plugin/secret-detect/vault-write.ts +56 -0
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/server.ts +171 -0
- package/telegram-plugin/session-tail.ts +884 -0
- package/telegram-plugin/shared/bot-runtime.ts +324 -0
- package/telegram-plugin/silent-reply.ts +58 -0
- package/telegram-plugin/slot-banner-driver.ts +147 -0
- package/telegram-plugin/slot-banner.ts +86 -0
- package/telegram-plugin/start.js +26 -0
- package/telegram-plugin/startup-reset.ts +45 -0
- package/telegram-plugin/status-reactions.ts +332 -0
- package/telegram-plugin/steering.ts +155 -0
- package/telegram-plugin/sticker-aliases.ts +249 -0
- package/telegram-plugin/stream-controller.ts +311 -0
- package/telegram-plugin/stream-reply-handler.ts +664 -0
- package/telegram-plugin/streaming-metrics.ts +134 -0
- package/telegram-plugin/streaming-report.ts +204 -0
- package/telegram-plugin/subagent-watcher.ts +880 -0
- package/telegram-plugin/telegram-button-constraints.ts +191 -0
- package/telegram-plugin/telegraph.ts +381 -0
- package/telegram-plugin/tests/HARNESS.md +340 -0
- package/telegram-plugin/tests/_progress-card-harness.ts +105 -0
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +211 -0
- package/telegram-plugin/tests/active-pins-sweep.test.ts +309 -0
- package/telegram-plugin/tests/active-pins.test.ts +187 -0
- package/telegram-plugin/tests/active-reactions-sweep.test.ts +116 -0
- package/telegram-plugin/tests/active-reactions.test.ts +198 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +352 -0
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +236 -0
- package/telegram-plugin/tests/answer-stream.test.ts +878 -0
- package/telegram-plugin/tests/ask-user.test.ts +203 -0
- package/telegram-plugin/tests/attachment-path.test.ts +199 -0
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +118 -0
- package/telegram-plugin/tests/auth-code-auto-capture.test.ts +144 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +248 -0
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +260 -0
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +140 -0
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +559 -0
- package/telegram-plugin/tests/auth-dashboard.test.ts +1045 -0
- package/telegram-plugin/tests/auth-login-url-button.test.ts +122 -0
- package/telegram-plugin/tests/auth-slot-commands.test.ts +640 -0
- package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +183 -0
- package/telegram-plugin/tests/auto-fallback.test.ts +381 -0
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +137 -0
- package/telegram-plugin/tests/boot-card-dedupe.test.ts +154 -0
- package/telegram-plugin/tests/boot-card-probe-target.test.ts +194 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +103 -0
- package/telegram-plugin/tests/boot-card-render.test.ts +219 -0
- package/telegram-plugin/tests/boot-probes.test.ts +451 -0
- package/telegram-plugin/tests/bot-api.harness.ts +116 -0
- package/telegram-plugin/tests/bot-runtime.test.ts +190 -0
- package/telegram-plugin/tests/bridge-anonymous-refuse.test.ts +60 -0
- package/telegram-plugin/tests/context-exhaustion.test.ts +114 -0
- package/telegram-plugin/tests/credits-watch.test.ts +221 -0
- package/telegram-plugin/tests/dm-command-gate.test.ts +176 -0
- package/telegram-plugin/tests/draft-stream.test.ts +752 -0
- package/telegram-plugin/tests/draft-transport.test.ts +141 -0
- package/telegram-plugin/tests/e2e.test.ts +436 -0
- package/telegram-plugin/tests/fake-bot-api.test.ts +213 -0
- package/telegram-plugin/tests/fake-bot-api.ts +617 -0
- package/telegram-plugin/tests/false-restart-banner.test.ts +253 -0
- package/telegram-plugin/tests/first-paint.test.ts +257 -0
- package/telegram-plugin/tests/fixtures/pty-tail-tmux-fragment.bin +6 -0
- package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +3624 -0
- package/telegram-plugin/tests/fleet-state-watcher.test.ts +101 -0
- package/telegram-plugin/tests/fleet-state.test.ts +185 -0
- package/telegram-plugin/tests/foreman-create-flow.test.ts +359 -0
- package/telegram-plugin/tests/foreman-handlers.test.ts +347 -0
- package/telegram-plugin/tests/foreman-state.test.ts +164 -0
- package/telegram-plugin/tests/foreman-write-ops.test.ts +214 -0
- package/telegram-plugin/tests/gateway-409-retry-banner.test.ts +173 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +72 -0
- package/telegram-plugin/tests/gateway-bridge.test.ts +811 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +414 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +144 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +133 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +103 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +127 -0
- package/telegram-plugin/tests/gateway-startup-mutex.test.ts +284 -0
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +185 -0
- package/telegram-plugin/tests/gateway-startup-reset.test.ts +72 -0
- package/telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts +125 -0
- package/telegram-plugin/tests/handoff-continuity.test.ts +249 -0
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +243 -0
- package/telegram-plugin/tests/harness-parse-mode-validation.test.ts +114 -0
- package/telegram-plugin/tests/history.test.ts +364 -0
- package/telegram-plugin/tests/html-balanced.ts +63 -0
- package/telegram-plugin/tests/html-sanitize.test.ts +146 -0
- package/telegram-plugin/tests/idle-footer-wiring.test.ts +88 -0
- package/telegram-plugin/tests/idle-footer.test.ts +66 -0
- package/telegram-plugin/tests/inbound-coalesce.test.ts +127 -0
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +150 -0
- package/telegram-plugin/tests/interrupt-marker.test.ts +126 -0
- package/telegram-plugin/tests/ipc-protocol.test.ts +218 -0
- package/telegram-plugin/tests/ipc-server-anonymous-refuse.test.ts +82 -0
- package/telegram-plugin/tests/ipc-server-client.test.ts +323 -0
- package/telegram-plugin/tests/ipc-server-race.test.ts +183 -0
- package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +91 -0
- package/telegram-plugin/tests/ipc-server-validate-pty-partial.test.ts +64 -0
- package/telegram-plugin/tests/ipc-server-validate-update-placeholder.test.ts +77 -0
- package/telegram-plugin/tests/ipc-validator.test.ts +274 -0
- package/telegram-plugin/tests/issues-card.test.ts +495 -0
- package/telegram-plugin/tests/issues-watcher.test.ts +165 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +303 -0
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +159 -0
- package/telegram-plugin/tests/operator-events-history.test.ts +125 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +192 -0
- package/telegram-plugin/tests/operator-events.test.ts +331 -0
- package/telegram-plugin/tests/outbound-ordering.test.ts +293 -0
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +164 -0
- package/telegram-plugin/tests/permission-rule.test.ts +121 -0
- package/telegram-plugin/tests/permission-title.test.ts +106 -0
- package/telegram-plugin/tests/pin-event-log.test.ts +124 -0
- package/telegram-plugin/tests/plugin-logger.test.ts +97 -0
- package/telegram-plugin/tests/poll-health.test.ts +86 -0
- package/telegram-plugin/tests/preamble-suppressor.test.ts +285 -0
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +73 -0
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +272 -0
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +258 -0
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +81 -0
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +80 -0
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +215 -0
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +123 -0
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +76 -0
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +62 -0
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +84 -0
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +139 -0
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +773 -0
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +66 -0
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +64 -0
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +190 -0
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +146 -0
- package/telegram-plugin/tests/progress-update.test.ts +236 -0
- package/telegram-plugin/tests/protocol-fixtures.test.ts +59 -0
- package/telegram-plugin/tests/protocol-fixtures.ts +198 -0
- package/telegram-plugin/tests/pty-partial-handler.test.ts +326 -0
- package/telegram-plugin/tests/pty-tail-real-fixture.test.ts +114 -0
- package/telegram-plugin/tests/pty-tail-tmux-fragment.test.ts +71 -0
- package/telegram-plugin/tests/pty-tail.test.ts +525 -0
- package/telegram-plugin/tests/quota-cache.test.ts +187 -0
- package/telegram-plugin/tests/quota-check.test.ts +622 -0
- package/telegram-plugin/tests/races.test.ts +842 -0
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +123 -0
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +82 -0
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +114 -0
- package/telegram-plugin/tests/real-gateway-harness.ts +699 -0
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +313 -0
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +299 -0
- package/telegram-plugin/tests/real-gateway-spec.test.ts +487 -0
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +101 -0
- package/telegram-plugin/tests/recent-outbound-dedup.test.ts +192 -0
- package/telegram-plugin/tests/registry-turns.test.ts +432 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +149 -0
- package/telegram-plugin/tests/resolve-calling-subagent.test.ts +269 -0
- package/telegram-plugin/tests/restart-watchdog.test.ts +224 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +287 -0
- package/telegram-plugin/tests/secret-detect-audit.test.ts +58 -0
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +83 -0
- package/telegram-plugin/tests/secret-detect-gitleaks.test.ts +32 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +308 -0
- package/telegram-plugin/tests/secret-detect-pipeline.test.ts +123 -0
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +101 -0
- package/telegram-plugin/tests/secret-detect-staging.test.ts +45 -0
- package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +67 -0
- package/telegram-plugin/tests/secret-detect.test.ts +223 -0
- package/telegram-plugin/tests/secret-guard-pretool.test.ts +194 -0
- package/telegram-plugin/tests/send-typing-action-validation.test.ts +61 -0
- package/telegram-plugin/tests/session-tail-capped.test.ts +285 -0
- package/telegram-plugin/tests/session-tail.test.ts +524 -0
- package/telegram-plugin/tests/setup-flow.test.ts +510 -0
- package/telegram-plugin/tests/setup-state.test.ts +146 -0
- package/telegram-plugin/tests/silent-reply-guard.test.ts +122 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +350 -0
- package/telegram-plugin/tests/slot-banner.test.ts +74 -0
- package/telegram-plugin/tests/snapshot-serializer.ts +79 -0
- package/telegram-plugin/tests/spawn-detached-cgroup-escape.test.ts +51 -0
- package/telegram-plugin/tests/status-accent.test.ts +186 -0
- package/telegram-plugin/tests/status-reactions-allowed-filter.test.ts +132 -0
- package/telegram-plugin/tests/status-reactions.test.ts +314 -0
- package/telegram-plugin/tests/steering.test.ts +282 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +232 -0
- package/telegram-plugin/tests/stream-controller-html-fallback.test.ts +127 -0
- package/telegram-plugin/tests/stream-controller.test.ts +262 -0
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +208 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +1292 -0
- package/telegram-plugin/tests/streaming-e2e.test.ts +389 -0
- package/telegram-plugin/tests/streaming-metrics.test.ts +201 -0
- package/telegram-plugin/tests/streaming-orchestration.test.ts +756 -0
- package/telegram-plugin/tests/subagent-registry-bugs.test.ts +725 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +213 -0
- package/telegram-plugin/tests/subagent-watcher-parent-marker.test.ts +274 -0
- package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +243 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +877 -0
- package/telegram-plugin/tests/subagents-schema-init-order.test.ts +109 -0
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +116 -0
- package/telegram-plugin/tests/telegram-button-constraints.test.ts +194 -0
- package/telegram-plugin/tests/telegram-format.test.ts +1093 -0
- package/telegram-plugin/tests/telegraph.test.ts +246 -0
- package/telegram-plugin/tests/tool-labels.test.ts +383 -0
- package/telegram-plugin/tests/turn-active-marker.test.ts +195 -0
- package/telegram-plugin/tests/turn-end-regressions.test.ts +489 -0
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +218 -0
- package/telegram-plugin/tests/turn-flush-dedup-controller.test.ts +144 -0
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +78 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +189 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +107 -0
- package/telegram-plugin/tests/turns-writer.test.ts +323 -0
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +131 -0
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +120 -0
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +87 -0
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +211 -0
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +62 -0
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +68 -0
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +110 -0
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +128 -0
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +58 -0
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +133 -0
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +155 -0
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +117 -0
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +143 -0
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +149 -0
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +105 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +141 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +147 -0
- package/telegram-plugin/tests/update-factory-edited-and-reactions.test.ts +108 -0
- package/telegram-plugin/tests/update-factory.ts +305 -0
- package/telegram-plugin/tests/vault-grant-wizard.test.ts +84 -0
- package/telegram-plugin/tests/vault-grants-revoke.test.ts +265 -0
- package/telegram-plugin/tests/vault-subcommands.test.ts +234 -0
- package/telegram-plugin/tests/voice-transcribe.test.ts +196 -0
- package/telegram-plugin/tests/waiting-ux-harness.ts +381 -0
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +233 -0
- package/telegram-plugin/tests/welcome-text.test.ts +407 -0
- package/telegram-plugin/tool-error-filter.ts +89 -0
- package/telegram-plugin/tool-labels.ts +330 -0
- package/telegram-plugin/tool-names.ts +53 -0
- package/telegram-plugin/turn-flush-prose-recovery.ts +40 -0
- package/telegram-plugin/turn-flush-safety.ts +109 -0
- package/telegram-plugin/turn-signal-tracker.ts +79 -0
- package/telegram-plugin/two-zone-card.ts +249 -0
- package/telegram-plugin/typing-wrap.ts +92 -0
- package/telegram-plugin/voice-transcribe.ts +166 -0
- package/telegram-plugin/welcome-text.ts +359 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram-flavored markdown→HTML rendering and chunking.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from server.ts so tests can import these helpers without
|
|
5
|
+
* triggering the bot startup side effects (env loading, token check,
|
|
6
|
+
* grammy instantiation). server.ts re-exports the public API for
|
|
7
|
+
* backwards compatibility with any external callers.
|
|
8
|
+
*
|
|
9
|
+
* Three pieces:
|
|
10
|
+
* - markdownToHtml + isLikelyTelegramHtml: convert model output to
|
|
11
|
+
* Telegram-safe HTML, preserving any embedded whitelisted Telegram
|
|
12
|
+
* HTML tags so the model can mix markdown bold with raw <b>/<i>/<a>.
|
|
13
|
+
* - splitHtmlChunks: split a long HTML message into <=4096-char chunks
|
|
14
|
+
* that preserve open/close tag balance and don't bisect HTML entities.
|
|
15
|
+
* - escapeHtml: the three-char escape used everywhere.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Telegram-supported HTML tags. Anything outside this set is either
|
|
20
|
+
* unrecognized (Telegram strips it) or actively dangerous (the API
|
|
21
|
+
* rejects the message). Source: https://core.telegram.org/bots/api#html-style
|
|
22
|
+
*/
|
|
23
|
+
export const TELEGRAM_HTML_TAGS = new Set([
|
|
24
|
+
'b', 'strong',
|
|
25
|
+
'i', 'em',
|
|
26
|
+
'u', 'ins',
|
|
27
|
+
's', 'strike', 'del',
|
|
28
|
+
'span', // requires class="tg-spoiler"
|
|
29
|
+
'tg-spoiler',
|
|
30
|
+
'a',
|
|
31
|
+
'tg-emoji',
|
|
32
|
+
'code',
|
|
33
|
+
'pre',
|
|
34
|
+
'blockquote',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Heuristic: does this look like already-rendered Telegram HTML rather
|
|
39
|
+
* than markdown waiting to be converted?
|
|
40
|
+
*
|
|
41
|
+
* Returns true when ALL the tags we find are recognized Telegram HTML
|
|
42
|
+
* tags AND there's at least one of them AND the text doesn't also have
|
|
43
|
+
* markdown-only syntax (** for bold, [text](url) for links). This is
|
|
44
|
+
* conservative: if the model wrote `<div>foo</div>` (not Telegram HTML),
|
|
45
|
+
* we treat it as markdown and escape it. If the model wrote `<b>foo</b>`,
|
|
46
|
+
* we trust it.
|
|
47
|
+
*
|
|
48
|
+
* Critical: we strip markdown code spans and fenced code blocks BEFORE
|
|
49
|
+
* scanning for tags, because the model frequently writes things like
|
|
50
|
+
* `\`<b>tag</b>\`` (an inline code example showing literal HTML). Without
|
|
51
|
+
* the strip, the heuristic would see `<b>` inside the code span and
|
|
52
|
+
* misclassify the whole text as raw HTML.
|
|
53
|
+
*/
|
|
54
|
+
export function isLikelyTelegramHtml(text: string): boolean {
|
|
55
|
+
// Strip fenced code blocks first (greedy, cross-line)
|
|
56
|
+
let scanText = text.replace(/```[\s\S]*?```/g, '')
|
|
57
|
+
// Then strip inline code spans (single backticks, no newlines)
|
|
58
|
+
scanText = scanText.replace(/`[^`\n]+`/g, '')
|
|
59
|
+
|
|
60
|
+
// If the stripped text contains markdown-only syntax (**bold**,
|
|
61
|
+
// [text](url), or markdown headings), the caller is writing markdown
|
|
62
|
+
// even if they ALSO sprinkled some <b> tags in. Treat as markdown.
|
|
63
|
+
if (/\*\*[^\n*]+\*\*/.test(scanText)) return false
|
|
64
|
+
if (/\[[^\]]+\]\([^)]+\)/.test(scanText)) return false
|
|
65
|
+
if (/^#{1,6}\s+/m.test(scanText)) return false
|
|
66
|
+
|
|
67
|
+
// Now count remaining HTML tags
|
|
68
|
+
const tagMatches = scanText.matchAll(/<\/?([a-z][a-z0-9-]*)\b[^>]*>/gi)
|
|
69
|
+
let count = 0
|
|
70
|
+
for (const m of tagMatches) {
|
|
71
|
+
const tag = m[1].toLowerCase()
|
|
72
|
+
if (!TELEGRAM_HTML_TAGS.has(tag)) {
|
|
73
|
+
// Found an unsupported tag — caller didn't intend Telegram HTML
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
count++
|
|
77
|
+
}
|
|
78
|
+
return count > 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Markdown table → Telegram HTML
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse a contiguous block of lines as a markdown table.
|
|
87
|
+
*
|
|
88
|
+
* A valid markdown table requires:
|
|
89
|
+
* - A header row: | col | col | (leading/trailing pipes optional)
|
|
90
|
+
* - A separator row: | --- | --- | (cells are only dashes, colons, spaces)
|
|
91
|
+
* - At least one data row.
|
|
92
|
+
*
|
|
93
|
+
* The separator row is the discriminating signal — it prevents plain prose
|
|
94
|
+
* lines that happen to contain a pipe (e.g. `echo foo | bar`) from being
|
|
95
|
+
* mistaken for tables.
|
|
96
|
+
*
|
|
97
|
+
* Returns null when the block is not a valid table.
|
|
98
|
+
*/
|
|
99
|
+
function parseMarkdownTable(lines: string[]): { headers: string[]; rows: string[][] } | null {
|
|
100
|
+
if (lines.length < 3) return null
|
|
101
|
+
|
|
102
|
+
// Separator line: cells contain only dashes, colons, and spaces.
|
|
103
|
+
const sepRe = /^\|?(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*:?-*:?[ \t]*\|?$/
|
|
104
|
+
// A pipe-delimited row: must contain at least one |
|
|
105
|
+
const rowRe = /\|/
|
|
106
|
+
|
|
107
|
+
// Find the separator line index (must be index 1 in this block)
|
|
108
|
+
if (!sepRe.test(lines[1].trim())) return null
|
|
109
|
+
// Double-check: the header row must also look like a table row
|
|
110
|
+
if (!rowRe.test(lines[0])) return null
|
|
111
|
+
// Must have at least one data row
|
|
112
|
+
if (lines.length < 3 || !rowRe.test(lines[2])) return null
|
|
113
|
+
|
|
114
|
+
const splitRow = (line: string): string[] =>
|
|
115
|
+
line
|
|
116
|
+
.replace(/^\|/, '')
|
|
117
|
+
.replace(/\|$/, '')
|
|
118
|
+
.split('|')
|
|
119
|
+
.map(c => c.trim())
|
|
120
|
+
|
|
121
|
+
const headers = splitRow(lines[0])
|
|
122
|
+
const rows: string[][] = []
|
|
123
|
+
for (let i = 2; i < lines.length; i++) {
|
|
124
|
+
if (!rowRe.test(lines[i])) break
|
|
125
|
+
rows.push(splitRow(lines[i]))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (rows.length === 0) return null
|
|
129
|
+
return { headers, rows }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render a parsed markdown table as Telegram-compatible HTML.
|
|
134
|
+
*
|
|
135
|
+
* Branch rules:
|
|
136
|
+
* - ≤3 columns AND ≤6 rows → bullet list:
|
|
137
|
+
* Each row is one bullet. First column in <b>; subsequent columns
|
|
138
|
+
* appended as " — value".
|
|
139
|
+
* - otherwise → <pre> block with padded columns.
|
|
140
|
+
*/
|
|
141
|
+
function renderTable(headers: string[], rows: string[][]): string {
|
|
142
|
+
const colCount = headers.length
|
|
143
|
+
const rowCount = rows.length
|
|
144
|
+
|
|
145
|
+
if (colCount <= 3 && rowCount <= 6) {
|
|
146
|
+
// Bullet list rendering
|
|
147
|
+
const bullets = rows.map(row => {
|
|
148
|
+
// Normalise row length to match header count (guard empty cells)
|
|
149
|
+
const cells = headers.map((_, i) => (row[i] ?? '').trim())
|
|
150
|
+
const key = escapeHtml(cells[0] || '—')
|
|
151
|
+
const rest = cells
|
|
152
|
+
.slice(1)
|
|
153
|
+
.filter(v => v !== '')
|
|
154
|
+
.map(v => ` — ${escapeHtml(v)}`)
|
|
155
|
+
.join('')
|
|
156
|
+
return `• <b>${key}</b>${rest}`
|
|
157
|
+
})
|
|
158
|
+
// Prepend header names as a label line when there are 2+ columns
|
|
159
|
+
const headerLine =
|
|
160
|
+
colCount >= 2
|
|
161
|
+
? `<b>${headers.map(h => escapeHtml(h)).join(' / ')}</b>\n`
|
|
162
|
+
: ''
|
|
163
|
+
return headerLine + bullets.join('\n')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pre-block with padded columns
|
|
167
|
+
// Compute column widths across headers + all rows
|
|
168
|
+
const allRows = [headers, ...rows]
|
|
169
|
+
const widths = headers.map((_, ci) =>
|
|
170
|
+
Math.max(...allRows.map(r => (r[ci] ?? '').length))
|
|
171
|
+
)
|
|
172
|
+
const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length))
|
|
173
|
+
|
|
174
|
+
const formatRow = (r: string[]) =>
|
|
175
|
+
headers.map((_, ci) => pad(r[ci] ?? '', widths[ci])).join(' ')
|
|
176
|
+
|
|
177
|
+
const sepLine = widths.map(w => '-'.repeat(w)).join(' ')
|
|
178
|
+
|
|
179
|
+
const lines = [
|
|
180
|
+
formatRow(headers),
|
|
181
|
+
sepLine,
|
|
182
|
+
...rows.map(r => formatRow(r)),
|
|
183
|
+
]
|
|
184
|
+
return `<pre>${escapeHtml(lines.join('\n'))}</pre>`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Replace markdown table blocks in `text` with rendered HTML, storing the
|
|
189
|
+
* rendered output in `store` and emitting `placeholderPrefix<n>\x00` tokens
|
|
190
|
+
* so the rest of the pipeline does not re-process them.
|
|
191
|
+
*
|
|
192
|
+
* Tables are identified by their separator line (`| --- |`) which prevents
|
|
193
|
+
* plain prose containing a pipe (e.g. `echo foo | bar`) from being mistaken
|
|
194
|
+
* for a table. Fenced code blocks are extracted before this runs, so
|
|
195
|
+
* table-looking rows inside ``` blocks are already protected.
|
|
196
|
+
*/
|
|
197
|
+
function extractMarkdownTables(
|
|
198
|
+
text: string,
|
|
199
|
+
store: string[],
|
|
200
|
+
placeholderPrefix: string,
|
|
201
|
+
): string {
|
|
202
|
+
const inputLines = text.split('\n')
|
|
203
|
+
const outputLines: string[] = []
|
|
204
|
+
let i = 0
|
|
205
|
+
|
|
206
|
+
while (i < inputLines.length) {
|
|
207
|
+
const line = inputLines[i]
|
|
208
|
+
if (!line.includes('|')) {
|
|
209
|
+
outputLines.push(line)
|
|
210
|
+
i++
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Collect a run of pipe-containing lines as a candidate block
|
|
215
|
+
let j = i
|
|
216
|
+
while (j < inputLines.length && inputLines[j].includes('|')) {
|
|
217
|
+
j++
|
|
218
|
+
}
|
|
219
|
+
const block = inputLines.slice(i, j)
|
|
220
|
+
|
|
221
|
+
const parsed = parseMarkdownTable(block)
|
|
222
|
+
if (parsed) {
|
|
223
|
+
const tableLineCount = 2 + parsed.rows.length
|
|
224
|
+
const remainder = block.slice(tableLineCount)
|
|
225
|
+
const idx = store.length
|
|
226
|
+
store.push(renderTable(parsed.headers, parsed.rows))
|
|
227
|
+
outputLines.push(`${placeholderPrefix}${idx}\x00`)
|
|
228
|
+
for (const r of remainder) outputLines.push(r)
|
|
229
|
+
i = j
|
|
230
|
+
} else {
|
|
231
|
+
for (const b of block) outputLines.push(b)
|
|
232
|
+
i = j
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return outputLines.join('\n')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert markdown to Telegram-compatible HTML.
|
|
241
|
+
* Handles bold, italic, code, code blocks, strikethrough, links.
|
|
242
|
+
* Escapes HTML entities in plain text. Wraps file references in <code>.
|
|
243
|
+
* Preserves embedded whitelisted Telegram HTML tags so the model can
|
|
244
|
+
* mix markdown and raw HTML in the same message.
|
|
245
|
+
*/
|
|
246
|
+
export function markdownToHtml(text: string): string {
|
|
247
|
+
// Smart pass-through: if the input is already valid Telegram HTML
|
|
248
|
+
// (every tag is in the supported list), trust the caller and return
|
|
249
|
+
// it unchanged.
|
|
250
|
+
if (isLikelyTelegramHtml(text)) {
|
|
251
|
+
return text
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// First, extract code blocks and inline code to protect them from other transforms.
|
|
255
|
+
const codeBlocks: string[] = []
|
|
256
|
+
const BLOCK_PH = '\x00CODEBLOCK'
|
|
257
|
+
const INLINE_PH = '\x00CODEINLINE'
|
|
258
|
+
|
|
259
|
+
// Tables are extracted after code blocks so that table-looking rows inside
|
|
260
|
+
// fenced code blocks are already parked in codeBlocks placeholders and
|
|
261
|
+
// won't be touched. Rendered table HTML is stored alongside codeBlocks and
|
|
262
|
+
// uses the same placeholder so restoration happens in a single pass.
|
|
263
|
+
const TABLE_PH = '\x00TABLEBLOCK'
|
|
264
|
+
|
|
265
|
+
// Code blocks: ```lang\ncode\n```
|
|
266
|
+
let result = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang: string, code: string) => {
|
|
267
|
+
const escaped = escapeHtml(code.replace(/\n$/, ''))
|
|
268
|
+
const cls = lang ? ` class="language-${lang}"` : ''
|
|
269
|
+
const idx = codeBlocks.length
|
|
270
|
+
codeBlocks.push(`<pre><code${cls}>${escaped}</code></pre>`)
|
|
271
|
+
return `${BLOCK_PH}${idx}\x00`
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Extract markdown tables after fenced code blocks are parked. Rendered
|
|
275
|
+
// HTML is stored in codeBlocks (shared store); TABLE_PH is a distinct
|
|
276
|
+
// prefix so the two restore regexes below can target each independently.
|
|
277
|
+
result = extractMarkdownTables(result, codeBlocks, TABLE_PH)
|
|
278
|
+
|
|
279
|
+
// Convert markdown headings (# / ## / ### ...) to bold lines on their
|
|
280
|
+
// own. Telegram has no <h1> tag, and rendering ## as plain text leaves
|
|
281
|
+
// ugly hash marks in the message.
|
|
282
|
+
result = result.replace(/^(#{1,6})\s+(.+?)\s*$/gm, (_m, _hashes, title: string) => {
|
|
283
|
+
return `**${title}**`
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// Inline code: `code`
|
|
287
|
+
const inlineCodes: string[] = []
|
|
288
|
+
result = result.replace(/`([^`\n]+)`/g, (_m, code: string) => {
|
|
289
|
+
const idx = inlineCodes.length
|
|
290
|
+
inlineCodes.push(`<code>${escapeHtml(code)}</code>`)
|
|
291
|
+
return `${INLINE_PH}${idx}\x00`
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Telegram HTML tag pass-through. Extract any opening/closing tag
|
|
295
|
+
// whose name is in the whitelist into placeholders. The TEXT BETWEEN
|
|
296
|
+
// tags still flows through escapeHtml and the markdown conversions
|
|
297
|
+
// below, so `<b>**bold**</b>` and `<b>plain</b>` both work. Tags are
|
|
298
|
+
// restored verbatim at the very end.
|
|
299
|
+
const htmlTags: string[] = []
|
|
300
|
+
const HTMLTAG_PH = '\x00HTMLTAG'
|
|
301
|
+
const tagNamePattern = Array.from(TELEGRAM_HTML_TAGS).join('|')
|
|
302
|
+
const htmlTagRe = new RegExp(`</?(?:${tagNamePattern})\\b[^>]*>`, 'gi')
|
|
303
|
+
result = result.replace(htmlTagRe, (match: string) => {
|
|
304
|
+
const idx = htmlTags.length
|
|
305
|
+
htmlTags.push(match)
|
|
306
|
+
return `${HTMLTAG_PH}${idx}\x00`
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Escape HTML entities in remaining plain text
|
|
310
|
+
result = escapeHtml(result)
|
|
311
|
+
|
|
312
|
+
// Bold: **text** (must come before italic)
|
|
313
|
+
result = result.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
|
|
314
|
+
|
|
315
|
+
// Italic: *text* (single asterisk, not preceded by another *)
|
|
316
|
+
result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<i>$1</i>')
|
|
317
|
+
|
|
318
|
+
// Italic: _text_ (underscore form). Lookarounds guard snake_case,
|
|
319
|
+
// __double__, and word-internal underscores. Emoji codepoints are not
|
|
320
|
+
// \w, so emoji-leading/trailing italics like `_📥 queued_` work correctly.
|
|
321
|
+
result = result.replace(/(?<![\w_])_(?!_)([^_\n]+?)_(?![\w_])/g, '<i>$1</i>')
|
|
322
|
+
|
|
323
|
+
// Strikethrough: ~~text~~
|
|
324
|
+
result = result.replace(/~~(.+?)~~/g, '<s>$1</s>')
|
|
325
|
+
|
|
326
|
+
// Restore inline-code, code-block, and table-block placeholders ONLY
|
|
327
|
+
// AFTER bold/italic/strike have run. If the inline-code placeholder
|
|
328
|
+
// is restored before italic, an inline-code span containing asterisks
|
|
329
|
+
// (e.g. `\`size_t *p\``) gets matched by the italic regex on the
|
|
330
|
+
// restored `<code>...*p</code>` buffer and produces invalid HTML
|
|
331
|
+
// that Telegram rejects with 400 Bad Request — sending the caller
|
|
332
|
+
// into a `format: text` fallback for the rest of the chunk. Same
|
|
333
|
+
// fault class for code blocks containing `**` literals. See #415.
|
|
334
|
+
result = result.replace(new RegExp(`${escapeHtml(BLOCK_PH)}(\\d+)${escapeHtml('\x00')}`, 'g'), (_m, idx) => codeBlocks[Number(idx)])
|
|
335
|
+
result = result.replace(new RegExp(`${escapeHtml(TABLE_PH)}(\\d+)${escapeHtml('\x00')}`, 'g'), (_m, idx) => codeBlocks[Number(idx)])
|
|
336
|
+
result = result.replace(new RegExp(`${escapeHtml(INLINE_PH)}(\\d+)${escapeHtml('\x00')}`, 'g'), (_m, idx) => inlineCodes[Number(idx)])
|
|
337
|
+
|
|
338
|
+
// Links: [text](url). Two safety requirements here:
|
|
339
|
+
//
|
|
340
|
+
// 1. URL scheme allowlist. Unrestricted href accepts `javascript:` and
|
|
341
|
+
// `data:` URIs; Telegram historically renders tg:// links directly
|
|
342
|
+
// (opening another bot) which is a phishing primitive. Anything not
|
|
343
|
+
// in the allowlist falls back to `#`.
|
|
344
|
+
//
|
|
345
|
+
// 2. Escape the URL before interpolating into the attribute. The HTML
|
|
346
|
+
// tag extraction above parks whitelisted tags in \x00HTMLTAG<n>\x00
|
|
347
|
+
// placeholders that get restored AFTER this replace. Without escaping
|
|
348
|
+
// the href value, an adversarial `[text](x"></a><a href="evil">)` in
|
|
349
|
+
// model output produces two <a> tags after placeholder restoration —
|
|
350
|
+
// the second hijacks the visible link target. escapeAttr covers both
|
|
351
|
+
// the placeholder-restoration attack and plain `"` breakout.
|
|
352
|
+
const ALLOWED_LINK_SCHEMES = /^(?:https?|mailto|tel|tg):/i
|
|
353
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText: string, url: string) => {
|
|
354
|
+
const safe = ALLOWED_LINK_SCHEMES.test(url.trim()) ? url.trim() : '#'
|
|
355
|
+
return `<a href="${escapeHtml(safe)}">${linkText}</a>`
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// File references: wrap filename.ext patterns in <code> tags.
|
|
359
|
+
// Lookbehind excludes `>` so we don't double-wrap filenames that are
|
|
360
|
+
// already inside a restored inline-code placeholder like
|
|
361
|
+
// `<code>settings.json</code>`. Without this, the regex matched the
|
|
362
|
+
// filename character immediately after the `>` of the opening <code>
|
|
363
|
+
// tag and re-wrapped it, producing `<code><code>settings.json</code></code>`.
|
|
364
|
+
result = result.replace(/(?<![<\/\w>])(\b[\w][\w.-]*\.(?:ts|js|py|rs|go|json|yaml|yml|toml|md|txt|sh|bash|zsh|css|html|xml|sql|env|cfg|conf|ini|log|csv|tsx|jsx|vue|svelte|rb|java|kt|swift|c|cpp|h|hpp|zig|asm|wasm|lock|mod|sum)\b)(?![^<]*>)/g, '<code>$1</code>')
|
|
365
|
+
|
|
366
|
+
// Restore preserved Telegram HTML tags (must run last so the file-ref
|
|
367
|
+
// regex above doesn't accidentally match characters inside our placeholders).
|
|
368
|
+
result = result.replace(new RegExp(`${escapeHtml(HTMLTAG_PH)}(\\d+)${escapeHtml('\x00')}`, 'g'), (_m, idx) => htmlTags[Number(idx)])
|
|
369
|
+
|
|
370
|
+
return result
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function escapeHtml(text: string): string {
|
|
374
|
+
// Also escape `"` so callers that interpolate into HTML attribute values
|
|
375
|
+
// don't need a second helper. Safe for tag-content use too.
|
|
376
|
+
return text
|
|
377
|
+
.replace(/&/g, '&')
|
|
378
|
+
.replace(/</g, '<')
|
|
379
|
+
.replace(/>/g, '>')
|
|
380
|
+
.replace(/"/g, '"')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Output sanitizer — enforces fleet-wide Telegram formatting invariants
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Normalize outbound Telegram HTML text against well-known invariants.
|
|
389
|
+
*
|
|
390
|
+
* Runs AFTER markdownToHtml, just before the text is sent to the Bot API.
|
|
391
|
+
* Conservative by design: only rewrites things that are universally wrong;
|
|
392
|
+
* leaves semantic decisions (where to bold, link choice, list-vs-prose) to
|
|
393
|
+
* the agent.
|
|
394
|
+
*
|
|
395
|
+
* Rules applied (in order):
|
|
396
|
+
* 1. Strip markdown heading markers (`## Foo` → `<b>Foo</b>\n\n`).
|
|
397
|
+
* Headings that survived the markdown→HTML pass (e.g. when the input
|
|
398
|
+
* was already HTML and passed through isLikelyTelegramHtml) would render
|
|
399
|
+
* as ugly `## Foo` plain text. Convert to bold + blank line.
|
|
400
|
+
* 2. Flatten nested bullet indentation: `\n - sub` → `\n· sub`.
|
|
401
|
+
* 3. Collapse 3+ consecutive blank lines to exactly 2.
|
|
402
|
+
* 4. Strip trailing whitespace on each line.
|
|
403
|
+
* 5. Ensure `<` `>` `&` inside `<code>` and `<pre>` blocks are
|
|
404
|
+
* HTML-escaped (idempotent: won't double-escape existing `&` etc.).
|
|
405
|
+
*
|
|
406
|
+
* The function is idempotent: sanitize(sanitize(x)) === sanitize(x).
|
|
407
|
+
* Content inside `<code>` / `<pre>` blocks is excluded from rules 1–4.
|
|
408
|
+
*/
|
|
409
|
+
export function sanitizeForTelegram(text: string): string {
|
|
410
|
+
// ── Phase 1: extract <code> and <pre> blocks so rules 1-4 don't touch them.
|
|
411
|
+
//
|
|
412
|
+
// We capture the full tag with its content so we can round-trip correctly.
|
|
413
|
+
// Placeholders are non-printing control sequences that cannot appear in
|
|
414
|
+
// normal text.
|
|
415
|
+
const CODE_PH = '\x00SANCODE'
|
|
416
|
+
const PRE_PH = '\x00SANPRE'
|
|
417
|
+
const codeSegments: string[] = []
|
|
418
|
+
const preSegments: string[] = []
|
|
419
|
+
|
|
420
|
+
// Extract <pre>...</pre> blocks first (they may contain <code> inside).
|
|
421
|
+
let result = text.replace(/<pre>([\s\S]*?)<\/pre>/gi, (_m, inner: string) => {
|
|
422
|
+
const idx = preSegments.length
|
|
423
|
+
// Rule 5: escape unescaped < > & inside pre blocks.
|
|
424
|
+
preSegments.push(`<pre>${escapeUnescapedEntities(inner)}</pre>`)
|
|
425
|
+
return `${PRE_PH}${idx}\x00`
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Extract standalone <code>...</code> blocks (not nested inside <pre>).
|
|
429
|
+
result = result.replace(/<code([^>]*)>([\s\S]*?)<\/code>/gi, (_m, attrs: string, inner: string) => {
|
|
430
|
+
const idx = codeSegments.length
|
|
431
|
+
// Rule 5: escape unescaped < > & inside code spans.
|
|
432
|
+
codeSegments.push(`<code${attrs}>${escapeUnescapedEntities(inner)}</code>`)
|
|
433
|
+
return `${CODE_PH}${idx}\x00`
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// ── Phase 2: apply text-level rules to the remaining (non-code) content.
|
|
437
|
+
|
|
438
|
+
// Rule 1: strip markdown heading markers that survived markdown→HTML pass.
|
|
439
|
+
// Matches lines starting with one or more `#` followed by a space.
|
|
440
|
+
// Preserves the heading text as bold + trailing blank line.
|
|
441
|
+
result = result.replace(/^(#{1,6}) +(.+?)\s*$/gm, (_m, _hashes, title: string) => {
|
|
442
|
+
return `<b>${title}</b>\n`
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// Rule 2: flatten nested bullet indentation.
|
|
446
|
+
// Matches lines with a tab OR 2+ spaces at the start followed by - or *.
|
|
447
|
+
// A single tab is treated as sufficient indentation (standard 4-space equiv).
|
|
448
|
+
// Converts to a middle-dot bullet so the sub-detail survives as readable text.
|
|
449
|
+
result = result.replace(/^(?:\t+[ \t]*|[ \t]{2,})[*-] /gm, '· ')
|
|
450
|
+
|
|
451
|
+
// Rule 4: strip trailing whitespace on each line.
|
|
452
|
+
result = result.replace(/[ \t]+$/gm, '')
|
|
453
|
+
|
|
454
|
+
// Rule 3: collapse 3+ consecutive blank lines to exactly 2.
|
|
455
|
+
// A "blank line" is a line that contains only optional whitespace (already
|
|
456
|
+
// stripped above, but let's be safe).
|
|
457
|
+
result = result.replace(/(\n[ \t]*){3,}/g, '\n\n')
|
|
458
|
+
|
|
459
|
+
// ── Phase 3: restore placeholders.
|
|
460
|
+
result = result.replace(new RegExp(`${CODE_PH}(\\d+)\x00`, 'g'), (_m, idx) => codeSegments[Number(idx)])
|
|
461
|
+
result = result.replace(new RegExp(`${PRE_PH}(\\d+)\x00`, 'g'), (_m, idx) => preSegments[Number(idx)])
|
|
462
|
+
|
|
463
|
+
return result
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Escape `<`, `>`, and `&` characters that are NOT already part of an HTML
|
|
468
|
+
* entity or tag. Used inside `<code>` and `<pre>` content to correct
|
|
469
|
+
* unescaped characters without double-escaping existing `&`, `<`, etc.
|
|
470
|
+
*
|
|
471
|
+
* Strategy: we walk the string and escape `&` only when it is not the start
|
|
472
|
+
* of a valid entity (`&name;` or `&#digits;` or `&#xhex;`). We always escape
|
|
473
|
+
* bare `<` and `>` because they cannot appear literally inside code content
|
|
474
|
+
* that is correct Telegram HTML.
|
|
475
|
+
*/
|
|
476
|
+
function escapeUnescapedEntities(inner: string): string {
|
|
477
|
+
// Escape bare & first: replace & that is NOT followed by a valid entity
|
|
478
|
+
// pattern. A valid entity is: &[a-zA-Z][a-zA-Z0-9]*; or &#[0-9]+; or &#x[0-9a-fA-F]+;
|
|
479
|
+
let out = inner.replace(/&(?!(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);)/g, '&')
|
|
480
|
+
// Escape bare < and > (they should never appear literally in code content)
|
|
481
|
+
out = out.replace(/</g, '<')
|
|
482
|
+
out = out.replace(/>/g, '>')
|
|
483
|
+
return out
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Repair LLM-side JSON escape bungles.
|
|
488
|
+
*
|
|
489
|
+
* Some MCP clients (and some LLM tool-call generators) occasionally emit a
|
|
490
|
+
* tool-argument string whose whitespace has been double-escaped — real
|
|
491
|
+
* newlines become the two-character sequence `\n`, tabs become `\t`, etc.
|
|
492
|
+
* The message then ships to Telegram intact and the user sees literal
|
|
493
|
+
* `\n\n` in the chat instead of paragraph breaks.
|
|
494
|
+
*
|
|
495
|
+
* Heuristic: if the text contains ZERO real newlines AND has at least one
|
|
496
|
+
* literal `\n`, `\r`, or `\t` escape sequence, the caller almost certainly
|
|
497
|
+
* intended those as real whitespace and the client serializer ate them.
|
|
498
|
+
* Unescape them (also `\\` and `\"`). If the text has any real newline,
|
|
499
|
+
* trust the caller exactly as given and do nothing — legitimate content
|
|
500
|
+
* may contain a literal `\n` inside a shell snippet or regex.
|
|
501
|
+
*
|
|
502
|
+
* This is intentionally narrow: it only fires on the clear bug signature
|
|
503
|
+
* (multi-line-looking content collapsed to one physical line). False
|
|
504
|
+
* positives on a single-line message that legitimately contains `\n` are
|
|
505
|
+
* possible but rare — users writing single-line shell snippets typically
|
|
506
|
+
* wrap them in backticks, and this runs before markdown→HTML so the
|
|
507
|
+
* unescape has no effect on text inside fenced code blocks if it already
|
|
508
|
+
* has real newlines around them.
|
|
509
|
+
*/
|
|
510
|
+
export function repairEscapedWhitespace(text: string): string {
|
|
511
|
+
if (text.includes('\n') || text.includes('\r')) return text
|
|
512
|
+
if (!/\\[nrt"\\]/.test(text)) return text
|
|
513
|
+
// Order matters: protect existing `\\` first so `\\n` stays as `\n`
|
|
514
|
+
// literal and doesn't become a newline.
|
|
515
|
+
const BACKSLASH_PH = '\x00BKSL\x00'
|
|
516
|
+
return text
|
|
517
|
+
.replace(/\\\\/g, BACKSLASH_PH)
|
|
518
|
+
.replace(/\\n/g, '\n')
|
|
519
|
+
.replace(/\\r/g, '\r')
|
|
520
|
+
.replace(/\\t/g, '\t')
|
|
521
|
+
.replace(/\\"/g, '"')
|
|
522
|
+
.replace(new RegExp(BACKSLASH_PH, 'g'), '\\')
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// Smart HTML chunking — preserves open/close tag boundaries
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Split HTML text into chunks that fit within maxLen, preserving tag integrity.
|
|
531
|
+
* At split boundaries, open tags are closed and reopened in the next chunk.
|
|
532
|
+
* Prefers splitting at \n\n, then \n, then spaces.
|
|
533
|
+
*/
|
|
534
|
+
export function splitHtmlChunks(html: string, maxLen = 4000): string[] {
|
|
535
|
+
if (html.length <= maxLen) return [html]
|
|
536
|
+
|
|
537
|
+
const chunks: string[] = []
|
|
538
|
+
let rest = html
|
|
539
|
+
|
|
540
|
+
while (rest.length > 0) {
|
|
541
|
+
if (rest.length <= maxLen) {
|
|
542
|
+
chunks.push(rest)
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Find a good split point
|
|
547
|
+
let cut = maxLen
|
|
548
|
+
const paraIdx = rest.lastIndexOf('\n\n', maxLen)
|
|
549
|
+
const lineIdx = rest.lastIndexOf('\n', maxLen)
|
|
550
|
+
const spaceIdx = rest.lastIndexOf(' ', maxLen)
|
|
551
|
+
|
|
552
|
+
if (paraIdx > maxLen / 3) {
|
|
553
|
+
cut = paraIdx
|
|
554
|
+
} else if (lineIdx > maxLen / 3) {
|
|
555
|
+
cut = lineIdx
|
|
556
|
+
} else if (spaceIdx > 0) {
|
|
557
|
+
cut = spaceIdx
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Defense-in-depth: refuse to split inside an HTML entity (&,
|
|
561
|
+
// <, 💩). If the cut would land mid-entity, back up to
|
|
562
|
+
// before the `&`. Telegram rejects messages with broken entities.
|
|
563
|
+
cut = backOffEntity(rest, cut)
|
|
564
|
+
// Same idea for a bisected tag: if the cut lands inside `<...>` (or
|
|
565
|
+
// between `<` and its closing `>`), back up to before the `<`.
|
|
566
|
+
// Otherwise we'd emit a chunk ending in `<a` or `<a href="..` which
|
|
567
|
+
// Telegram rejects outright.
|
|
568
|
+
cut = backOffOpenTag(rest, cut)
|
|
569
|
+
// Pathological: the tag-back-off retreated to 0 because `rest`
|
|
570
|
+
// begins with a tag and the nearest space we picked landed inside
|
|
571
|
+
// that tag. Fall back to the hard maxLen cut — that position lives
|
|
572
|
+
// in content past the opening tag (since the tag itself is at the
|
|
573
|
+
// start) so it won't bisect anything, and we make forward progress.
|
|
574
|
+
if (cut <= 0) {
|
|
575
|
+
cut = Math.min(maxLen, rest.length)
|
|
576
|
+
cut = backOffOpenTag(rest, cut)
|
|
577
|
+
// If even the maxLen cut bisects a tag, emit the whole remainder
|
|
578
|
+
// as one chunk rather than spin forever. Telegram will reject
|
|
579
|
+
// a 4k+ message before it rejects a split one, but this only
|
|
580
|
+
// fires on genuinely malformed input.
|
|
581
|
+
if (cut <= 0) cut = rest.length
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let segment = rest.slice(0, cut)
|
|
585
|
+
rest = rest.slice(cut).replace(/^\n+/, '')
|
|
586
|
+
|
|
587
|
+
// Track open tags in this segment — we keep the FULL opening tag
|
|
588
|
+
// string (including attributes) so we can reopen `<a href="...">`
|
|
589
|
+
// in the next chunk without dropping the href.
|
|
590
|
+
const openTags = getOpenTags(segment)
|
|
591
|
+
|
|
592
|
+
// Close any open tags at the end of this chunk (by tag name)
|
|
593
|
+
for (let i = openTags.length - 1; i >= 0; i--) {
|
|
594
|
+
segment += `</${openTags[i].name}>`
|
|
595
|
+
}
|
|
596
|
+
chunks.push(segment)
|
|
597
|
+
|
|
598
|
+
// Reopen tags at the start of the next chunk, preserving attrs
|
|
599
|
+
if (rest.length > 0 && openTags.length > 0) {
|
|
600
|
+
const reopenPrefix = openTags.map(t => t.openTag).join('')
|
|
601
|
+
rest = reopenPrefix + rest
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return chunks
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* If `cut` lies inside an HTML entity (a `&...;` sequence), back it up to
|
|
610
|
+
* just before the `&` so the chunk boundary doesn't bisect the entity.
|
|
611
|
+
*/
|
|
612
|
+
function backOffEntity(text: string, cut: number): number {
|
|
613
|
+
if (cut <= 0 || cut >= text.length) return cut
|
|
614
|
+
// Look backward up to 10 chars for an unterminated entity
|
|
615
|
+
const lookback = Math.max(0, cut - 10)
|
|
616
|
+
for (let i = cut - 1; i >= lookback; i--) {
|
|
617
|
+
const ch = text[i]
|
|
618
|
+
if (ch === ';') return cut // entity already closed before cut → safe
|
|
619
|
+
if (ch === '&') {
|
|
620
|
+
const closeIdx = text.indexOf(';', cut)
|
|
621
|
+
if (closeIdx !== -1 && closeIdx - i <= 10) {
|
|
622
|
+
// The entity spans the cut — back up to just before the `&`
|
|
623
|
+
return i
|
|
624
|
+
}
|
|
625
|
+
return cut
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return cut
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* If `cut` lands inside an HTML tag (between `<` and the next `>`), back
|
|
633
|
+
* up to before the `<`. Telegram rejects messages that contain a stray
|
|
634
|
+
* `<` without a matching `>` (e.g. chunk ending `<a href="..`).
|
|
635
|
+
*/
|
|
636
|
+
function backOffOpenTag(text: string, cut: number): number {
|
|
637
|
+
if (cut <= 0 || cut >= text.length) return cut
|
|
638
|
+
// Scan backward for the nearest `<` or `>` before the cut. If we hit
|
|
639
|
+
// `>` first the cut is outside any tag → safe. If we hit `<` first,
|
|
640
|
+
// check whether its closing `>` lies at or after the cut → bisected.
|
|
641
|
+
for (let i = cut - 1; i >= 0; i--) {
|
|
642
|
+
const ch = text[i]
|
|
643
|
+
if (ch === '>') return cut
|
|
644
|
+
if (ch === '<') {
|
|
645
|
+
const closeIdx = text.indexOf('>', i)
|
|
646
|
+
if (closeIdx >= cut) return i
|
|
647
|
+
return cut
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return cut
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** A tag still open at the end of a fragment. */
|
|
654
|
+
interface OpenTag {
|
|
655
|
+
name: string // lowercase tag name, e.g. "a", "tg-spoiler"
|
|
656
|
+
openTag: string // full opening string with attrs, e.g. `<a href="...">`
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Parse an HTML fragment and return the list of tags still open at the end. */
|
|
660
|
+
function getOpenTags(html: string): OpenTag[] {
|
|
661
|
+
const tagStack: OpenTag[] = []
|
|
662
|
+
// Allow hyphens in tag names so `tg-spoiler` and `tg-emoji` parse as a
|
|
663
|
+
// single tag rather than `tg` plus stray text.
|
|
664
|
+
const tagRe = /<(\/?)([a-z][a-z0-9-]*)\b[^>]*>/gi
|
|
665
|
+
let m: RegExpExecArray | null
|
|
666
|
+
while ((m = tagRe.exec(html)) !== null) {
|
|
667
|
+
const full = m[0]
|
|
668
|
+
const isClosing = m[1] === '/'
|
|
669
|
+
const tagName = m[2].toLowerCase()
|
|
670
|
+
if (isClosing) {
|
|
671
|
+
// Closing tag — pop the most recent matching entry off the stack
|
|
672
|
+
for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
673
|
+
if (tagStack[i].name === tagName) {
|
|
674
|
+
tagStack.splice(i, 1)
|
|
675
|
+
break
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} else if (!full.endsWith('/>')) {
|
|
679
|
+
// Opening tag (not self-closing) — remember the full open string
|
|
680
|
+
// so reopen in the next chunk preserves attributes.
|
|
681
|
+
tagStack.push({ name: tagName, openTag: full })
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return tagStack
|
|
685
|
+
}
|