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,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-card probes — live evidential data gathered at gateway startup.
|
|
3
|
+
*
|
|
4
|
+
* Each probe returns a ProbeResult within its timeout budget. All probes
|
|
5
|
+
* are run concurrently via Promise.allSettled; callers supply a 2.5s wall
|
|
6
|
+
* clock budget and let this module own the per-probe 2s guard.
|
|
7
|
+
*
|
|
8
|
+
* Probes are defensive by design: every file read guards ENOENT, every
|
|
9
|
+
* network call is wrapped in a race timeout, every field access uses
|
|
10
|
+
* optional-chaining. A failure in one probe must never surface to the
|
|
11
|
+
* caller as a thrown error — only as ProbeResult{ status:'fail', ... }.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
15
|
+
import { join } from 'path'
|
|
16
|
+
import { execFile as execFileCb } from 'child_process'
|
|
17
|
+
import { promisify } from 'util'
|
|
18
|
+
|
|
19
|
+
import { readQuotaCache, writeQuotaCache } from './quota-cache.js'
|
|
20
|
+
|
|
21
|
+
const execFile = promisify(execFileCb)
|
|
22
|
+
|
|
23
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export type ProbeStatus = 'ok' | 'degraded' | 'fail'
|
|
26
|
+
|
|
27
|
+
export interface ProbeResult {
|
|
28
|
+
status: ProbeStatus
|
|
29
|
+
label: string
|
|
30
|
+
detail: string
|
|
31
|
+
/** True when a 429 caused the probe to skip the live check. Used by
|
|
32
|
+
* writeQuotaCache to select the short RATE_LIMIT_TTL_MS instead of the
|
|
33
|
+
* default 5-min TTL. Keying off this boolean avoids matching on the
|
|
34
|
+
* user-facing detail string, which is a maintenance trap. */
|
|
35
|
+
rateLimited?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const PROBE_TIMEOUT_MS = 2000
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Race a probe against a hard timeout. Returns a fail ProbeResult if the
|
|
44
|
+
* probe doesn't settle within timeoutMs.
|
|
45
|
+
*/
|
|
46
|
+
async function withTimeout<T extends ProbeResult>(
|
|
47
|
+
label: string,
|
|
48
|
+
p: Promise<T>,
|
|
49
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
50
|
+
): Promise<ProbeResult> {
|
|
51
|
+
let timer: ReturnType<typeof setTimeout>
|
|
52
|
+
const timeout = new Promise<ProbeResult>((resolve) => {
|
|
53
|
+
timer = setTimeout(() => resolve({ status: 'fail', label, detail: 'timed out' }), timeoutMs)
|
|
54
|
+
})
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race([p, timeout])
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer!)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatMs(ms: number): string {
|
|
63
|
+
if (ms < 1000) return `${ms}ms`
|
|
64
|
+
const s = ms / 1000
|
|
65
|
+
if (s < 60) return `${s.toFixed(1)}s`
|
|
66
|
+
const m = Math.floor(s / 60)
|
|
67
|
+
const r = Math.round(s % 60)
|
|
68
|
+
return r > 0 ? `${m}m ${r}s` : `${m}m`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatDaysFromNow(expiresAt: number): string {
|
|
72
|
+
const days = Math.round((expiresAt - Date.now()) / 86_400_000)
|
|
73
|
+
if (days < 0) return 'expired'
|
|
74
|
+
return `token ${days}d`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Probe: Account ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
interface ClaudeJson {
|
|
80
|
+
oauthAccount?: {
|
|
81
|
+
emailAddress?: string
|
|
82
|
+
displayName?: string
|
|
83
|
+
billingType?: string
|
|
84
|
+
hasExtraUsageEnabled?: boolean
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface OauthTokenMeta {
|
|
89
|
+
expiresAt?: number
|
|
90
|
+
createdAt?: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mapPlan(billingType?: string, hasExtra?: boolean): string {
|
|
94
|
+
if (!billingType) return 'unknown plan'
|
|
95
|
+
if (billingType === 'stripe_subscription') {
|
|
96
|
+
return hasExtra ? 'Pro+' : 'Pro'
|
|
97
|
+
}
|
|
98
|
+
if (billingType.toLowerCase().includes('max')) return 'Max'
|
|
99
|
+
return billingType
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Threshold below which a still-valid OAuth token is treated as
|
|
104
|
+
* `degraded` so the boot card surfaces it before the user is locked
|
|
105
|
+
* out mid-turn. 7 days is the smallest window that still gives
|
|
106
|
+
* comfortable lead time for a manual reauth in normal use.
|
|
107
|
+
*/
|
|
108
|
+
const TOKEN_EXPIRING_SOON_DAYS = 7
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read account info from the agent's .claude.json.
|
|
112
|
+
* agentDir: e.g. /home/user/.switchroom/agents/clerk
|
|
113
|
+
*/
|
|
114
|
+
export async function probeAccount(agentDir: string): Promise<ProbeResult> {
|
|
115
|
+
return withTimeout('Account', (async (): Promise<ProbeResult> => {
|
|
116
|
+
const claudeDir = join(agentDir, '.claude')
|
|
117
|
+
const claudeJsonPath = join(claudeDir, '.claude.json')
|
|
118
|
+
let cfg: ClaudeJson = {}
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(claudeJsonPath, 'utf8')
|
|
121
|
+
cfg = JSON.parse(raw) as ClaudeJson
|
|
122
|
+
} catch {
|
|
123
|
+
return { status: 'fail', label: 'Account', detail: 'no .claude.json' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const acc = cfg.oauthAccount
|
|
127
|
+
if (!acc?.emailAddress) {
|
|
128
|
+
return { status: 'degraded', label: 'Account', detail: 'not signed in' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const plan = mapPlan(acc.billingType, acc.hasExtraUsageEnabled)
|
|
132
|
+
|
|
133
|
+
// Read token expiry. Status is driven by the days-remaining bucket:
|
|
134
|
+
// < 0 days → fail (already expired — agent is locked out)
|
|
135
|
+
// < 7 days → degraded (surface so the user can reauth in time)
|
|
136
|
+
// ≥ 7 days → ok (no row in the boot card)
|
|
137
|
+
let tokenStr = ''
|
|
138
|
+
let status: ProbeStatus = 'ok'
|
|
139
|
+
for (const candidate of [
|
|
140
|
+
join(claudeDir, '.oauth-token.meta.json'),
|
|
141
|
+
join(claudeDir, 'accounts', 'default', '.oauth-token.meta.json'),
|
|
142
|
+
]) {
|
|
143
|
+
if (existsSync(candidate)) {
|
|
144
|
+
try {
|
|
145
|
+
const meta = JSON.parse(readFileSync(candidate, 'utf8')) as OauthTokenMeta
|
|
146
|
+
if (meta.expiresAt) {
|
|
147
|
+
tokenStr = ' · ' + formatDaysFromNow(meta.expiresAt)
|
|
148
|
+
const daysLeft = Math.round((meta.expiresAt - Date.now()) / 86_400_000)
|
|
149
|
+
if (daysLeft < 0) status = 'fail'
|
|
150
|
+
else if (daysLeft < TOKEN_EXPIRING_SOON_DAYS) status = 'degraded'
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
status,
|
|
159
|
+
label: 'Account',
|
|
160
|
+
detail: `${acc.emailAddress} · ${plan}${tokenStr}`,
|
|
161
|
+
}
|
|
162
|
+
})())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Probe: Agent process ────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function parseSystemctlKv(output: string): Record<string, string> {
|
|
168
|
+
const result: Record<string, string> = {}
|
|
169
|
+
for (const line of output.split('\n')) {
|
|
170
|
+
const eq = line.indexOf('=')
|
|
171
|
+
if (eq > 0) {
|
|
172
|
+
result[line.slice(0, eq).trim()] = line.slice(eq + 1).trim()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatUptime(activeEnterTimestamp: string): string {
|
|
179
|
+
if (!activeEnterTimestamp || activeEnterTimestamp === '0') return ''
|
|
180
|
+
// systemctl outputs like "Thu 2026-04-26 10:15:30 UTC" or epoch microseconds
|
|
181
|
+
let ms: number
|
|
182
|
+
const epoch = Number(activeEnterTimestamp)
|
|
183
|
+
if (!isNaN(epoch) && epoch > 0) {
|
|
184
|
+
ms = Date.now() - Math.round(epoch / 1000)
|
|
185
|
+
} else {
|
|
186
|
+
const d = new Date(activeEnterTimestamp)
|
|
187
|
+
if (isNaN(d.getTime())) return ''
|
|
188
|
+
ms = Date.now() - d.getTime()
|
|
189
|
+
}
|
|
190
|
+
return ms > 0 ? `up ${formatMs(ms)}` : ''
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatMemory(memoryCurrent: string): string {
|
|
194
|
+
const bytes = Number(memoryCurrent)
|
|
195
|
+
if (!isFinite(bytes) || bytes <= 0) return ''
|
|
196
|
+
const mb = Math.round(bytes / 1024 / 1024)
|
|
197
|
+
return `${mb} MB`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* How often to retry after a non-active state during the re-probe loop.
|
|
202
|
+
* Exported for test injection.
|
|
203
|
+
*/
|
|
204
|
+
export const AGENT_RETRY_INTERVAL_MS = 1500
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Maximum additional wait beyond the settle window before committing to
|
|
208
|
+
* whatever the final state is. Exported for test injection.
|
|
209
|
+
*/
|
|
210
|
+
export const AGENT_RETRY_MAX_MS = 12_000
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* How long the boot-card live-agent-status loop keeps polling and editing
|
|
214
|
+
* the card in-place after the initial probe run. The loop exits early as
|
|
215
|
+
* soon as the agent reaches `active`. If the window expires without the
|
|
216
|
+
* agent becoming active, the card commits to whatever state is current.
|
|
217
|
+
*
|
|
218
|
+
* 45 s covers the typical systemd restart cycle (deactivating → inactive →
|
|
219
|
+
* activating → active) even under load, while staying short enough that a
|
|
220
|
+
* genuinely stuck unit (still `inactive` at 45 s) is a real problem.
|
|
221
|
+
* Exported for test injection.
|
|
222
|
+
*/
|
|
223
|
+
export const AGENT_LIVE_WINDOW_MS = 45_000
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* How often the live-watch loop re-polls systemd while waiting for the
|
|
227
|
+
* agent to become active. Exported for test injection.
|
|
228
|
+
*/
|
|
229
|
+
export const AGENT_LIVE_POLL_INTERVAL_MS = 2_000
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* After the live window expires with the agent still not `active`, the
|
|
233
|
+
* generator schedules ONE follow-up re-poll this many ms later. If the
|
|
234
|
+
* agent has reached `active` by then, an updated ✅ ProbeResult is
|
|
235
|
+
* yielded and the boot card edits in place. Otherwise no further yield.
|
|
236
|
+
*
|
|
237
|
+
* Pre-#296 fix the generator returned immediately at window-expiry, so
|
|
238
|
+
* an agent that became active 1-30s after the window stayed visibly
|
|
239
|
+
* 🟡 "service inactive" forever (until the user noticed and asked).
|
|
240
|
+
*
|
|
241
|
+
* 30 s is the recommended-by-issue-author value: long enough to catch
|
|
242
|
+
* the common late-boot scenario (slow disk, claude-cli npm install
|
|
243
|
+
* ticking down), short enough that genuinely stuck units still surface
|
|
244
|
+
* as a real problem within ~75 s total.
|
|
245
|
+
*/
|
|
246
|
+
export const AGENT_LIVE_FOLLOWUP_REPOLL_MS = 30_000
|
|
247
|
+
|
|
248
|
+
type ExecFileResult = { stdout: string; stderr: string }
|
|
249
|
+
type ExecFileFnType = (
|
|
250
|
+
cmd: string,
|
|
251
|
+
args: string[],
|
|
252
|
+
) => Promise<ExecFileResult>
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the "real" agent PID under tmux supervisor by walking the
|
|
256
|
+
* unit's cgroup and picking the heaviest-RSS claude/node process.
|
|
257
|
+
*
|
|
258
|
+
* Returns null on any failure — caller should fall back to MainPID.
|
|
259
|
+
*
|
|
260
|
+
* Mirrors `resolveAgentPid()` in `src/agents/lifecycle.ts` and
|
|
261
|
+
* `agent_main_pid()` in `bin/bridge-watchdog.sh`. Kept duplicated rather
|
|
262
|
+
* than imported because the gateway runs in a separate package and we
|
|
263
|
+
* don't want a cross-package import for a 30-line helper.
|
|
264
|
+
*/
|
|
265
|
+
async function resolveTmuxSupervisorPid(
|
|
266
|
+
agentName: string,
|
|
267
|
+
execFileImpl: ExecFileFnType,
|
|
268
|
+
): Promise<number | null> {
|
|
269
|
+
try {
|
|
270
|
+
const { stdout: cgOut } = await execFileImpl('systemctl', [
|
|
271
|
+
'--user', 'show', `switchroom-${agentName}.service`,
|
|
272
|
+
'-p', 'ControlGroup', '--value',
|
|
273
|
+
])
|
|
274
|
+
const cgroup = cgOut.trim()
|
|
275
|
+
if (!cgroup) return null
|
|
276
|
+
const procsPath = `/sys/fs/cgroup${cgroup}/cgroup.procs`
|
|
277
|
+
if (!existsSync(procsPath)) return null
|
|
278
|
+
const pidsRaw = readFileSync(procsPath, 'utf-8')
|
|
279
|
+
const pids = pidsRaw.split('\n').map(s => s.trim()).filter(Boolean)
|
|
280
|
+
if (pids.length === 0) return null
|
|
281
|
+
|
|
282
|
+
type Candidate = { pid: number; rss: number; comm: string }
|
|
283
|
+
const candidates: Candidate[] = []
|
|
284
|
+
for (const pidStr of pids) {
|
|
285
|
+
const pid = parseInt(pidStr, 10)
|
|
286
|
+
if (!Number.isFinite(pid) || pid <= 0) continue
|
|
287
|
+
let rss = 0
|
|
288
|
+
let comm = ''
|
|
289
|
+
try {
|
|
290
|
+
const status = readFileSync(`/proc/${pid}/status`, 'utf-8')
|
|
291
|
+
const rssLine = status.split('\n').find(l => l.startsWith('VmRSS:'))
|
|
292
|
+
if (rssLine) {
|
|
293
|
+
const m = rssLine.match(/(\d+)/)
|
|
294
|
+
if (m) rss = parseInt(m[1], 10) || 0
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
comm = readFileSync(`/proc/${pid}/comm`, 'utf-8').trim()
|
|
301
|
+
} catch { /* ignore */ }
|
|
302
|
+
candidates.push({ pid, rss, comm })
|
|
303
|
+
}
|
|
304
|
+
if (candidates.length === 0) return null
|
|
305
|
+
|
|
306
|
+
const isAgent = (c: Candidate): boolean => c.comm === 'claude' || c.comm === 'node'
|
|
307
|
+
const isWrapper = (c: Candidate): boolean =>
|
|
308
|
+
c.comm === 'tmux' || c.comm.startsWith('tmux:') ||
|
|
309
|
+
c.comm === 'expect' || c.comm === 'script' ||
|
|
310
|
+
c.comm === 'bash' || c.comm === 'sh'
|
|
311
|
+
|
|
312
|
+
const agentMatches = candidates.filter(isAgent)
|
|
313
|
+
if (agentMatches.length > 0) {
|
|
314
|
+
agentMatches.sort((a, b) => b.rss - a.rss)
|
|
315
|
+
return agentMatches[0].pid
|
|
316
|
+
}
|
|
317
|
+
const nonWrapper = candidates.filter(c => !isWrapper(c))
|
|
318
|
+
if (nonWrapper.length > 0) {
|
|
319
|
+
nonWrapper.sort((a, b) => b.rss - a.rss)
|
|
320
|
+
return nonWrapper[0].pid
|
|
321
|
+
}
|
|
322
|
+
// Candidates enumerated but every one was a wrapper (tmux/expect/
|
|
323
|
+
// script/bash/sh). Emit a breadcrumb mirroring the one in
|
|
324
|
+
// src/agents/lifecycle.ts:resolveAgentPid so journalctl shows the
|
|
325
|
+
// same state on both sides. The boot-window race (zero pids) returns
|
|
326
|
+
// earlier without logging, by design.
|
|
327
|
+
process.stderr.write(
|
|
328
|
+
`[switchroom] resolveTmuxSupervisorPid: cgroup walk found ${candidates.length} processes, no claude match — falling back to MainPID for unit=switchroom-${agentName}.service\n`,
|
|
329
|
+
)
|
|
330
|
+
return null
|
|
331
|
+
} catch {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Query systemctl for the agent service and return a snapshot of its state.
|
|
338
|
+
* Extracted so the re-probe loop can call it multiple times.
|
|
339
|
+
*/
|
|
340
|
+
async function queryAgentState(
|
|
341
|
+
agentName: string,
|
|
342
|
+
execFileImpl: ExecFileFnType,
|
|
343
|
+
): Promise<{
|
|
344
|
+
state: string
|
|
345
|
+
kv: Record<string, string>
|
|
346
|
+
} | { error: string }> {
|
|
347
|
+
let stdout: string
|
|
348
|
+
try {
|
|
349
|
+
const result = await execFileImpl('systemctl', [
|
|
350
|
+
'--user', 'show',
|
|
351
|
+
`switchroom-${agentName}.service`,
|
|
352
|
+
'-p', 'MainPID,ActiveState,MemoryCurrent,ActiveEnterTimestamp',
|
|
353
|
+
])
|
|
354
|
+
stdout = result.stdout
|
|
355
|
+
} catch (err: unknown) {
|
|
356
|
+
return { error: `systemctl failed: ${(err as Error).message ?? String(err)}` }
|
|
357
|
+
}
|
|
358
|
+
const kv = parseSystemctlKv(stdout)
|
|
359
|
+
return { state: kv['ActiveState'] ?? 'unknown', kv }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function probeAgentProcess(
|
|
363
|
+
agentName: string,
|
|
364
|
+
opts: {
|
|
365
|
+
retryIntervalMs?: number
|
|
366
|
+
retryMaxMs?: number
|
|
367
|
+
/** Override for tests — replaces real delays */
|
|
368
|
+
sleepImpl?: (ms: number) => Promise<void>
|
|
369
|
+
/** Override for tests — replaces real execFile calls */
|
|
370
|
+
execFileImpl?: ExecFileFnType
|
|
371
|
+
/** When true, resolve PID via cgroup walk (heaviest claude/node) — under
|
|
372
|
+
* tmux supervisor MainPID is the tmux server (~2MB) which is misleading. */
|
|
373
|
+
tmuxSupervisor?: boolean
|
|
374
|
+
} = {},
|
|
375
|
+
): Promise<ProbeResult> {
|
|
376
|
+
const retryIntervalMs = opts.retryIntervalMs ?? AGENT_RETRY_INTERVAL_MS
|
|
377
|
+
const retryMaxMs = opts.retryMaxMs ?? AGENT_RETRY_MAX_MS
|
|
378
|
+
const sleep = opts.sleepImpl ?? ((ms: number) => new Promise(resolve => setTimeout(resolve, ms)))
|
|
379
|
+
const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
|
|
380
|
+
|
|
381
|
+
return withTimeout('Agent', (async (): Promise<ProbeResult> => {
|
|
382
|
+
const startMs = Date.now()
|
|
383
|
+
|
|
384
|
+
// Re-probe loop: if state is not yet `active`, retry every retryIntervalMs
|
|
385
|
+
// up to retryMaxMs total elapsed. Transients (deactivating, activating,
|
|
386
|
+
// auto-restart) typically resolve within one or two retries.
|
|
387
|
+
// eslint-disable-next-line no-constant-condition
|
|
388
|
+
while (true) {
|
|
389
|
+
const snapshot = await queryAgentState(agentName, execFileFn)
|
|
390
|
+
|
|
391
|
+
if ('error' in snapshot) {
|
|
392
|
+
return { status: 'fail', label: 'Agent', detail: snapshot.error }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { state, kv } = snapshot
|
|
396
|
+
|
|
397
|
+
if (state === 'active') {
|
|
398
|
+
let pid: string = kv['MainPID'] ?? '?'
|
|
399
|
+
if (opts.tmuxSupervisor) {
|
|
400
|
+
const resolved = await resolveTmuxSupervisorPid(agentName, execFileFn)
|
|
401
|
+
if (resolved && resolved > 0) pid = String(resolved)
|
|
402
|
+
}
|
|
403
|
+
const uptime = formatUptime(kv['ActiveEnterTimestamp'] ?? '')
|
|
404
|
+
const mem = formatMemory(kv['MemoryCurrent'] ?? '')
|
|
405
|
+
const parts = [`PID ${pid}`, uptime, mem].filter(Boolean)
|
|
406
|
+
return { status: 'ok', label: 'Agent', detail: parts.join(' · ') }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const elapsedMs = Date.now() - startMs
|
|
410
|
+
if (elapsedMs >= retryMaxMs) {
|
|
411
|
+
// Committed to the current non-active state.
|
|
412
|
+
// `deactivating`, `activating`, and `auto-restart` are unambiguous
|
|
413
|
+
// transients — honest severity is degraded (🟡), not fail (🔴).
|
|
414
|
+
// Any other non-active state (inactive, failed, …) is a hard fail.
|
|
415
|
+
const isTransient =
|
|
416
|
+
state === 'deactivating' ||
|
|
417
|
+
state === 'activating' ||
|
|
418
|
+
state === 'auto-restart'
|
|
419
|
+
const status = isTransient ? 'degraded' : 'fail'
|
|
420
|
+
return { status, label: 'Agent', detail: `service ${state}` }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Still within retry budget — wait and try again.
|
|
424
|
+
await sleep(retryIntervalMs)
|
|
425
|
+
}
|
|
426
|
+
})(), PROBE_TIMEOUT_MS + retryMaxMs) // extend outer timeout to cover full retry budget
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Async generator that watches the agent systemd unit and yields a
|
|
431
|
+
* ProbeResult each time the meaningful state changes, for up to
|
|
432
|
+
* `liveWindowMs` total. Exits early as soon as the unit reaches `active`.
|
|
433
|
+
*
|
|
434
|
+
* Designed for the boot-card live-update loop in `boot-card.ts`: the
|
|
435
|
+
* caller iterates, edits the card on each yielded result, and breaks once
|
|
436
|
+
* it sees `status === 'ok'` or the generator exhausts.
|
|
437
|
+
*
|
|
438
|
+
* Key contract:
|
|
439
|
+
* - First yield is immediate (no initial delay) so the card can show
|
|
440
|
+
* the current state right away.
|
|
441
|
+
* - Subsequent yields happen every `pollIntervalMs`.
|
|
442
|
+
* - `inactive` and `activating` within the window → status `degraded`
|
|
443
|
+
* (🟡 "starting"), not `fail`. Only `failed` or window-expired-`inactive`
|
|
444
|
+
* commits to `fail`.
|
|
445
|
+
* - When the window expires without `active` the generator yields a
|
|
446
|
+
* final committed result and then ends.
|
|
447
|
+
*/
|
|
448
|
+
export async function* watchAgentProcess(
|
|
449
|
+
agentName: string,
|
|
450
|
+
opts: {
|
|
451
|
+
liveWindowMs?: number
|
|
452
|
+
pollIntervalMs?: number
|
|
453
|
+
/**
|
|
454
|
+
* Wait this many ms after the live window expires before doing one
|
|
455
|
+
* follow-up state check. If the agent reached `active` in that
|
|
456
|
+
* window, yield an updated ✅ ProbeResult so the boot card flips
|
|
457
|
+
* from 🟡 "service inactive" to ✅. See #296. Set to 0 to disable.
|
|
458
|
+
*/
|
|
459
|
+
followupRepollMs?: number
|
|
460
|
+
/** Override for tests — replaces real delays */
|
|
461
|
+
sleepImpl?: (ms: number) => Promise<void>
|
|
462
|
+
/** Override for tests — replaces real execFile calls */
|
|
463
|
+
execFileImpl?: ExecFileFnType
|
|
464
|
+
/**
|
|
465
|
+
* Override for tests. Defaults to Date.now. The within-window
|
|
466
|
+
* check uses this; injecting lets tests advance "time" without
|
|
467
|
+
* real sleeps.
|
|
468
|
+
*/
|
|
469
|
+
nowImpl?: () => number
|
|
470
|
+
/** When true, resolve PID via cgroup walk (heaviest claude/node). */
|
|
471
|
+
tmuxSupervisor?: boolean
|
|
472
|
+
} = {},
|
|
473
|
+
): AsyncGenerator<ProbeResult> {
|
|
474
|
+
const liveWindowMs = opts.liveWindowMs ?? AGENT_LIVE_WINDOW_MS
|
|
475
|
+
const pollIntervalMs = opts.pollIntervalMs ?? AGENT_LIVE_POLL_INTERVAL_MS
|
|
476
|
+
const followupRepollMs = opts.followupRepollMs ?? AGENT_LIVE_FOLLOWUP_REPOLL_MS
|
|
477
|
+
const sleep = opts.sleepImpl ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))
|
|
478
|
+
const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
|
|
479
|
+
const now = opts.nowImpl ?? (() => Date.now())
|
|
480
|
+
|
|
481
|
+
const startMs = now()
|
|
482
|
+
let lastYieldedDetail: string | null = null
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Convert a raw systemd state into a ProbeResult suitable for the boot card.
|
|
486
|
+
* Within the live window: inactive, activating, auto-restart, and
|
|
487
|
+
* deactivating are all 🟡 "starting" — we don't know they're stuck yet.
|
|
488
|
+
* Only `failed` is immediately 🔴. Everything else (unknown) is also 🔴.
|
|
489
|
+
*/
|
|
490
|
+
async function toProbeResult(
|
|
491
|
+
state: string,
|
|
492
|
+
kv: Record<string, string>,
|
|
493
|
+
withinWindow: boolean,
|
|
494
|
+
): Promise<ProbeResult> {
|
|
495
|
+
if (state === 'active') {
|
|
496
|
+
let pid: string = kv['MainPID'] ?? '?'
|
|
497
|
+
if (opts.tmuxSupervisor) {
|
|
498
|
+
const resolved = await resolveTmuxSupervisorPid(agentName, execFileFn)
|
|
499
|
+
if (resolved && resolved > 0) pid = String(resolved)
|
|
500
|
+
}
|
|
501
|
+
const uptime = formatUptime(kv['ActiveEnterTimestamp'] ?? '')
|
|
502
|
+
const mem = formatMemory(kv['MemoryCurrent'] ?? '')
|
|
503
|
+
const parts = [`PID ${pid}`, uptime, mem].filter(Boolean)
|
|
504
|
+
return { status: 'ok', label: 'Agent', detail: parts.join(' · ') }
|
|
505
|
+
}
|
|
506
|
+
if (withinWindow) {
|
|
507
|
+
// Treat all non-active states as transient while still within the
|
|
508
|
+
// window. `failed` is the only exception — hard fail even in-window.
|
|
509
|
+
if (state === 'failed') {
|
|
510
|
+
return { status: 'fail', label: 'Agent', detail: 'service failed' }
|
|
511
|
+
}
|
|
512
|
+
return { status: 'degraded', label: 'Agent', detail: 'service starting' }
|
|
513
|
+
}
|
|
514
|
+
// Window expired — commit to the actual state.
|
|
515
|
+
const isTransient =
|
|
516
|
+
state === 'deactivating' ||
|
|
517
|
+
state === 'activating' ||
|
|
518
|
+
state === 'auto-restart' ||
|
|
519
|
+
state === 'inactive'
|
|
520
|
+
const status = isTransient ? 'degraded' : 'fail'
|
|
521
|
+
return { status, label: 'Agent', detail: `service ${state}` }
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
while (true) {
|
|
525
|
+
const elapsedMs = now() - startMs
|
|
526
|
+
const withinWindow = elapsedMs < liveWindowMs
|
|
527
|
+
|
|
528
|
+
const snapshot = await queryAgentState(agentName, execFileFn)
|
|
529
|
+
|
|
530
|
+
if ('error' in snapshot) {
|
|
531
|
+
yield { status: 'fail', label: 'Agent', detail: snapshot.error }
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const result = await toProbeResult(snapshot.state, snapshot.kv, withinWindow)
|
|
536
|
+
|
|
537
|
+
// Only yield when the result detail actually changed — avoids
|
|
538
|
+
// redundant card edits ("service starting" → "service starting").
|
|
539
|
+
if (result.detail !== lastYieldedDetail) {
|
|
540
|
+
lastYieldedDetail = result.detail
|
|
541
|
+
yield result
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Terminal states: active (ok) or genuinely failed.
|
|
545
|
+
if (result.status === 'ok' || (result.status === 'fail' && snapshot.state === 'failed')) {
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// If window expired, we already yielded the final committed result.
|
|
550
|
+
if (!withinWindow) {
|
|
551
|
+
// #296 follow-up: schedule ONE re-poll after the live window so a
|
|
552
|
+
// late-boot transition (active arriving 1-30s after the window) flips
|
|
553
|
+
// the card from 🟡 "service inactive" to ✅ instead of staying stale
|
|
554
|
+
// until the next user-driven event. Skipped when:
|
|
555
|
+
// - followupRepollMs <= 0 (test override / explicit disable)
|
|
556
|
+
// - the final result was already 'ok' (handled by the early-return above)
|
|
557
|
+
// - the final result was 'fail' due to systemd reporting `failed`
|
|
558
|
+
// (also handled above) — anything reaching here is degraded
|
|
559
|
+
if (followupRepollMs <= 0) return
|
|
560
|
+
await sleep(followupRepollMs)
|
|
561
|
+
const followup = await queryAgentState(agentName, execFileFn)
|
|
562
|
+
if ('error' in followup) return
|
|
563
|
+
// Only yield on a state we DIDN'T see before — silently no-op if the
|
|
564
|
+
// agent is still inactive/activating/etc., to avoid card flapping.
|
|
565
|
+
if (followup.state !== 'active') return
|
|
566
|
+
const okResult = await toProbeResult(followup.state, followup.kv, false)
|
|
567
|
+
if (okResult.detail !== lastYieldedDetail) {
|
|
568
|
+
yield okResult
|
|
569
|
+
}
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await sleep(pollIntervalMs)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─── Probe: Gateway ──────────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
export interface GatewayRuntimeInfo {
|
|
580
|
+
pid: number
|
|
581
|
+
startedAtMs: number
|
|
582
|
+
lastPollMs?: number
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export async function probeGateway(info: GatewayRuntimeInfo): Promise<ProbeResult> {
|
|
586
|
+
return withTimeout('Gateway', (async (): Promise<ProbeResult> => {
|
|
587
|
+
const uptime = formatMs(Date.now() - info.startedAtMs)
|
|
588
|
+
const lastPoll = info.lastPollMs != null
|
|
589
|
+
? `last poll ${formatMs(Date.now() - info.lastPollMs)} ago`
|
|
590
|
+
: ''
|
|
591
|
+
const parts = [`PID ${info.pid}`, `up ${uptime}`, lastPoll].filter(Boolean)
|
|
592
|
+
return { status: 'ok', label: 'Gateway', detail: parts.join(' · ') }
|
|
593
|
+
})())
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── Probe: Quota ─────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
const QUOTA_DEBUG_FILE = 'quota-debug.json'
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Attempt to read quota info via the /api/oauth/usage endpoint.
|
|
602
|
+
* The response schema is undocumented — we probe defensively and
|
|
603
|
+
* save the raw response to a debug file on first 2xx hit.
|
|
604
|
+
*
|
|
605
|
+
* Result is cached for 5 min in `~/.switchroom/quota-cache.json` and
|
|
606
|
+
* shared across all agents. Without the cache, every gateway boot +
|
|
607
|
+
* bridge-reconnect across 4 agents hits the endpoint, triggering 429s
|
|
608
|
+
* that surface as 🟡 "rate limited" in the boot card. See `quota-cache.ts`.
|
|
609
|
+
*
|
|
610
|
+
* Tests can override the cache path via SWITCHROOM_QUOTA_CACHE_PATH.
|
|
611
|
+
*/
|
|
612
|
+
export async function probeQuota(
|
|
613
|
+
claudeConfigDir: string,
|
|
614
|
+
agentDir: string,
|
|
615
|
+
fetchImpl: typeof fetch = fetch,
|
|
616
|
+
): Promise<ProbeResult> {
|
|
617
|
+
return withTimeout('Quota', (async (): Promise<ProbeResult> => {
|
|
618
|
+
// Cache hit → return early (avoids the rate-limit cascade)
|
|
619
|
+
const cached = readQuotaCache()
|
|
620
|
+
if (cached) {
|
|
621
|
+
return cached
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Read token
|
|
625
|
+
let token: string | null = null
|
|
626
|
+
for (const candidate of [
|
|
627
|
+
join(claudeConfigDir, '.oauth-token'),
|
|
628
|
+
join(claudeConfigDir, 'accounts', 'default', '.oauth-token'),
|
|
629
|
+
]) {
|
|
630
|
+
if (existsSync(candidate)) {
|
|
631
|
+
try {
|
|
632
|
+
const raw = readFileSync(candidate, 'utf8').trim()
|
|
633
|
+
if (raw.length > 0) { token = raw; break }
|
|
634
|
+
} catch {}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!token) {
|
|
638
|
+
return { status: 'degraded', label: 'Quota', detail: 'no OAuth token' }
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let resp: Response
|
|
642
|
+
try {
|
|
643
|
+
const controller = new AbortController()
|
|
644
|
+
const t = setTimeout(() => controller.abort(), 1800)
|
|
645
|
+
resp = await fetchImpl('https://api.anthropic.com/api/oauth/usage', {
|
|
646
|
+
method: 'GET',
|
|
647
|
+
headers: {
|
|
648
|
+
'Authorization': `Bearer ${token}`,
|
|
649
|
+
'Accept': 'application/json',
|
|
650
|
+
'anthropic-version': '2023-06-01',
|
|
651
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
652
|
+
'User-Agent': 'switchroom-boot/0.1',
|
|
653
|
+
},
|
|
654
|
+
signal: controller.signal,
|
|
655
|
+
})
|
|
656
|
+
clearTimeout(t)
|
|
657
|
+
} catch (err: unknown) {
|
|
658
|
+
return { status: 'fail', label: 'Quota', detail: `request failed: ${(err as Error).message ?? String(err)}` }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (resp.status === 429) {
|
|
662
|
+
// A 429 from /api/oauth/usage means the endpoint is rate-limiting our
|
|
663
|
+
// probe calls — it does NOT mean the user is out of quota. Conflating
|
|
664
|
+
// the two is the root cause of the false 🟡 "rate limited" alarm
|
|
665
|
+
// reported in #210. Return ok-with-note and cache it for 30 s so
|
|
666
|
+
// simultaneous fleet restarts read the cached result instead of piling
|
|
667
|
+
// up on the same endpoint (see quota-cache.ts: RATE_LIMIT_TTL_MS).
|
|
668
|
+
//
|
|
669
|
+
// We assume 429 from /api/oauth/usage signals endpoint rate-limiting,
|
|
670
|
+
// not quota exhaustion. Anthropic uses 403 / 200-with-flag for the
|
|
671
|
+
// latter today; if that changes, revisit this 🟢 mapping.
|
|
672
|
+
const rateLimitResult: ProbeResult = {
|
|
673
|
+
status: 'ok',
|
|
674
|
+
label: 'Quota',
|
|
675
|
+
detail: 'quota check skipped: rate limited',
|
|
676
|
+
rateLimited: true,
|
|
677
|
+
}
|
|
678
|
+
writeQuotaCache(rateLimitResult)
|
|
679
|
+
return rateLimitResult
|
|
680
|
+
}
|
|
681
|
+
if (!resp.ok) {
|
|
682
|
+
return { status: 'degraded', label: 'Quota', detail: `HTTP ${resp.status}` }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let body: unknown
|
|
686
|
+
try {
|
|
687
|
+
body = await resp.json()
|
|
688
|
+
} catch {
|
|
689
|
+
return { status: 'degraded', label: 'Quota', detail: 'invalid JSON response' }
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Defensive schema discovery — save raw response for tightening
|
|
693
|
+
const debugPath = join(agentDir, 'telegram', QUOTA_DEBUG_FILE)
|
|
694
|
+
try {
|
|
695
|
+
// Redact token/UUID fields before saving
|
|
696
|
+
const redacted = JSON.parse(JSON.stringify(body, (k, v) => {
|
|
697
|
+
if (/token|uuid|id|key/i.test(k) && typeof v === 'string' && v.length > 10) return '[REDACTED]'
|
|
698
|
+
return v
|
|
699
|
+
}))
|
|
700
|
+
mkdirSync(join(agentDir, 'telegram'), { recursive: true })
|
|
701
|
+
writeFileSync(debugPath, JSON.stringify({ capturedAt: new Date().toISOString(), body: redacted }, null, 2))
|
|
702
|
+
} catch {}
|
|
703
|
+
|
|
704
|
+
// Try common field paths — schema not yet locked
|
|
705
|
+
const b = body as Record<string, unknown>
|
|
706
|
+
const sessionQuota =
|
|
707
|
+
(b?.['data'] as Record<string, unknown> | undefined)?.['session_quota'] ??
|
|
708
|
+
b?.['session_quota'] ??
|
|
709
|
+
(b?.['quota'] as Record<string, unknown> | undefined)?.['session'] ??
|
|
710
|
+
(b?.['usage'] as Record<string, unknown> | undefined)?.['session']
|
|
711
|
+
|
|
712
|
+
if (!sessionQuota) {
|
|
713
|
+
return {
|
|
714
|
+
status: 'degraded',
|
|
715
|
+
label: 'Quota',
|
|
716
|
+
detail: `schema unknown — first call captured (debug: ${debugPath})`,
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const sq = sessionQuota as Record<string, unknown>
|
|
721
|
+
const parts: string[] = []
|
|
722
|
+
if (typeof sq['sonnet_used_pct'] === 'number') parts.push(`Sonnet ${Math.round(sq['sonnet_used_pct'] as number)}%`)
|
|
723
|
+
if (typeof sq['opus_used_pct'] === 'number') parts.push(`Opus ${Math.round(sq['opus_used_pct'] as number)}%`)
|
|
724
|
+
if (typeof sq['used_pct'] === 'number') parts.push(`${Math.round(sq['used_pct'] as number)}% used`)
|
|
725
|
+
if (typeof sq['resets_in_sec'] === 'number') {
|
|
726
|
+
const sec = sq['resets_in_sec'] as number
|
|
727
|
+
const h = Math.floor(sec / 3600)
|
|
728
|
+
const m = Math.round((sec % 3600) / 60)
|
|
729
|
+
parts.push(`resets in ${h}h ${m}m`)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (parts.length === 0) {
|
|
733
|
+
return { status: 'degraded', label: 'Quota', detail: 'schema unknown — saving raw response' }
|
|
734
|
+
}
|
|
735
|
+
const result: ProbeResult = { status: 'ok', label: 'Quota', detail: parts.join(' · ') }
|
|
736
|
+
writeQuotaCache(result)
|
|
737
|
+
return result
|
|
738
|
+
})())
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ─── Probe: Hindsight ────────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
export async function probeHindsight(
|
|
744
|
+
bankName?: string,
|
|
745
|
+
fetchImpl: typeof fetch = fetch,
|
|
746
|
+
): Promise<ProbeResult> {
|
|
747
|
+
return withTimeout('Hindsight', (async (): Promise<ProbeResult> => {
|
|
748
|
+
const base = 'http://127.0.0.1:18888'
|
|
749
|
+
let resp: Response | null = null
|
|
750
|
+
|
|
751
|
+
for (const path of ['/health', '/']) {
|
|
752
|
+
try {
|
|
753
|
+
const controller = new AbortController()
|
|
754
|
+
const t = setTimeout(() => controller.abort(), 1800)
|
|
755
|
+
resp = await fetchImpl(`${base}${path}`, { signal: controller.signal })
|
|
756
|
+
clearTimeout(t)
|
|
757
|
+
if (resp.status !== 404) break
|
|
758
|
+
} catch {}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!resp || !resp.ok) {
|
|
762
|
+
return { status: 'fail', label: 'Hindsight', detail: 'unreachable' }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const bankSuffix = bankName ? ` · bank=${bankName}` : ''
|
|
766
|
+
return { status: 'ok', label: 'Hindsight', detail: `reachable${bankSuffix}` }
|
|
767
|
+
})())
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ─── Probe: Cron timers ──────────────────────────────────────────────────────
|
|
771
|
+
|
|
772
|
+
interface SystemctlTimerEntry {
|
|
773
|
+
next?: string
|
|
774
|
+
left?: string
|
|
775
|
+
last?: string
|
|
776
|
+
unit?: string
|
|
777
|
+
activates?: string
|
|
778
|
+
passed?: string
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function parseTimerLeft(left: string | undefined): number | null {
|
|
782
|
+
if (!left) return null
|
|
783
|
+
// format: "1h 32min left" or "2min 5s left" or similar
|
|
784
|
+
let ms = 0
|
|
785
|
+
const h = left.match(/(\d+)h/)
|
|
786
|
+
const m = left.match(/(\d+)min/)
|
|
787
|
+
const s = left.match(/(\d+)s/)
|
|
788
|
+
if (h) ms += Number(h[1]) * 3600_000
|
|
789
|
+
if (m) ms += Number(m[1]) * 60_000
|
|
790
|
+
if (s) ms += Number(s[1]) * 1000
|
|
791
|
+
return ms > 0 ? ms : null
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export async function probeCronTimers(
|
|
795
|
+
agentName: string,
|
|
796
|
+
opts: { execFileImpl?: ExecFileFnType } = {},
|
|
797
|
+
): Promise<ProbeResult> {
|
|
798
|
+
const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
|
|
799
|
+
return withTimeout('Crons', (async (): Promise<ProbeResult> => {
|
|
800
|
+
let stdout: string
|
|
801
|
+
try {
|
|
802
|
+
const result = await execFileFn('systemctl', [
|
|
803
|
+
'--user', 'list-timers',
|
|
804
|
+
`switchroom-${agentName}-cron-*`,
|
|
805
|
+
'--output=json',
|
|
806
|
+
'--all',
|
|
807
|
+
])
|
|
808
|
+
stdout = result.stdout.trim()
|
|
809
|
+
} catch (err: unknown) {
|
|
810
|
+
// systemctl exits non-zero when no units match
|
|
811
|
+
const msg = (err as NodeJS.ErrnoException)?.message ?? String(err)
|
|
812
|
+
// child_process exec errors have `code` typed as string in
|
|
813
|
+
// NodeJS.ErrnoException, but at runtime it's numeric for shell
|
|
814
|
+
// exit codes. Stringify to avoid the type-system mismatch and
|
|
815
|
+
// the comparison "looks unintentional" warning.
|
|
816
|
+
if (msg.includes('No timers found') || String((err as NodeJS.ErrnoException)?.code) === '1') {
|
|
817
|
+
return { status: 'ok', label: 'Crons', detail: '0 timers' }
|
|
818
|
+
}
|
|
819
|
+
return { status: 'fail', label: 'Crons', detail: `systemctl failed: ${msg}` }
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!stdout || stdout === '[]' || stdout.length === 0) {
|
|
823
|
+
return { status: 'ok', label: 'Crons', detail: '0 timers' }
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let timers: SystemctlTimerEntry[] = []
|
|
827
|
+
try {
|
|
828
|
+
timers = JSON.parse(stdout) as SystemctlTimerEntry[]
|
|
829
|
+
} catch {
|
|
830
|
+
// Fall back to line-count if JSON failed
|
|
831
|
+
const count = stdout.split('\n').filter(l => l.includes('cron')).length
|
|
832
|
+
return { status: 'ok', label: 'Crons', detail: `${count} timers` }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (!Array.isArray(timers) || timers.length === 0) {
|
|
836
|
+
return { status: 'ok', label: 'Crons', detail: '0 timers' }
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Find the timer that fires soonest
|
|
840
|
+
let earliest: { name: string; leftMs: number } | null = null
|
|
841
|
+
for (const t of timers) {
|
|
842
|
+
const ms = parseTimerLeft(t.left)
|
|
843
|
+
const name = (t.unit ?? t.activates ?? '').replace(/^switchroom-[^-]+-cron-/, '').replace(/\.timer$/, '')
|
|
844
|
+
if (ms != null && (earliest == null || ms < earliest.leftMs)) {
|
|
845
|
+
earliest = { name, leftMs: ms }
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const count = timers.length
|
|
850
|
+
if (!earliest) {
|
|
851
|
+
return { status: 'ok', label: 'Crons', detail: `${count} timers` }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const h = Math.floor(earliest.leftMs / 3600_000)
|
|
855
|
+
const m = Math.round((earliest.leftMs % 3600_000) / 60_000)
|
|
856
|
+
const timeStr = h > 0 ? `${h}h ${m}m` : `${m}m`
|
|
857
|
+
return {
|
|
858
|
+
status: 'ok',
|
|
859
|
+
label: 'Crons',
|
|
860
|
+
detail: `${count} timers · next: ${earliest.name} in ${timeStr}`,
|
|
861
|
+
}
|
|
862
|
+
})())
|
|
863
|
+
}
|