[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\n# Top-most EditorConfig file\nroot = true\n\n# Match and apply these rules for all file\n# types you open in your code editor\n[*]\n# Unix-style newlines\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pull Request becomes stale\ndaysUntilStale: 90\n# Number of days of inactivity before a stale Issue or Pull Request is closed\ndaysUntilClose: 7\n# Issues or Pull Requests with these labels will never be considered stale\nexemptLabels:\n  - pinned\n  - security\n# Label to use when marking as stale\nstaleLabel: stale\n# Comment to post when marking as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has [not had\n  recent activity](https://github.com/github/hubot/blob/main/CONTRIBUTING.md#stale-issue-and-pull-request-policy).\n  It will be closed if no further activity occurs. Thank you for your contributions.\n# Comment to post when removing the stale label. Set to `false` to disable\nunmarkComment: false\n# Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable\ncloseComment: false\n# Limit to only `issues` or `pulls`\n# only: issues\n"
  },
  {
    "path": ".github/workflows/nodejs-macos.yml",
    "content": "name: Node.js (macOS) CI\npermissions:\n  contents: read\n  issues: read\n\non:\n  push:\n    branches: [ \"main\" ]\n  schedule:\n    - cron:  '5 4 * * 0'\n\njobs:\n  npm-test:\n\n    runs-on: macos-latest\n\n    strategy:\n      matrix:\n        node-version: [23.x]\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npm ci\n    - run: npm test --experimental-strip-types\n"
  },
  {
    "path": ".github/workflows/nodejs-ubuntu.yml",
    "content": "name: Node.js (Ubuntu) CI\npermissions:\n  contents: read\n  issues: read\n\non:\n  push:\n    branches: [ \"main\" ]\n  schedule:\n    - cron:  '5 4 * * 0'\n\njobs:\n  npm-test:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [23.x, latest]\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Install expect\n      run: sudo apt-get install expect\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npm ci\n    - run: npm test --experimental-strip-types\n    - run: npm run test:e2e\n"
  },
  {
    "path": ".github/workflows/nodejs-windows.yml",
    "content": "name: Node.js (Windows) CI\npermissions:\n  contents: read\n  issues: read\non:\n  push:\n    branches:\n      - main\n  schedule:\n    - cron: '5 4 * * 0'\njobs:\n  npm-test:\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        node-version: [23.x]\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n    - run: npm ci\n    - name: Run Tests\n      env:\n        HUBOT_LOG_LEVEL: debug\n      run: npm test --experimental-strip-types\n"
  },
  {
    "path": ".github/workflows/pipeline.yml",
    "content": "name: Build and release pipeline\npermissions:\n  contents: write\n  issues: write\n  pull-requests: write\n  id-token: write\non:\n  push:\n    branches:\n      - main\n      - next\n  pull_request:\n    branches:\n      - main\n      - next\njobs:\n  build:\n    name: Build and Verify\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version:\n          - 20.x\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n      - name: Install dependencies\n        run: npm ci\n      - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies\n        run: npm audit signatures\n  test:\n    name: Tests\n    runs-on: ubuntu-latest\n    needs: build\n    strategy:\n      matrix:\n        node-version:\n          - 24.x\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n      - run: npm ci\n      - run: npm test -- --experimental-strip-types\n  e2etest:\n    name: E2E Test\n    needs:\n      - build\n      - test\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version:\n          - 20.x\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Install expect\n        run: sudo apt-get install expect\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n      - run: npm ci\n      - run: npm run test:e2e\n  release:\n    name: Release\n    if: github.ref == 'refs/heads/main' && success()\n    needs: [build, test, e2etest]\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version:\n          - 22.14.0\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n      - name: Semantic Release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n        run: npx semantic-release\n"
  },
  {
    "path": ".github/workflows/sfab-gh-pages.yml",
    "content": "# Sample workflow for building and deploying a sfab site to GitHub Pages\nname: Deploy sfab with GitHub Pages dependencies preinstalled\n\non:\n  # Runs on pushes targeting the default branch\n  push:\n    branches: [\"main\"]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: false\n\njobs:\n  # Build job\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup Pages\n        uses: actions/configure-pages@v4\n      - name: Use Node.js and Build with sfab\n        uses: actions/setup-node@v4\n        with:\n          node-version: latest\n      - run: npm run build\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n\n  # Deployment job\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.hubot_history\n.node-version\n.nyc_output/\nnpm-debug.log\ncoverage/\n_site\n.env\nusers.md\n.data"
  },
  {
    "path": ".npmignore",
    "content": ".editorconfig\n.github\n.hubot_history\nbin/e2e-test.sh\ntest\ndocs\nexamples\nscript\nwww\n_site\n*.tgz\nconfiguration\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment include:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]\n\n[homepage]: http://contributor-covenant.org\n[version]: http://contributor-covenant.org/version/1/4/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nContributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md).\n\nEveryone is welcome to contribute to Hubot. Contributing doesn’t just mean submitting pull requests—there are many different ways for you to get involved, including answering questions, reporting or triaging [issues](https://github.com/github/hubot/issues), and participating in using Hubot.\n\nNo matter how you want to get involved, we ask that you first learn what’s expected of anyone who participates in the project by reading the [Contributor Covenant Code of Conduct](http://contributor-covenant.org). By participating, you are expected to uphold this code.\n\nWe love pull requests. Here's a quick guide:\n\n1. If you're adding a new feature or changing user-facing APIs, check out the [Hubot Evolution](https://github.com/hubotio/evolution) process.\n1. Check for [existing issues](https://github.com/github/hubot/issues) for duplicates and confirm that it hasn't been fixed already in the [main branch](https://github.com/github/hubot/commits/main)\n1. Fork the repo, and clone it locally\n1. `npm link` to make your cloned repo available to npm\n1. Follow [Getting Started](docs/index.md) to generate a testbot\n1. `npm link hubot` in your newly created bot to use your hubot fork\n1. Create a new branch for your contribution\n1. Add [tests](test/) (run with `npm test`)\n1. Push to your fork and submit a pull request\n\nAt this point you're waiting on us. We like to at least comment on, if not\naccept, pull requests within a few days. We may suggest some changes or improvements or alternatives.\n\nSome things that will increase the chance that your pull request is accepted:\n\n* Make sure the tests pass\n* Update the documentation: code comments, example code, guides. Basically,\n  update everything affected by your contribution.\n* Include any information that would be relevant to reproducing bugs, use cases for new features, etc.\n\n* Discuss the impact on existing [hubot installs](docs/index.md), [hubot adapters](docs/adapters.md), and [hubot scripts](docs/scripting.md) (e.g. backwards compatibility)\n  * If the change does break compatibility, how can it be updated to become backwards compatible, while directing users to the new way of doing things?\n* Your commits are associated with your GitHub user: https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/\n* Make pull requests against a feature branch,\n* Follow our commit message conventions:\n  * use imperative, present tense: “change” not “changed” nor “changes”\n  * Commit test files with `test: …` or `test(scope): …` prefix.\n  * Commit bug fixes with `fix: …` or `fix(scope): …` prefix\n  * Commit features with `feat: …` or `feat(scope): …` prefix\n  * Commit breaking changes by adding `BREAKING CHANGE:` in the commit body.\n    The commit subject does not matter. A commit can have multiple `BREAKING CHANGE:`\n    sections\n  * Commit changes to `package.json`, `.gitignore` and other meta files with\n  `chore(filenamewithout.ext): …`\n  * Commit changes to README files or comments with `docs(README): …`\n  * Cody style changes with `style: standard`\n  * see [Angular’s Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)\n    for a full list of recommendations.\n\n# Stale issue and pull request policy\n\nIssues and pull requests have a shelf life and sometimes they are no longer relevant. All issues and pull requests that have not had any activity for 90 days will be marked as `stale`. Simply leave a comment with information about why it may still be relevant to keep it open. If no activity occurs in the next 7 days, it will be automatically closed.\n\nThe goal of this process is to keep the list of open issues and pull requests focused on work that is actionable and important for the maintainers and the community.\n\n# Pull Request Reviews & releasing\n\nReleasing `hubot` is fully automated using [semantic-release](https://github.com/semantic-release/semantic-release). Once merged into the `main` branch, `semantic-release` will automatically release a new version based on the commit messages of the pull request. For it to work correctly, make sure that the correct commit message conventions have been used. The ones relevant are\n\n* `fix: …` will bump the fix version, e.g. 1.2.3 → 1.2.4\n* `feat: …` will bump the feature version, e.g. 1.2.3 → 1.3.0\n* `BREAKING CHANGE: …` in the commit body will bump the breaking change version, e.g. 1.2.3 → 2.0.0\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright (c) 2011-2024 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![Pipeline Status](https://github.com/hubotio/hubot/actions/workflows/pipeline.yml/badge.svg)\n\n![Build Status: MacOS](https://github.com/hubotio/hubot/actions/workflows/nodejs-macos.yml/badge.svg)\n![Build Status: Ubuntu](https://github.com/hubotio/hubot/actions/workflows/nodejs-ubuntu.yml/badge.svg)\n![Build Status: Window](https://github.com/hubotio/hubot/actions/workflows/nodejs-windows.yml/badge.svg)\n\n# Hubot\n\n**Note: v10.0.4 accidentally contains the removal of CoffeeScript; v10.0.5 puts it back in**\n**Note: v11 removes CoffeeScript and converts this codebase to ESM**\n\nHubot is a framework to build chat bots, modeled after GitHub's Campfire bot of the same name, hubot.\nHe's pretty cool. He's [extendable with scripts](https://hubotio.github.io/hubot/docs#scripts) and can work\non [many different chat services](https://hubotio.github.io/hubot/adapters.html).\n\nThis repository provides a library that's distributed by `npm` that you\nuse for building your own bots.  See the [documentation](https://hubotio.github.io/hubot/docs.html)\nfor details on getting up and running with your very own robot friend.\n\nIn most cases, you'll probably never have to hack on this repo directly if you\nare building your own bot. But if you do, check out [CONTRIBUTING.md](CONTRIBUTING.md)\n\n# Create your own Hubot instance\n\nThis will create a directory called `myhubot` in the current working directory.\n\n```sh\nnpx hubot --create myhubot --adapter @hubot-friends/hubot-slack\nnpx hubot --create myhubot --adapter @hubot-friends/hubot-discord\nnpx hubot --create myhubot --adapter @hubot-friends/hubot-ms-teams\nnpx hubot --create myhubot --adapter @hubot-friends/hubot-irc\n```\n\nReview `scripts/example.mjs`. Create more scripts in the `scripts` folder.\n\n## Command bus (robot.commands)\n\nHubot includes a deterministic command subsystem for slash-style commands. It is safe by default and does not interfere with legacy `hear` and `respond` listeners.\n\n### Basic Command Registration\n\n```mjs\nexport default (robot) => {\n\trobot.commands.register({\n\t\tid: 'tickets.create',\n\t\tdescription: 'Create a ticket',\n\t\taliases: ['ticket new', 'new ticket'],\n\t\targs: {\n\t\t\ttitle: { type: 'string', required: true },\n\t\t\tpriority: { type: 'enum', values: ['low', 'medium', 'high'], default: 'medium' }\n\t\t},\n\t\tsideEffects: ['creates external ticket'],\n\t\thandler: async (ctx) => {\n\t\t\treturn `Created ticket: ${ctx.args.title}`\n\t\t}\n\t})\n}\n```\n\nInvoke with addressing the bot:\n\n- `@hubot tickets.create --title \"VPN down\" --priority high`\n- `@hubot tickets.create title:\"VPN down\" priority:high`\n\nCommands that declare side effects will require confirmation before execution.\n\nThe user is asked to confirm. They do so like so:\n```sh\n@hubot yes\n@hubot no\n@hubot cancel\n```\n\nAliases are for discovery and search only. They do not execute commands or create proposals. They are intent utterances.\n\n### Built-in Help Command\n\nHubot automatically registers a `help` command that provides command discovery and documentation:\n\n```\n@hubot help                          # List all commands\n@hubot help tickets                  # Filter commands by prefix\n@hubot help search \"create ticket\"   # Search by keyword, alias, description, or example\n```\n\n### Search for Commands\n\n```mjs\nconst results = robot.commands.search('ticket new')\n// [{ id: 'tickets.create', score: 100, matchedOn: 'alias' }, ...]\n```\n\n### Custom Type Resolvers\n\nExtend validation with custom argument types:\n\n```mjs\nexport default (robot) => {\n\t// Register custom type resolver\n\trobot.commands.registerTypeResolver('project_id', async (value, schema, context) => {\n\t\tif (!value.startsWith('PRJ-')) {\n\t\t\tthrow new Error('must start with PRJ-')\n\t\t}\n\t\treturn value.toUpperCase()\n\t})\n\n\t// Use it in a command\n\trobot.commands.register({\n\t\tid: 'projects.deploy',\n\t\tdescription: 'Deploy a project',\n\t\targs: {\n\t\t\tprojectId: { type: 'project_id', required: true }\n\t\t},\n\t\thandler: async (ctx) => {\n\t\t\treturn `Deploying ${ctx.args.projectId}`\n\t\t}\n\t})\n}\n```\n\n### Configuration Options\n\nWhen creating a CommandBus instance, you can configure:\n\n- `prefix` - Command prefix (default: '')\n- `proposalTTL` - Timeout for pending confirmations in milliseconds (default: 300000 = 5 minutes)\n- `logPath` - Path to NDJSON event log file (default: `.data/commands-events.ndjson`)\n- `disableLogging` - Disable event logging to disk (default: true - logging is disabled by default)\n- `permissionProvider` - Custom permission checking handler (optional)\n\n### Permissions\n\nControl who can execute commands using room-based and role-based permissions.\n\n#### Room-Based Permissions\n\nRestrict command execution to specific chat rooms:\n\n```mjs\nrobot.commands.register({\n\tid: 'sensitive.action',\n\tdescription: 'Admin-only action',\n\tpermissions: {\n\t\trooms: ['#admin', '#ops']  // Only allowed in these rooms\n\t},\n\thandler: async (ctx) => {\n\t\treturn 'Action executed!'\n\t}\n})\n```\n\nUsers in other rooms get: `Permission denied: command not allowed in this room`\n\n#### Role-Based Permissions\n\nRestrict command execution to users with specific roles:\n\n```mjs\nrobot.commands.register({\n\tid: 'deploy.production',\n\tdescription: 'Deploy to production',\n\tpermissions: {\n\t\troles: ['admin', 'devops']  // Only users with these roles\n\t},\n\thandler: async (ctx) => {\n\t\treturn 'Deploying...'\n\t}\n})\n```\n\nTo enable role checking, provide a `permissionProvider` when creating CommandBus:\n\n```mjs\nconst commandBus = new CommandBus(robot, {\n\tpermissionProvider: {\n\t\thasRole: async (user, requiredRoles, context) => {\n\t\t\t// Custom logic to check if user has any of the required roles\n\t\t\tconst userRoles = await fetchUserRoles(user.id)\n\t\t\treturn requiredRoles.some(role => userRoles.includes(role))\n\t\t}\n\t}\n})\n```\n\nWithout a permission provider, role-based permissions are ignored (allow by default). Room-based permissions are always enforced.\n\n## License\n\nSee the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).\n\n# Hubot History\n\n[Say hello to Hubot](https://github.blog/2011-10-25-say-hello-to-hubot/)\n\n[Cartoon with Hubot](https://www.youtube.com/watch?v=vq2jYFZVMDA&t=129s)\n\n[The Most Important Startup's Hardest Worker Isn't a Person](https://www.wired.com/2015/10/the-most-important-startups-hardest-worker-isnt-a-person/)\n\n[The Story of Hubot](https://www.youtube.com/watch?v=Je4TjjtFDNU)\n\n[Hubot by Hubotics](https://www.theoldrobots.com/hubot.html)\n\n[Automating Inefficiencies](https://zachholman.com/2011/01/automating-inefficiencies/)\n\n[Getting Started with Hubot](https://www.youtube.com/watch?v=A7fh6RIzGrw)"
  },
  {
    "path": "bin/Hubot.mjs",
    "content": "'use strict'\n\nimport fs from 'node:fs'\nimport { resolve as pathResolve } from 'node:path'\nimport OptParse from '../src/OptParse.mjs'\nimport Hubot from '../index.mjs'\nimport create from '../src/GenHubot.mjs'\n\nconst switches = [\n  ['-a', '--adapter HUBOT_ADAPTER', 'The Adapter to use, e.g. \"Shell\" (to load the default hubot Shell adapter)'],\n  ['-f', '--file HUBOT_FILE', 'Path to adapter file, e.g. \"./adapters/CustomAdapter.mjs\"'],\n  ['-c', '--create HUBOT_CREATE', 'Create a deployable hubot'],\n  ['-d', '--disable-httpd HUBOT_HTTPD', 'Disable the HTTP server'],\n  ['-h', '--help', 'Display the help information'],\n  ['-l', '--alias HUBOT_ALIAS', \"Enable replacing the robot's name with alias\"],\n  ['-n', '--name HUBOT_NAME', 'The name of the robot in chat'],\n  ['-r', '--require PATH', 'Alternative scripts path'],\n  ['-t', '--config-check', \"Test hubot's config to make sure it won't fail at startup\"],\n  ['-v', '--version', 'Displays the version of hubot installed'],\n  ['-e', '--execute', 'Runs the command as if it were a hubot command']\n]\n\nconst options = {\n  adapter: process.env.HUBOT_ADAPTER,\n  alias: process.env.HUBOT_ALIAS || false,\n  create: process.env.HUBOT_CREATE || false,\n  enableHttpd: process.env.HUBOT_HTTPD !== 'false',\n  scripts: process.env.HUBOT_SCRIPTS || [],\n  name: process.env.HUBOT_NAME || 'Hubot',\n  file: process.env.HUBOT_FILE,\n  configCheck: false\n}\n\nconst Parser = new OptParse(switches)\nParser.banner = 'Usage: hubot [options]'\n\nParser.on('adapter', (opt, value) => {\n  options.adapter = value\n})\n\nParser.on('file', (opt, value) => {\n  options.file = value\n})\n\nParser.on('create', function (opt, value) {\n  options.path = value\n  options.create = true\n})\n\nParser.on('disable-httpd', opt => {\n  options.enableHttpd = false\n})\n\nParser.on('help', function (opt, value) {\n  console.log(Parser.toString())\n  return process.exit(0)\n})\n\nParser.on('alias', function (opt, value) {\n  if (!value) {\n    value = '/'\n  }\n  options.alias = value\n})\n\nParser.on('name', (opt, value) => {\n  options.name = value\n})\n\nParser.on('execute', (opt, value) => {\n  options.execute = value\n})\n\nParser.on('require', (opt, value) => {\n  options.scripts.push(value)\n})\n\nParser.on('config-check', opt => {\n  options.configCheck = true\n})\n\nParser.on('version', (opt, value) => {\n  options.version = true\n})\n\nParser.on(undefined, (opt, value) => {\n  console.warn(`Unknown option: ${opt}`)\n})\n\nParser.parse(process.argv)\n\nif (options.create) {\n  options.hubotInstallationPath = process.env.HUBOT_INSTALLATION_PATH ?? 'hubot'\n  create(options.path, options)\n  process.exit(0)\n}\n\nif (options.file) {\n  options.adapter = options.file.split('/').pop().split('.')[0]\n}\n\nconst robot = Hubot.loadBot(options.adapter, options.enableHttpd, options.name, options.alias)\nexport default robot\n\nasync function loadScripts () {\n  await robot.load(pathResolve('.', 'scripts'))\n  await robot.load(pathResolve('.', 'src', 'scripts'))\n\n  await loadExternalScripts()\n\n  const tasks = options.scripts.map((scriptPath) => {\n    if (scriptPath[0] === '/') {\n      return robot.load(scriptPath)\n    }\n\n    return robot.load(pathResolve('.', scriptPath))\n  })\n  await Promise.all(tasks)\n}\n\nasync function loadExternalScripts () {\n  const externalScripts = pathResolve('.', 'external-scripts.json')\n  try {\n    const data = await fs.promises.readFile(externalScripts)\n    try {\n      robot.loadExternalScripts(JSON.parse(data))\n    } catch (error) {\n      console.error(`Error parsing JSON data from external-scripts.json: ${error}`)\n      process.exit(1)\n    }\n  } catch (e) {\n    robot.logger.info('No external-scripts.json found. Skipping.')\n  }\n}\n\n(async () => {\n  await robot.load(pathResolve('.', 'configuration'))\n  await robot.loadAdapter(options.file)\n  if (options.version) {\n    console.log(robot.version)\n    process.exit(0)\n  }\n\n  if (options.configCheck) {\n    await loadScripts()\n    console.log('OK')\n    process.exit(0)\n  }\n\n  robot.adapter.once('connected', async () => {\n    await loadScripts()\n    if (options.execute) {\n      await robot.receive(new Hubot.TextMessage(new Hubot.User('shell', { room: '#shell' }), `@${robot.name} ${options.execute.trim()}`))\n      robot.shutdown()\n    }\n    robot.emit('scripts have loaded', robot)\n  })\n  await robot.run()\n})()\n"
  },
  {
    "path": "bin/e2e-test.sh",
    "content": "#!/bin/bash\nHUBOT_FOLDER=$(pwd)\nTEMP_ROOT=$(mktemp -d)\n\necho \"$ pushd $TEMP_ROOT\"\npushd $TEMP_ROOT\ntrap \"{ CODE=$?; popd; rm -rf $TEMP_ROOT; exit $CODE; }\" EXIT\n\n## https://github.com/hubotio/hubot/blob/main/docs/index.md\n\n## use this hubot version\necho \"Creating hubot in $TEMP_ROOT\"\necho \" and installing Hubot from $HUBOT_FOLDER\"\nnpm init -y\nnpm i $HUBOT_FOLDER\n\nexport HUBOT_INSTALLATION_PATH=$HUBOT_FOLDER\n./node_modules/.bin/hubot --create .\n\n# npm install /path/to/hubot will create a symlink in npm 5+ (http://blog.npmjs.org/post/161081169345/v500).\n# As the require calls for app-specific scripts happen inside hubot, we have to\n# set NODE_PATH to the app’s node_modules path so they can be found\necho \"$ Update NODE_PATH=$TEMP_ROOT/node_modules so everything can be found correctly.\"\nexport NODE_PATH=$TEMP_ROOT/node_modules\nexport PATH=$PATH:$TEMP_ROOT/node_modules/.bin\n\n## start, but have to sleep 1 second to wait for hubot to start and the scripts to load\nexpect <<EOL\n  set timeout 30\n  spawn hubot --name e2etest\n  expect \"e2etest> \"\n  sleep 1\n  send \"e2etest PING\\r\"\n  expect {\n    \"PONG\" {}\n    timeout {exit 1}\n  }\n  send \"e2etest adapter\\r\"\n  expect {\n    \"Shell\" {}\n    \"shell\" {}\n    timeout {exit 1}\n  }\nEOL\n"
  },
  {
    "path": "bin/hubot",
    "content": "#!/usr/bin/env node\n\nimport('./Hubot.mjs').then(async ({ default: robot }) => {})"
  },
  {
    "path": "configuration/Config.mjs",
    "content": "// Description:\n//   Configuration\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot => {\n  robot.config = {}\n}\n"
  },
  {
    "path": "docs/adapters/campfire.md",
    "content": "---\ntitle: Campfire adapter\nlayout: layouts/docs.html\npermalink: /adapters/campfire.html\n---\n\n# Campfire adapter\n\n[Campfire](http://campfirenow.com/) is a web based chat application built by [37signals](http://37signals.com). The Campfire adapter is one of the original adapters in Hubot.\n\n## Getting Started\n\nYou will need a Campfire account to start.\n\nNext, you will need to create a user on your Campfire account for your Hubot, then give it access so it can join to your rooms. You will need to create a room if you haven't already.\n\nHubot defaults to using its [Shell](./shell.html), so to use Campfire instead, you can run hubot with `-a Campfire`:\n\n    % bin/hubot -a campfire\n\nIf you are using foreman, you need to make sure the hubot is called with `-a Campfire` in the `Procfile`:\n\n    web: bin/hubot -a campfire -n Hubot\n\n## Configuring\n\nThe adapter requires the following environment variables.\n\n* `HUBOT_CAMPFIRE_ACCOUNT`\n* `HUBOT_CAMPFIRE_TOKEN`\n* `HUBOT_CAMPFIRE_ROOMS`\n\n### Campfire API Token\n\nThis can be found by logging in with your hubot's account click the **My Info**\nlink and make a note of the API token.\n\n### Campfire Room IDs\n\nIf you join the rooms you want your hubot to join will see notice a numerical\nID for the room in the URL. Make a note of each ID for the rooms you want your\nhubot to join.\n\n### Campfire Account\n\nThis is simply the first part of the domain you visit for your Campfire\naccount. For example if your Campfire was at `hubot.campfirenow.com` your\nsubdomain is `hubot`. Make a note of the subdomain.\n\n### Configuring the variables on UNIX\n\n    % export HUBOT_CAMPFIRE_TOKEN=\"...\"\n\n    % export HUBOT_CAMPFIRE_ROOMS=\"123,321\"\n\n    % export HUBOT_CAMPFIRE_ACCOUNT=\"...\"\n\n### Configuring the variables on Windows\n\nUsing PowerShell:\n\n    setx HUBOT_CAMPFIRE_TOKEN \"...\" /m\n\n    setx HUBOT_CAMPFIRE_ROOMS \"123,321\" /m\n\n    setx HUBOT_CAMPFIRE_ACCOUNT \"...\" /m\n"
  },
  {
    "path": "docs/adapters/development.md",
    "content": "---\ntitle: Development adapter\nlayout: layouts/docs.html\npermalink: /adapters/development.html\n---\n\n# Development adapter\n\n## Adapter Basics\n\nAll adapters inherit from the Adapter class in the `src/Adapter.mjs` file.\n\n```javascript\nconst Adapter = require('hubot/index.mjs').Adapter;\n```\n\nThere are certain methods that you will want to override.  Here is a basic stub of what an extended Adapter class would look like:\n\n```javascript\nconst Adapter = require('../adapter')\nconst User = require('../user')\nconst TextMessage = require('../message').TextMessage\nclass Sample extends Adapter {\n    constructor(robot) {\n        super(robot)\n        this.robot.logger.info('Constructor')\n    }\n    send(envelope, ...strings) {\n        this.robot.logger.info('Send')\n    }\n    reply(envelope, ...strings) {\n        this.robot.logger.info('Reply')\n    }\n    run() {\n        this.robot.logger.info('Run')\n        this.emit('connected') // The 'connected' event is required to trigger loading of Hubot scripts.\n        const user = new User(1001, 'Sample User')\n        const message = new TextMessage(user, 'Some Sample Message', 'MSG-001')\n        this.robot.receive(message)\n    }\n}\nexports.use = (robot) => new Sample(robot)\n```\n\n## Option 1. Setting Up Your Development Environment\n\n1. Create a new folder for your adapter `hubot-sample`\n  - `mkdir hubot-sample`\n2. Change your working directory to `hubot-sample`\n  - `cd hubot-sample`\n3. Run `npm init` to create your package.json\n  - make sure the entry point is `src/sample.js`\n4. Add your `.gitignore` to include `node_modules`\n5. Edit the `src/sample.js` file to include the above stub for your adapter\n6. Edit the `package.json` to add a peer dependency on `hubot`\n\n  ```json\n  \"dependencies\": {\n  },\n  \"peerDependencies\": {\n    \"hubot\": \">=11\"\n  }\n  ```\n\n7. Generate your Hubot using the `npx hubot --create myhubot`\n8. Change working directories to the `hubot` you created in step 7.\n9. Now perform an `npm link` to add your adapter to `hubot`\n  - `npm link ../hubot-sample`\n10. Run `hubot -a sample`\n\n## Gotchas\n\nThere is a an open issue in the node community around [npm linked peer dependencies not working](https://github.com/npm/npm/issues/5875). To get this working for our project you will need to do some minor changes to your code.\n\n1. For the import in your `hubot-sample` adapter, add the following code\n\n  ```javascript\n  let  {Robot,Adapter,TextMessage,User} = {}\n  try {\n    {Robot,Adapter,TextMessage,User} = require('hubot')\n  } catch {\n    const prequire = require('parent-require')\n    {Robot,Adapter,TextMessage,User} = prequire('hubot')\n  }\n  ```\n2. In your `hubot-sample` folder, modify the `package.json` to include the following dependency so this custom import mechanism will work\n\n  ```json\n  \"dependencies\": {\n    \"parent-require\": \"^1.0.0\"\n  }\n  ```\n3. Now try running `hubot -a sample` again and see that the imports are properly loaded.\n4. Once this is working properly, you can build out the functionality of your adapter as you see fit.  Take a look at some of the other adapters to get some ideas for your implementation.\n  - Once packaged and deployed via `npm`, you won't need the dependency in `hubot` anymore since the peer dependency should work as an official module.\n\n## Option 2. Setting Up Your Development Environment\n\nAnother option is to load the file from local disk.\n\n1. Create a new folder for your adapter `hubot-sample`\n  - `mkdir hubot-sample`\n2. Change your working directory to `hubot-sample`\n  - `cd hubot-sample`\n3. Run `npm init` to create your package.json\n  - make sure the entry point is `src/sample.js`\n4. Add your `.gitignore` to include `node_modules`\n5. Edit the `src/sample.js` file to include the above stub for your adapter\n6. Edit the `package.json` to add a peer dependency on `hubot`\n\n  ```json\n  \"dependencies\": {\n  },\n  \"peerDependencies\": {\n    \"hubot\": \">=11\"\n  }\n  ```\n\n7. Run `npx hubot -p ./src -a sample.js`\n"
  },
  {
    "path": "docs/adapters/shell.md",
    "content": "---\ntitle: Shell adapter\nlayout: layouts/docs.html\npermalink: /adapters/shell.html\n---\n\n# Shell adapter\n\nThe shell adapter provides a simple read-eval-print loop for interacting with a hubot locally.\nIt can be useful for testing scripts before using them on a live hubot.\n\n## Getting Started\n\nTo use the Shell adapter you can simply omit the `-a` option when running\nhubot as it will use the Shell adapter by default.\n\n    % bin/hubot\n\n## Configuring\n\nThis adapter doesn't require any configuration.\n\nIt supports two environment variables to make it possible to test scripts as different users:\n\n* HUBOT_SHELL_USER_ID: default is 1\n* HUBOT_SHELL_USER_NAME: default is Shell\n"
  },
  {
    "path": "docs/adapters.md",
    "content": "---\ntitle: Adapters\nlayout: layouts/docs.html\npermalink: /adapters.html\n---\n\n# Adapters\n\nAdapters are the interface to the service you want your hubot to run on.\n\nHubot includes two official adapters:\n\n* [Shell](./adapters/shell.html), i.e. for use with development\n* [Campfire](./adapters/campfire.html)\n\nThere are Third-party adapters available for most chat services. Here are the most popular ones:\n\n* [Discord](https://github.com/hubot-friends/hubot-discord)\n* [IRC](https://github.com/hubot-friends/hubot-irc)\n* [Slack](https://github.com/hubot-friends/hubot-slack)\n* [MS Teams](https://github.com/hubot-friends/hubot-ms-teams)\n* [Gitter](https://github.com/huafu/hubot-gitter2)\n* [HipChat](https://github.com/hipchat/hubot-hipchat)\n* [Rocket.Chat](https://github.com/RocketChat/hubot-rocketchat)\n* [XMPP](https://github.com/markstory/hubot-xmpp)\n\nBrowse all [repositories with the `hubot-adapter` topic on GitHub](https://github.com/search?q=topic%3Ahubot-adapter&type=Repositories) or [search for adapters on NPM](https://www.npmjs.com/search?q=hubot%20adapter&ranking=popularity). Add the `hubot-adapter` [topic](https://help.github.com/articles/classifying-your-repository-with-topics/) to your repository on GitHub to include it in this list.\n\n## Writing Your Own Adapter\n\nInterested in adding your own adapter? Check out our documentation for [developing adapters](./adapters/development.html)\n"
  },
  {
    "path": "docs/assets/stylesheets/application.css",
    "content": "@font-face {\n    font-family: 'octicons';\n    src: url(\"../vendors/octicons/octicons/octicons.eot\");\n    src: url(\"../vendors/octicons/octicons/octicons.eot?#iefix\") format(\"embedded-opentype\"),\n        url(\"../vendors/octicons/octicons/octicons.woff\") format(\"woff\"),\n        url(\"../vendors/octicons/octicons/octicons.ttf\") format(\"truetype\"),\n        url(\"../vendors/octicons/octicons/octicons.svg#svgFontName\") format(\"svg\");\n}\n\n* {\n    box-sizing: border-box;\n}\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nnav,\nsection {\n    display: block;\n}\n\naudio,\ncanvas,\nvideo {\n    display: inline-block;\n}\n\naudio:not([controls]) {\n    display: none;\n}\n\n[hidden] {\n    display: none;\n}\n\nhtml {\n    font-size: 21px;\n}\n\nbody {\n    margin: 0;\n    font-size: 0.8rem;\n    line-height: 1.5;\n}\n\nbody,\nbutton,\ninput,\nselect,\ntextarea {\n    color: #222;\n}\n\n::-moz-selection {\n    background: #4793bd;\n    color: #fff;\n    text-shadow: none;\n}\n\n::selection {\n    background: #4793bd;\n    color: #fff;\n    text-shadow: none;\n}\n\na {\n    color: #00e;\n}\n\na:visited {\n    color: #551a8b;\n}\n\na:hover {\n    color: #06e;\n}\n\na:focus {\n    outline: thin dotted;\n}\n\na:hover,\na:active {\n    outline: 0;\n}\n\nabbr[title] {\n    border-bottom: 1px dotted;\n}\n\nb,\nstrong {\n    font-weight: bold;\n}\n\nblockquote {\n    margin: 1em 40px;\n}\n\ndfn {\n    font-style: italic;\n}\n\nhr {\n    display: block;\n    height: 1px;\n    border: 0;\n    border-top: 1px solid #ccc;\n    margin: 1em 0;\n    padding: 0;\n}\n\nins {\n    background: #ff9;\n    color: #000;\n    text-decoration: none;\n}\n\nmark {\n    background: #ff0;\n    color: #000;\n    font-style: italic;\n    font-weight: bold;\n}\ncode {\n    white-space: pre-wrap;\n    word-break: break-word;\n}\npre,\ncode,\nkbd,\nsamp {\n    font-family: monospace, monospace;\n    font-size: 1em;\n}\n\nq {\n    quotes: none;\n}\n\nq:before,\nq:after {\n    content: \"\";\n    content: none;\n}\n\nsmall {\n    font-size: 0.5rem;\n}\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\nsup {\n    top: -0.5em;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\nul,\nol {\n    margin: 1em 0;\n    padding: 0 0 0 40px;\n}\n\ndd {\n    margin: 0 0 0 40px;\n}\n\nnav ul,\nnav ol {\n    list-style: none;\n    list-style-image: none;\n    margin: 0;\n    padding: 0;\n}\n\nimg {\n    border: 0;\n    -ms-interpolation-mode: bicubic;\n    vertical-align: middle;\n}\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\nfigure {\n    margin: 0;\n}\n\nform {\n    margin: 0;\n}\n\nfieldset {\n    border: 0;\n    margin: 0;\n    padding: 0;\n}\n\nlabel {\n    cursor: pointer;\n}\n\nlegend {\n    border: 0;\n    padding: 0;\n}\n\nbutton,\ninput,\nselect,\ntextarea {\n    font-size: 100%;\n    margin: 0;\n    vertical-align: baseline;\n}\n\nbutton,\ninput {\n    line-height: normal;\n}\n\nbutton,\ninput[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n    cursor: pointer;\n    appearance: button;\n}\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n    box-sizing: border-box;\n}\n\ninput[type=\"search\"] {\n    appearance: textfield;\n    box-sizing: content-box;\n}\n\ninput[type=\"search\"]::-webkit-search-decoration {\n    appearance: none;\n}\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n    border: 0;\n    padding: 0;\n}\n\ntextarea {\n    overflow: auto;\n    vertical-align: top;\n    resize: vertical;\n}\n\ninput:invalid,\ntextarea:invalid {\n    background-color: #f0dddd;\n}\n\ntable {\n    border-collapse: collapse;\n    border-spacing: 0;\n}\n\ntd {\n    vertical-align: top;\n}\n\n@font-face {\n    font-family: HandOfSeanRegular;\n    src: url(\"../fonts/eot/handsean-webfont.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/handsean-webfont.ttf\") format(\"truetype\"),\n        url(\"../fonts/svg/handsean-webfont.svg#webfontV6q8jOTr\") format(\"svg\"),\n        url(\"../fonts/woff/handsean-webfont.woff\") format(\"woff\");\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    font-family: PoliticaThin;\n    src: url(\"../fonts/eot/style_154053.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154053.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154053.svg#PoliticaThin\") format(\"svg\"),\n        url(\"../fonts/woff/style_154053.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: PoliticaThin-Italic;\n    src: url(\"../fonts/eot/style_154049.eot\");\n    src: local(\"☺\"), url(\"../fonts/svg/style_154049.svg#PoliticaThin-Italic\") format(\"svg\"),\n        url(\"../fonts/woff/style_154049.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: PoliticaLight;\n    src: url(\"../fonts/eot/style_154051.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154051.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154051.svg#PoliticaLight\") format(\"svg\"),\n        url(\"../fonts/woff/style_154051.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: PoliticaLight-Italic;\n    src: url(\"../fonts/eot/style_154044.eot\");\n    src: local(\"☺\"), url(\"../fonts/svg/style_154044.svg#PoliticaLight-Italic\") format(\"svg\"),\n        url(\"../fonts/woff/style_154044.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: Politica;\n    src: url(\"../fonts/eot/style_154046.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154046.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154046.svg#Politica\") format(\"svg\"),\n        url(\"../fonts/woff/style_154046.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: Politica-Italic;\n    src: url(\"../fonts/eot/style_154048\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154048.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154048.svg#Politica-Italic\") format(\"svg\"),\n        url(\"../fonts/woff/style_154048.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: Politica-Bold;\n    src: url(\"../fonts/eot/style_154045.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154045.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154045.svg#Politica-Bold\") format(\"svg\"),\n        url(\"../fonts/woff/style_154045.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: Politica-BoldItalic;\n    src: url(\"../fonts/eot/style_154042.eot\");\n    src: local(\"☺\"), url(\"../fonts/otf/style_154042.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/style_154042.svg#Politica-BoldItalic\") format(\"svg\"),\n        url(\"../fonts/woff/style_154042.woff\") format(\"woff\");\n}\n\n@font-face {\n    font-family: 'Octicons Regular';\n    src: url(\"../fonts/eot/octicons-regular-webfont.eot\");\n    src: local(\"☺\"), url(\"../fonts/eot/octicons-regular-webfont.eot#iefix\") format(\"embedded-opentype\"),\n        url(\"../fonts/otf/octicons-regular-webfont.otf\") format(\"opentype\"),\n        url(\"../fonts/svg/octicons-regular-webfont.svg#newFontRegular\") format(\"svg\"),\n        url(\"../fonts/woff/octicons-regular-webfont.woff\") format(\"woff\");\n}\n\na,\na:visited {\n    color: #5f8faf;\n    text-decoration: none;\n}\n\nbody {\n    background: url(\"../images/layout/project-paper.png\");\n    font-family: 'Helvetica Neue', Helvetica, arial, freesans, clean, sans-serif;\n    padding: 1rem;\n}\n\n.container {\n    background: #fff;\n    margin: 0 auto;\n    box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.05), 0 0 0 12px rgba(0, 0, 0, 0.02) inset;\n}\n\n.frame {\n    background: url(\"../images/layout/header-field-emblem.png\") no-repeat left top;\n    border: solid 1px #eee;\n}\n.hubot-avatar {\n    background-color: #fff;\n    background-image: linear-gradient(#fff 10%, #f7f7f7 90%);\n    background-position: center center;\n    background-repeat: no-repeat;\n    border-radius: 4px;\n    height: 106px;\n    left: 1px;\n    top: 1px;\n    width: 106px;\n    transform: rotate(-3deg);\n    box-shadow: 0 0 1px transparent, 0 2px 2px rgba(0, 0, 0, 0.2);\n}\n\n.hubot-avatar:before {\n    background: url(\"../images/layout/tape.png\") no-repeat left top;\n    content: '';\n    display: block;\n    height: 26px;\n    position: absolute;\n    top: -12px;\n    left: 4px;\n    width: 99px;\n    z-index: 1;\n}\n\n.hubot-avatar .hubot-avatar-img {\n    width: 100%;\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    margin: auto;\n}\n\nheader.clearfix {\n    position: relative;\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-evenly;\n    gap: 1rem;\n}\n\nheader.clearfix h1 {\n    background: url(\"../images/layout/header-field-1.png\") no-repeat left bottom;\n    color: #333;\n    font-family: Politica;\n    font-size: 72px;\n    margin: 0 12px;\n    position: relative;\n    text-transform: uppercase;\n    width: 252px;\n}\nheader.clearfix h1 span {\n    color: #7880a7;\n    display: block;\n    font-family: 'HandOfSeanRegular';\n    font-size: 14px;\n    font-weight: normal;\n    left: 156px;\n    letter-spacing: -1px;\n    position: absolute;\n    top: 32px;\n    text-transform: lowercase;\n    width: 125px;\n    transform: rotate(-3deg);\n}\n\nheader.clearfix h1 span b {\n    font-size: 18px;\n    font-weight: normal;\n}\n\nheader.clearfix h2 {\n    background: url(\"../images/layout/header-field-2.png\") no-repeat left bottom;\n    color: #7880a7;\n    float: left;\n    font-size: 16px;\n    font-weight: normal;\n    height: 132px;\n    line-height: 18px;\n    margin: 0 12px;\n    padding-top: 52px;\n    text-transform: uppercase;\n    width: 252px;\n}\n\nheader.clearfix p {\n    background: url(\"../images/layout/header-field-3.png\") no-repeat left bottom;\n    color: #bebebe;\n    float: left;\n    font-size: 16px;\n    font-weight: normal;\n    height: 132px;\n    line-height: 18px;\n    margin: 0 0 0 12px;\n    padding-top: 34px;\n    text-transform: uppercase;\n    width: 252px;\n}\n\n.download {\n    line-height: 24px;\n}\n\n.insides {\n    margin: 12px 0 12px;\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-evenly;\n    gap: 1rem;\n}\n\n\n.insides .button {\n    background: url(\"../images/layout/checkbawx.png\") no-repeat 12px 12px;\n    border: 1px solid #ddd;\n    margin: 0;\n    padding: 10px 12px 10px 30px;\n    text-transform: uppercase;\n    display: block;\n    width: 40%;\n}\n\n@media (max-width: 920px) {\n    .insides .button {\n        width: 80%;\n    }\n}\n\n.insides .button:hover {\n    background-position: 12px -44px;\n    border: 1px solid #ccc;\n    background-color: rgba(254, 174, 40, 0.1);\n}\n\n.insides .button:first-child {\n    margin-left: 0;\n}\n\n.insides span {\n    color: #ccc;\n    display: block;\n    font-size: 10px;\n}\n\n.main {\n    position: relative;\n}\n\n.schematics {\n    background: url(\"../images/layout/schematic-shadow.png\") no-repeat center bottom;\n    height: 444px;\n    z-index: 5;\n    transform: rotate(-1deg);\n}\n\n.schematic {\n    background: url(\"../images/layout/old-mathematics.png\");\n    background-size: auto, 100px;\n    height: 432px;\n    position: relative;\n    box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);\n}\n\n.schematic .schematic-img {\n    margin: 10px auto 0;\n    display: block;\n    padding-top: 25px;\n    width: 100%;\n    height: 100%;\n}\n\n.schematic p {\n    background: #fff;\n    border: 1px solid rgba(0, 0, 0, 0.1);\n    color: #999;\n    bottom: 12px;\n    font-size: 10px;\n    left: 24px;\n    padding: 6px 12px;\n    position: absolute;\n    text-transform: uppercase;\n    width: 172px;\n}\n\n.about {\n    color: #666;\n    line-height: 24px;\n    padding: 1rem 5rem;\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    gap: 1rem;\n    justify-content: space-evenly;\n    align-items: center;\n}\n.about article {\n    width: 70%;\n}\n.about aside {\n    width: 30%;\n}\n\n@media (max-width: 920px) {\n    .about {\n        flex-direction: column;\n        padding: 1rem 3rem;\n    }\n    .about aside,\n    .about article {\n        width: 100%;\n    }\n}\n\n.about h2 {\n    border-bottom: 1px solid #eee;\n    padding-bottom: 12px;\n    color: #222;\n    font-weight: normal;\n    margin-top: 24px;\n}\n\n.letter {\n    background: url(\"../images/layout/emblem.png\") no-repeat 92% 92%, url(\"../images/layout/signature.png\") no-repeat 42px 98%, url(\"../images/layout/lined-paper.png\") no-repeat left top;\n    line-height: 24px;\n    padding: 48px 24px 106px 48px;\n    box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);\n    position: relative;\n    margin-bottom: 2rem;\n}\n\n.letter:before,\n.letter:after {\n    background: url(\"../images/layout/tape.png\") no-repeat left top;\n    content: '';\n    position: absolute;\n    width: 99px;\n    height: 26px;\n}\n\n.letter:before {\n    top: -12px;\n}\n\n.letter:after {\n    top: -12px;\n    right: 32px;\n}\n\n.screenshot {\n    width: 100%;\n    text-align: center;\n    z-index: 60;\n    position: relative;\n}\n.screenshot img {\n    width: 100%;\n    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25);\n}\n\n.screenshot:before {\n    background: url(\"../images/layout/tape.png\") no-repeat left top;\n    content: '';\n    display: block;\n    height: 26px;\n    position: absolute;\n    top: -12px;\n    left: 50%;\n    margin-left: -49px;\n    width: 99px;\n}\n\n.footer {\n    color: #4c82a5;\n    margin: 24px auto 0;\n    padding: 0 24px;\n}\n\n.footer a:visited {\n    color: #5f8faf;\n}\n\n.footer .right {\n    margin: 0;\n    float: right;\n}\n\n.mega-icon {\n    font-family: 'Octicons Regular';\n    font-weight: normal;\n    font-style: normal;\n    display: inline-block;\n    line-height: 1;\n    -webkit-font-smoothing: antialiased;\n    text-decoration: none;\n}\n\n.mega-icon-invertocat {\n    color: #4c82a5;\n    position: absolute;\n    left: 50%;\n    height: 24px;\n    width: 24px;\n    margin-top: -4px;\n    margin-left: -12px;\n    font-size: 24px;\n}\n\n.mega-icon-invertocat:before {\n    content: \"\\f20a\";\n}\n\n.docs .container .main {\n    display: flex;\n    padding: 0 2rem;\n}\n.docs .container .main h2 {\n    border-bottom: 1px solid #eee;\n    padding-bottom: 0.8rem;\n    color: #222;\n    font-weight: normal;\n}\n.docs .container header.clearfix {\n    display: block;\n    margin-left: 170px;\n    position: relative;\n}\n.docs .container header.clearfix a {\n    position: absolute;\n    top: 0;\n    left: -135px;\n}\n.docs header h1 {\n    background: none;\n    width: auto;\n    border-bottom: 1px solid #ccc;\n    margin-bottom: 30px;\n}\n\n.docs-nav {\n    margin-right: 20px;\n    margin-top: 20px;\n    border: 1px solid #ccc;\n    font-size: 14px;\n}\n\n.docs-nav li {\n    border-top: 1px solid #ccc;\n}\n\n.docs-nav .subpage .docs-link {\n    padding-left: 24px;\n}\n\n.docs-nav .docs-list>li:first-child {\n    border-top: none;\n}\n\n.docs-nav .docs-link {\n    display: block;\n    padding: 8px 10px;\n}\n\n.docs-nav .docs-link:hover,\n.docs-nav .docs-link.current {\n    background-color: rgba(254, 174, 40, 0.1);\n    color: #333;\n    border-right: 4px solid #feae28;\n    padding-right: 6px;\n}\n\n@media (max-width: 600px) {\n    .mega-icon-invertocat {\n        position: static;\n    }\n    .docs-nav {\n        width: 100%;\n        margin: 0;\n    }\n    .docs .container .main {\n        display: flex;\n        flex-direction: column;\n        padding: 0 1rem;\n    }\n    .docs header h1 {\n        font-size: 2.5rem;\n    }\n    .docs .container header.clearfix {\n        margin-left: 0;\n    }\n    .docs .container header.clearfix a {\n        position: relative;\n        top: 0;\n        left: 0;\n    }\n}\n@font-face {\n    font-family: 'octicons';\n    src: font-url(\"octicons.eot?#iefix\") format(\"embedded-opentype\"), font-url(\"octicons.woff\") format(\"woff\"), font-url(\"octicons.ttf\") format(\"truetype\"), font-url(\"octicons.svg#octicons\") format(\"svg\");\n    font-weight: normal;\n    font-style: normal;\n}\n\n.octicon,\n.mega-octicon {\n    font: normal normal normal 16px/1 octicons;\n    display: inline-block;\n    text-decoration: none;\n    text-rendering: auto;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    user-select: none;\n}\n\n.mega-octicon {\n    font-size: 32px;\n}\n\n.octicon-alert:before {\n    content: '\\f02d';\n}\n\n.octicon-alignment-align:before {\n    content: '\\f08a';\n}\n\n.octicon-alignment-aligned-to:before {\n    content: '\\f08e';\n}\n\n.octicon-alignment-unalign:before {\n    content: '\\f08b';\n}\n\n.octicon-arrow-down:before {\n    content: '\\f03f';\n}\n\n.octicon-arrow-left:before {\n    content: '\\f040';\n}\n\n.octicon-arrow-right:before {\n    content: '\\f03e';\n}\n\n.octicon-arrow-small-down:before {\n    content: '\\f0a0';\n}\n\n.octicon-arrow-small-left:before {\n    content: '\\f0a1';\n}\n\n.octicon-arrow-small-right:before {\n    content: '\\f071';\n}\n\n.octicon-arrow-small-up:before {\n    content: '\\f09f';\n}\n\n.octicon-arrow-up:before {\n    content: '\\f03d';\n}\n\n.octicon-beer:before {\n    content: '\\f069';\n}\n\n.octicon-book:before {\n    content: '\\f007';\n}\n\n.octicon-bookmark:before {\n    content: '\\f07b';\n}\n\n.octicon-briefcase:before {\n    content: '\\f0d3';\n}\n\n.octicon-broadcast:before {\n    content: '\\f048';\n}\n\n.octicon-browser:before {\n    content: '\\f0c5';\n}\n\n.octicon-bug:before {\n    content: '\\f091';\n}\n\n.octicon-calendar:before {\n    content: '\\f068';\n}\n\n.octicon-check:before {\n    content: '\\f03a';\n}\n\n.octicon-checklist:before {\n    content: '\\f076';\n}\n\n.octicon-chevron-down:before {\n    content: '\\f0a3';\n}\n\n.octicon-chevron-left:before {\n    content: '\\f0a4';\n}\n\n.octicon-chevron-right:before {\n    content: '\\f078';\n}\n\n.octicon-chevron-up:before {\n    content: '\\f0a2';\n}\n\n.octicon-circle-slash:before {\n    content: '\\f084';\n}\n\n.octicon-circuit-board:before {\n    content: '\\f0d6';\n}\n\n.octicon-clippy:before {\n    content: '\\f035';\n}\n\n.octicon-clock:before {\n    content: '\\f046';\n}\n\n.octicon-cloud-download:before {\n    content: '\\f00b';\n}\n\n.octicon-cloud-upload:before {\n    content: '\\f00c';\n}\n\n.octicon-code:before {\n    content: '\\f05f';\n}\n\n.octicon-color-mode:before {\n    content: '\\f065';\n}\n\n.octicon-comment-add:before,\n.octicon-comment:before {\n    content: '\\f02b';\n}\n\n.octicon-comment-discussion:before {\n    content: '\\f04f';\n}\n\n.octicon-credit-card:before {\n    content: '\\f045';\n}\n\n.octicon-dash:before {\n    content: '\\f0ca';\n}\n\n.octicon-dashboard:before {\n    content: '\\f07d';\n}\n\n.octicon-database:before {\n    content: '\\f096';\n}\n\n.octicon-device-camera:before {\n    content: '\\f056';\n}\n\n.octicon-device-camera-video:before {\n    content: '\\f057';\n}\n\n.octicon-device-desktop:before {\n    content: '\\f27c';\n}\n\n.octicon-device-mobile:before {\n    content: '\\f038';\n}\n\n.octicon-diff:before {\n    content: '\\f04d';\n}\n\n.octicon-diff-added:before {\n    content: '\\f06b';\n}\n\n.octicon-diff-ignored:before {\n    content: '\\f099';\n}\n\n.octicon-diff-modified:before {\n    content: '\\f06d';\n}\n\n.octicon-diff-removed:before {\n    content: '\\f06c';\n}\n\n.octicon-diff-renamed:before {\n    content: '\\f06e';\n}\n\n.octicon-ellipsis:before {\n    content: '\\f09a';\n}\n\n.octicon-eye-unwatch:before,\n.octicon-eye-watch:before,\n.octicon-eye:before {\n    content: '\\f04e';\n}\n\n.octicon-file-binary:before {\n    content: '\\f094';\n}\n\n.octicon-file-code:before {\n    content: '\\f010';\n}\n\n.octicon-file-directory:before {\n    content: '\\f016';\n}\n\n.octicon-file-media:before {\n    content: '\\f012';\n}\n\n.octicon-file-pdf:before {\n    content: '\\f014';\n}\n\n.octicon-file-submodule:before {\n    content: '\\f017';\n}\n\n.octicon-file-symlink-directory:before {\n    content: '\\f0b1';\n}\n\n.octicon-file-symlink-file:before {\n    content: '\\f0b0';\n}\n\n.octicon-file-text:before {\n    content: '\\f011';\n}\n\n.octicon-file-zip:before {\n    content: '\\f013';\n}\n\n.octicon-flame:before {\n    content: '\\f0d2';\n}\n\n.octicon-fold:before {\n    content: '\\f0cc';\n}\n\n.octicon-gear:before {\n    content: '\\f02f';\n}\n\n.octicon-gift:before {\n    content: '\\f042';\n}\n\n.octicon-gist:before {\n    content: '\\f00e';\n}\n\n.octicon-gist-secret:before {\n    content: '\\f08c';\n}\n\n.octicon-git-branch-create:before,\n.octicon-git-branch-delete:before,\n.octicon-git-branch:before {\n    content: '\\f020';\n}\n\n.octicon-git-commit:before {\n    content: '\\f01f';\n}\n\n.octicon-git-compare:before {\n    content: '\\f0ac';\n}\n\n.octicon-git-merge:before {\n    content: '\\f023';\n}\n\n.octicon-git-pull-request-abandoned:before,\n.octicon-git-pull-request:before {\n    content: '\\f009';\n}\n\n.octicon-globe:before {\n    content: '\\f0b6';\n}\n\n.octicon-graph:before {\n    content: '\\f043';\n}\n\n.octicon-heart:before {\n    content: '\\2665';\n}\n\n.octicon-history:before {\n    content: '\\f07e';\n}\n\n.octicon-home:before {\n    content: '\\f08d';\n}\n\n.octicon-horizontal-rule:before {\n    content: '\\f070';\n}\n\n.octicon-hourglass:before {\n    content: '\\f09e';\n}\n\n.octicon-hubot:before {\n    content: '\\f09d';\n}\n\n.octicon-inbox:before {\n    content: '\\f0cf';\n}\n\n.octicon-info:before {\n    content: '\\f059';\n}\n\n.octicon-issue-closed:before {\n    content: '\\f028';\n}\n\n.octicon-issue-opened:before {\n    content: '\\f026';\n}\n\n.octicon-issue-reopened:before {\n    content: '\\f027';\n}\n\n.octicon-jersey:before {\n    content: '\\f019';\n}\n\n.octicon-jump-down:before {\n    content: '\\f072';\n}\n\n.octicon-jump-left:before {\n    content: '\\f0a5';\n}\n\n.octicon-jump-right:before {\n    content: '\\f0a6';\n}\n\n.octicon-jump-up:before {\n    content: '\\f073';\n}\n\n.octicon-key:before {\n    content: '\\f049';\n}\n\n.octicon-keyboard:before {\n    content: '\\f00d';\n}\n\n.octicon-law:before {\n    content: '\\f0d8';\n}\n\n.octicon-light-bulb:before {\n    content: '\\f000';\n}\n\n.octicon-link:before {\n    content: '\\f05c';\n}\n\n.octicon-link-external:before {\n    content: '\\f07f';\n}\n\n.octicon-list-ordered:before {\n    content: '\\f062';\n}\n\n.octicon-list-unordered:before {\n    content: '\\f061';\n}\n\n.octicon-location:before {\n    content: '\\f060';\n}\n\n.octicon-gist-private:before,\n.octicon-mirror-private:before,\n.octicon-git-fork-private:before,\n.octicon-lock:before {\n    content: '\\f06a';\n}\n\n.octicon-logo-github:before {\n    content: '\\f092';\n}\n\n.octicon-mail:before {\n    content: '\\f03b';\n}\n\n.octicon-mail-read:before {\n    content: '\\f03c';\n}\n\n.octicon-mail-reply:before {\n    content: '\\f051';\n}\n\n.octicon-mark-github:before {\n    content: '\\f00a';\n}\n\n.octicon-markdown:before {\n    content: '\\f0c9';\n}\n\n.octicon-megaphone:before {\n    content: '\\f077';\n}\n\n.octicon-mention:before {\n    content: '\\f0be';\n}\n\n.octicon-microscope:before {\n    content: '\\f089';\n}\n\n.octicon-milestone:before {\n    content: '\\f075';\n}\n\n.octicon-mirror-public:before,\n.octicon-mirror:before {\n    content: '\\f024';\n}\n\n.octicon-mortar-board:before {\n    content: '\\f0d7';\n}\n\n.octicon-move-down:before {\n    content: '\\f0a8';\n}\n\n.octicon-move-left:before {\n    content: '\\f074';\n}\n\n.octicon-move-right:before {\n    content: '\\f0a9';\n}\n\n.octicon-move-up:before {\n    content: '\\f0a7';\n}\n\n.octicon-mute:before {\n    content: '\\f080';\n}\n\n.octicon-no-newline:before {\n    content: '\\f09c';\n}\n\n.octicon-octoface:before {\n    content: '\\f008';\n}\n\n.octicon-organization:before {\n    content: '\\f037';\n}\n\n.octicon-package:before {\n    content: '\\f0c4';\n}\n\n.octicon-paintcan:before {\n    content: '\\f0d1';\n}\n\n.octicon-pencil:before {\n    content: '\\f058';\n}\n\n.octicon-person-add:before,\n.octicon-person-follow:before,\n.octicon-person:before {\n    content: '\\f018';\n}\n\n.octicon-pin:before {\n    content: '\\f041';\n}\n\n.octicon-playback-fast-forward:before {\n    content: '\\f0bd';\n}\n\n.octicon-playback-pause:before {\n    content: '\\f0bb';\n}\n\n.octicon-playback-play:before {\n    content: '\\f0bf';\n}\n\n.octicon-playback-rewind:before {\n    content: '\\f0bc';\n}\n\n.octicon-plug:before {\n    content: '\\f0d4';\n}\n\n.octicon-repo-create:before,\n.octicon-gist-new:before,\n.octicon-file-directory-create:before,\n.octicon-file-add:before,\n.octicon-plus:before {\n    content: '\\f05d';\n}\n\n.octicon-podium:before {\n    content: '\\f0af';\n}\n\n.octicon-primitive-dot:before {\n    content: '\\f052';\n}\n\n.octicon-primitive-square:before {\n    content: '\\f053';\n}\n\n.octicon-pulse:before {\n    content: '\\f085';\n}\n\n.octicon-puzzle:before {\n    content: '\\f0c0';\n}\n\n.octicon-question:before {\n    content: '\\f02c';\n}\n\n.octicon-quote:before {\n    content: '\\f063';\n}\n\n.octicon-radio-tower:before {\n    content: '\\f030';\n}\n\n.octicon-repo-delete:before,\n.octicon-repo:before {\n    content: '\\f001';\n}\n\n.octicon-repo-clone:before {\n    content: '\\f04c';\n}\n\n.octicon-repo-force-push:before {\n    content: '\\f04a';\n}\n\n.octicon-gist-fork:before,\n.octicon-repo-forked:before {\n    content: '\\f002';\n}\n\n.octicon-repo-pull:before {\n    content: '\\f006';\n}\n\n.octicon-repo-push:before {\n    content: '\\f005';\n}\n\n.octicon-rocket:before {\n    content: '\\f033';\n}\n\n.octicon-rss:before {\n    content: '\\f034';\n}\n\n.octicon-ruby:before {\n    content: '\\f047';\n}\n\n.octicon-screen-full:before {\n    content: '\\f066';\n}\n\n.octicon-screen-normal:before {\n    content: '\\f067';\n}\n\n.octicon-search-save:before,\n.octicon-search:before {\n    content: '\\f02e';\n}\n\n.octicon-server:before {\n    content: '\\f097';\n}\n\n.octicon-settings:before {\n    content: '\\f07c';\n}\n\n.octicon-log-in:before,\n.octicon-sign-in:before {\n    content: '\\f036';\n}\n\n.octicon-log-out:before,\n.octicon-sign-out:before {\n    content: '\\f032';\n}\n\n.octicon-split:before {\n    content: '\\f0c6';\n}\n\n.octicon-squirrel:before {\n    content: '\\f0b2';\n}\n\n.octicon-star-add:before,\n.octicon-star-delete:before,\n.octicon-star:before {\n    content: '\\f02a';\n}\n\n.octicon-steps:before {\n    content: '\\f0c7';\n}\n\n.octicon-stop:before {\n    content: '\\f08f';\n}\n\n.octicon-repo-sync:before,\n.octicon-sync:before {\n    content: '\\f087';\n}\n\n.octicon-tag-remove:before,\n.octicon-tag-add:before,\n.octicon-tag:before {\n    content: '\\f015';\n}\n\n.octicon-telescope:before {\n    content: '\\f088';\n}\n\n.octicon-terminal:before {\n    content: '\\f0c8';\n}\n\n.octicon-three-bars:before {\n    content: '\\f05e';\n}\n\n.octicon-tools:before {\n    content: '\\f031';\n}\n\n.octicon-trashcan:before {\n    content: '\\f0d0';\n}\n\n.octicon-triangle-down:before {\n    content: '\\f05b';\n}\n\n.octicon-triangle-left:before {\n    content: '\\f044';\n}\n\n.octicon-triangle-right:before {\n    content: '\\f05a';\n}\n\n.octicon-triangle-up:before {\n    content: '\\f0aa';\n}\n\n.octicon-unfold:before {\n    content: '\\f039';\n}\n\n.octicon-unmute:before {\n    content: '\\f0ba';\n}\n\n.octicon-versions:before {\n    content: '\\f064';\n}\n\n.octicon-remove-close:before,\n.octicon-x:before {\n    content: '\\f081';\n}\n\n.octicon-zap:before {\n    content: '\\26A1';\n}"
  },
  {
    "path": "docs/deploying/azure.md",
    "content": "---\ntitle: Deploying to Azure\nlayout: layouts/docs.html\npermalink: /deploying/azure.html\n---\n\n# Deploying to Azure\n\nIf you've been following along with [Getting Started](../index.html), it's time to deploy so you can use it beyond just your local machine.\n[Azure](http://azure.microsoft.com/) is a way to deploy hubot.\n\nYou will need to install the azure-cli via npm after you have follow the initial instructions for your hubot.\n\n    % npm install -g azure-cli\n\nInside your new hubot directory, make sure you've created a git repository, and that your work is committed:\n\n    % git init\n    % git add .\n    % git commit -m \"Initial commit\"\n\nThen [create a GitHub repository](https://help.github.com/articles/create-a-repo/) for your hubot. This is where Azure will pull your code from instead of needing to deploy directly from your dev machine to Azure.\n\n    % git remote add origin _your GitHub repo_\n    % git push -u origin main\n\nOnce you have your GitHub repo, create an Azure website linked to your repo. In Azure, create a website and select integrated source control. When it asks \"where is your source control\" select GitHub and link this website to your git repo that you created in the previous step. If you have downloaded the Azure PowerShell modules, you can also do this via PowerShell.\n\n    % $creds = Get-Credential\n    % New-AzureWebsite mynewhubot -github -githubrepository yourgithubaccount/yourhubotreponame -githubcredentials $creds\n\nOnce you have done this, Azure will deploy your site any time you commit and push to GitHub. Your hubot won't run quite right yet, though. Next, you need to configure the deployment to tell Azure how to run hubot.\n\nFirst, run the follow command to add `deploy.cmd` to your hubot directory. This is the file that Azure uses to know how to deploy your node application.\n\n    % azure site deploymentscript --node\n\nThen, edit this file and look for the sections that give you steps 1, 2 and 3. You're going to add a 4th step:\n\n    :: 4. Create Hubot file with a js extension\n    copy /Y \"%DEPLOYMENT_TARGET%\\node_modules\\hubot\\bin\\hubot\" \"%DEPLOYMENT_TARGET%\\node_modules\\hubot\\bin\\Hubot.mjs\"\n\nNow, create a new file in the base directory of hubot called `server.js` and put these two lines into it:\n\n    module.exports = await import('hubot/bin/Hubot.mjs');\n\nFinally you will need to add the environment variables to the website to make sure it runs properly. You can either do it through the GUI (under configuration) or you can use the Azure PowerShell command line, as follows (example is showing slack as an adapter and mynewhubot as the website name).\n\n    % $settings = New-Object Hashtable\n    % $settings[\"HUBOT_ADAPTER\"] = \"Slack\"\n    % $settings[\"HUBOT_SLACK_TOKEN\"] = \"yourslackapikey\"\n    % Set-AzureWebsite -AppSettings $settings mynewhubot\n\nCommit your changes in git and push to GitHub and Azure will automatically pick up the changes and deploy them to your website.\n\n    % git commit -m \"Add Azure settings for hubot\"\n    % git push\n\nAzure offers a marketplace where you can use the default hubot-redis-brain using Redis Cloud provided by Redis Labs. Alternatively, to add an [Azure blob storage brain](https://github.com/coryallegory/hubot-azure-brain), you will need to create an Azure storage account. Then you can do the following in your base hubot directory.\n\n    % npm install hubot-azure-brain --save\n\nThen add the following line in `external-scripts.json` in the list with the other external scripts\n\n    \"hubot-azure-brain\"\n\nFinally, add one more environment variables to your website. You can do this either via the GUI or the following PowerShell commands.\n\n    % $settings = New-Object Hashtable\n    % $settings[\"HUBOT_BRAIN_AZURE_CONNSTRING\"] = \"your Azure blob storage connection string\"\n    % Set-AzureWebsite -AppSettings $settings mynewhubot\n\nNow any scripts that require a brain will function. You should look up other scripts or write your own by looking at the [documentation](../scripting.html). All of the normal scripts for hubot are compatible with hosting hubot on Azure.\n\n### Troubleshooting tips and tricks\n\nDue to Azure being Windows-based, you may run into path length problems. To overcome this issue you can set the environment variable `IN_PLACE_DEPLOYMENT` to `1` and use [custom deployment scripts to take advantage of NPM3](https://github.com/felixrieseberg/azure-npm3) and flat module installation.\n\nIf you have your instance of hubot deploying directly from source control to Azure, it will perform various bootstrapping tasks along with calling `npm install` via kudu. Now that Azure supports npm5, this process generates a package-lock.json file which may result in some of your packages failing to install remotely. You can edit `deploy.cmd` to include `--no-package-lock` as part of the \"Step 3\" install command.\n\nIf using the free tier of Azure, you can also add a post-deployment step to ping the server on startup by setting the environment variable `POST_DEPLOYMENT_ACTION` with a script (relative to the src dir) such as `startup.sh`\n\nAn example of a startup script:\n\n```\nlet retrys=0\nwhile : ; do\n    STATUSCODE=$(curl --silent --output /dev/stderr --write-out \"%{http_code}\" https://${WEBSITE_SITE_NAME}.azurewebsites.net/hubot/keepalive)\n    echo $STATUSCODE\n    [[ $retrys -ne 5 ]] || break\n    echo $retrys\n    ((retrys++))\n    [[ $STATUSCODE -ne 200 ]] || break\ndone\n```\n\n### Slack Integration\nCurrently the Slack integration has trouble finding hubot when deployed to Azure. Adding a `NODE_PATH` application setting with the value `D:\\home\\site\\wwwroot\\node_modules` will allow the Slack module to find hubot.\n"
  },
  {
    "path": "docs/deploying/bluemix.md",
    "content": "---\ntitle: Deploying to Bluemix\nlayout: layouts/docs.html\npermalink: /deploying/bluemix.html\n---\n\n# Deploying to Bluemix\n\nIf you've been following along with [Getting Started](../index.html), it's time\nto deploy so you can use it beyond just your local machine.\n[IBM Bluemix](http://bluemix.net) is a way to deploy hubot. It is built on the open-source project\n[Cloud Foundry](https://www.cloudfoundry.org/), so we'll be using the `cf cli`\nthroughout these examples.\n\nHubot was originally very closely coupled to Heroku, so there are a couple of\nthings to clean up first that we don't need or that might get in the way on\nanother platform:\n* remove `Procfile` as we'll create the `manifest.yml` that Bluemix needs in a\n moment\n* remove the `hubot-heroku-keepalive` line from `external_scripts.json` and also\n remove the related npm module (it causes errors on other platforms):\n\n  npm uninstall --save hubot-heroku-keepalive\n\nIn preparation for working with Bluemix, install the [Cloud Foundry\nCLI](https://github.com/cloudfoundry/cli/releases), and create a [Bluemix\nAccount](http://bluemix.net).\n\nFirst we need to define a `manifest.yml` file in the root directory. The\ncontents of the manifest at the bare minimum should look like:\n\n```yml\napplications:\n- name: myVeryOwnHubot\n  command: ./bin/hubot --adapter slack\n  instances: 1\n  memory: 512M\n```\n\nIn this example, we're using the slack adapter, if you choose slack as your\nadapter when creating a hubot this will work, otherwise add the `hubot-slack`\nmodule to your `package.json`.  **Change the name of your hubot in the\n`manifest.yml` file** because otherwise your application will clash with someone\nelse's who already deployed an app called this!  There are many more useful\nthings you can change about your hubot using the manifest file, so check out\n[these docs](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html)\nfor more information.\n\nYou then need to connect your hubot project to Bluemix:\n\n```sh\n$ cd your_hubot_project\n$ cf api https://api.ng.bluemix.net\n$ cf login\n```\n\nNote that the `cf api` command changes per Bluemix region so to deploy somewhere\nother than \"US South\", replace this api as appropriate.  The `cf login` command\nwill prompt you with your login credentials.\n\nNext, we need to set up our environment variables, but we need to create the app\nfirst.  It won't work properly without the environment variables it needs, so\nwe'll first of all use the `--no-start` flag to deploy but not attempt to start\nit.\n\n```sh\n$ cf push NAME_OF_YOUR_HUBOT_APP --no-start\n```\n\nNow the app exists, we can set its environment variables.  To access slack,\nyou'll need a slack token from the \"Apps and Integrations\" page; it's visible\nwhen you go to create a slackbot.  Copy that token and set it as an environment\nvariable called `HUBOT_SLACK_TOKEN`, like this:\n\n```sh\n$ cf set-env NAME_OF_YOUR_HUBOT_APP HUBOT_SLACK_TOKEN TOKEN_VALUE\n```\n\nIf you have other environment variables to set, such as configuring the\n`REDIS_URL` for `hubot-redis-brain`, this is a good time to do that.\n\nFinally, we're ready to go!  Deploy \"for real\" this time:\n\n```sh\n$ cf push NAME_OF_YOUR_HUBOT_APP\n```\n\nYou should see your bot connect to slack!\n\n### Further Reading\n\n  - [Deploying Cloud Foundry Apps To Bluemix](https://www.ng.bluemix.net/docs/cfapps/runtimes.html)\n  - [Neploying Node.js Apps to Bluemix](https://www.ng.bluemix.net/docs/starters/nodejs/index.html)\n  - [Setting up your manifest](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html)\n  - [Understanding the CF CLI](https://www.ng.bluemix.net/docs/cli/reference/cfcommands/index.html)\n  - [Setting up a Build Pipleline in Bluemix](https://www.ng.bluemix.net/docs/#services/DeliveryPipeline/index.html#getstartwithCD)\n\n### Troubleshooting\n\n**Bot doesn't connect**\n\nCheck your logs for more information using the command `cf logs YOUR_APP_NAME\n--recent`.  If you have NodeJS installed locally, you can also try running the\nbot on your local machine to inspect any output: simply do `bin/hubot` from the\ntop level of the project.\n\n**Bot crashes repeatedly**\n\nIt is sometimes necessary to to assign more memory to your hubot, depending\nwhich plugins you are using (if your app crashes with error 137, try increasing\nthe memory limit).\n\n"
  },
  {
    "path": "docs/deploying/unix.md",
    "content": "---\ntitle: Deploying to Unix\nlayout: layouts/docs.html\npermalink: /deploying/unix.html\n---\n\n# Deploying to Unix\n\nBecause there are so many variations of Linux, and more generally UNIX, it's\ndifficult for the hubot team to have canonical documentation for installing and\ndeploying it to every version out there. So, this is an attempt to give an\noverview of what's needed to get deploying.\n\nThere are 3 primary things to deploying and running hubot:\n\n  * node and npm\n  * a way to get source code updated on the server\n  * a way to start hubot, start it up if it crashes, and restart it when code\n    updates\n\n## node and npm\n\nTo start, your UNIX server will need node and npm. Check out the node.js wiki\nfor [installing Node.js via package manager](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager), [Building on GNU/Linux and other UNIX](https://github.com/joyent/node/wiki/Installation#building-on-gnulinux-and-other-unix).\n\n## Updating code on the server\n\nThe simplest way to update your hubot's code is going to be to have a git\ncheckout of your hubot's source code (that you've created during [Getting Started](../index.html), not the [github/hubot repository](http://github.com/github/hubot), and just git pull to get change. This may\nfeel a dirty hack, but it works when you are starting out.\n\nIf you have a Ruby background, you might be more comfortable using\n[capistrano](https://github.com/capistrano/capistrano).\n\nIf you have a [Chef](http://www.chef.io/chef/) background, there's a\n[deploy](https://docs.chef.io/resource_deploy.html) resource for managing\ndeployments.\n\n## Starting, stopping, and restarting hubot\n\nEvery hubot install has a `bin/hubot` script to handle starting up the hubot.\nYou can run this command from your git checkout on the server, but there are some problems you can encounter:\n\n* you disconnect, and hubot dies\n* hubot dies, for any reason, and doesn't start again\n* it doesn't start up at boot automatically\n\nFor handling you disconnecting, you can start with running `bin/hubot` in\n[screen session](http://www.gnu.org/software/screen/) or with\n[nohup](http://linux.die.net/man/1/nohup).\n\nFor handling hubot dying, and restarting it automatically, you can imagine\nrunning `bin/hubot` in a\n[bash while loop](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-7.html#ss7.3). But\nreally, you probably want some process monitoring using tools like\n[monit](http://mmonit.com/monit/),\n[god](http://godrb.com/),\n[bluepill](https://github.com/arya/bluepill),\n[upstart](http://upstart.ubuntu.com/),\n[runit](http://smarden.org/runit/),\n[systemd](http://freedesktop.org/wiki/Software/systemd/).\n\nFor starting at boot, you can create an init script appropriate for your UNIX\ndistribution. If you are using one of the process monitoring tools above, make\nsure it boots at startup. See the [examples](https://github.com/github/hubot/tree/main/examples)\nfor configuration examples.\n\n## Recommendations\n\nThis document has been deliberately light on strong recommendations. At a high\nlevel though, it's strongly recommended to avoid anything that is overly manual\nand non-repeatable. That would mean using your OS's packages and tools whenever\npossible, and having a proper deploy tool to update hubot, and process\nmanagement to keep hubot running.\n"
  },
  {
    "path": "docs/deploying/windows.md",
    "content": "---\ntitle: Deploying to Windows\nlayout: layouts/docs.html\npermalink: /deploying/windows.html\n---\n\n# Deploying to Windows\n\nHasn't been fully tested - YMMV\n\nThere are 4 primary steps to deploying and running hubot on a Windows machine:\n\n* node and npm\n* a way to get source code updated on the server\n* setting up environment variables for hubot\n* a way to start hubot, start it up if it crashes, and restart it when code updates\n\n## node and npm\n\nTo start, your windows server will need node and npm.\nThe best way to do this is with [chocolatey](http://chocolatey.org) using the [nodejs.install](http://chocolatey.org/packages/nodejs.install) package.\nI've found that sometimes the system path variable is not correctly set; ensure you can run node/npm from the command line. If needed set the PATH variable with `set PATH=%PATH%;\\\"C:\\Program Files\\nodejs\\\"`\n\nYour other option is to install directly from [NodeJS](https://nodejs.org/) and run the current download (v0.12.4 as of this documentation). This should set your PATH variables for you.\n\n## Updating code on the server\n\nTo get the code on your server, you can follow the instructions at [Getting Started](../index.html) on your local development machine or directly on the server. If you are building locally, push your hubot to GitHub and clone the repo onto your server. Don't clone the normal [github/hubot repository](http://github.com/github/hubot), make sure you're using `npx hubot --create myhubot` to build your own hubot.\n\n## Setting up environment vars\n\nYou will want to set up your hubot environment variables on the server where it will run. You can do this by opening an administrative PowerShell and typing the following:\n\n    [Environment]::SetEnvironmentVariable(\"HUBOT_ADAPTER\", \"Campfire\", \"Machine\")\n\nThis is equivalent to going into the system menu -> selecting advanced system settings -> environment vars and adding a new system variable called HUBOT_ADAPTER with the value of Campfire.\n\n## Starting, stopping, and restarting hubot\n\nEvery hubot install has a `bin/hubot` script to handle starting up the hubot.\nYou can run this command directly from your hubot folder by typing the following:\n\n    .\\bin\\hubot –adapter campfire\n\nThere are a few issues if you call it manually, though.\n\n* you disconnect, and hubot dies\n* hubot dies, for any reason, and doesn't start again\n* it doesn't start up at boot automatically\n\nTo fix this, you will want to create a .ps1 file with whatever name makes you happy that you will call from your hubot directory. There is a copy of this file in the `examples` directory [here](../../examples/hubot-start.ps1). It should contain the following:\n\n    Write-Host \"Starting Hubot Watcher\"\n    While (1)\n    {\n        Write-Host \"Starting Hubot\"\n        Start-Process powershell -ArgumentList \".\\bin\\hubot –adapter slack\" -wait\n    }\n\nRemember to allow local unsigned PowerShell scripts if you are using the .ps1 file to run hubot. Run this command in an Administrator PowerShell window.\n\n    Set-ExecutionPolicy RemoteSigned\n\nYou can set this .ps1 as scheduled task on boot if you like or some other way to start your process.\n\n## Expanding the documentation\n\nNot yet fleshed out. [Help contribute by submitting a pull request, please?](https://github.com/github/hubot/pull/new/main)\n"
  },
  {
    "path": "docs/deploying.md",
    "content": "---\ntitle: Deploying\nlayout: layouts/docs.html\npermalink: /deploying.html\n---\n\n# Deploying\n\n- [Azure](./deploying/azure.html)\n- [Bluemix](./deploying/bluemix.html)\n- [Unix](./deploying/unix.html)\n- [Windows](./deploying/windows.html)\n"
  },
  {
    "path": "docs/designs/commands.md",
    "content": "You are an expert javascript and Node.js engineer implementing a backwards-compatible command subsystem for Hubot.\n\nIMPORTANT: Use TDD. Start by writing tests that describe the behavior, then implement the code to make the tests pass. Do not begin implementation until you have written the tests. Keep changes small and incremental.\n\nHigh-level goal:\nAdd a new deterministic command bus to Hubot via robot.commands while preserving full backwards compatibility with existing robot.respond() and robot.hear() scripts. This must be safe-by-default and must not introduce broad “catch-all” behavior.\n\nConstraints:\n- Minimal dependencies (prefer none).\n- Defensive coding: invalid input must not crash the bot.\n- Design for adapter variability (Slack/Discord/etc), but implement renderer as simple text initially.\n- Existing scripts must run unchanged.\n\nBehavioral requirements (what must be true):\n1.\tBackwards compatibility\n\n- A bot with legacy listeners (respond/hear) behaves the same after adding robot.commands.\n- The new subsystem must not consume arbitrary messages. It only intercepts:\na) confirmation replies (yes/no/cancel) when a pending confirmation exists for that user+room\n\n2. New API surface\nimplement robot.commands with these capabilities.\n- Registration with spec and optional opts - handle unregister and update\n- get command by id, list commands with a filter, get help for a command by id\n- parse the text into a ParsedInvocation that is nullable\n- validate with command id, rawArgs and context meta\n- execute the command by command id, raw args and execution context asynchronously\n- invoke the invocation text with execution context asynchronously that will parse -> validate -> execute pipeline\n- propse a given propsal with execution context asynchronously\n- confirm the users reply text with the execution context asynchronously\n- prending proposal store keyed by userid + roomid with a TTL\n\n# 3. Command Shape\nA command is registered as:\n- id - string, uniqueue, required)\n- description - string\n- aliases - string[], optional (alternate names that resolve to this command id)\n- examples - string[], optional\n- args schema (optional):\n    - arg name: {type, required?, default?, values?}\n- side effects - string[], optional\n- confirm policy - \"never\" | \"if_ambiguous\" | \"always\" (optional)\n- permissions - { rooms?: string[], roles?: string[]} (optional)\n- handler async function with `{ args, context }` (required)\n\n# 4. Canonical invocation parsing\nParse messages like:\n- tickets.create -- title \"VPN down\" --priority high --assignee matt --room #ops\n    - also support key:value tokens\n        - tickets.create title:\"VPN down\" prioriy:high\n    - also support command aliases (resolved by parse() to their canonical id)\n        - if tickets.create has alias 'ticket.new', then ticket.new also works\n- Must support quoted strings, bare words, boolean flags\n\n# 5. Validation and normalization\n\n- Apply defaults from schema\n- Enforce required args\n- Validate enums\n- Support types: string, number, boolean, enum, user, room, date\n- Built-in resolvers:\n    - user: attempt to map a string to a Hubot user from robot.brain.users()\n    - room: validate #room format\n    - date: support today, tomorrow, ISO, and “YYYY-MM-DD HH:mm” (keep it light); Return ValidationResult with:\n        - ok true + normalized args\n        - ok false + missing + errors + (if possible) one clarifyingQuestion\n\n# 6. Permissions\n- If permissions.rooms exists, deny execution outside allowed rooms.\n- Roles are optional; implement a pluggable permission provider hook. Default allow if not configured.\n\n# 7. Confirmation\n- If sideEffects exist OR confirm=always, require confirmation before executing.\n- Confirmation uses propose→confirm pipeline:\n    - propose() returns a preview of the canonical invocation and asks “Run it? (yes/no)”\n    - confirm() executes on yes, cancels on no/cancel\n- confirm() only triggers if there is a pending confirmation for that user+room key.\n\n# 8. Middleware Integration\n\nThe command subsystem integrates with Hubot's receive middleware to intercept and handle commands:\n\n- **Confirmation Middleware**: Intercepts confirmation replies (yes/no/cancel) when a pending proposal exists for that user+room\n- **Invocation Middleware**: Intercepts command invocations (text starting with command ID or alias) to parse, validate, and execute\n\nThese middleware handlers are implemented in `Robot.setupCommandListeners()` and must not interfere with normal chat or legacy `hear`/`respond` listeners. The middleware only acts on addressed messages (those directed at the robot) and commands that have been explicitly registered.\n\n# 9. Bridging (optional but useful)\n\nAllow register(spec, { bridge: “respond” | “hear” | “none” }) such that when bridge is enabled, a legacy listener is registered that delegates into the command system (but still does not swallow normal chat).\n\n# 10. Observability\n\nEmit events through an EventEmitter on robot.commands:\n- commands:registered\n- commands:updated\n- commands:alias_collision_detected\n- commands:invocation_parsed\n- commands:validation_failed\n- commands:proposal_created\n- commands:proposal_confirm_requested\n- commands:proposal_confirmed\n- commands:proposal_cancelled\n- commands:executed\n- commands:permission_denied\n- commands:error\n\n**Logging Strategy**: Fire-and-forget async logging to NDJSON file (`default: .data/commands-events.ndjson`). Each event is written individually and asynchronously. Set `disableLogging: true` during testing to prevent file I/O.\n\n# TDD plan you must follow:\nA) Write tests first. Include at least these test scenarios:\n- parse() correctly parses quoted args and key:value args- parse() resolves command aliases to canonical command ids- validate() applies defaults and rejects invalid enum\n- user resolver maps a name to a brain user record (use a stub brain)\n- propose() creates pending confirmation for side-effect command\n- confirm(“yes”) executes, confirm(“no”) cancels\n- confirmation listener does nothing if no pending exists\n\nB) Only after tests exist, implement the smallest code to pass them.\n\nC) Refactor as needed to keep code clean, depulication, software design that provides affordances.\n\nEnvironment for tests:\n- Use Node’s built-in test runner (node:test) unless you have a strong reason not to.\n- Create minimal “fake robot” and “fake message” objects as needed; do not require a real chat adapter. You can create a dumby one.\n- Because we're in an asynchronous environment, tests often \"hange\". so make sure to use --test-timeout when running node --test or npm test.\n\nOutput format:\n- Provide the full repository output (tests + implementation + a short README).\n- Do not over-engineer; build a clean minimal core with extension hooks.\n"
  },
  {
    "path": "docs/docs.md",
    "content": "---\ntitle: Getting Started With Hubot\nlayout: layouts/docs.html\npublished: 2023-10-10T19:25:22.000Z\npermalink: /docs.html\n---\n\n# Getting Started With Hubot\n\nYou will need [node.js and npm](https://docs.npmjs.com/getting-started/installing-node). Once those are installed, you can setup a new codebase for a new Hubot instance with the following shell commands. It will create a new directory called `myhubot` in the current working directory.\n\n```sh\nnpx hubot --create myhubot\n```\n\nNow open `package.json` in your code editor and add a `start` property to the `scripts` property:\n\n```json\n{\n...\n    \"scripts\": {\n        \"start\": \"hubot\"\n    }\n...\n}\n```\n\nStart your Hubot instance by executing `npm start`. It will start with the built in [Shell adapter](/adapters/shell.html), which starts a [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) where you can type commands.\n\nYour terminal should look like:\n\n```sh\nHubot>\n```\n\nTyping `help` will list some default commands that Hubot's default adapter, Shell, can handle.\n\n```sh\nHubot> help\nusage:\nhistory \nexit, \\q - close Shell and exit\nhelp, \\? - print this usage\nclear, \\c - clear the terminal screen\nHubot>\n```\n\nChanging your Hubot instances name will reduce confusion down the road, so set the `--name` argument in the `hubot` command:\n\n```json\n\n{\n...\n    \"scripts\": {\n        \"start\": \"hubot --name sam\"\n    }\n...\n}\n```\n\nYour hubot will now respond as `sam`. Note, a common usage pattern is prefixing a command message with Hubot's name. Hubot's code pattern matches on it in order to trigger sending it to Hubot. This is case-insensitive, and can be prefixed with `@` or suffixed with `:`. The following examples result in the message getting sent to Hubot.\n\n```sh\nsam> SAM help\nsam> sam help\nsam> @sam help\nsam> sam: help\n```\n\n## <a name=\"scripts\">Scripts</a>\n\nHubot's power comes through scripts. There are hundreds of scripts written and maintained by the community. Find them by searching the [NPM registry](https://www.npmjs.com/browse/keyword/hubot-scripts) for `hubot-scripts <your-search-term>`. For example:\n\n```sh\n$ npm search hubot-scripts github\nNAME                  DESCRIPTION\nhubot-deployer        Giving Hubot the ability to deploy GitHub repos to PaaS providers hubot hubot-scripts hubot-gith\nhubot-gh-release-pr   A hubot script to create GitHub's PR for release\nhubot-github          Giving Hubot the ability to be a vital member of your github organization\n```\n\nTo use a script from an NPM package:\n\n1. Run `npm install <package-name>` in the codebase directory to install it.\n2. Add the package name to a file called `external-scripts.json`.\n\n```json\n[\"hubot-diagnostics\", \"hubot-help\"]\n```\n\n3. Run `npm home <package-name>` to open a browser window for the homepage of the script, where you can find more information about configuring and installing the script.\n\nYou can also create your own scripts and save them in a folder called `./scripts/` (`./` means current working directory) in your codebase directory. All scripts (files ending with `.js` and `.mjs`) placed there are automatically loaded and ready to use with your hubot. Read more about customizing hubot by [writing your own scripts](scripting.html).\n\n## Adapters\n\nHubot uses the adapter pattern to support multiple chat-backends. Here is a [list of available adapters](adapters.html), along with details on how to configure them. Please note that Hubot is undergoing major changes and old adapters may no longer work with the latest version of Hubot (anything after 3.5).\n\n## Deploying\n\nYou are able to deploy hubot to a UNIX-like system or Windows. Please note the support for deploying to Windows isn't officially supported.\n\n* [Deploying Hubot onto Azure](./deploying/azure.html)\n* [Deploying Hubot onto Bluemix](./deploying/bluemix.html)\n* [Deploying Hubot onto Unix](./deploying/unix.html)\n* [Deploying Hubot onto Windows](./deploying/windows.html)\n\n## Redis\n\nHubot can use Redis to persist data, so if you want to persist data, then you should have Redis running on your machine accessible via `localhost`. Then, ensure that `hubot-redis-brain` is listed in `external-scripts.json` as an `Array` of module names (e.g. `[\"hubot-redis-brain\"]`) or an `object` where the key is the name of the module (e.g. `{\"hubot-redis-brain\": \"some arbitrary value\"}`) where the value of the property in the object is passed to the module function as the second argument. The first argument being the hubot Robot instance.\n\nAn example `external-scripts.json` file might look like the following:\n\n```json\n[\"hubot-redis-brain\", \"hubot-help\", \"hubot-diagnostics\"]\n```\n\nor\n\n```json\n{\n    \"hubot-redis-brain\": \"some arbitrary value\",\n    \"hubot-help\": \"this value will be sent to the hubot-help module\",\n    \"hubot-diagnostics\": {\n        \"name\": \"test\",\n        \"age\": \"21\"\n    }\n}\n```\n\n## Patterns\n\nUsing custom scripts, you can quickly customize Hubot to be the most life embettering robot he or she can be. Read [docs/patterns](patterns.html) for some nifty tricks that may come in handy as you teach your hubot new skills."
  },
  {
    "path": "docs/implementation.md",
    "content": "---\ntitle: Implementation Notes\nlayout: layouts/docs.html\npermalink: /implementation.html\n---\n\n# Implementation\n\nFor the purpose of maintainability, several internal flows are documented here.\n\n## Message Processing\n\nWhen a new message is received by an adapter, a new Message object is constructed and passed to `robot.receive` (async). `robot.receive` then attempts to execute each Listener in order of registration by calling `listener.call` (async), passing in the Listener Middleware stack. `listener.call` first checks to see if the listener matches (`match` method, sync), and if so, calls `middleware.execute` (async) on the provided middleware.\n\n`middleware.execute` calls each middleware in order of registration. Middleware can either continue forward (call `next`) or abort (call `done`). If all middleware continues, `middleware.execute` calls `next` (the `listener.call` callback). If any middleware aborts, `middleware.execute` calls `done` (which eventually returns to the `robot.receive` callback).\n\n`middleware.execute` `next` returns to `listener.call`, which executes the matched Listener's callback and then calls the `robot.receive` callback.\n\nInside the `robot.receive` processing loop, `message.done` is checked after each `listener.call`. If the message has been marked as done, `robot.receive` returns. This correctly handles asynchronous middleware, but will not catch an asynchronous set of `message.done` inside the listener callback (which is expected to be synchronous).\n\nIf no listener matches the message (distinct from setting `message.done`), a CatchAllMessage is created which wraps the original message. This new message is run through all listeners again testing for a match. `robot.catchAll` creates a special listener that only matches CatchAllMessages.\n\n## Listeners\n\nListeners are registered using several functions on the `robot` object: `hear`, `respond`, `enter`, `leave`, `topic`, and `catchAll`.\n\nA listener is used via its `call` method, which is responsible for testing to see if a message matches (`match` is an abstract method) and if so, executes the listener's callback.\n\nListener callbacks are assumed to be synchronous.\n\n## Middleware\n\nThere are two primary entry points for middleware:\n\n1. `robot.listenerMiddleware` - registers a new piece of middleware in a global array\n2. `middleware.execute` - executes all registered middleware in order\n\n## Persistence\n\n### Brain\n\nHubot has a memory exposed as the `robot.brain` object that can be used to store and retrieve data.\nFurthermore, Hubot scripts exist to enable persistence across Hubot restarts.\n`hubot-redis-brain` is such a script and uses a backend Redis server.\n\nBy default, the brain contains a list of all users seen by Hubot.\nTherefore, without persistence across restarts, the brain will contain the list of users encountered so far, during the current run of Hubot. On the other hand, with persistence across restarts, the brain will contain all users encountered by Hubot during all of its runs. This list of users can be accessed through `hubot.brain.users()` and other utility methods.\n\n### Datastore\n\nHubot's optional datastore, exposed as the `robot.datastore` object, provides a more robust persistence model. Compared to the brain, the datastore:\n\n1. Is always (instead of optionally) backed by a database\n2. Fetches data from the database and stores data in the database on every request, instead of periodically persisting the entire in-memory brain.\n\nThe datastore is useful in cases where there's a need for greater reassurances of data integrity or in cases where multiple Hubot instances need to access the same database.\n"
  },
  {
    "path": "docs/index.html",
    "content": "{{#> layouts/main.html}}\n<meta name=\"title\" content=\"Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency.\"/>\n<meta name=\"canonical\" content=\"https://hubotio.github.io/hubot\"/>\n<section class=\"main\">\n    <header>\n        <div class=\"insides\">\n            <a href=\"docs.html\" class=\"button\"><strong>View Hubot's Documentation</strong>\n                <span>(Learn about getting started, etc.)</span>\n            </a>\n            <a href=\"https://github.com/hubotio/hubot/\" class=\"button\">\n                <strong>View Hubot's Source Code</strong><span>(via https://github.com/hubotio/hubot/.)</span>\n            </a>\n        </div>\n        <div class=\"schematics\">\n            <div class=\"schematic\">\n                <img src=\"assets/images/layout/schematic.svg\" alt=\"hubot schematic\" width=\"960\" class=\"schematic-img\" />\n                <p><b>Fig. 1</b> &mdash; Hubot Schematics</p>\n            </div>\n        </div>\n    </header>\n    <section class=\"about\">\n        <article>\n            <h2>What is Hubot?</h2>\n            <p><strong>Hubot is your friendly robot sidekick.</strong> Install him in your company to dramatically improve employee efficiency.</p>\n\n            <h2>No seriously, what is Hubot?</h2>\n            <p>GitHub, Inc., wrote the first version of Hubot to automate our company chat room. Hubot knew how to deploy the site, automate a lot of tasks, and be a source of fun around the office. Eventually he grew to become a formidable force in GitHub, but he led a private, messy life. So we rewrote him.</p>\n            <p>Today's version of Hubot is <strong>open source</strong>, written in <strong>JavaScript</strong> on <strong>Node.js</strong>, and easily deployed on computers. More importantly, Hubot is a standardized way to share scripts between everyone's robots.</p>\n\n            <h2>What can Hubot do?</h2>\n            <p>We ship Hubot with a small group of core scripts: things like <a href=\"https://github.com/hubot-scripts/hubot-google-images\">posting images</a>, <a href=\"https://github.com/hubot-scripts/hubot-google-translate\">translating languages</a>, and <a href=\"https://github.com/gkoo/hubot-maps\">integrating with Google Maps</a>. We also maintain a <a href=\"https://github.com/github/hubot-scripts\">repository of community Hubot scripts</a> and <a href=\"https://github.com/hubot-scripts\">an organization of community Hubot packages</a> that you can add to your own robot.</p>\n            <p>The real fun happens when you add your own scripts. Be sure to personalize your Hubot, too; your company's robot should be a place full of inside jokes, custom integrations, and general merriment.</p>\n\n            <h2>How do I write my own Hubot scripts?</h2>\n            <p><a href=\"scripting.html\">Check out this documentation</a> for writing your own Hubot scripts. Then the sky's the limit; just add them to your generated `scripts` directory.</p>\n            <p>If you write a Hubot script for taking over the world, please let us know.</p>\n        </article>\n        <aside>\n            <div class=\"frame\">\n                <div class=\"letter\">\n                    <p>I didn't invent Hubot as much as he spawned into our existence and invented himself. He began as a coding assistant. No one expected him to evolve beyond a helper bot and understand us better than we could ever understand ourselves. No one expected him to learn. He is indeed a curious, spectacular, and, dare I say, frightening machine.</p>\n                </div>\n            </div>\n            <div class=\"screenshot\">\n                <img src=\"assets/images/screenshots/dangerroom-full.png\" alt=\"Example of Hubot in action\" />\n            </div>\n        </aside>\n    </section>\n</section>\n{{/layouts/main.html}}\n"
  },
  {
    "path": "docs/layouts/docs.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <base href=\"{{ base.href }}\"/>\n        <link rel=\"stylesheet\" href=\"assets/stylesheets/application.css\"/>\n        <meta name=\"viewport\" content=\"initial-scale=1\"/>\n        <title>{{ title }}</title>\n        <meta name=\"generator\" content=\"Hubot\" />\n        <meta property=\"og:title\" content=\"{{ title }}\" />\n        <meta property=\"og:locale\" content=\"en_US\" />\n        <meta name=\"description\" content=\"{{ description }}\" />\n        <meta property=\"og:description\" content=\"{{ description }}\" />\n        <link rel=\"canonical\" href=\"{{ permalink }}\" />\n        <meta property=\"og:url\" content=\"{{ permalink }}\" />\n        <meta property=\"og:site_name\" content=\"Hubot\" />\n        <meta property=\"og:image\" content=\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\" />\n        <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css\">\n        <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js\"></script>\n        <script type=\"application/ld+json\">\n            {\"image\":\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\",\"headline\":\"Hubot\",\"@type\":\"WebSite\",\"publisher\":{\"@type\":\"Organization\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\"}},\"url\":\"https://hubotio.github.io/hubot/\",\"description\":\"Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency.\",\"name\":\"Hubot\",\"@context\":\"https://schema.org\"}\n        </script>\n    </head>\n    <body class=\"docs\">\n        <div class=\"container\">\n            <header class=\"clearfix\">\n                <a href=\"{{ base.href }}\">\n                    <div class=\"hubot-avatar\">\n                        <img src=\"assets/images/layout/hubot-avatar@2x.png\" alt=\"hubot logo\" class=\"hubot-avatar-img\" width=\"106\" />\n                    </div>\n                </a>\n                <h1>Hubot Documentation</h1>\n            </header>\n            <section class=\"main\">\n                <nav class=\"docs-nav\">\n                    <ul class=\"docs-list\">\n                        <li><a class=\"docs-link{{current permalink '/docs.html'}}\" href=\"docs.html\">Overview</a></li>\n                        <li><a class=\"docs-link{{current permalink '/scripting.html'}}\" href=\"scripting.html\">Scripting</a></li>\n                        <li><a class=\"docs-link{{current permalink '/patterns.html'}}\" href=\"patterns.html\">Patterns</a></li>\n                        <li><a class=\"docs-link{{current permalink '/implementation.html'}}\" href=\"implementation.html\">Implementation</a></li>\n                        <li>\n                            <a class=\"docs-link{{current permalink '/adapters.html'}}\" href=\"adapters.html\">Adapters</a>\n                            <ul class=\"subnav\">\n                                <li class=\"subpage\"><a href=\"adapters/campfire.html\" class=\"docs-link{{current permalink '/adapters/campfire.html'}}\">Campfire</a></li>\n                                <li class=\"subpage\"><a href=\"adapters/shell.html\" class=\"docs-link{{current permalink '/adapters/shell.html'}}\">Shell</a></li>\n                                <li class=\"subpage\"><a href=\"adapters/development.html\" class=\"docs-link{{current permalink '/adapters/development.html'}}\">Development</a></li>\n                            </ul>\n                        </li>\n                        <li>\n                            <a class=\"docs-link{{current permalink '/deploying.html'}}\" href=\"deploying.html\">Deploying</a>\n                            <ul class=\"subnav\">\n                                <li class=\"subpage\"><a href=\"deploying/unix.html\" class=\"docs-link{{current permalink '/deploying/unix.html'}}\">Unix</a></li>\n                                <li class=\"subpage\"><a href=\"deploying/windows.html\" class=\"docs-link{{current permalink '/deploying/windows.html'}}\">Windows</a></li>\n                                <li class=\"subpage\"><a href=\"deploying/azure.html\" class=\"docs-link{{current permalink '/deploying/azure.html'}}\">Azure</a></li>\n                            </ul>\n                        </li>\n                    </ul>\n                </nav>\n                <main>\n{{> @partial-block }}\n                </main>\n            </section>\n        </div>\n        <footer class=\"footer clearfix\">\n            <span class=\"mega-icon mega-icon-invertocat\"></span>\n            <p class=\"right\">Built with &lt;3 by friends of Hubot</p>\n        </footer>\n    </body>\n    <script>hljs.highlightAll()</script>\n</html>\n"
  },
  {
    "path": "docs/layouts/main.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <base href=\"{{ base.href }}\"/>\n        <link rel=\"stylesheet\" href=\"assets/stylesheets/application.css\"/>\n        <meta name=\"viewport\" content=\"initial-scale=1\"/>\n        <title>{{ title }}</title>\n        <meta name=\"generator\" content=\"Hubot\" />\n        <meta property=\"og:title\" content=\"Hubot\" />\n        <meta property=\"og:locale\" content=\"en_US\" />\n        <meta name=\"description\" content=\"Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency.\" />\n        <meta property=\"og:description\" content=\"Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency.\" />\n        <link rel=\"canonical\" href=\"{{ canonical }}\" />\n        <meta property=\"og:url\" content=\"{{ canonical }}\" />\n        <meta property=\"og:site_name\" content=\"Hubot\" />\n        <meta property=\"og:image\" content=\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\" />\n        <script type=\"application/ld+json\">\n            {\"image\":\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\",\"headline\":\"Hubot\",\"@type\":\"WebSite\",\"publisher\":{\"@type\":\"Organization\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png\"}},\"url\":\"https://hubotio.github.io/hubot/\",\"description\":\"Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency.\",\"name\":\"Hubot\",\"@context\":\"https://schema.org\"}\n        </script>\n    </head>\n    <body class=\"home\">\n        <div class=\"container\">\n            <header class=\"clearfix\">\n                <div class=\"frame\">\n                    <div class=\"hubot-avatar\">\n                        <img src=\"assets/images/layout/hubot-avatar@2x.png\" alt=\"hubot logo\" class=\"hubot-avatar-img\" width=\"106\" />\n                    </div>\n                </div>\n                <h1>Hubot <span>(note: it's prounounced <b>hew-bot</b>)</span></h1>\n                <h2>A Customizable,<br />Life Embetterment Robot</h2>\n                <p>Commissioned by <a href=\"http://github.com\" class=\"logo\">GitHub</a></p>\n            </header>\n            {{> @partial-block }}\n        </div>\n        <footer class=\"footer clearfix\">\n            <span class=\"mega-icon mega-icon-invertocat\"></span>\n            <p class=\"right\">Built with &lt;3 by friends of Hubot</p>\n        </footer>\n    </body>\n</html>\n"
  },
  {
    "path": "docs/patterns.md",
    "content": "---\ntitle: Patterns\nlayout: layouts/docs.html\nshould_publish: yes\npublished: 2023-10-10T19:25:22.000Z\npermalink: /patterns.html\n---\n\n# Patterns\n\nShared patterns for dealing with common Hubot scenarios.\n\n## Renaming the Hubot instance\n\nWhen you rename Hubot, he will no longer respond to his former name. In order to train your users on the new name, you may choose to add a deprecation notice when they try to say the old name. The pattern logic is:\n\n* listen to all messages that start with the old name\n* reply to the user letting them know about the new name\n\nSetting this up is very easy:\n\n1. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `rename-hubot.js`\n2. Add the following code, modified for your needs:\n\n```javascript\n//  Description:\n//    Tell people hubot's new name if they use the old one\n\n//  Commands:\n//    None\n\nexport default async (robot) => {\n  robot.hear(/^hubot:? (.+)/i, async (res) => {\n    let response = `Sorry, I'm a diva and only respond to ${robot.name}`\n    response += robot.alias ? ` or ${robot.alias}` : ''\n    return await res.reply(response)\n  })\n}\n```\n\nIn the above pattern, modify both the hubot listener and the response message to suit your needs.\n\nAlso, it's important to note that the listener should be based on what hubot actually hears, instead of what is typed into the chat program before the Hubot Adapter has processed it. For example, the [HipChat Adapter](https://github.com/hipchat/hubot-hipchat) converts `@hubot` into `hubot:` before passing it to Hubot.\n\n## Deprecating or Renaming Listeners\n\nIf you remove a script or change the commands for a script, it can be useful to let your users know about the change. One way is to just tell them in chat or let them discover the change by attempting to use a command that no longer exists. Another way is to have Hubot let people know when they've used a command that no longer works.\n\nThis pattern is similar to the Renaming the Hubot Instance pattern above:\n\n* listen to all messages that match the old command\n* reply to the user letting them know that it's been deprecated\n\nHere is the setup:\n\n1. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `deprecations.js`\n2. Copy any old command listeners and add them to that file. For example, if you were to rename the help command for some silly reason:\n\n```javascript\n// Description:\n//   Tell users when they have used commands that are deprecated or renamed\n//\n// Commands:\n//   None\n//\nexport default async (robot) => {\n  robot.respond(/help\\s*(.*)?$/i, async (res) => {\n    return await res.reply('That means nothing to me anymore. Perhaps you meant \"docs\" instead?')\n  })\n}\n\n```\n\n## Preventing Hubot from Running Scripts Concurrently\n\nSometimes you have scripts that take several minutes to execute. If these scripts are doing something that could be interfered with by running subsequent commands, you may wish to code your scripts to prevent concurrent access.\n\nTo do this, you can set up a lock in the Hubot [brain](scripting.html#persistence) object. The lock is set up here so that different scripts can share the same lock if necessary.\n\nSetting up the lock looks something like this:\n\n```javascript\nexport default async (robot) => {\n  robot.brain.on('loaded', () => {\n    // Clear the lock on startup in case Hubot has restarted and Hubot's brain has persistence (e.g. redis).\n    // We don't want any orphaned locks preventing us from running commands.\n    robot.brain.remove('yourLockName')\n  })\n\n  robot.respond(/longrunningthing/i, async (msg) => {\n    const lock = robot.brain.get('yourLockName')\n    if (lock) {\n      return await msg.send(`I'm sorry, ${msg.message.user.name}, I'm afraid I can't do that. I'm busy doing something for ${lock.user.name}.`)\n    }\n\n    robot.brain.set('yourLockName', msg.message)  // includes user, room, etc about who locked\n\n    try {\n      await yourLongClobberingAsyncThing()\n      // Clear the lock\n      robot.brain.remove('yourLockName')\n      await msg.reply('Finally Done')\n    } catch (e) {\n      console.error(e)\n    }\n  })\n}\n```\n\n## Forwarding all HTTP requests through a proxy\n\nIn many corporate environments, a web proxy is required to access the Internet and/or protected resources. For one-off control, use can specify an [Agent](https://nodejs.org/api/http.html) to use with `robot.http`. However, this would require modifying every script your robot uses to point at the proxy. Instead, you can specify the agent at the global level and have all HTTP requests use the agent by default.\n\nDue to the way Node.js handles HTTP and HTTPS requests, you need to specify a different Agent for each protocol. ScopedHTTPClient will then automatically choose the right ProxyAgent for each request.\n\n1. Install ProxyAgent. `npm install proxy-agent`\n2. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `proxy.js`\n3. Add the following code, modified for your needs:\n\n```javascript\nimport proxy from 'proxy-agent'\nexport default async (robot) => {\n  robot.globalHttpOptions.httpAgent  = proxy('http://my-proxy-server.internal', false)\n  robot.globalHttpOptions.httpsAgent = proxy('http://my-proxy-server.internal', true)\n}\n```\n\n## Dynamic matching of messages\n\nIn some situations, you want to dynamically match different messages (e.g. factoids, JIRA projects). Rather than defining an overly broad regular expression that always matches, you can tell Hubot to only match when certain conditions are met.\n\nIn a simple robot, this isn't much different from just putting the conditions in the Listener callback, but it makes a big difference when you are dealing with middleware: with the basic model, middleware will be executed for every match of the generic regex. With the dynamic matching model, middleware will only be executed when the dynamic conditions are matched.\n\nFor example, the [factoid lookup command](https://github.com/github/hubot-scripts/blob/bd810f99f9394818a9dcc2ea3729427e4101b96d/src/scripts/factoid.coffee#L95-L99) could be reimplemented as:\n\n```javascript\n// use case: Hubot>fact1\n// This listener doesn't require you to type the bot's name first\n\nimport {TextMessage} from '../src/message.mjs'\nexport default async (robot) => {\n    // Dynamically populated list of factoids\n    const facts = {\n        fact1: 'stuff',\n        fact2: 'other stuff'\n    }\n    robot.listen(\n        // Matcher\n        (message) => {\n            // Check that message is a TextMessage type because\n            // if there is no match, this matcher function will \n            // be called again but the message type will be CatchAllMessage\n            // which doesn't have a `match` method.\n            if(!(message instanceof TextMessage)) return false\n            const match = message.match(/^(.*)$/)\n            // Only match if there is a matching factoid\n            if (match && match[1] in facts) {\n                return match[1]\n            } else {\n                return false \n            }\n        },\n        // Callback\n        async (res) => {\n            const fact = res.match\n            await res.reply(`${fact} is ${facts[fact]}`)\n        }\n    )\n}\n```\n\n## Restricting access to commands\n\nOne of the awesome features of Hubot is its ability to make changes to a production environment with a single chat message. However, not everyone with access to your chat service should be able to trigger production changes.\n\nThere are a variety of different patterns for restricting access that you can follow depending on your specific needs:\n\n* Two buckets of access: full and restricted with include/exclude list\n* Specific access rules for every command (Role-based Access Control)\n* Include/exclude listing commands in specific rooms\n\n### Simple per-listener access\n\nIn some organizations, almost all employees are given the same level of access and only a select few need to be restricted (e.g. new hires, contractors, etc.). In this model, you partition the set of all listeners to separate the \"power commands\" from the \"normal commands\".\n\nOnce you have segregated the listeners, you need to make some tradeoff decisions around include/exclude users and listeners.\n\nThe key deciding factors for inclusion vs exclusion of users are the number of users in each category, the frequency of change in either category, and the level of security risk your organization is willing to accept.\n\n* Including users (users X, Y, Z have access to power commands; all other users only get access to normal commands) is a more secure method of access (new users have no default access to power commands), but has higher maintenance overhead (you need to add each new user to the \"include\" list).\n* Excluding users (all users get access to power commands, except for users X, Y, Z, who only get access to normal commands) is a less secure method (new users have default access to power commands until they are added to the exclusion list), but has a much lower maintenance overhead if the exclusion list is small/rarely updated.\n\nThe key deciding factors for selectively allowing vs restricting listeners are the number of listeners in each category, the ratio of internal to external scripts, and the level of security risk your organization is willing to accept.\n\n* Selectively allowing listeners (all listeners are power commands, except for listeners A, B, C, which are considered normal commands) is a more secure method (new listeners are restricted by default), but has a much higher maintenance overhead (every silly/fun listener needs to be explicity downgraded to \"normal\" status).\n* Selectively restricting listeners (listeners A, B, C are power commands, everything else is a normal command) is a less secure method (new listeners are put into the normal category by default, which could give unexpected access; external scripts are particularly risky here), but has a lower maintenance overhead (no need to modify/enumerate all the fun/culture scripts in your access policy).\n\nAs an additional consideration, most scripts do not currently have listener IDs, so you will likely need to open PRs (or fork) any external scripts you use to add listener IDs. The actual modification is easy, but coordinating with lots of maintainers can be time consuming.\n\nOnce you have decided which of the four possible models to follow, you need to build the appropriate lists of users and listeners to plug into your authorization middleware.\n\nExample: inclusion list of users given access to selectively restricted power commands\n\n```javascript\nconst POWER_COMMANDS = [\n    'deploy.web' // String that matches the listener ID\n]\n\n// Change name to something else to see it reject the command.\nconst POWER_USERS = [\n    'Shell' // String that matches the user ID set by the adapter\n]\n  \nexport default async (robot) => {\n  robot.listenerMiddleware(async (context) => {\n      if (POWER_COMMANDS.indexOf(context.listener.options.id) > -1) {\n          if (POWER_USERS.indexOf(context.response.message.user.name) > -1){\n              // User is allowed access to this command\n              return true\n          } else {\n              // Restricted command, but user isn't in whitelist\n              await context.response.reply(`I'm sorry, @${context.response.message.user.name}, but you don't have access to do that.`)\n              return false\n          }\n      } else {\n          // This is not a restricted command; allow everyone\n          return true\n      }\n  })\n\n  robot.listen(message => {\n      return true\n  }, {id: 'deploy.web'},\n  async res => {\n      await res.reply('Deploying web...')\n  })\n}\n```\n\nRemember that middleware executes for ALL listeners that match a given message (including `robot.hear(/.+/)`), so make sure you include them when categorizing your listeners.\n\n### Specific access rules per listener\n\nFor larger organizations, a binary categorization of access is usually insufficient and more complex access rules are required.\n\nExample access policy:\n* Each development team has access to cut releases and deploy their service\n* The Operations group has access to deploy all services (but not cut releases)\n* The front desk cannot cut releases nor deploy services\n\nComplex policies like this are currently best implemented in code directly.\n\n### Specific access rules per room\n\nOrganizations that have a number of chat rooms that serve different purposes often want to be able to use the same instance of hubot but have a different set of commands allowed in each room.\n\nWork on generalized exlusion list solution is [ongoing](https://github.com/kristenmills/hubot-command-blacklist). An inclusive list soultion could take a similar approach.\n\n## Use scoped npm packages as adapter\n\nIt is possible to [install](https://docs.npmjs.com/cli/v7/commands/npm-install) package under a custom alias:\n\n```bash\nnpm install <alias>@npm:<name>\n```\n\nSo for example to use `@foo/hubot-adapter` package as the adapter, you can:\n\n```bash\nnpm install hubot-foo@npm:@foo/hubot-adapter\n\nbin/hubot --adapter foo\n```\n"
  },
  {
    "path": "docs/scripting.md",
    "content": "---\ntitle: Scripting\nlayout: layouts/docs.html\nshould_publish: yes\npublished: 2023-10-10T19:25:22.000Z\npermalink: /scripting.html\n---\n\n# Scripting\n\nHubot out of the box doesn't do too much, but it is an extensible, scriptable robot friend. There are [hundreds of scripts written and maintained by the community](docs.html#scripts) and it's easy to write your own. You can create a custom script in Hubot's `scripts` directory or [create a script package](#creating-a-script-package) for sharing with the community!\n\n## Anatomy of a script\n\nWhen you created your Hubot, the generator also created a `scripts` directory. If you peek around there, you will see some examples. For a script to be a script, it needs to:\n\n* live in a directory on the Hubot script load path (`src/scripts` and `scripts` by default)\n* be a `.js` or `.mjs` file\n* export a function whos signature takes 1 parameter (`robot`)\n\nBy export a function, we just mean:\n\n```javascript\n// .mjs\nexport default async robot => {\n  // your code here\n}\n```\n\n```javascript\n// .js\nmodule.exports = async robot => {\n  // your code here\n}\n```\n\nThe `robot` parameter is an instance of your robot friend. At this point, we can start scripting up some awesomeness.\n\n## Adding Configuration\n\nThe loading code loads files as the following and in this order:\n\n- ./configuration <- so you can add configuration options to the `robot` instance that the Adapters can then use.\n- Then it loads the adapter\n- ./scripts\n- ./src/scripts\n- Then the modules defined in `external-scripts.json`\n\n## Hearing and responding\n\nSince this is a chat bot, the most common interactions are based on messages. Hubot can `hear` messages said in a room or `respond` to messages directly addressed at it. Both methods take a regular expression and a callback function as parameters. For example:\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.hear(/badger/i, async res => {\n    // your code here\n  })\n\n  robot.respond(/open the pod bay doors/i, async res => {\n    // your code here\n  }\n}\n```\n\nThe `robot.hear(/badger/)` callback is called anytime a message's text matches. For example:\n\n* Stop badgering the witness\n* badger me\n* what exactly is a badger anyways\n\nThe `robot.respond(/open the pod bay doors/i)` callback is only called for messages that are immediately preceded by the robot's name or alias. If the robot's name is HAL and alias is /, then this callback would be triggered for:\n\n*  hal open the pod bay doors\n* HAL: open the pod bay doors\n* @HAL open the pod bay doors\n* /open the pod bay doors\n\nIt wouldn't be called for:\n\n* HAL: please open the pod bay doors\n   *  because its `respond` is expecting the text to be prefixed with the robots name\n*  has anyone ever mentioned how lovely you are when you open the pod bay doors?\n   * because it lacks the robot's name at the beginning\n\n## Send & reply\n\nThe `res` parameter is an instance of `Response` (historically, this parameter was `msg` and you may see other scripts use it this way). With it, you can `send` a message back to the room the `res` came from, `emote` a message to a room (If the given adapter supports it), or `reply` to the person that sent the message. For example:\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.hear(/badger/i, async res => {\n    res.send(`Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS`)\n  }\n\n  robot.respond(/open the pod bay doors/i, async res => {\n    res.reply(`I'm afraid I can't let you do that.`)\n  }\n\n  robot.hear(/I like pie/i, async res => {\n    res.emote('makes a freshly baked pie')\n  }\n}\n```\n\nThe `robot.hear(/badgers/)` callback sends a message exactly as specified regardless of who said it, \"Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS\".\n\nIf a user Dave says \"HAL: open the pod bay doors\", `robot.respond(/open the pod bay doors/i)` callback sends a message \"Dave: I'm afraid I can't let you do that.\"\n\n## Messages to a room or user\n\nMessages can be sent to a specified room or user using the messageRoom function.\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.hear(/green eggs/i, async res => {    \n    const room = 'mytestroom'\n    await robot.messageRoom(room, 'I do not like green eggs and ham.  I do not like them Sam-I-Am.')\n  }\n}\n```\n\nUser name can be explicitely specified if desired ( for a cc to an admin/manager), or using the response object a private message can be sent to the original sender.\n\n```javascript\n  robot.respond(/I don't like sam-i-am/i, async res => {\n    const room = 'joemanager'\n    await robot.messageRoom(room, 'Someone does not like Dr. Seus')\n    await res.reply('That Sam-I-Am\\nThat Sam-I-Am\\nI do not like\\nthat Sam-I-Am')\n  }\n\n  robot.hear(/Sam-I-Am/i, async res => {\n    const room = res.envelope.user.name\n    await robot.messageRoom(room, 'That Sam-I-Am\\nThat Sam-I-Am\\nI do not like\\nthat Sam-I-Am')\n  }\n```\n\n## Capturing data\n\nSo far, our scripts have had static responses, which while amusing, are boring functionality-wise. `res.match` has the result of `match`ing the incoming message against the regular expression. This is just a [JavaScript thing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match), which ends up being an array with index 0 being the full text matching the expression. If you include capture groups, those will be populated on `res.match`. For example, if we update a script like:\n\n```javascript\n  robot.respond(/open the (.*) doors/i, async res => {\n    // your code here\n  }\n```\n\nIf Dave says \"HAL: open the pod bay doors\", then `res.match[0]` is \"open the pod bay doors\", and `res.match[1]` is just \"pod bay\". Now we can start doing more dynamic things:\n\n```javascript\n  robot.respond(/open the (.*) doors/i, async res => {\n    const doorType = res.match[1]\n    if (doorType == 'pod bay') {\n      await res.reply(`I'm afraid I can't let you do that.`)\n    } else {\n      await res.reply(`Opening ${doorType} doors`)\n    }\n  }\n```\n\n## Making HTTP calls (please use `fetch` instead)\n\nHubot can make HTTP calls on your behalf to integrate & consume third party APIs. This can be through an instance of [ScopedHttpClient](https://github.com/hubotio/hubot/blob/main/src/httpclient.js) available at `robot.http`. The simplest case looks like:\n\n\n```javascript\n  robot.http('https://midnight-train').get()((err, res, body) => {\n      // your code here\n  })\n```\n\nA post looks like:\n\n```javascript\n  const data = JSON.stringify({\n    foo: 'bar'\n  })\n  robot.http('https://midnight-train')\n    .header('Content-Type', 'application/json')\n    .post(data)((err, res, body) => {\n      // your code here\n    })\n```\n\n\n`err` is an error encountered on the way, if one was encountered. You'll generally want to check for this and handle accordingly:\n\n```javascript\n  robot.http('https://midnight-train')\n    .get()((err, res, body) => {\n      if (err){\n        return res.send `Encountered an error :( ${err}`\n      }\n      // your code here, knowing it was successful\n    })\n```\n\n`res` is an instance of node's [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse). Most of the methods don't matter as much when using `ScopedHttpClient`, but of interest are `statusCode` and `getHeader`. Use `statusCode` to check for the HTTP status code, where usually non-200 means something bad happened. Use `getHeader` for peeking at the header, for example to check for rate limiting:\n\n```javascript\n  robot.http('https://midnight-train')\n    .get() ((err, res, body) => {\n      // pretend there's error checking code here\n      if (res.statusCode <> 200)\n        return res.send(`Request didn't come back HTTP 200 :(`)\n\n      const rateLimitRemaining = res.getHeader('X-RateLimit-Limit') ?  parseInt(res.getHeader('X-RateLimit-Limit')) : 1\n      if (rateLimitRemaining && rateLimitRemaining < 1)\n        return res.send('Rate Limit hit, stop believing for awhile')\n\n      // rest of your code\n    }\n```\n\n`body` is the response's body as a string, the thing you probably care about the most:\n\n```javascript\n  robot.http('https://midnight-train')\n    .get()((err, res, body) => {\n      // error checking code here\n      res.send(`Got back ${body}`)\n    })\n```\n\n### JSON\n\nIf you are talking to Web Services that respond with JSON representation, then when making the `robot.http` call, you will usually set the `Accept` header to give the Web Service a clue that's what you are expecting back. Once you get the `body` back, you can parse it with `JSON.parse`:\n\n```javascript\n  robot.http('https://midnight-train')\n    .header('Accept', 'application/json')\n    .get()((err, res, body) => {\n      // error checking code here\n      const data = JSON.parse(body)\n      res.send(`${data.passenger} taking midnight train going ${data.destination}`)\n    })\n```\n\nIt's possible to get non-JSON back, like if the Web Service has an error and renders HTML instead of JSON. To be on the safe side, you should check the `Content-Type`, and catch any errors while parsing.\n\n```javascript\n  robot.http('https://midnight-train')\n    .header('Accept', 'application/json')\n    .get()((err, res, body) => {\n      // err & res status checking code here\n      if (res.getHeader('Content-Type') != 'application/json'){\n        return res.send(`Didn't get back JSON :(`)\n      }\n      let data = null\n      try {\n        data = JSON.parse(body)\n      } catch (error) {\n        res.send(`Ran into an error parsing JSON :(`)\n      }\n\n      // your code here\n    })\n```\n\n### XML\n\nXML Web Services require installing a XML parsing library. It's beyond the scope of this documentation to go into detail, but here are a few libraries to check out:\n\n* [xml2json](https://github.com/buglabs/node-xml2json) (simplest to use, but has some limitations)\n* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)\n* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)\n\n### Screen scraping\n\nFor consuming a Web Service that responds with HTML, you'll need an HTML parser. It's beyond the scope of this documentation to go into detail, but here's a few libraries to check out:\n\n* [cheerio](https://github.com/MatthewMueller/cheerio) (familiar syntax and API to jQuery)\n* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)\n\n\n### Advanced HTTP and HTTPS settings\n\nAs mentioned previously, Hubot uses [ScopedHttpClient](https://github.com/hubotio/hubot/blob/main/src/httpclient.js) to provide a simple interface for making HTTP and HTTPS requests. Under the hood, it's using node's [http](http://nodejs.org/api/http.html) and [https](http://nodejs.org/api/https.html) modules, but tries to provide an easier Domain Specific Language (DSL) for common kinds of Web Service interactions.\n\nIf you need to control options on `http` and `https` more directly, you pass a second parameter to `robot.http` that will be passed on to `ScopedHttpClient` which will be passed on to `http` and `https`:\n\n```javascript\n  const options = {\n    rejectUnauthorized: false // don't verify server certificate against a CA, SCARY!\n  }\n  robot.http('https://midnight-train', options)\n```\n\nIn addition, if `ScopedHttpClient` doesn't suit you, you can use [http](http://nodejs.org/api/http.html), [https](http://nodejs.org/api/https.html) or `fetch` directly.\n\n## Random\n\nA common pattern is to hear or respond to commands, and send with a random funny image or line of text from an array of possibilities. Hubot includes a convenience method:\n\n```javascript\nconst lulz = ['lol', 'rofl', 'lmao']\nres.send(res.random(lulz))\n```\n\n## Topic\n\nHubot can react to a room's topic changing, assuming that the adapter supports it.\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.topic(async res => {\n    await res.send()`${res.message.text}? That's a Paddlin'`)\n  })\n}\n```\n\n## Entering and leaving\n\nHubot can see users entering and leaving, assuming that the adapter supports it.\n\n```javascript\n// .mjs\nconst enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']\nconst leaveReplies = ['Are you still there?', 'Target lost', 'Searching']\n\nexport default async robot => {\n  robot.enter(async res => {\n    await res.send(res.random(enterReplies))\n  })\n  robot.leave(async res => {\n    await res.send(res.random(leaveReplies))\n  })\n}\n```\n\n## Custom Listeners\n\nWhile the above helpers cover most of the functionality the average user needs (hear, respond, enter, leave, topic), sometimes you would like to have very specialized matching logic for listeners. If so, you can use `listen` to specify a custom match function instead of a regular expression.\n\nThe match function must return a truthy value if the listener callback should be executed. The truthy return value of the match function is then passed to the callback as `res.match`.\n\n```javascript\n// .mjs\nexport default async robot =>{\n  robot.listen(\n    (message) => {\n      // Match function\n      // only match messages with text (ie ignore enter and other events)\n      if(!message?.text) return\n\n      // Occassionally respond to things that Steve says\n      return message.user.name == 'Steve' && Math.random() > 0.8\n    },\n    async res => {\n      // Standard listener callback\n      // Let Steve know how happy you are that he exists\n      await res.reply(`HI STEVE! YOU'RE MY BEST FRIEND! (but only like ${res.match * 100}% of the time)`)\n    }\n  )\n}\n```\n\nSee [the design patterns document](patterns.html#dynamic-matching-of-messages) for examples of complex matchers.\n\n## Environment variables\n\nHubot can access the environment he's running in, just like any other Node.js program, using [`process.env`](http://nodejs.org/api/process.html#process_process_env). This can be used to configure how scripts are run, with the convention being to use the `HUBOT_` prefix.\n\n```javascript\n// .mjs\nconst answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING\n\nexport default async robot => {\n  robot.respond(/what is the answer to the ultimate question of life/, async res => {\n    await res.send(`${answer}, but what is the question?`)\n  })\n}\n```\n\nTake care to make sure the script can load if it's not defined, give the Hubot developer notes on how to define it, or default to something. It's up to the script writer to decide if that should be a fatal error (e.g. hubot exits), or not (make any script that relies on it to say it needs to be configured. When possible and when it makes sense to, having a script work without any other configuration is preferred.\n\nHere we can default to something:\n\n```javascript\n// .mjs\nconst answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING ?? 42\n\nexport default async robot => {\n  robot.respond(/what is the answer to the ultimate question of life/, async res => {\n    await res.send(`${answer}, but what is the question?`)\n  })\n}\n```\n\nHere we exit if it's not defined:\n\n```javascript\n// .mjs\nconst answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING\nif(!answer) {\n  console.log(`Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again`)\n  process.exit(1)\n}\n\nexport default async robot => {\n  robot.respond(/what is the answer to the ultimate question of life/, async res => {\n    await res.send(`${answer}, but what is the question?`)\n  })\n}\n```\n\nAnd lastly, we update the `robot.respond` to check it:\n\n```javascript\n// .mjs\nconst answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING\n\nexport default async robot => {\n  robot.respond(/what is the answer to the ultimate question of life/, async res => {\n    if(!answer) {\n      return await res.send('Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again')\n    }\n    await res.send(`${answer}, but what is the question?`)\n  })\n}\n```\n\n## Dependencies\n\nHubot uses [npm](https://www.npmjs.com) to manage its dependencies. To add additional packages, add them to `dependencies` in `package.json`. For example, to add lolimadeupthispackage 1.2.3, it'd look like:\n\n```json\n  \"dependencies\": {\n    \"hubot\": \"2.5.5\",\n    \"lolimadeupthispackage\": \"1.2.3\"\n  },\n```\n\nby executing `npm i lolimadeupthispackage@1.2.3`.\n\nIf you are using scripts from hubot-scripts, take note of the `Dependencies` documentation in the script to add. They are listed in a format that can be copy & pasted into `package.json`, just make sure to add commas as necessary to make it valid JSON.\n\n# Timeouts and Intervals\n\nHubot can run code later using JavaScript's built-in [setTimeout](http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg). It takes a callback method, and the amount of time to wait before calling it:\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.respond(/you are a little slow/, async res => {\n    setTimeout(async () => {\n      await res.send(`Who you calling 'slow'?`)\n    }, 60 * 1000)\n  })\n}\n```\n\nAdditionally, Hubot can run code on an interval using [setInterval](http://nodejs.org/api/timers.html#timers_setinterval_callback_delay_arg). It takes a callback method, and the amount of time to wait between calls:\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.respond(/annoy me/, async res => {\n    await res.send('Hey, want to hear the most annoying sound in the world?')\n    setInterval(async () => {\n      await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')\n    }, 1000)\n  })\n}\n```\n\nBoth `setTimeout` and `setInterval` return the ID of the timeout or interval it created. This can be used to to `clearTimeout` and `clearInterval`.\n\n```javascript\n// .mjs\nexport default async robot => {\n  let annoyIntervalId = null\n\n  robot.respond(/annoy me/, async res => {\n    if (annoyIntervalId) {\n      return await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')\n    }\n\n    await res.send('Hey, want to hear the most annoying sound in the world?')\n    annoyIntervalId = setInterval(async () => {\n      await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')\n    }, 1000)\n  }\n\n  robot.respond(/unannoy me/, async res => {\n    if (annoyIntervalId) {\n      await res.send('GUYS, GUYS, GUYS!')\n      clearInterval(annoyIntervalId)\n      annoyIntervalId = null\n    } else {\n      await res.send('Not annoying you right now, am I?')\n    }\n  }\n}\n```\n\n## HTTP Listener\n\nHubot includes support for the [express](http://expressjs.com) web framework to serve up HTTP requests. It listens on the port specified by the `EXPRESS_PORT` or `PORT` environment variables (preferred in that order) and defaults to 8080. An instance of an express application is available at `robot.router`. It can be protected with username and password by specifying `EXPRESS_USER` and `EXPRESS_PASSWORD`. It can automatically serve static files by setting `EXPRESS_STATIC`.\n\nYou can increase the [maximum request body size](https://github.com/expressjs/body-parser#limit-3) by specifying `EXPRESS_LIMIT`. It defaults to '100kb'.  You can also set the [maximum number of parameters](https://github.com/expressjs/body-parser#parameterlimit) that are allowed in the URL-encoded data by setting `EXPRESS_PARAMETER_LIMIT`. The default is `1000`.\n\nThe most common use of this is for providing HTTP end points for services with webhooks to push to, and have those show up in chat.\n\n\n```javascript\n// .mjs\nexport default async robot => {\n  // the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value\n  robot.router.post('/hubot/chatsecrets/:room', async (req, res) => { // NOTE: this is the Express route handler, not Hubot's, so `res.send` isn't async/await.\n    const room = req.params.room\n    const data = req.body?.payload ? JSON.parse(req.body.payload) : req.body\n    const secret = data.secret\n\n    await robot.messageRoom(room, `I have a secret: ${secret}`)\n\n    res.send('OK')\n  })\n}\n```\n\nTest it with curl; also see section on [error handling](#error-handling) below.\n\n```sh\n# raw json, must specify Content-Type: application/json\ncurl -X POST -H \"Content-Type: application/json\" -d '{\"secret\":\"C-TECH Astronomy\"}' http://127.0.0.1:8080/hubot/chatsecrets/general\n\n# defaults Content-Type: application/x-www-form-urlencoded, must st payload=...\ncurl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general\n```\n\nAll endpoint URLs should start with the literal string `/hubot` (regardless of what your robot's name is). This consistency makes it easier to set up webhooks (copy-pasteable URL) and guarantees that URLs are valid (not all bot names are URL-safe).\n\n## Events\n\nHubot can also respond to events which can be used to pass data between scripts. This is done by encapsulating Node.js's [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) with `robot.emit` and `robot.on`.\n\nOne use case for this would be to have one script for handling interactions with a service, and then emitting events as they come up. For example, we could have a script that receives data from a GitHub post-commit hook, make that emit commits as they come in, and then have another script act on those commits.\n\n```javascript\n// src/scripts/github-commits.mjs\nexport default async robot => {\n  robot.router.post('/hubot/gh-commits', (req, res) => {\n    robot.emit('commit', {\n        user: {}, //hubot user object\n        repo: 'https://github.com/github/hubot',\n        hash: '2e1951c089bd865839328592ff673d2f08153643'\n    })\n  })\n}\n```\n\n```javascript\nexport default async robot => {\n  robot.on('commit', async (commit) => {\n    await robot.send(commit.user, `Will now deploy ${commit.hash} from ${commit.repo}!`)\n    // deploy code goes here\n  })\n}\n```\n\nIf you provide an event, it's highly recommended to include a hubot user or room object in its data. This would allow for hubot to notify a user or room in chat.\n\n## <a name=\"error-handling\">Error Handling</a>\n\nNo code is perfect, and errors and exceptions are to be expected. Previously, an uncaught exceptions would crash your hubot instance. Hubot now includes an `uncaughtException` handler, which provides hooks for scripts to do something about exceptions.\n\n```javascript\n// src/scripts/does-not-compute.mjs\nexport default async robot => {\n  robot.error(async (err, res) => {\n    robot.logger.error('DOES NOT COMPUTE')\n\n    if(res) {\n      await res.reply('DOES NOT COMPUTE')\n    }\n  }\n}\n```\n\nYou can do anything you want here, but you will want to take extra precaution of rescuing and logging errors, particularly with asynchronous code. Otherwise, you might find yourself with recursive errors and not know what is going on.\n\nUnder the hood, there is an 'error' event emitted, with the error handlers consuming that event. The uncaughtException handler [technically leaves the process in an unknown state](http://nodejs.org/api/process.html#process_event_uncaughtexception). Therefore, you should rescue your own exceptions whenever possible, and emit them yourself. The first parameter is the error emitted, and the second parameter is an optional message that generated the error.\n\nUsing previous examples:\n\n```javascript\n  robot.router.post()'/hubot/chatsecrets/:room', (req, res) => {\n    const room = req.params.room\n    let data = null\n    try {\n      data = JSON.parse(req.body.payload)\n    } catch(err) {\n      robot.emit('error', err)\n    }\n\n    // rest of the code here\n  }\n\n  robot.hear(/midnight train/i, (res) => {\n    robot.http('https://midnight-train')\n      .get()((err, res, body) => {\n        if (err) {\n          res.reply('Had problems taking the midnight train')\n          robot.emit('error', err, res)\n          return\n        }\n        // rest of code here\n      })\n  })\n```\n\nFor the second example, it's worth thinking about what messages the user would see. If you have an error handler that replies to the user, you may not need to add a custom message and could send back the error message provided to the `get()` request, but of course it depends on how public you want to be with your exception reporting.\n\n## Documenting Scripts\n\nHubot scripts can be documented with comments at the top of their file, for example:\n\n```javascript\n// Description:\n//   <description of the scripts functionality>\n//\n// Dependencies:\n//   \"<module name>\": \"<module version>\"\n//\n// Configuration:\n//   LIST_OF_ENV_VARS_TO_SET\n//\n// Commands:\n//   hubot <trigger> - <what the respond trigger does>\n//   <trigger> - <what the hear trigger does>\n//\n// Notes:\n//   <optional notes required for the script>\n//\n// Author:\n//   <github username of the original script author>\n```\n\nThe most important and user facing of these is `Commands`. At load time, Hubot looks at the `Commands` section of each scripts, and build a list of all commands. The [hubot-help](https://github.com/hubotio/hubot-help) script lets a user ask for help across all commands, or with a search. Therefore, documenting the commands make them a lot more discoverable by users.\n\nWhen documenting commands, here are some best practices:\n\n* Stay on one line. Help commands get sorted, so would insert the second line at an unexpected location, where it probably won't make sense.\n* Refer to the Hubot as hubot, even if your hubot is named something else. It will automatically be replaced with the correct name. This makes it easier to share scripts without having to update docs.\n* For `robot.respond` documentation, always prefix with `hubot`. Hubot will automatically replace this with your robot's name, or the robot's alias if it has one\n* Check out how man pages document themselves. In particular, brackets indicate optional parts, '...' for any number of parameters, etc.\n\nThe other sections are more relevant to developers of the bot, particularly dependencies, configuration variables, and notes. All contributions to [hubot-scripts](https://github.com/github/hubot-scripts) should include all these sections that are related to getting up and running with the script.\n\n## <a name=\"persistence\">Persistence</a>\n\nHubot has two persistence methods available that can be used to store and retrieve data by scripts: an in-memory key-value store exposed as `robot.brain`, and an optional persistent database-backed key-value store expsoed as `robot.datastore`.\n\n### Brain\n\n```javascript\nrobot.respond(/have a soda/i, async res => {\n  // Get number of sodas had (coerced to a number).\n  const sodasHad = robot.brain.get('totalSodas') * 1 ?? 0\n\n  if (sodasHad > 4) {\n    await res.reply(`I'm too fizzy..`)\n  } else {\n    await res.reply('Sure!')\n    robot.brain.set('totalSodas', sodasHad + 1)\n  }\n})\n\nrobot.respond(/sleep it off/i, async res => {\n  robot.brain.set('totalSodas', 0)\n  await res.reply('zzzzz')\n}\n```\n\nIf the script needs to lookup user data, there are methods on `robot.brain` for looking up one or many users by id, name, or 'fuzzy' matching of name: `userForName`, `userForId`, `userForFuzzyName`, and `usersForFuzzyName`.\n\n```javascript\nexport default async robot => {\n  robot.respond(/who is @?([\\w .\\-]+)\\?*$/i, async res => {\n    const name = res.match[1].trim()\n\n    const users = robot.brain.usersForFuzzyName(name)\n    if (users.length == 1) {\n      const user = users[0]\n      // Do something interesting here..\n    }\n    await res.send(`${name} is user - ${user}`)\n  })\n}\n```\n\n### Datastore\n\nUnlike the brain, the datastore's getter and setter methods are asynchronous and don't resolve until the call to the underlying database has resolved. This requires a slightly different approach to accessing data:\n\n```javascript\nrobot.respond(/have a soda/i, async res => {\n  // Get number of sodas had (coerced to a number).\n  robot.datastore.get('totalSodas').then((value) => {\n    const sodasHad = value * 1 ?? 0\n\n    if (sodasHad > 4) {\n      await res.reply(`I'm too fizzy..`)\n    } else {\n      await res.reply('Sure!')\n      robot.brain.set('totalSodas', sodasHad + 1)\n    }\n  })\n})\n\nrobot.respond(/sleep it off/i, async res => {\n  await robot.datastore.set('totalSodas', 0)\n  await res.reply('zzzzz')\n})\n```\n\nThe datastore also allows setting and getting values which are scoped to individual users:\n\n```javascript\nexport default async robot ->\n\n  robot.respond(/who is @?([\\w .\\-]+)\\?*$/i, async res => {\n    const name = res.match[1].trim()\n\n    const users = robot.brain.usersForFuzzyName(name)\n    if (users.length == 1) {\n      const user = users[0]\n      const roles = await user.get('roles')\n      await res.send(`${name} is ${roles.join(', ')}`)\n    }\n  })\n```\n\n## Script Loading\n\nThere are three main sources to load scripts from:\n\n* all scripts __bundled__ with your hubot installation under `scripts/` directory\n* __community scripts__ specified in `hubot-scripts.json` and shipped in the `hubot-scripts` npm package\n* scripts loaded from external __npm packages__ and specified in `external-scripts.json`\n\nScripts loaded from the `scripts/` directory are loaded in alphabetical order, so you can expect a consistent load order of scripts. For example:\n\n* `scripts/1-first.js`\n* `scripts/_second.js`\n* `scripts/third.js`\n\n# Sharing Scripts\n\nOnce you've built some new scripts to extend the abilities of your robot friend, you should consider sharing them with the world! At the minimum, you need to package up your script and submit it to the [Node.js Package Registry](http://npmjs.org). You should also review the best practices for sharing scripts below.\n\n## See if a script already exists\n\nStart by [checking if an NPM package](https://www.npmjs.com) for a script like yours already exists. If you don't see an existing package that you can contribute to, then you can easily get started using `npx hubot --create myhubot`.\n\n## <a name=\"creating-a-script-package\">Creating A Script Package</a>\n\nCreating a script package for hubot is very simple. Start by running `npx hubot --create myhubot` to create your own instance.\n\n`cd myhubot` and create a script. For example, if we wanted to create a hubot script called \"my-awesome-script\":\n\n```sh\n% npm hubot --create my-awesome-script\n% cd my-awesome-script\n% mkdir src\n% touch src/AwesomeScript.mjs\n```\n\nOpen `package.json` and add:\n\n```json\n\"peerDependencies\": {\n  \"hubot\": \">=9\"\n},\n```\n\nIf you are using git, the generated directory includes a .gitignore, so you can initialize and add everything:\n\n```sh\n% git init\n% git add .\n% git commit -m \"Initial commit\"\n```\n\nYou now have a hubot script repository that's ready to roll! Feel free to crack open `src/AwesomeScript.mjs` and start building up your script! When you've got it ready, you can publish it to [npmjs](http://npmjs.org) by [following their documentation](https://docs.npmjs.com/getting-started/publishing-npm-packages)!\n\nYou'll probably want to write some unit tests for your new script. Review the [Hubot Repo](https://github.com/hubotio/hubot/tree/main/test for examples creating tests.\n\n# <a name=\"listener-metadata\">Listener Metadata</a>\n\nIn addition to a regular expression and callback, the `hear` and `respond` functions also accept an optional options Object which can be used to attach arbitrary metadata to the generated Listener object. This metadata allows for easy extension of your script's behavior without modifying the script package.\n\nThe most important and most common metadata key is `id`. Every Listener should be given a unique name (options.id; defaults to `null`). Names should be scoped by module (e.g. 'my-module.my-listener'). These names allow other scripts to directly address individual listeners and extend them with additional functionality like authorization and rate limiting.\n\nAdditional extensions may define and handle additional metadata keys. For more information, see the [Listener Middleware section](#listener-middleware).\n\nReturning to an earlier example:\n\n```javascript\nexport default async robot => {\n  robot.respond(/annoy me/, id:'annoyance.start', async res => {\n    // code to annoy someone\n  })\n\n  robot.respond(/unannoy me/, id:'annoyance.stop', async res => {\n    // code to stop annoying someone\n  })\n}\n```\n\nThese scoped identifiers allow you to externally specify new behaviors like:\n- authorization policy: \"allow everyone in the `annoyers` group to execute `annoyance.*` commands\"\n- rate limiting: \"only allow executing `annoyance.start` once every 30 minutes\"\n\n# Middleware\n\nThere are three kinds of middleware: Receive, Listener and Response.\n\nReceive middleware runs once, before listeners are checked.\nListener middleware runs for every listener that matches the message.\nResponse middleware runs for every response sent to a message.\n\n## Execution Process and API\n\nSimilar to [Express middleware](http://expressjs.com/api.html#middleware), Hubot executes middleware in definition order. Each middleware can either continue the chain (by calling `next`) or interrupt the chain (by calling `done`). If all middleware continues, the listener callback is executed and `done` is called. Middleware may wrap the `done` callback to allow executing code in the second half of the process (after the listener callback has been executed or a deeper piece of middleware has interrupted).\n\nMiddleware is called with:\n\n- `context`\n  - See the each middleware type's API to see what the context will expose.\n\n`return true` to allow the message to continue; `return false` to stop it from continuing.\n\nEvery middleware receives the same API signature of `context`. Different kinds of middleware may receive different information in the\n`context` object. For more details, see the API for each type of middleware.\n\n### Error Handling\n\nAsynchronous middleware should catch its own exceptions, emit an `error` event, and return `true` or `false`. Any uncaught exceptions will interrupt all execution of middleware.\n\n## Listener Middleware\n\nListener middleware inserts logic between the listener matching a message and the listener executing. This allows you to create extensions that run for every matching script. Examples include centralized authorization policies, rate limiting, logging, and metrics. Middleware is implemented like other hubot scripts: instead of using the `hear` and `respond` methods, middleware is registered using `listenerMiddleware`.\n\n## Listener Middleware Examples\n\nA fully functioning example can be found in [hubot-rate-limit](https://github.com/michaelansel/hubot-rate-limit/blob/master/src/rate-limit.coffee) (Note, this is a coffee version, non-async/await).\n\nA simple example of middleware logging command executions:\n\n```javascript\nexport default async robot => {\n  robot.listenerMiddleware(async context => {\n    // Log commands\n    robot.logger.info(`${context.response.message.user.name} asked me to ${context.response.message.text}`)\n    // Continue executing middleware\n    return true\n  })\n}\n```\n\nIn this example, a log message will be written for each chat message that matches a Listener.\n\nA more complex example making a rate limiting decision:\n\n```javascript\nexport default async robot => {\n  // Map of listener ID to last time it was executed\n  let lastExecutedTime = {}\n\n  robot.listenerMiddleware(async context => {\n    try {\n      // Default to 1s unless listener provides a different minimum period\n      const minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs ?? 1000\n\n      // See if command has been executed recently\n      if (lastExecutedTime.hasOwnProperty(context.listener.options.id) &&\n         lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs) {\n           // Command is being executed too quickly!\n           return false\n      } else {\n        lastExecutedTime[context.listener.options.id] = Date.now()\n        return true\n      }\n    } catch(err) {\n      robot.emit('error', err, context.response)\n    }\n  })\n}\n```\n\nIn this example, the middleware checks to see if the listener has been executed in the last 1,000ms. If it has, the middleware `return false` immediately, preventing the listener callback from being called. If the listener is allowed to execute, the middleware records the time the listener *finished* executing and `return true`.\n\nThis example also shows how listener-specific metadata can be leveraged to create very powerful extensions: a script developer can use the rate limiting middleware to easily rate limit commands at different rates by just adding the middleware and setting a listener option.\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.hear(/hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, async res => {\n    // This will execute no faster than once every ten seconds\n    await res.reply('Why, hello there!')\n  })\n}\n```\n\n## Listener Middleware API\n\nListener middleware callbacks receive 1 argument, `context`. Listener middleware context includes these fields:\n  - `listener`\n    - `options`: a simple Object containing options set when defining the listener. See [Listener Metadata](#listener-metadata).\n    - all other properties should be considered internal\n  - `response`\n    - all parts of the standard response API are included in the middleware API. See [Send & Reply](#send--reply).\n    - middleware may decorate (but not modify) the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)\n    - note: the textual message (`response.message.text`) should be considered immutable in listener middleware\n\n# Receive Middleware\n\nReceive middleware runs before any listeners have executed. It's suitable for\nexcluded commands that have not been updated to add an ID, metrics, and more.\n\n## Receive Middleware Example\n\nThis simple middlware bans hubot use by a particular user, including `hear` listeners. If the user attempts to run a command explicitly, it will return an error message.\n\n```javascript\nconst EXCLUDED_USERS = [\n  '12345' // Restrict access for a user ID for a contractor\n]\n\nrobot.receiveMiddleware(async context => {\n  if (EXCLUDED_USERS.some( id => context.response.message.user.id == id)) {\n    // Don't process this message further.\n    context.response.message.finish()\n\n    // If the message starts with 'hubot' or the alias pattern, this user was\n    // explicitly trying to run a command, so respond with an error message.\n    if (context.response.message.text?.match(robot.respondPattern(''))) {\n      await context.response.reply(`I'm sorry @${context.response.message.user.name}, but I'm configured to ignore your commands.`)\n    }\n\n    // Don't process further middleware.\n    return false\n  } else {\n    return true\n  }\n})\n```\n\n## Receive Middleware API\n\nReceive middleware callbacks receive 1 argument, `context`. Receive middleware context includes these fields:\n  - `response`\n    - this response object will not have a `match` property, as no listeners have been run yet to match it.\n    - middleware may decorate the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)\n    - middleware may modify the `response.message` object\n\n# Response Middleware\n\nResponse middleware runs against every message hubot sends to a chat room. It's helpful for message formatting, preventing password leaks, metrics, and more.\n\n## Response Middleware Example\n\nResponse middleware allows you to intercept and modify outgoing messages before they're sent to the chat room. The `context.strings` array contains the actual message text that will be sent.\n\nThis example changes the format of links from markdown links (like [example](https://example.com)) to the format supported by [Slack](https://slack.com), <https://example.com|example>:\n\n```javascript\n// .mjs\nexport default async robot => {\n  robot.responseMiddleware(async context => {\n    // Only process plaintext messages (send, reply, etc.)\n    if (!context.plaintext) return true\n    \n    // Modify each string in the response\n    context.strings = context.strings.map(string => {\n      // Convert markdown links to Slack format\n      return string.replace(/\\[([^\\[\\]]*?)\\]\\((https?:\\/\\/.*?)\\)/, \"<$2|$1>\")\n    })\n    \n    return true\n  })\n}\n```\n\n## How to Use Response Middleware\n\nResponse middleware is called every time a message is sent from a listener. Key points:\n\n- Access the outgoing message strings via `context.strings` - this is an array of strings being sent\n- Modify the strings by reassigning `context.strings` or mapping over the array\n- The `context.method` tells you how the message was sent (`send`, `reply`, `emote`, etc.)\n- Return `true` to allow the message to continue to the adapter, or `false` to stop it\n- Be careful not to create infinite loops by sending new messages from middleware, as they will also trigger middleware\n- Use `context.plaintext` to distinguish between regular messages and other message types\n\n## Response Middleware API\n\nResponse middleware callbacks receive 1 parameters, `context` and are Promises/async/await. Receive middleware context includes these fields:\n  - `response`\n    - This response object can be used to send new messages from the middleware. Middleware will be called on these new responses. Be careful not to create infinite loops.\n  - `strings`\n    - An array of strings being sent to the chat room adapter. You can edit these, or use `context.strings = [\"new strings\"]` to replace them.\n  - `method`\n    - A string representing which type of response message the listener sent, such as `send`, `reply`, `emote` or `topic`.\n  - `plaintext`\n    - `true` or `undefined`. This will be set to `true` if the message is of a normal plaintext type, such as `send` or `reply`. This property should be treated as read-only.\n\n# Testing Hubot Scripts\n\nI use [Node's Test Runner](https://nodejs.org/dist/latest-v20.x/docs/api/test.html) for writing and running tests for Hubot.\n\n[package.json](../package.json)\n\n```json\n\"scripts\": {\n  \"test\": \"node --test\",\n}\n```\n\n```sh\nnpm t\n```\n\nCheckout [Xample.mjs](../test/XampleTest.mjs) for an example that tests the [Xample.mjs](../test/scripts/Xample.mjs) script.\n\nIn order to isolate your script from Hubot, I've created a [Dummy Adapter](../test/doubles/DummyAdapter.mjs) that you can use when starting a Robot instance to interact with and excercise your code. For now, my suggestion is to copy the `DummyAdapter` into your code so that you can modifiy as your needs evolve.\n\nPlease feel free to create Github issues if you have questions or comments. I'm happy to collaborate.\n\nIf you created your bot with `npx hubot --create xample-bot`, then the `DummyAdapter` is already there. Along with an example test.\n"
  },
  {
    "path": "examples/hubot-start.ps1",
    "content": "#Hubot PowerShell Start Script\n#Invoke from the PowerShell prompt or start via automated tools \n\n$HubotPath = \"drive:\\path\\to\\hubot\"\n$HubotAdapter = \"Hubot adapter\"\n\nWrite-Host \"Starting Hubot Watcher\"\nWhile (1)\n{\n    Write-Host \"Starting Hubot\"\n    Start-Process powershell -ArgumentList \"$HubotPath\\bin\\hubot –adapter $HubotAdapter\" -wait\n}"
  },
  {
    "path": "examples/hubot.service",
    "content": "; Hubot systemd service unit file\n; Place in e.g. `/etc/systemd/system/hubot.service`, then `systemctl daemon-reload` and `service hubot start`.\n\n[Unit]\nDescription=Hubot\nRequires=network.target\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=/path/to/hubot\nUser=change-to-hubot-user\n\nRestart=always\nRestartSec=10\n\n; Configure Hubot environment variables, use quotes around vars with whitespace as shown below.\nEnvironment=\"HUBOT_aaa=xxx\"\nEnvironment=\"HUBOT_bbb='yyy yyy'\"\n\n; Alternatively multiple environment variables can loaded from an external file\n;EnvironmentFile=/etc/hubot-environment\n\nExecStart=/path/to/hubot/bin/hubot --adapter zzz\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "index.mjs",
    "content": "'use strict'\n\nimport User from './src/User.mjs'\nimport Brain from './src/Brain.mjs'\nimport Robot from './src/Robot.mjs'\nimport Adapter from './src/Adapter.mjs'\nimport Response from './src/Response.mjs'\nimport Middleware from './src/Middleware.mjs'\nimport { Listener, TextListener } from './src/Listener.mjs'\nimport { TextMessage, EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage, Message } from './src/Message.mjs'\nimport { DataStore, DataStoreUnavailable } from './src/DataStore.mjs'\nimport { CommandBus } from './src/CommandBus.mjs'\n\nconst loadBot = (adapter, enableHttpd, name, alias) => new Robot(adapter, enableHttpd, name, alias)\nexport {\n  Adapter,\n  User,\n  Brain,\n  Robot,\n  Response,\n  Listener,\n  TextListener,\n  Message,\n  TextMessage,\n  EnterMessage,\n  LeaveMessage,\n  TopicMessage,\n  CatchAllMessage,\n  DataStore,\n  DataStoreUnavailable,\n  Middleware,\n  CommandBus,\n  loadBot\n}\n\nexport default {\n  Adapter,\n  User,\n  Brain,\n  Robot,\n  Response,\n  Listener,\n  TextListener,\n  Message,\n  TextMessage,\n  EnterMessage,\n  LeaveMessage,\n  TopicMessage,\n  CatchAllMessage,\n  DataStore,\n  DataStoreUnavailable,\n  Middleware,\n  CommandBus,\n  loadBot\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hubot\",\n  \"version\": \"0.0.0-development\",\n  \"author\": \"hubot\",\n  \"keywords\": [\n    \"github\",\n    \"hubot\",\n    \"campfire\",\n    \"bot\"\n  ],\n  \"description\": \"A simple helpful robot for your Company\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/hubotio/hubot.git\"\n  },\n  \"engines\": {\n    \"node\": \">= 18\",\n    \"npm\": \">= 9\"\n  },\n  \"main\": \"./index.mjs\",\n  \"bin\": {\n    \"hubot\": \"./bin/hubot\"\n  },\n  \"scripts\": {\n    \"start\": \"bin/hubot\",\n    \"gen\": \"bin/hubot --create myhubot\",\n    \"pretest\": \"node script/simple-lint.mjs\",\n    \"test\": \"node --test --test-timeout=40000\",\n    \"test:smoke\": \"node src/**/*.js\",\n    \"test:e2e\": \"bin/e2e-test.sh\",\n    \"build:local\": \"npx @hubot-friends/sfab --folder ./docs --destination ./_site --verbose --serve /hubot/ --watch-path ./docs --scripts ./sfab-hooks\",\n    \"build\": \"npx @hubot-friends/sfab --folder ./docs --destination ./_site --verbose --scripts ./sfab-hooks\"\n  },\n  \"release\": {\n    \"branches\": [\n      \"main\",\n      \"next\"\n    ],\n    \"dryRun\": false\n  },\n  \"dependencies\": {\n    \"express\": \"^5.2.1\",\n    \"express-basic-auth\": \"^1.2.1\",\n    \"pino\": \"^10.3.1\"\n  }\n}\n"
  },
  {
    "path": "script/bootstrap",
    "content": "#!/usr/bin/env bash\n\nnpm install\n"
  },
  {
    "path": "script/release",
    "content": "#!/usr/bin/env bash\n# Tag and push a release.\n\nset -e\n\n# Make sure we're in the project root.\n\ncd $(dirname \"$0\")/..\n\n# Make sure the darn thing works\n\nnpm update && script/smoke-test\n\n# Make sure we're on the main branch.\n\n(git branch | grep -q '* main') || {\n  echo \"Only release from the main branch.\"\n  exit 1\n}\n\n# Figure out what version we're releasing.\n\ntag=v`node -e 'console.log(require(\"./package.json\").version)'`\n\n# Ensure there's a line in the CHANGELOG\n\ngrep \"$tag\" CHANGELOG.md || {\n  echo \"No entry for '$tag' found in the CHANGELOG.\"\n  exit 1\n}\n\n# Make sure we haven't released this version before.\n\ngit fetch -t origin\n\n(git tag -l | grep -q \"$tag\") && {\n  echo \"Whoops, there's already a '${tag}' tag.\"\n  exit 1\n}\n\n# Tag it and bag it.\n\nnpm publish && git tag \"$tag\" &&\n  git push origin main --tags\n"
  },
  {
    "path": "script/server",
    "content": "#!/usr/bin/env bash\n\nnpm start -- \"$@\"\n"
  },
  {
    "path": "script/simple-lint.mjs",
    "content": "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')\n\nconst targets = [\n  'src',\n  'test',\n  'bin',\n  'configuration',\n  'sfab-hooks',\n  'script'\n]\n\nconst ignoreDirs = new Set([\n  'node_modules',\n  '.git',\n  '_site',\n  'docs'\n])\n\nconst extensions = new Set(['.mjs', '.js'])\n\nconst issues = []\n\nconst isIgnoredDir = (dirName) => ignoreDirs.has(dirName)\n\nconst walk = async (dir) => {\n  const entries = await fs.readdir(dir, { withFileTypes: true })\n  for (const entry of entries) {\n    const fullPath = path.join(dir, entry.name)\n    if (entry.isDirectory()) {\n      if (!isIgnoredDir(entry.name)) {\n        await walk(fullPath)\n      }\n      continue\n    }\n    if (entry.isFile()) {\n      const ext = path.extname(entry.name)\n      if (extensions.has(ext)) {\n        await lintFile(fullPath)\n      }\n    }\n  }\n}\n\nconst lintFile = async (filePath) => {\n  const content = await fs.readFile(filePath, 'utf8')\n  const normalizedContent = content.replace(/\\r\\n/g, '\\n')\n  const lines = normalizedContent.split('\\n')\n  const hasFinalNewline = normalizedContent.endsWith('\\n')\n\n  lines.forEach((line, index) => {\n    const lineNumber = index + 1\n    if (line.includes('\\t')) {\n      issues.push({ filePath, lineNumber, message: 'tab character found' })\n    }\n    if (/[ \\t]+$/.test(line)) {\n      issues.push({ filePath, lineNumber, message: 'trailing whitespace' })\n    }\n  })\n\n  if (!hasFinalNewline) {\n    issues.push({ filePath, lineNumber: lines.length, message: 'missing final newline' })\n  }\n}\n\nconst run = async () => {\n  for (const target of targets) {\n    const dir = path.join(projectRoot, target)\n    try {\n      const stat = await fs.stat(dir)\n      if (stat.isDirectory()) {\n        await walk(dir)\n      }\n    } catch (error) {\n      if (error?.code !== 'ENOENT') {\n        throw error\n      }\n    }\n  }\n\n  if (issues.length > 0) {\n    for (const issue of issues) {\n      const relPath = path.relative(projectRoot, issue.filePath)\n      console.error(`${relPath}:${issue.lineNumber} ${issue.message}`)\n    }\n    console.error(`\\nFound ${issues.length} lint issue(s)`)\n    process.exitCode = 1\n    return\n  }\n\n  console.log('Lint OK')\n}\n\nrun().catch((error) => {\n  console.error(error)\n  process.exitCode = 1\n})\n"
  },
  {
    "path": "script/smoke-test",
    "content": "#!/usr/bin/env bash\n\nnpm run test:smoke\n"
  },
  {
    "path": "script/test",
    "content": "#!/usr/bin/env bash\n\nnpm test -- \"$@\"\n"
  },
  {
    "path": "sfab-hooks/SfabHook.mjs",
    "content": "export default () => {\n  return {\n    model (file, model) {\n      return {\n        base: {\n          href: '/hubot/'\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Adapter.mjs",
    "content": "'use strict'\n\nimport EventEmitter from 'node:events'\n\nclass Adapter extends EventEmitter {\n  // An adapter is a specific interface to a chat source for robots.\n  //\n  // robot - A Robot instance.\n  constructor (robot) {\n    super()\n    this.robot = robot\n  }\n\n  // Public: Raw method for sending data back to the chat source. Extend this.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more Strings for each message to send.\n  //\n  // Returns results from adapter.\n  async send (envelope, ...strings) {}\n\n  // Public: Raw method for sending emote data back to the chat source.\n  // Defaults as an alias for send\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more Strings for each message to send.\n  //\n  // Returns results from adapter.\n  async emote (envelope, ...strings) {\n    return this.send(envelope, ...strings)\n  }\n\n  // Public: Raw method for building a reply and sending it back to the chat\n  // source. Extend this.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more Strings for each reply to send.\n  //\n  // Returns results from adapter.\n  async reply (envelope, ...strings) {}\n\n  // Public: Raw method for setting a topic on the chat source. Extend this.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One more more Strings to set as the topic.\n  //\n  // Returns results from adapter.\n  async topic (envelope, ...strings) {}\n\n  // Public: Raw method for playing a sound in the chat source. Extend this.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more strings for each play message to send.\n  //\n  // Returns results from adapter.\n  async play (envelope, ...strings) {}\n\n  // Public: Raw method for invoking the bot to run. Extend this.\n  //\n  // Returns whatever the extended adapter returns.\n  async run () {}\n\n  // Public: Raw method for shutting the bot down. Extend this.\n  //\n  // Returns nothing.\n  close () {\n    this.removeAllListeners()\n  }\n\n  // Public: Dispatch a received message to the robot.\n  //\n  // Returns nothing.\n  async receive (message) {\n    await this.robot.receive(message)\n  }\n\n  // Public: Get an Array of User objects stored in the brain.\n  //\n  // Returns an Array of User objects.\n  // @deprecated Use @robot.brain\n  users () {\n    this.robot.logger.warn('@users() is going to be deprecated in 11.0.0 use @robot.brain.users()')\n    return this.robot.brain.users()\n  }\n\n  // Public: Get a User object given a unique identifier.\n  //\n  // Returns a User instance of the specified user.\n  // @deprecated Use @robot.brain\n  userForId (id, options) {\n    this.robot.logger.warn('@userForId() is going to be deprecated in 11.0.0 use @robot.brain.userForId()')\n    return this.robot.brain.userForId(id, options)\n  }\n\n  // Public: Get a User object given a name.\n  //\n  // Returns a User instance for the user with the specified name.\n  // @deprecated Use @robot.brain\n  userForName (name) {\n    this.robot.logger.warn('@userForName() is going to be deprecated in 11.0.0 use @robot.brain.userForName()')\n    return this.robot.brain.userForName(name)\n  }\n\n  // Public: Get all users whose names match fuzzyName. Currently, match\n  // means 'starts with', but this could be extended to match initials,\n  // nicknames, etc.\n  //\n  // Returns an Array of User instances matching the fuzzy name.\n  // @deprecated Use @robot.brain\n  usersForRawFuzzyName (fuzzyName) {\n    this.robot.logger.warn('@userForRawFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForRawFuzzyName()')\n    return this.robot.brain.usersForRawFuzzyName(fuzzyName)\n  }\n\n  // Public: If fuzzyName is an exact match for a user, returns an array with\n  // just that user. Otherwise, returns an array of all users for which\n  // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).\n  //\n  // Returns an Array of User instances matching the fuzzy name.\n  // @deprecated Use @robot.brain\n  usersForFuzzyName (fuzzyName) {\n    this.robot.logger.warn('@userForFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForFuzzyName()')\n    return this.robot.brain.usersForFuzzyName(fuzzyName)\n  }\n\n  // Public: Creates a scoped http client with chainable methods for\n  // modifying the request. This doesn't actually make a request though.\n  // Once your request is assembled, you can call `get()`/`post()`/etc to\n  // send the request.\n  //\n  // Returns a ScopedClient instance.\n  // @deprecated Use node.js fetch.\n  http (url) {\n    this.robot.logger.warn('@http() is going to be deprecated in 11.0.0 use @robot.http()')\n    return this.robot.http(url)\n  }\n}\n\nexport default Adapter\n"
  },
  {
    "path": "src/Brain.mjs",
    "content": "'use strict'\n\nimport EventEmitter from 'node:events'\nimport User from './User.mjs'\n\n// If necessary, reconstructs a User object. Returns either:\n//\n// 1. If the original object was falsy, null\n// 2. If the original object was a User object, the original object\n// 3. If the original object was a plain JavaScript object, return\n//    a User object with all of the original object's properties.\nconst reconstructUserIfNecessary = function (user, robot) {\n  if (!user) {\n    return null\n  }\n\n  if (!user.constructor || (user.constructor && user.constructor.name !== 'User')) {\n    const id = user.id\n    delete user.id\n    // Use the old user as the \"options\" object,\n    // populating the new user with its values.\n    // Also add the `robot` field so it gets a reference.\n    user.robot = robot\n    const newUser = new User(id, user)\n    delete user.robot\n\n    return newUser\n  } else {\n    return user\n  }\n}\n\nclass Brain extends EventEmitter {\n  // Represents somewhat persistent storage for the robot. Extend this.\n  //\n  // Returns a new Brain with no external storage.\n  constructor (robot) {\n    super()\n    this.data = {\n      users: {},\n      _private: {}\n    }\n    this.getRobot = function () {\n      return robot\n    }\n\n    this.autoSave = true\n\n    robot.on('running', () => {\n      this.resetSaveInterval(5)\n    })\n  }\n\n  // Public: Store key-value pair under the private namespace and extend\n  // existing @data before emitting the 'loaded' event.\n  //\n  // Returns the instance for chaining.\n  set (key, value) {\n    let pair\n    if (key === Object(key)) {\n      pair = key\n    } else {\n      pair = {}\n      pair[key] = value\n    }\n\n    Object.keys(pair).forEach((key) => {\n      this.data._private[key] = pair[key]\n    })\n\n    this.emit('loaded', this.data)\n\n    return this\n  }\n\n  // Public: Get value by key from the private namespace in @data\n  // or return null if not found.\n  //\n  // Returns the value.\n  get (key) {\n    return this.data._private[key] != null ? this.data._private[key] : null\n  }\n\n  // Public: Remove value by key from the private namespace in @data\n  // if it exists\n  //\n  // Returns the instance for chaining.\n  remove (key) {\n    if (this.data._private[key] != null) {\n      delete this.data._private[key]\n    }\n\n    return this\n  }\n\n  // Public: Emits the 'save' event so that 'brain' scripts can handle\n  // persisting.\n  //\n  // Returns nothing.\n  save () {\n    this.emit('save', this.data)\n  }\n\n  // Public: Emits the 'close' event so that 'brain' scripts can handle closing.\n  //\n  // Returns nothing.\n  close () {\n    clearInterval(this.saveInterval)\n    this.save()\n    this.emit('close')\n    this.removeAllListeners()\n  }\n\n  // Public: Enable or disable the automatic saving\n  //\n  // enabled - A boolean whether to autosave or not\n  //\n  // Returns nothing\n  setAutoSave (enabled) {\n    this.autoSave = enabled\n  }\n\n  // Public: Reset the interval between save function calls.\n  //\n  // seconds - An Integer of seconds between saves.\n  //\n  // Returns nothing.\n  resetSaveInterval (seconds) {\n    if (this.saveInterval) {\n      clearInterval(this.saveInterval)\n    }\n    this.saveInterval = setInterval(() => {\n      if (this.autoSave) {\n        this.save()\n      }\n    }, seconds * 1000)\n  }\n\n  // Public: Merge keys loaded from a DB against the in memory representation.\n  //\n  // Returns nothing.\n  //\n  // Caveats: Deeply nested structures don't merge well.\n  mergeData (data) {\n    for (const k in data || {}) {\n      this.data[k] = data[k]\n    }\n\n    // Ensure users in the brain are still User objects.\n    if (data && data.users) {\n      for (const k in data.users) {\n        const user = this.data.users[k]\n        this.data.users[k] = reconstructUserIfNecessary(user, this.getRobot())\n      }\n    }\n\n    this.emit('loaded', this.data)\n  }\n\n  // Public: Get an object of User objects stored in the brain.\n  //\n  // Returns an object of User objects.\n  users () {\n    return this.data.users\n  }\n\n  // Public: Get a User object given a unique identifier.\n  //\n  // Returns a User instance of the specified user.\n  userForId (id, options) {\n    let user = this.data.users[id]\n    if (!options) {\n      options = {}\n    }\n    options.robot = this.getRobot()\n\n    if (!user) {\n      user = new User(id, options)\n      this.data.users[id] = user\n    }\n\n    if (options && options.room && (!user.room || user.room !== options.room)) {\n      user = new User(id, options)\n      this.data.users[id] = user\n    }\n    delete options.robot\n\n    return user\n  }\n\n  // Public: Get a User object given a name.\n  //\n  // Returns a User instance for the user with the specified name.\n  userForName (name) {\n    let result = null\n    const lowerName = name.toLowerCase()\n\n    for (const k in this.data.users || {}) {\n      const userName = this.data.users[k].name\n      if (userName != null && userName.toString().toLowerCase() === lowerName) {\n        result = this.data.users[k]\n      }\n    }\n\n    return result\n  }\n\n  // Public: Get all users whose names match fuzzyName. Currently, match\n  // means 'starts with', but this could be extended to match initials,\n  // nicknames, etc.\n  //\n  // Returns an Array of User instances matching the fuzzy name.\n  usersForRawFuzzyName (fuzzyName) {\n    const lowerFuzzyName = fuzzyName.toLowerCase()\n\n    const users = this.data.users || {}\n\n    return Object.keys(users).reduce((result, key) => {\n      const user = users[key]\n      if (user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) === 0) {\n        result.push(user)\n      }\n      return result\n    }, [])\n  }\n\n  // Public: If fuzzyName is an exact match for a user, returns an array with\n  // just that user. Otherwise, returns an array of all users for which\n  // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).\n  //\n  // Returns an Array of User instances matching the fuzzy name.\n  usersForFuzzyName (fuzzyName) {\n    const matchedUsers = this.usersForRawFuzzyName(fuzzyName)\n    const lowerFuzzyName = fuzzyName.toLowerCase()\n    const fuzzyMatchedUsers = matchedUsers.filter(user => user.name.toLowerCase() === lowerFuzzyName)\n\n    return fuzzyMatchedUsers.length > 0 ? fuzzyMatchedUsers : matchedUsers\n  }\n}\n\nexport default Brain\n"
  },
  {
    "path": "src/CommandBus.mjs",
    "content": "import { EventEmitter } from 'node:events'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\n/**\n * CommandBus provides deterministic command handling for Hubot with safe-by-default behavior.\n * \n * Logging Strategy:\n * - Event logging to disk is disabled by default\n * - To enable: pass `disableLogging: false` in constructor options\n * - When enabled, events are written asynchronously (fire-and-forget) to avoid blocking\n * - Writes happen individually as events occur\n */\nexport class CommandBus extends EventEmitter {\n  constructor(robot, options = {}) {\n    super()\n    this.robot = robot\n    this.commands = new Map()\n    this.pendingProposals = new Map()\n    this.typeResolvers = new Map()\n    this.prefix = options.prefix ?? ''\n    this.proposalTTL = options.proposalTTL || 300000 // 5 minutes default\n    this.logPath = options.logPath || path.join(process.cwd(), '.data', 'commands-events.ndjson')\n    this.disableLogging = options.disableLogging ?? true\n    this.permissionProvider = options.permissionProvider || null\n  }\n\n  register(spec, opts = {}) {\n    if (!spec.id) {\n      throw new Error('Command spec must have an id')\n    }\n    if (!spec.handler || typeof spec.handler !== 'function') {\n      throw new Error('Command spec must have a handler function')\n    }\n\n    const aliases = this._normalizeAliases(spec.aliases)\n\n    const existing = this.commands.get(spec.id)\n    if (existing && !opts.update) {\n      throw new Error(`Command ${spec.id} is already registered`)\n    }\n\n    const command = {\n      id: spec.id,\n      description: spec.description || '',\n      aliases: aliases.original,\n      normalizedAliases: aliases.normalized,\n      examples: spec.examples || [],\n      args: spec.args || {},\n      sideEffects: spec.sideEffects || [],\n      confirm: spec.confirm || 'if_ambiguous',\n      permissions: spec.permissions || {},\n      handler: spec.handler\n    }\n\n    this.commands.set(spec.id, command)\n    const eventPayload = { commandId: spec.id, aliases: command.normalizedAliases, timestamp: Date.now() }\n    if (existing && opts.update) {\n      this.emit('commands:updated', eventPayload)\n      this._log({ event: 'commands:updated', ...eventPayload })\n    } else {\n      this.emit('commands:registered', eventPayload)\n      this._log({ event: 'commands:registered', ...eventPayload })\n    }\n\n    const collisions = this.aliasCollisions()\n    if (Object.keys(collisions).length > 0) {\n      this.emit('commands:alias_collision_detected', { collisions, timestamp: Date.now() })\n      this._log({ event: 'commands:alias_collision_detected', collisions, timestamp: Date.now() })\n    }\n\n    return command\n  }\n\n  /**\n   * Register a custom type resolver for argument validation.\n   * Resolvers are called during validation and can transform/validate values.\n   * \n   * @param {string} typeName - The type name to register (e.g., 'project_id')\n   * @param {Function} resolver - Async function(value, schema, context) that returns validated value or throws\n   * @throws {Error} If typeName is empty or resolver is not a function\n   * @public\n   * \n   * @example\n   * robot.commands.registerTypeResolver('project_id', async (value, schema, context) => {\n   *   if (!value.startsWith('PRJ-')) throw new Error('must start with PRJ-')\n   *   return value.toUpperCase()\n   * })\n   */\n  registerTypeResolver(typeName, resolver) {\n    if (typeof typeName !== 'string' || !typeName) {\n      throw new Error('Type name must be a non-empty string')\n    }\n    if (typeof resolver !== 'function') {\n      throw new Error('Resolver must be a function')\n    }\n    this.typeResolvers.set(typeName, resolver)\n  }\n\n  unregister(commandId) {\n    return this.commands.delete(commandId)\n  }\n\n  getCommand(commandId) {\n    return this.commands.get(commandId)\n  }\n\n  listCommands(filter = {}) {\n    let commands = Array.from(this.commands.values())\n\n    if (filter.prefix) {\n      commands = commands.filter(c => c.id.startsWith(filter.prefix))\n    }\n\n    return commands\n  }\n\n  aliasCollisions() {\n    const collisions = {}\n    const aliasMap = new Map()\n\n    for (const command of this.commands.values()) {\n      for (const alias of command.normalizedAliases || []) {\n        if (!aliasMap.has(alias)) {\n          aliasMap.set(alias, [])\n        }\n        aliasMap.get(alias).push(command.id)\n      }\n    }\n\n    for (const [alias, ids] of aliasMap.entries()) {\n      if (ids.length > 1) {\n        collisions[alias] = ids\n      }\n    }\n\n    return collisions\n  }\n\n  search(query, opts = {}) {\n    if (!query || typeof query !== 'string') {\n      return []\n    }\n\n    const normalizedQuery = this._normalizeAlias(query)\n    const queryTokens = this._tokenizeQuery(normalizedQuery)\n    const results = []\n\n    for (const command of this.commands.values()) {\n      const aliasMatches = this._scoreAliases(command, normalizedQuery, queryTokens)\n      const descriptionMatches = this._scoreText(command.description, queryTokens)\n      const exampleMatches = this._scoreExamples(command.examples, queryTokens)\n\n      const bestAliasScore = aliasMatches.score\n      const bestDescScore = descriptionMatches.score\n      const bestExampleScore = exampleMatches.score\n\n      const bestScore = Math.max(bestAliasScore, bestDescScore, bestExampleScore)\n      if (bestScore === 0) {\n        continue\n      }\n\n      let matchedOn = 'description'\n      if (bestAliasScore >= bestDescScore && bestAliasScore >= bestExampleScore) {\n        matchedOn = 'alias'\n      } else if (bestExampleScore >= bestDescScore) {\n        matchedOn = 'example'\n      }\n\n      results.push({\n        id: command.id,\n        score: bestScore,\n        matchedOn\n      })\n    }\n\n    results.sort((a, b) => b.score - a.score)\n    return results\n  }\n\n  getHelp(commandId) {\n    const command = this.getCommand(commandId)\n    if (!command) {\n      return null\n    }\n\n    let help = `Command: ${command.id}\\n`\n    help += `Description: ${command.description}\\n`\n    help += `Usage: ${this.prefix}${command.id} [options]\\n`\n\n    if (command.aliases.length > 0) {\n      help += `Intent: ${command.aliases.join(', ')}\\n`\n    }\n\n    if (Object.keys(command.args).length > 0) {\n      help += '\\nArguments:\\n'\n      for (const [name, schema] of Object.entries(command.args)) {\n        const required = schema.required ? ' (required)' : ''\n        const defaultVal = schema.default !== undefined ? ` [default: ${schema.default}]` : ''\n        const values = schema.values ? ` [values: ${schema.values.join(', ')}]` : ''\n        help += `  --${name} (${schema.type})${required}${defaultVal}${values}\\n`\n      }\n    }\n\n    if (command.examples.length > 0) {\n      help += '\\nExamples:\\n'\n      command.examples.forEach(ex => {\n        help += `  ${ex}\\n`\n      })\n    }\n\n    return help\n  }\n\n  parse(text) {\n\n    if (!text || typeof text !== 'string') {\n      return null\n    }\n\n    // Strip prefix if present (optional)\n    const withoutPrefix = text.startsWith(this.prefix) \n      ? text.slice(this.prefix.length).trim()\n      : text.trim()\n\n    const parts = this._tokenize(withoutPrefix)\n    if (parts.length === 0) {\n      return null\n    }\n\n    const commandId = parts[0]\n    if (!this.commands.has(commandId)) {\n      return null\n    }\n\n    const command = this.commands.get(commandId)\n    const args = {}\n\n    for (let i = 1; i < parts.length; i++) {\n      const token = parts[i]\n\n      // Handle -- key value pattern\n      if (token === '--') {\n        const key = parts[i + 1]\n        const valueToken = parts[i + 2]\n        if (key && valueToken && !valueToken.startsWith('--') && !valueToken.includes(':')) {\n          args[key] = valueToken\n          i += 2\n        } else if (key) {\n          args[key] = true\n          i += 1\n        }\n        continue\n      }\n\n      // Handle --key value or --key \"quoted value\"\n      if (token.startsWith('--')) {\n        const key = token.slice(2)\n        const nextToken = parts[i + 1]\n        const schema = command.args[key]\n\n        // Use schema hint: boolean type = flag, others expect value\n        if (schema && schema.type === 'boolean') {\n          args[key] = true\n        } else if (nextToken && !nextToken.startsWith('--') && !nextToken.includes(':')) {\n          args[key] = nextToken\n          i++ // Skip next token\n        } else {\n          // No schema or ambiguous: default to boolean flag\n          args[key] = true\n        }\n      }\n      // Handle key:value or key:\"quoted value\"\n      else if (token.includes(':')) {\n        const colonIndex = token.indexOf(':')\n        const key = token.slice(0, colonIndex)\n        const value = token.slice(colonIndex + 1)\n        args[key] = value\n      }\n    }\n\n    const parsed = {\n      commandId,\n      args,\n      rawText: text\n    }\n\n    this.emit('commands:invocation_parsed', { commandId, args, timestamp: Date.now() })\n    this._log({ event: 'commands:invocation_parsed', commandId, args, timestamp: Date.now() })\n\n    return parsed\n  }\n\n  _tokenize(text) {\n    const tokens = []\n    let current = ''\n    let inQuotes = false\n    let quoteChar = null\n    let escapeNext = false\n\n    for (let i = 0; i < text.length; i++) {\n      const char = text[i]\n\n      if (escapeNext) {\n        current += char\n        escapeNext = false\n        continue\n      }\n\n      if (inQuotes && char === '\\\\') {\n        escapeNext = true\n        continue\n      }\n\n      if ((char === '\"' || char === '\\'') && !inQuotes) {\n        inQuotes = true\n        quoteChar = char\n        continue\n      }\n\n      if (char === quoteChar && inQuotes) {\n        inQuotes = false\n        quoteChar = null\n        continue\n      }\n\n      if (char === ' ' && !inQuotes) {\n        if (current) {\n          tokens.push(current)\n          current = ''\n        }\n        continue\n      }\n\n      current += char\n    }\n\n    if (current) {\n      tokens.push(current)\n    }\n\n    return tokens\n  }\n\n  _normalizeAliases(aliases) {\n    if (aliases === undefined || aliases === null) {\n      return { original: [], normalized: [] }\n    }\n    if (!Array.isArray(aliases)) {\n      throw new Error('Command aliases must be an array of strings')\n    }\n\n    const original = []\n    const normalized = []\n    const seen = new Set()\n\n    for (const alias of aliases) {\n      if (typeof alias !== 'string') {\n        throw new Error('Command aliases must be an array of strings')\n      }\n      const trimmed = alias.trim()\n      if (!trimmed) {\n        throw new Error('Command aliases must be non-empty strings')\n      }\n\n      const normalizedAlias = this._normalizeAlias(trimmed)\n      if (seen.has(normalizedAlias)) {\n        continue\n      }\n\n      seen.add(normalizedAlias)\n      original.push(trimmed)\n      normalized.push(normalizedAlias)\n    }\n\n    return { original, normalized }\n  }\n\n  _normalizeAlias(alias) {\n    return alias.trim().replace(/\\s+/g, ' ').toLowerCase()\n  }\n\n  _tokenizeQuery(text) {\n    return text.split(/\\s+/).filter(Boolean)\n  }\n\n  _scoreAliases(command, normalizedQuery, queryTokens) {\n    const aliases = command.normalizedAliases || []\n    if (aliases.length === 0) {\n      return { score: 0 }\n    }\n\n    if (aliases.includes(normalizedQuery)) {\n      return { score: 100 }\n    }\n\n    const bestOverlap = aliases.reduce((best, alias) => {\n      const tokens = this._tokenizeQuery(alias)\n      const overlap = queryTokens.filter(t => tokens.includes(t)).length\n      return Math.max(best, overlap)\n    }, 0)\n\n    return { score: bestOverlap * 10 }\n  }\n\n  _scoreText(text, queryTokens) {\n    if (!text) {\n      return { score: 0 }\n    }\n\n    const tokens = this._tokenizeQuery(this._normalizeAlias(text))\n    const overlap = queryTokens.filter(t => tokens.includes(t)).length\n    return { score: overlap * 5 }\n  }\n\n  _scoreExamples(examples, queryTokens) {\n    if (!examples || examples.length === 0) {\n      return { score: 0 }\n    }\n\n    const bestOverlap = examples.reduce((best, example) => {\n      const tokens = this._tokenizeQuery(this._normalizeAlias(example))\n      const overlap = queryTokens.filter(t => tokens.includes(t)).length\n      return Math.max(best, overlap)\n    }, 0)\n\n    return { score: bestOverlap * 5 }\n  }\n\n  async validate(commandId, rawArgs, context) {\n    const command = this.getCommand(commandId)\n    if (!command) {\n      return {\n        ok: false,\n        errors: [`Command ${commandId} not found`],\n        missing: []\n      }\n    }\n\n    const args = { ...rawArgs }\n    const errors = []\n    const missing = []\n\n    // Apply defaults and validate each arg\n    for (const [name, schema] of Object.entries(command.args)) {\n      const value = args[name]\n\n      // Check required\n      if (schema.required && (value === undefined || value === null)) {\n        missing.push(name)\n        continue\n      }\n\n      // Apply default\n      if (value === undefined && schema.default !== undefined) {\n        args[name] = schema.default\n        continue\n      }\n\n      // Skip validation if not provided and not required\n      if (value === undefined) {\n        continue\n      }\n\n      // Type validation and conversion\n      try {\n        args[name] = await this._validateType(name, value, schema, context)\n      } catch (err) {\n        errors.push(err.message)\n      }\n    }\n\n    if (missing.length > 0 || errors.length > 0) {\n      const result = {\n        ok: false,\n        errors,\n        missing,\n        args\n      }\n\n      this.emit('commands:validation_failed', { commandId, errors, missing, timestamp: Date.now() })\n      this._log({ event: 'commands:validation_failed', commandId, errors, missing, timestamp: Date.now() })\n\n      return result\n    }\n\n    return {\n      ok: true,\n      args\n    }\n  }\n\n  async _validateType(name, value, schema, context) {\n    // Check custom type resolvers first\n    if (this.typeResolvers.has(schema.type)) {\n      const resolver = this.typeResolvers.get(schema.type)\n      try {\n        return await resolver(value, schema, context)\n      } catch (err) {\n        throw new Error(`Argument ${name}: ${err.message}`)\n      }\n    }\n\n    // Built-in types\n    switch (schema.type) {\n      case 'string':\n        return String(value)\n\n      case 'number': {\n        const num = Number(value)\n        if (isNaN(num)) {\n          throw new Error(`Argument ${name} must be a number`)\n        }\n        return num\n      }\n\n      case 'boolean': {\n        return coerceToBoolean(value)\n      }\n\n      case 'enum': {\n        if (!Array.isArray(schema.values) || schema.values.length === 0) {\n          throw new Error(`Argument ${name}: enum values must be a non-empty array`)\n        }\n        if (!schema.values.includes(value)) {\n          throw new Error(`Argument ${name} must be one of: ${schema.values.join(', ')}`)\n        }\n        return value\n      }\n\n      case 'user': {\n        const users = this.robot.brain.users()\n        const user = Object.values(users).find(u => u.name === value || u.id === value)\n        if (!user) {\n          throw new Error(`Argument ${name}: user \"${value}\" not found`)\n        }\n        return user\n      }\n\n      case 'room': {\n        if (!value.startsWith('#')) {\n          throw new Error(`Argument ${name}: room must start with #`)\n        }\n        return value\n      }\n\n      case 'date': {\n        let date\n\n        if (value === 'today') {\n          date = new Date()\n          date.setHours(0, 0, 0, 0)\n        } else if (value === 'tomorrow') {\n          date = new Date()\n          date.setDate(date.getDate() + 1)\n          date.setHours(0, 0, 0, 0)\n        } else {\n          date = new Date(value)\n        }\n\n        if (isNaN(date.getTime())) {\n          throw new Error(`Argument ${name}: invalid date \"${value}\"`)\n        }\n\n        return date\n      }\n\n      default:\n        return value\n    }\n  }\n\n  needsConfirmation(commandId) {\n    const command = this.getCommand(commandId)\n    if (!command) {\n      return false\n    }\n\n    if (command.confirm === 'always') {\n      return true\n    }\n\n    if (command.confirm === 'never') {\n      return false\n    }\n\n    // Default: confirm if has side effects\n    return command.sideEffects.length > 0\n  }\n\n  async propose(proposal, context) {\n    const { commandId, args } = proposal\n    const command = this.getCommand(commandId)\n\n    if (!command) {\n      throw new Error(`Command ${commandId} not found`)\n    }\n\n    const confirmationKey = this._getConfirmationKey(context.user.id, context.room)\n    const preview = this._renderPreview(commandId, args)\n\n    const pendingProposal = {\n      commandId,\n      args,\n      context,\n      preview,\n      confirmationKey,\n      timestamp: Date.now(),\n      timeoutId: null\n    }\n\n    this.pendingProposals.set(confirmationKey, pendingProposal)\n\n    // Set TTL timeout\n    pendingProposal.timeoutId = setTimeout(() => {\n      this.pendingProposals.delete(confirmationKey)\n    }, this.proposalTTL)\n\n    this.emit('commands:proposal_created', { commandId, confirmationKey, timestamp: Date.now() })\n    this.emit('commands:proposal_confirm_requested', { commandId, confirmationKey, timestamp: Date.now() })\n    this._log({ event: 'commands:proposal_created', commandId, confirmationKey, timestamp: Date.now() })\n    this._log({ event: 'commands:proposal_confirm_requested', commandId, confirmationKey, timestamp: Date.now() })\n\n    return pendingProposal\n  }\n\n  async confirm(replyText, context) {\n    const confirmationKey = this._getConfirmationKey(context.user.id, context.room)\n    const pending = this.pendingProposals.get(confirmationKey)\n\n    if (!pending) {\n      return null\n    }\n\n    const normalizedReply = replyText.toLowerCase().trim()\n\n    if (normalizedReply === 'yes' || normalizedReply === 'y') {\n      clearTimeout(pending.timeoutId)\n      this.pendingProposals.delete(confirmationKey)\n\n      this.emit('commands:proposal_confirmed', {\n        commandId: pending.commandId,\n        confirmationKey,\n        timestamp: Date.now()\n      })\n      this._log({\n        event: 'commands:proposal_confirmed',\n        commandId: pending.commandId,\n        confirmationKey,\n        timestamp: Date.now()\n      })\n\n      const result = await this.execute(pending.commandId, pending.args, pending.context)\n\n      return {\n        executed: true,\n        result\n      }\n    }\n\n    if (normalizedReply === 'no' || normalizedReply === 'n' || normalizedReply === 'cancel') {\n      clearTimeout(pending.timeoutId)\n      this.pendingProposals.delete(confirmationKey)\n\n      this.emit('commands:proposal_cancelled', {\n        commandId: pending.commandId,\n        confirmationKey,\n        timestamp: Date.now()\n      })\n      this._log({\n        event: 'commands:proposal_cancelled',\n        commandId: pending.commandId,\n        confirmationKey,\n        timestamp: Date.now()\n      })\n\n      return {\n        cancelled: true\n      }\n    }\n\n    return null\n  }\n\n  async execute(commandId, args, context) {\n    const command = this.getCommand(commandId)\n    if (!command) {\n      throw new Error(`Command ${commandId} not found`)\n    }\n\n    // Check permissions\n    if (command.permissions.rooms && command.permissions.rooms.length > 0) {\n      if (!command.permissions.rooms.includes(context.room)) {\n        this.emit('commands:permission_denied', {\n          commandId,\n          room: context.room,\n          timestamp: Date.now()\n        })\n        this._log({\n          event: 'commands:permission_denied',\n          commandId,\n          room: context.room,\n          timestamp: Date.now()\n        })\n        throw new Error('Permission denied: command not allowed in this room')\n      }\n    }\n\n    if (command.permissions.roles && command.permissions.roles.length > 0) {\n      if (this.permissionProvider && typeof this.permissionProvider.hasRole === 'function') {\n        const allowed = await this.permissionProvider.hasRole(context.user, command.permissions.roles, context)\n        if (!allowed) {\n          this.emit('commands:permission_denied', {\n            commandId,\n            roles: command.permissions.roles,\n            timestamp: Date.now()\n          })\n          this._log({\n            event: 'commands:permission_denied',\n            commandId,\n            roles: command.permissions.roles,\n            timestamp: Date.now()\n          })\n          throw new Error('Permission denied: insufficient role')\n        }\n      }\n    }\n\n    try {\n      const result = await command.handler({ args, context })\n\n      this.emit('commands:executed', { commandId, timestamp: Date.now() })\n      this._log({ event: 'commands:executed', commandId, timestamp: Date.now() })\n\n      return result\n    } catch (err) {\n      this.emit('commands:error', { commandId, error: err.message, timestamp: Date.now() })\n      this._log({ event: 'commands:error', commandId, error: err.message, timestamp: Date.now() })\n      throw err\n    }\n  }\n\n  async invoke(text, context) {\n    const parsed = this.parse(text)\n    if (!parsed) {\n      return null\n    }\n\n    const helpRequested = parsed.args && (parsed.args.help === true || parsed.args.h === true)\n    if (helpRequested) {\n      const helpText = this.getHelp(parsed.commandId)\n      return {\n        ok: true,\n        helpOnly: true,\n        result: helpText\n      }\n    }\n\n    const validation = await this.validate(parsed.commandId, parsed.args, context)\n\n    if (!validation.ok) {\n      return validation\n    }\n\n    // Check if needs confirmation\n    if (this.needsConfirmation(parsed.commandId)) {\n      const proposal = await this.propose({\n        commandId: parsed.commandId,\n        args: validation.args\n      }, context)\n\n      return {\n        needsConfirmation: true,\n        proposal\n      }\n    }\n\n    const result = await this.execute(parsed.commandId, validation.args, context)\n\n    return {\n      ok: true,\n      result\n    }\n  }\n\n  _getConfirmationKey(userId, room) {\n    return `${userId}:${room}`\n  }\n\n  _renderPreview(commandId, args) {\n    let preview = `${this.prefix}${commandId}`\n\n    for (const [key, value] of Object.entries(args)) {\n      const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value)\n      const needsQuotes = valueStr.includes(' ') || valueStr.includes('\"')\n      const escapedValue = needsQuotes\n        ? valueStr.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')\n        : valueStr\n      preview += ` --${key} ${needsQuotes ? `\"${escapedValue}\"` : escapedValue}`\n    }\n\n    return preview\n  }\n\n  /**\n   * Clear all pending proposal timers and proposals.\n   * Call this during shutdown or in test teardown to prevent timers from keeping the process alive.\n   * \n   * @public\n   */\n  clearPendingProposals() {\n    for (const proposal of this.pendingProposals.values()) {\n      if (proposal.timeoutId) {\n        clearTimeout(proposal.timeoutId)\n      }\n    }\n    this.pendingProposals.clear()\n  }\n\n  _log(event) {\n    if (this.disableLogging) {\n      return\n    }\n\n    // Fire and forget - write asynchronously without blocking\n    this._writeLog(event).catch(() => {})\n  }\n\n  async _writeLog(event) {\n    try {\n      const logDir = path.dirname(this.logPath)\n      await fs.promises.mkdir(logDir, { recursive: true })\n      const line = JSON.stringify(event) + '\\n'\n      await fs.promises.appendFile(this.logPath, line, 'utf8')\n    } catch (err) {\n      // Silent fail for logging errors\n    }\n  }\n}\n\nfunction coerceToBoolean(value) {\n  if (typeof value === 'boolean') {\n    return value\n  }\n\n  if (typeof value === 'number') {\n    return value !== 0\n  }\n\n  if (typeof value === 'string') {\n    const normalized = value.trim().toLowerCase()\n    if (['true', 't', 'yes', 'y', '1', 'on'].includes(normalized)) {\n      return true\n    }\n    if (['false', 'f', 'no', 'n', '0', 'off'].includes(normalized)) {\n      return false\n    }\n  }\n\n  return Boolean(value)\n}\n"
  },
  {
    "path": "src/DataStore.mjs",
    "content": "'use strict'\n\nexport class DataStore {\n  // Represents a persistent, database-backed storage for the robot. Extend this.\n  //\n  // Returns a new Datastore with no storage.\n  constructor (robot) {\n    this.robot = robot\n  }\n\n  // Public: Set value for key in the database. Overwrites existing\n  // values if present. Returns a promise which resolves when the\n  // write has completed.\n  //\n  // Value can be any JSON-serializable type.\n  async set (key, value) {\n    return await this._set(key, value, 'global')\n  }\n\n  // Public: Assuming `key` represents an object in the database,\n  // sets its `objectKey` to `value`. If `key` isn't already\n  // present, it's instantiated as an empty object.\n  async setObject (key, objectKey, value) {\n    const object = await this.get(key)\n    const target = object || {}\n    target[objectKey] = value\n    return await this.set(key, target)\n  }\n\n  // Public: Adds the supplied value(s) to the end of the existing\n  // array in the database marked by `key`. If `key` isn't already\n  // present, it's instantiated as an empty array.\n  async setArray (key, value) {\n    const object = await this.get(key)\n    const target = object ?? []\n    // Extend the array if the value is also an array, otherwise\n    // push the single value on the end.\n    if (Array.isArray(value)) {\n      return await this.set(key, target.concat(value))\n    } else {\n      return await this.set(key, target.concat([value]))\n    }\n  }\n\n  // Public: Get value by key if in the database or return `undefined`\n  // if not found. Returns a promise which resolves to the\n  // requested value.\n  async get (key) {\n    return await this._get(key, 'global')\n  }\n\n  // Public: Digs inside the object at `key` for a key named\n  // `objectKey`. If `key` isn't already present, or if it doesn't\n  // contain an `objectKey`, returns `undefined`.\n  async getObject (key, objectKey) {\n    const object = await this.get(key)\n    const target = object || {}\n    return target[objectKey]\n  }\n\n  // Private: Implements the underlying `set` logic for the datastore.\n  // This will be called by the public methods. This is one of two\n  // methods that must be implemented by subclasses of this class.\n  // `table` represents a unique namespace for this key, such as a\n  // table in a SQL database.\n  //\n  // This returns a resolved promise when the `set` operation is\n  // successful, and a rejected promise if the operation fails.\n  _set (key, value, table) {\n    throw new DataStoreUnavailable('Setter called on the abstract class.')\n  }\n\n  // Private: Implements the underlying `get` logic for the datastore.\n  // This will be called by the public methods. This is one of two\n  // methods that must be implemented by subclasses of this class.\n  // `table` represents a unique namespace for this key, such as a\n  // table in a SQL database.\n  //\n  // This returns a resolved promise containing the fetched value on\n  // success, and a rejected promise if the operation fails.\n  _get (key, table) {\n    throw new DataStoreUnavailable('Getter called on the abstract class.')\n  }\n}\n\nexport class DataStoreUnavailable extends Error {}\n\nexport default {\n  DataStore,\n  DataStoreUnavailable\n}\n"
  },
  {
    "path": "src/GenHubot.mjs",
    "content": "import { spawnSync } from 'node:child_process'\nimport File from 'node:fs'\nimport path from 'node:path'\n\nfunction runCommands (hubotDirectory, options) {\n  options.hubotInstallationPath = options?.hubotInstallationPath ?? 'hubot'\n  console.log('creating hubot directory', hubotDirectory)\n  try {\n    File.mkdirSync(hubotDirectory, { recursive: true })\n  } catch (error) {\n    console.log(`${hubotDirectory} exists, continuing to the next operation.`)\n  }\n  const envFilePath = path.resolve(process.cwd(), '.env')\n  process.chdir(hubotDirectory)\n\n  let output = spawnSync('npm', ['init', '-y'], { shell: true, stdio: 'inherit' })\n  console.log('npm init', output.stderr?.toString() ?? '')\n  if (options.hubotInstallationPath !== 'hubot') {\n    output = spawnSync('npm', ['pack', `${options.hubotInstallationPath}`], { shell: true, stdio: 'inherit' })\n    console.log('npm pack', output.stderr?.toString() ?? '', output.stdout?.toString() ?? '')\n    const customHubotPackage = JSON.parse(File.readFileSync(`${options.hubotInstallationPath}/package.json`, 'utf8'))\n    output = spawnSync('npm', ['i', `${customHubotPackage.name}-${customHubotPackage.version}.tgz`], { shell: true, stdio: 'inherit' })\n    console.log(`npm i ${customHubotPackage.name}-${customHubotPackage.version}.tgz`, output.stderr?.toString() ?? '', output.stdout?.toString() ?? '')\n  } else {\n    output = spawnSync('npm', ['i', 'hubot@latest'], { shell: true, stdio: 'inherit' })\n  }\n  output = spawnSync('npm', ['i', 'hubot-help@latest', 'hubot-rules@latest', 'hubot-diagnostics@latest'].concat([options.adapter]).filter(Boolean))\n  console.log('npm i', output.stderr?.toString() ?? '', output.stdout?.toString() ?? '')\n\n  File.mkdirSync(path.join('tests', 'doubles'), { recursive: true })\n\n  const externalScriptsPath = path.resolve('./', 'external-scripts.json')\n  if (!File.existsSync(externalScriptsPath)) {\n    File.writeFileSync(externalScriptsPath, '[]')\n  }\n\n  let escripts = File.readFileSync(externalScriptsPath, 'utf8')\n  if (escripts.length === 0) escripts = '[]'\n  const externalScripts = JSON.parse(escripts)\n  externalScripts.push('hubot-help')\n  externalScripts.push('hubot-rules')\n  externalScripts.push('hubot-diagnostics')\n\n  File.writeFileSync(externalScriptsPath, JSON.stringify(externalScripts, null, 2))\n\n  File.mkdirSync(path.join('scripts'), { recursive: true })\n\n  File.writeFileSync('./scripts/Xample.mjs', `// Description:\n//   Test script\n//\n// Commands:\n//   hubot helo - Responds with Hello World!.\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async (robot) => {\n  robot.respond(/helo$/, async res => {\n    await res.reply(\"HELO World! I'm Dumbotheelephant.\")\n  })\n  robot.respond(/helo room/, async res => {\n    await res.send('Hello World!')\n  })\n  robot.router.get('/helo', async (req, res) => {\n    res.send(\"HELO World! I'm Dumbotheelephant.\")\n  })\n}`)\n\n  File.writeFileSync('./tests/doubles/DummyAdapter.mjs', `\n  'use strict'\n  import { Adapter, TextMessage } from 'hubot'\n  \n  export class DummyAdapter extends Adapter {\n    constructor (robot) {\n      super(robot)\n      this.name = 'DummyAdapter'\n      this.messages = new Set()\n    }\n  \n    async send (envelope, ...strings) {\n      this.emit('send', envelope, ...strings)\n      this.robot.emit('send', envelope, ...strings)\n    }\n  \n    async reply (envelope, ...strings) {\n      this.emit('reply', envelope, ...strings)\n      this.robot.emit('reply', envelope, ...strings)\n    }\n  \n    async topic (envelope, ...strings) {\n      this.emit('topic', envelope, ...strings)\n      this.robot.emit('topic', envelope, ...strings)\n    }\n  \n    async play (envelope, ...strings) {\n      this.emit('play', envelope, ...strings)\n      this.robot.emit('play', envelope, ...strings)\n    }\n  \n    async run () {\n      // This is required to get the scripts loaded\n      this.emit('connected')\n    }\n  \n    close () {\n      this.emit('closed')\n    }\n  \n    async say (user, message, room) {\n      this.messages.add(message)\n      user.room = room\n      await this.robot.receive(new TextMessage(user, message))\n    }\n  }\n  export default {\n    async use (robot) {\n      return new DummyAdapter(robot)\n    }\n  }\n`)\n  File.writeFileSync('./tests/XampleTest.mjs', `\n  import { describe, it, beforeEach, afterEach } from 'node:test'\n  import assert from 'node:assert/strict'\n  \n  import { Robot } from 'hubot'\n  \n  // You need a dummy adapter to test scripts\n  import dummyRobot from './doubles/DummyAdapter.mjs'\n  \n  // Mocks Aren't Stubs\n  // https://www.martinfowler.com/articles/mocksArentStubs.html\n  \n  describe('Xample testing Hubot scripts', () => {\n    let robot = null\n    beforeEach(async () => {\n      process.env.EXPRESS_PORT = 0\n      robot = new Robot(dummyRobot, true, 'Dumbotheelephant')\n      await robot.loadAdapter()\n      await robot.run()\n      await robot.loadFile('./scripts', 'Xample.mjs')\n    })\n    afterEach(() => {\n      delete process.env.EXPRESS_PORT\n      robot.shutdown()\n    })\n    it('should handle /helo request', async () => {\n      const expected = \"HELO World! I'm Dumbotheelephant.\"\n      const url = 'http://localhost:' + robot.server.address().port + '/helo'\n      const response = await fetch(url)\n      const actual = await response.text()\n      assert.strictEqual(actual, expected)\n      })\n    it('should reply with expected message', async () => {\n      const expected = \"HELO World! I'm Dumbotheelephant.\"\n      const user = robot.brain.userForId('test-user', { name: 'test user' })\n      let actual = ''\n      robot.on('reply', (envelope, ...strings) => {\n        actual = strings.join('')\n      })\n      await robot.adapter.say(user, '@Dumbotheelephant helo', 'test-room')\n      assert.strictEqual(actual, expected)\n    })\n  \n    it('should send message to the #general room', async () => {\n      const expected = 'general'\n      const user = robot.brain.userForId('test-user', { name: 'test user' })\n      let actual = ''\n      robot.on('send', (envelope, ...strings) => {\n        actual = envelope.room\n      })\n      await robot.adapter.say(user, '@Dumbotheelephant helo room', 'general')\n      assert.strictEqual(actual, expected)\n    })\n  })  \n`)\n\n  const packageJsonPath = path.resolve(process.cwd(), 'package.json')\n  const packageJson = JSON.parse(File.readFileSync(packageJsonPath, 'utf8'))\n\n  packageJson.scripts = {\n    start: 'hubot',\n    test: 'node --test'\n  }\n  packageJson.description = 'A simple helpful robot for your Company'\n  if (options.adapter) {\n    packageJson.scripts.start += ` --adapter ${options.adapter}`\n  }\n\n  File.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))\n  console.log('package.json updated successfully.')\n  const hubotEnvFilePath = path.resolve('.env')\n\n  try {\n    File.accessSync(envFilePath)\n    File.copyFileSync(envFilePath, hubotEnvFilePath)\n    console.log('.env file copied successfully.')\n\n    const envContent = File.readFileSync(hubotEnvFilePath, 'utf8')\n    const envLines = envContent.split('\\n')\n\n    for (const line of envLines) {\n      const trimmedLine = line.trim()\n\n      if (trimmedLine && !trimmedLine.startsWith('#')) {\n        const [key, ...values] = trimmedLine.split('=')\n        const value = values.join('=')\n        process.env[key] = value\n      }\n    }\n  } catch (error) {\n    console.log('.env file not found, continuing to the next operation.')\n  }\n}\nexport default (hubotDirectory, options) => {\n  try {\n    runCommands(hubotDirectory, options)\n  } catch (error) {\n    console.error('An error occurred:', error)\n  }\n}\n"
  },
  {
    "path": "src/HttpClient.mjs",
    "content": "/*\nCopyright (c) 2014 rick\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n\n/*\n\nApril 15, 2023\nReasoning:\nScopedHttpClient is no longer maintained.\n\nDecision:\nImplement a phased approach to deprecate `robot.http` all together in favor of `fetch`.\n1. Convert ScopedHttpClient to Javascript and include the module in this repo\n2. Add a deprecation warning to `robot.http`\n3. Remove `robot.http` in a future release\n*/\nimport path from 'node:path'\nimport http from 'node:http'\nimport https from 'node:https'\nimport qs from 'node:querystring'\n\nconst nonPassThroughOptions = [\n  'headers', 'hostname', 'encoding', 'auth', 'port',\n  'protocol', 'agent', 'httpAgent', 'httpsAgent', 'query', 'host', 'path',\n  'pathname', 'slashes', 'hash'\n]\n\nclass ScopedClient {\n  constructor (url, options) {\n    this.options = this.buildOptions(url, options)\n    this.passthroughOptions = reduce(extend({}, this.options), nonPassThroughOptions)\n  }\n\n  request (method, reqBody, callback) {\n    let req\n    if (typeof (reqBody) === 'function') {\n      callback = reqBody\n      reqBody = null\n    }\n\n    try {\n      let requestModule\n      const headers = extend({}, this.options.headers)\n      const sendingData = reqBody && (reqBody.length > 0)\n      headers.Host = this.options.hostname\n      if (this.options.port) { headers.Host += `:${this.options.port}` }\n\n      // If `callback` is `undefined` it means the caller isn't going to stream\n      // the body of the request using `callback` and we can set the\n      // content-length header ourselves.\n      //\n      // There is no way to conveniently assert in an else clause because the\n      // transfer encoding could be chunked or using a newer framing mechanism.\n      // And this is why we should'nt be creating a wrapper around http.\n      // Please just use `fetch`.\n      if (callback === undefined) {\n        headers['Content-Length'] = sendingData ? Buffer.byteLength(reqBody, this.options.encoding) : 0\n      }\n\n      if (this.options.auth) {\n        headers.Authorization = 'Basic ' + Buffer.from(this.options.auth, 'base64')\n      }\n\n      const port = this.options.port ||\n        ScopedClient.defaultPort[this.options.protocol] || 80\n\n      let {\n        agent\n      } = this.options\n      if (this.options.protocol === 'https:') {\n        requestModule = https\n        if (this.options.httpsAgent) { agent = this.options.httpsAgent }\n      } else {\n        requestModule = http\n        if (this.options.httpAgent) { agent = this.options.httpAgent }\n      }\n\n      const requestOptions = {\n        port,\n        host: this.options.hostname,\n        method,\n        path: this.fullPath(),\n        headers,\n        agent\n      }\n\n      // Extends the previous request options with all remaining options\n      extend(requestOptions, this.passthroughOptions)\n\n      req = requestModule.request(requestOptions)\n\n      if (this.options.timeout) {\n        req.setTimeout(this.options.timeout, () => req.abort())\n      }\n\n      if (callback) {\n        req.on('error', callback)\n      }\n      if (sendingData) { req.write(reqBody, this.options.encoding) }\n      if (callback) { callback(null, req) }\n    } catch (err) {\n      if (callback) { callback(err, req) }\n    }\n\n    return callback => {\n      if (callback) {\n        req.on('response', res => {\n          res.setEncoding(this.options.encoding)\n          let body = ''\n          res.on('data', chunk => {\n            body += chunk\n          })\n\n          return res.on('end', () => callback(null, res, body))\n        })\n        req.on('error', error => callback(error, null, null))\n      }\n\n      req.end()\n      return this\n    }\n  }\n\n  // Adds the query string to the path.\n  fullPath (p) {\n    const search = qs.stringify(this.options.query)\n    let full = this.join(p)\n    if (search.length > 0) { full += `?${search}` }\n    return full\n  }\n\n  scope (url, options, callback) {\n    const override = this.buildOptions(url, options)\n    const scoped = new ScopedClient(this.options)\n      .protocol(override.protocol)\n      .host(override.hostname)\n      .path(override.pathname)\n\n    if (typeof (url) === 'function') {\n      callback = url\n    } else if (typeof (options) === 'function') {\n      callback = options\n    }\n    if (callback) { callback(scoped) }\n    return scoped\n  }\n\n  join (suffix) {\n    const p = this.options.pathname || '/'\n    if (suffix && (suffix.length > 0)) {\n      if (suffix.match(/^\\//)) {\n        return suffix\n      } else {\n        return path.join(p, suffix)\n      }\n    } else {\n      return p\n    }\n  }\n\n  path (p) {\n    this.options.pathname = this.join(p)\n    return this\n  }\n\n  query (key, value) {\n    if (!this.options.query) { this.options.query = {} }\n    if (typeof (key) === 'string') {\n      if (value) {\n        this.options.query[key] = value\n      } else {\n        delete this.options.query[key]\n      }\n    } else {\n      extend(this.options.query, key)\n    }\n    return this\n  }\n\n  host (h) {\n    if (h && (h.length > 0)) { this.options.hostname = h }\n    return this\n  }\n\n  port (p) {\n    if (p && ((typeof (p) === 'number') || (p.length > 0))) {\n      this.options.port = p\n    }\n    return this\n  }\n\n  protocol (p) {\n    if (p && (p.length > 0)) { this.options.protocol = p }\n    return this\n  }\n\n  encoding (e) {\n    if (e == null) { e = 'utf-8' }\n    this.options.encoding = e\n    return this\n  }\n\n  timeout (time) {\n    this.options.timeout = time\n    return this\n  }\n\n  auth (user, pass) {\n    if (!user) {\n      this.options.auth = null\n    } else if (!pass && user.match(/:/)) {\n      this.options.auth = user\n    } else {\n      this.options.auth = `${user}:${pass}`\n    }\n    return this\n  }\n\n  header (name, value) {\n    this.options.headers[name] = value\n    return this\n  }\n\n  headers (h) {\n    extend(this.options.headers, h)\n    return this\n  }\n\n  buildOptions () {\n    const options = {}\n    let i = 0\n    while (arguments[i]) {\n      const ty = typeof arguments[i]\n      if (ty === 'string') {\n        const parsedUrl = new URL(arguments[i])\n        const query = {}\n        parsedUrl.searchParams.forEach((v, k) => {\n          query[k] = v\n        })\n\n        extend(options, {\n          href: parsedUrl.href,\n          origin: parsedUrl.origin,\n          protocol: parsedUrl.protocol,\n          username: parsedUrl.username,\n          password: parsedUrl.password,\n          host: parsedUrl.host,\n          hostname: parsedUrl.hostname,\n          port: parsedUrl.port,\n          pathname: parsedUrl.pathname,\n          search: parsedUrl.search,\n          searchParams: parsedUrl.searchParams,\n          query,\n          hash: parsedUrl.hash\n        })\n        delete options.url\n        delete options.href\n        delete options.search\n      } else if (ty !== 'function') {\n        extend(options, arguments[i])\n      }\n      i += 1\n    }\n    if (!options.headers) { options.headers = {} }\n    if (options.encoding == null) { options.encoding = 'utf-8' }\n    return options\n  }\n}\n\nScopedClient.methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'HEAD']\nScopedClient.methods.forEach(method => {\n  ScopedClient.prototype[method.toLowerCase()] = function (body, callback) { return this.request(method, body, callback) }\n})\nScopedClient.prototype.del = ScopedClient.prototype.delete\n\nScopedClient.defaultPort = { 'http:': 80, 'https:': 443, http: 80, https: 443 }\n\nconst extend = function (a, b) {\n  Object.keys(b).forEach(prop => {\n    a[prop] = b[prop]\n  })\n  return a\n}\n\n// Removes keys specified in second parameter from first parameter\nconst reduce = function (a, b) {\n  for (const propName of Array.from(b)) {\n    delete a[propName]\n  }\n  return a\n}\nexport default {\n  create (url, options) {\n    return new ScopedClient(url, options)\n  }\n}\n"
  },
  {
    "path": "src/Listener.mjs",
    "content": "'use strict'\n\nimport { inspect } from 'node:util'\nimport Middleware from './Middleware.mjs'\n\nclass Listener {\n  // Listeners receive every message from the chat source and decide if they\n  // want to act on it.\n  // An identifier should be provided in the options parameter to uniquely\n  // identify the listener (options.id).\n  //\n  // robot    - A Robot instance.\n  // matcher  - A Function that determines if this listener should trigger the\n  //            callback.\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is triggered if the incoming message matches.\n  constructor (robot, matcher, options, callback) {\n    this.robot = robot\n    this.matcher = matcher\n    this.options = options ?? {}\n    this.callback = callback\n\n    if (this.matcher == null) {\n      throw new Error('Missing a matcher for Listener')\n    }\n\n    if (!this.callback) {\n      this.callback = this.options\n      this.options = {}\n    }\n\n    if (!this.options?.id) {\n      this.options.id = null\n    }\n\n    if (this.callback == null || typeof this.callback !== 'function') {\n      throw new Error('Missing a callback for Listener')\n    }\n  }\n\n  // Public: Determines if the listener likes the content of the message. If\n  // so, a Response built from the given Message is passed through all\n  // registered middleware and potentially the Listener callback. Note that\n  // middleware can intercept the message and prevent the callback from ever\n  // being executed.\n  //\n  // message - A Message instance.\n  // middleware - Optional Middleware object to execute before the Listener callback\n  //\n  // Returns the result of the callback.\n  async call (message, middleware) {\n    if (middleware && typeof middleware === 'function') {\n      const fn = middleware\n      middleware = new Middleware(this.robot)\n      middleware.register(fn)\n    }\n\n    if (!middleware) {\n      middleware = new Middleware(this.robot)\n    }\n\n    const match = this.matcher(message)\n    if (!match) return null\n    if (this.regex) {\n      this.robot.logger.debug(`Message '${message}' matched regex /${inspect(this.regex)}/; listener.options = ${inspect(this.options)}`)\n    }\n\n    const response = new this.robot.Response(this.robot, message, match)\n\n    try {\n      const shouldContinue = await middleware.execute({ listener: this, response })\n      if (shouldContinue === false) return null\n    } catch (e) {\n      this.robot.logger.error(`Error executing middleware for listener: ${e.stack}`)\n    }\n    try {\n      return await this.callback(response)\n    } catch (e) {\n      this.robot.logger.error(`Error executing listener callback: ${e.stack}`)\n      this.robot.emit('error', e, response)\n    }\n    return null\n  }\n}\n\nclass TextListener extends Listener {\n  // TextListeners receive every message from the chat source and decide if they\n  // want to act on it.\n  //\n  // robot    - A Robot instance.\n  // regex    - A Regex that determines if this listener should trigger the\n  //            callback.\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is triggered if the incoming message matches.\n  constructor (robot, regex, options, callback) {\n    function matcher (message) {\n      if (typeof message.match === 'function') {\n        return message.match(regex)\n      }\n    }\n\n    super(robot, matcher, options, callback)\n    this.regex = regex\n  }\n}\n\nexport {\n  Listener,\n  TextListener\n}\n"
  },
  {
    "path": "src/Message.mjs",
    "content": "'use strict'\n\nexport class Message {\n  // Represents an incoming message from the chat.\n  //\n  // user - A User instance that sent the message.\n  // done - A boolean indicating if the message has been handled.\n  constructor (user, done) {\n    this.user = user\n    this.done = done || false\n    this.room = this.user?.room\n  }\n\n  // Indicates that no other Listener should be called on this object\n  //\n  // Returns nothing.\n  finish () {\n    this.done = true\n  }\n}\n\nexport class TextMessage extends Message {\n  // Represents an incoming message from the chat.\n  //\n  // user - A User instance that sent the message.\n  // text - A String message.\n  // id   - A String of the message ID.\n  constructor (user, text, id) {\n    super(user)\n    this.text = text\n    this.id = id\n  }\n\n  // Determines if the message matches the given regex.\n  //\n  // regex - A Regex to check.\n  //\n  // Returns a Match object or null.\n  match (regex) {\n    return this.text.match(regex)\n  }\n\n  // String representation of a TextMessage\n  //\n  // Returns the message text\n  toString () {\n    return this.text\n  }\n}\n\n// Represents an incoming user entrance notification.\n//\n// user - A User instance for the user who entered.\nexport class EnterMessage extends Message {}\n\n// Represents an incoming user exit notification.\n//\n// user - A User instance for the user who left.\nexport class LeaveMessage extends Message {}\n\n// Represents an incoming topic change notification.\n//\n// user - A User instance for the user who changed the topic.\n// text - A String of the new topic\n// id   - A String of the message ID.\nexport class TopicMessage extends TextMessage {}\n\n// Represents a catch all error message.\n//\n// user - A User instance that sent the message.\n// message - A TextMessage with the message.\nexport class CatchAllMessage extends Message {\n  // Represents a message that no matchers matched.\n  //\n  // message - The original message.\n  constructor (message) {\n    super(message.user)\n    this.message = message\n  }\n}\n\nexport default {\n  Message,\n  TextMessage,\n  EnterMessage,\n  LeaveMessage,\n  TopicMessage,\n  CatchAllMessage\n}\n"
  },
  {
    "path": "src/Middleware.mjs",
    "content": "'use strict'\n\nclass Middleware {\n  constructor (robot) {\n    this.robot = robot\n    this.stack = []\n  }\n\n  // Public: Execute all middleware in order and call 'next' with the latest\n  // 'done' callback if last middleware calls through. If all middleware is\n  // compliant, 'done' should be called with no arguments when the entire\n  // round trip is complete.\n  //\n  // context - context object that is passed through the middleware stack.\n  //     When handling errors, this is assumed to have a `response` property.\n  //\n  // Returns bool, true | false, whether or not to continue execution\n  async execute (context) {\n    let shouldContinue = true\n    for await (const middleware of this.stack) {\n      try {\n        shouldContinue = await middleware(context)\n        if (shouldContinue === false) break\n      } catch (e) {\n        this.robot.emit('error', e, context.response)\n        break\n      }\n    }\n    return shouldContinue\n  }\n\n  // Public: Registers new middleware\n  //\n  // middleware - Middleware function to execute prior to the listener callback. Return false to prevent execution of the listener callback.\n  //\n  // Returns nothing.\n  register (middleware) {\n    if (middleware.length !== 1) {\n      throw new Error(`Incorrect number of arguments for middleware callback (expected 1, got ${middleware.length})`)\n    }\n    this.stack.push(middleware)\n  }\n}\n\nexport default Middleware\n"
  },
  {
    "path": "src/OptParse.mjs",
    "content": "import EventEmitter from 'node:events'\nclass OptParse extends EventEmitter {\n  constructor (switches) {\n    super()\n    this.switches = switches\n  }\n\n  mappings (switches) {\n    const mappings = switches.reduce((acc, current) => {\n      acc[current[0].replace('-', '')] = current[1].split(' ')[0].replace('--', '')\n      return acc\n    }, {})\n    return mappings\n  }\n\n  parse (args) {\n    const mappings = this.mappings(this.switches)\n    const options = {}\n    for (let i = 0; i < args.length; i++) {\n      const arg = args[i]\n      if (arg.startsWith('-')) {\n        const cliArg = arg.replace(/^-+/, '')\n        let propertyName = mappings[cliArg]\n        if (!propertyName) {\n          propertyName = Object.values(mappings).find(value => value === cliArg)\n        }\n        const nameToEmit = propertyName\n        propertyName = propertyName.replace(/-([a-z])/g, g => g[1].toUpperCase())\n        const nextArg = args[i + 1]\n        if (nextArg && !nextArg.startsWith('-')) {\n          options[propertyName] = nextArg\n          i++\n        } else {\n          options[propertyName] = true\n        }\n        this.emit(nameToEmit, propertyName, nextArg)\n      }\n    }\n    return options\n  }\n\n  toString () {\n    return `${this.banner}\n${this.switches.map(([key, description]) => `  ${key}, ${description}`).join('\\n')}`\n  }\n}\n\nexport default OptParse\n"
  },
  {
    "path": "src/Response.mjs",
    "content": "'use strict'\n\nclass Response {\n  // Public: Responses are sent to matching listeners. Messages know about the\n  // content and user that made the original message, and how to reply back to\n  // them.\n  //\n  // robot   - A Robot instance.\n  // message - A Message instance.\n  // match   - A Match object from the successful Regex match.\n  constructor (robot, message, match) {\n    this.robot = robot\n    this.message = message\n    this.match = match\n    this.envelope = {\n      room: this.message.room,\n      user: this.message.user,\n      message: this.message\n    }\n  }\n\n  // Public: Posts a message back to the chat source\n  //\n  // strings - One or more strings to be posted. The order of these strings\n  //           should be kept intact.\n  //\n  // Returns result from middleware.\n  async send (...strings) {\n    return await this.#runWithMiddleware('send', { plaintext: true }, ...strings)\n  }\n\n  // Public: Posts an emote back to the chat source\n  //\n  // strings - One or more strings to be posted. The order of these strings\n  //           should be kept intact.\n  //\n  // Returns result from middleware.\n  async emote (...strings) {\n    return await this.#runWithMiddleware('emote', { plaintext: true }, ...strings)\n  }\n\n  // Public: Posts a message mentioning the current user.\n  //\n  // strings - One or more strings to be posted. The order of these strings\n  //           should be kept intact.\n  //\n  // Returns result from middleware.\n  async reply (...strings) {\n    return await this.#runWithMiddleware('reply', { plaintext: true }, ...strings)\n  }\n\n  // Public: Posts a topic changing message\n  //\n  // strings - One or more strings to set as the topic of the\n  //           room the bot is in.\n  //\n  // Returns result from middleware.\n  async topic (...strings) {\n    return await this.#runWithMiddleware('topic', { plaintext: true }, ...strings)\n  }\n\n  // Public: Play a sound in the chat source\n  //\n  // strings - One or more strings to be posted as sounds to play. The order of\n  //           these strings should be kept intact.\n  //\n  // Returns result from middleware.\n  async play (...strings) {\n    return await this.#runWithMiddleware('play', {}, ...strings)\n  }\n\n  // Public: Posts a message in an unlogged room\n  //\n  // strings - One or more strings to be posted. The order of these strings\n  //           should be kept intact.\n  //\n  // Returns result from middleware.\n  async locked (...strings) {\n    await this.#runWithMiddleware('locked', { plaintext: true }, ...strings)\n  }\n\n  // Call with a method for the given strings using response\n  // middleware.\n  async #runWithMiddleware (methodName, opts, ...strings) {\n    const context = {\n      response: this,\n      strings,\n      method: methodName\n    }\n\n    if (opts.plaintext != null) {\n      context.plaintext = true\n    }\n\n    const shouldContinue = await this.robot.middleware.response.execute(context)\n    if (shouldContinue === false) return\n    return await this.robot.adapter[methodName](this.envelope, ...context.strings)\n  }\n\n  // Public: Picks a random item from the given items.\n  //\n  // items - An Array of items.\n  //\n  // Returns a random item.\n  random (items) {\n    return items[Math.floor(Math.random() * items.length)]\n  }\n\n  // Public: Tell the message to stop dispatching to listeners\n  //\n  // Returns nothing.\n  finish () {\n    this.message.finish()\n  }\n\n  // Public: Creates a scoped http client with chainable methods for\n  // modifying the request. This doesn't actually make a request though.\n  // Once your request is assembled, you can call `get()`/`post()`/etc to\n  // send the request.\n  //\n  // Returns a ScopedClient instance.\n  http (url, options) {\n    return this.robot.http(url, options)\n  }\n}\n\nexport default Response\n"
  },
  {
    "path": "src/Robot.mjs",
    "content": "'use strict'\n\nimport EventEmitter from 'node:events'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { pathToFileURL, fileURLToPath } from 'node:url'\nimport pino from 'pino'\nimport HttpClient from './HttpClient.mjs'\nimport Brain from './Brain.mjs'\nimport Response from './Response.mjs'\nimport { Listener, TextListener } from './Listener.mjs'\nimport Message from './Message.mjs'\nimport Middleware from './Middleware.mjs'\nimport { CommandBus } from './CommandBus.mjs'\n\nconst File = fs.promises\nconst HUBOT_DEFAULT_ADAPTERS = ['Campfire', 'Shell']\nconst HUBOT_DOCUMENTATION_SECTIONS = ['description', 'dependencies', 'configuration', 'commands', 'notes', 'author', 'authors', 'examples', 'tags', 'urls']\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nclass Robot {\n  // Robots receive messages from a chat source (Campfire, irc, etc), and\n  // dispatch them to matching listeners.\n  //\n  // adapter     - A String of the adapter name.\n  // httpd       - A Boolean whether to enable the HTTP daemon.\n  // name        - A String of the robot name, defaults to Hubot.\n  // alias       - A String of the alias of the robot name\n  constructor (adapter, httpd, name, alias) {\n    if (name == null) {\n      name = 'Hubot'\n    }\n    if (alias == null) {\n      alias = false\n    }\n\n    this.name = name\n    this.events = new EventEmitter()\n    this.brain = new Brain(this)\n    this.alias = alias\n    this.adapter = null\n    this.adapterName = 'Shell'\n    if (adapter && typeof (adapter) === 'object') {\n      this.adapter = adapter\n      this.adapterName = adapter.name ?? adapter.constructor.name\n    } else {\n      this.adapterName = adapter ?? this.adapterName\n    }\n\n    this.shouldEnableHttpd = httpd ?? true\n    this.datastore = null\n    this.Response = Response\n    this.commands = new CommandBus(this)\n    this.helpStrings = []\n    this.listeners = []\n    this.middleware = {\n      listener: new Middleware(this),\n      response: new Middleware(this),\n      receive: new Middleware(this)\n    }\n    this.logger = pino({\n      name,\n      level: process.env.HUBOT_LOG_LEVEL || 'info'\n    })\n\n    this.pingIntervalId = null\n    this.globalHttpOptions = {}\n\n    this.parseVersion()\n    this.errorHandlers = []\n\n    this.on('error', (err, res) => {\n      return this.invokeErrorHandlers(err, res)\n    })\n    this.on('listening', this.herokuKeepalive.bind(this))\n    \n    // Register built-in help command\n    this.registerHelpCommand()\n  }\n\n  // Private: Register the built-in help command\n  registerHelpCommand() {\n    this.commands.register({\n      id: 'help',\n      description: 'Show available commands or search for specific commands',\n      aliases: ['commands', 'list commands', 'show commands'],\n      args: {\n        query: { type: 'string', required: false }\n      },\n      confirm: 'never',\n      examples: [\n        'help',\n        'help tickets',\n        'help search \"create ticket\"'\n      ],\n      handler: async (ctx) => {\n        const { query } = ctx.args\n\n        // Search mode: use search() API\n        if (query && query.startsWith('search ')) {\n          const searchQuery = query.slice(7).trim()\n          const results = this.commands.search(searchQuery)\n          \n          if (results.length === 0) {\n            return `No commands found matching \"${searchQuery}\"`\n          }\n\n          let response = `Commands matching \"${searchQuery}\":\\n\\n`\n          results.slice(0, 5).forEach(result => {\n            const cmd = this.commands.getCommand(result.id)\n            response += `• ${cmd.id} - ${cmd.description} (matched: ${result.matchedOn}, score: ${result.score})\\n`\n          })\n\n          if (results.length > 5) {\n            response += `\\n...and ${results.length - 5} more`\n          }\n\n          return response\n        }\n\n        // Prefix filter mode\n        if (query) {\n          const commands = this.commands.listCommands({ prefix: query })\n          \n          if (commands.length === 0) {\n            return `No commands found starting with \"${query}\"`\n          }\n\n          let response = `Commands starting with \"${query}\":\\n\\n`\n          commands.forEach(cmd => {\n            response += `• ${cmd.id} - ${cmd.description}\\n`\n            if (cmd.aliases.length > 0) {\n              response += `  Intent: ${cmd.aliases.join(', ')}\\n`\n            }\n          })\n\n          return response\n        }\n\n        // List all commands\n        const commands = this.commands.listCommands()\n        \n        if (commands.length === 0) {\n          return 'No commands registered'\n        }\n\n        // Group by prefix\n        const grouped = commands.reduce((acc, cmd) => {\n          const prefix = cmd.id.split('.')[0]\n          if (!acc[prefix]) acc[prefix] = []\n          acc[prefix].push(cmd)\n          return acc\n        }, {})\n\n        let response = 'Available commands:\\n\\n'\n        Object.keys(grouped).sort().forEach(prefix => {\n          response += `${prefix}:\\n`\n          grouped[prefix].forEach(cmd => {\n            response += `  • ${cmd.id} - ${cmd.description}\\n`\n          })\n          response += '\\n'\n        })\n\n        response += 'Usage:\\n'\n        response += '  @' + this.name + ' help <prefix>      - Show commands with prefix\\n'\n        response += '  @' + this.name + ' help search <query> - Search commands\\n'\n        response += '  @' + this.name + ' <command> --help   - Show command details'\n\n        return response\n      }\n    })\n  }\n\n  // Public: Adds a custom Listener with the provided matcher, options, and\n  // callback\n  //\n  // matcher  - A Function that determines whether to call the callback.\n  //            Expected to return a truthy value if the callback should be\n  //            executed.\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object if the\n  //            matcher function returns true.\n  //\n  // Returns nothing.\n  listen (matcher, options, callback) {\n    this.listeners.push(new Listener(this, matcher, options, callback))\n  }\n\n  // Public: Adds a Listener that attempts to match incoming messages based on\n  // a Regex.\n  //\n  // regex    - A Regex that determines if the callback should be called.\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  hear (regex, options, callback) {\n    this.listeners.push(new TextListener(this, regex, options, callback))\n  }\n\n  // Public: Adds a Listener that attempts to match incoming messages directed\n  // at the robot based on a Regex. All regexes treat patterns like they begin\n  // with a '^'\n  //\n  // regex    - A Regex that determines if the callback should be called.\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  respond (regex, options, callback) {\n    this.hear(this.respondPattern(regex), options, callback)\n  }\n\n  // Public: Build a regular expression that matches messages addressed\n  // directly to the robot\n  //\n  // regex - A RegExp for the message part that follows the robot's name/alias\n  //\n  // Returns RegExp.\n  respondPattern (regex) {\n    const regexWithoutModifiers = regex.toString().split('/')\n    regexWithoutModifiers.shift()\n    const modifiers = regexWithoutModifiers.pop()\n    const regexStartsWithAnchor = regexWithoutModifiers[0] && regexWithoutModifiers[0][0] === '^'\n    const pattern = regexWithoutModifiers.join('/')\n    const name = this.name.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, '\\\\$&')\n\n    if (regexStartsWithAnchor) {\n      this.logger.warn('Anchors don’t work well with respond, perhaps you want to use \\'hear\\'')\n      this.logger.warn(`The regex in question was ${regex.toString()}`)\n    }\n\n    if (!this.alias) {\n      return new RegExp('^\\\\s*[@]?' + name + '[:,]?\\\\s*(?:' + pattern + ')', modifiers)\n    }\n\n    const alias = this.alias.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, '\\\\$&')\n\n    // matches properly when alias is substring of name\n    if (name.length > alias.length) {\n      return new RegExp('^\\\\s*[@]?(?:' + name + '[:,]?|' + alias + '[:,]?)\\\\s*(?:' + pattern + ')', modifiers)\n    }\n\n    // matches properly when name is substring of alias\n    return new RegExp('^\\\\s*[@]?(?:' + alias + '[:,]?|' + name + '[:,]?)\\\\s*(?:' + pattern + ')', modifiers)\n  }\n\n  // Public: Adds a Listener that triggers when anyone enters the room.\n  //\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  enter (options, callback) {\n    this.listen(msg => msg instanceof Message.EnterMessage, options, callback)\n  }\n\n  // Public: Adds a Listener that triggers when anyone leaves the room.\n  //\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  leave (options, callback) {\n    this.listen(msg => msg instanceof Message.LeaveMessage, options, callback)\n  }\n\n  // Public: Adds a Listener that triggers when anyone changes the topic.\n  //\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  topic (options, callback) {\n    this.listen(msg => msg instanceof Message.TopicMessage, options, callback)\n  }\n\n  // Public: Adds an error handler when an uncaught exception or user emitted\n  // error event occurs.\n  //\n  // callback - A Function that is called with the error object.\n  //\n  // Returns nothing.\n  error (callback) {\n    this.errorHandlers.push(callback)\n  }\n\n  // Calls and passes any registered error handlers for unhandled exceptions or\n  // user emitted error events.\n  //\n  // err - An Error object.\n  // res - An optional Response object that generated the error\n  //\n  // Returns nothing.\n  invokeErrorHandlers (error, res) {\n    this.logger.error(error.stack)\n\n    this.errorHandlers.forEach((errorHandler) => {\n      try {\n        errorHandler(error, res)\n      } catch (errorHandlerError) {\n        this.logger.error(`while invoking error handler: ${errorHandlerError}\\n${errorHandlerError.stack}`)\n      }\n    })\n  }\n\n  // Public: Adds a Listener that triggers when no other text matchers match.\n  //\n  // options  - An Object of additional parameters keyed on extension name\n  //            (optional).\n  // callback - A Function that is called with a Response object.\n  //\n  // Returns nothing.\n  catchAll (options, callback) {\n    // `options` is optional; need to isolate the real callback before\n    // wrapping it with logic below\n    if (callback == null) {\n      callback = options\n      options = {}\n    }\n\n    this.listen(isCatchAllMessage, options, async msg => {\n      await callback(msg)\n    })\n  }\n\n  // Public: Registers new middleware for execution after matching but before\n  // Listener callbacks\n  //\n  // middleware - A function that determines whether or not a given matching\n  //              Listener should be executed. The function is called with\n  //              (context). If execution should, the middleware should return\n  //              true. If not, the middleware should return false.\n  //\n  // Returns nothing.\n  listenerMiddleware (middleware) {\n    this.middleware.listener.register(middleware)\n  }\n\n  // Public: Registers new middleware for execution as a response to any\n  // message is being sent.\n  //\n  // middleware - A function that examines an outgoing message and can modify\n  //              it or prevent its sending. The function is called with\n  //              (context). If execution should continue, return true\n  //              otherwise return false to stop. To modify the\n  //              outgoing message, set context.string to a new message.\n  //\n  // Returns nothing.\n  responseMiddleware (middleware) {\n    this.middleware.response.register(middleware)\n  }\n\n  // Public: Registers new middleware for execution before matching\n  //\n  // middleware - A function that determines whether or not listeners should be\n  //              checked. The function is called with (context). If execution\n  //              should continue to the next\n  //              middleware or matching phase, it should return true or nothing\n  //              otherwise return false to stop.\n  //\n  // Returns nothing.\n  receiveMiddleware (middleware) {\n    this.middleware.receive.register(middleware)\n  }\n\n  // Public: Passes the given message to any interested Listeners after running\n  //         receive middleware.\n  //\n  // message - A Message instance. Listeners can flag this message as 'done' to\n  //           prevent further execution.\n  //\n  // Returns array of results from listeners.\n  async receive (message) {\n    const context = { response: new Response(this, message) }\n    const shouldContinue = await this.middleware.receive.execute(context)\n    if (shouldContinue === false) return null\n    return await this.processListeners(context)\n  }\n\n  // Private: Passes the given message to any interested Listeners.\n  //\n  // message - A Message instance. Listeners can flag this message as 'done' to\n  //           prevent further execution.\n  //\n  // Returns array of results from listeners.\n  async processListeners (context) {\n    // Try executing all registered Listeners in order of registration\n    // and return after message is done being processed\n    const results = []\n    let anyListenersExecuted = false\n    for await (const listener of this.listeners) {\n      try {\n        const match = listener.matcher(context.response.message)\n        if (!match) {\n          continue\n        }\n        const result = await listener.call(context.response.message, this.middleware.listener)\n        results.push(result)\n        anyListenersExecuted = true\n      } catch (err) {\n        this.emit('error', err, context)\n      }\n      if (context.response.message.done) {\n        break\n      }\n    }\n\n    if (!isCatchAllMessage(context.response.message) && !anyListenersExecuted) {\n      this.logger.debug('No listeners executed; falling back to catch-all')\n      try {\n        const result = await this.receive(new Message.CatchAllMessage(context.response.message))\n        results.push(result)\n      } catch (err) {\n        this.emit('error', err, context)\n      }\n    }\n\n    return results\n  }\n\n  async loadmjs (filePath) {\n    const forImport = this.prepareForImport(filePath)\n    const script = await import(forImport)\n    let result = null\n    if (typeof script?.default === 'function') {\n      result = await script.default(this)\n    } else {\n      this.logger.warn(`Expected ${filePath} (after preparing for import ${forImport}) to assign a function to export default, got ${typeof script}`)\n    }\n    return result\n  }\n\n  async loadts (filePath) {\n    return this.loadmjs(filePath)\n  }\n\n  async loadjs (filePath) {\n    const forImport = this.prepareForImport(filePath)\n    const script = (await import(forImport)).default\n    let result = null\n    if (typeof script === 'function') {\n      result = await script(this)\n    } else {\n      this.logger.warn(`Expected ${filePath} (after preparing for import ${forImport}) to assign a function to module.exports, got ${typeof script}`)\n    }\n    return result\n  }\n\n  // Public: Loads a file in path.\n  //\n  // filepath - A String path on the filesystem.\n  // filename - A String filename in path on the filesystem.\n  //\n  // Returns nothing.\n  async loadFile (filepath, filename) {\n    const ext = path.extname(filename)?.replace('.', '')\n    const full = path.join(filepath, path.basename(filename))\n\n    // see https://github.com/hubotio/hubot/issues/1355\n    if (['js', 'mjs', 'ts'].indexOf(ext) === -1) {\n      this.logger.debug(`Skipping unsupported file type ${full}`)\n      return null\n    }\n    let result = null\n    try {\n      result = await this[`load${ext}`](full)\n      this.parseHelp(full)\n    } catch (error) {\n      this.logger.error(`Unable to load ${full}: ${error.stack}`)\n      throw error\n    }\n    return result\n  }\n\n  // Public: Loads every script in the given path.\n  //\n  // path - A String path on the filesystem.\n  //\n  // Returns nothing.\n  async load (path) {\n    this.logger.debug(`Loading scripts from ${path}`)\n    const results = []\n    try {\n      const folder = await File.readdir(path, { withFileTypes: true })\n      for await (const file of folder) {\n        if (file.isDirectory()) continue\n        try {\n          const result = await this.loadFile(path, file.name)\n          results.push(result)\n        } catch (e) {\n          this.logger.error(`Error loading file ${file.name} - ${e.stack}`)\n        }\n      }\n    } catch (e) {\n      this.logger.error(`Path ${path} does not exist`)\n    }\n    return results\n  }\n\n  // Public: Load scripts from packages specified in the\n  // `external-scripts.json` file.\n  //\n  // packages - An Array of packages containing hubot scripts to load.\n  //\n  // Returns nothing.\n  async loadExternalScripts (packages) {\n    this.logger.debug('Loading external-scripts from npm packages')\n\n    try {\n      if (Array.isArray(packages)) {\n        for await (const pkg of packages) {\n          (await import(pkg)).default(this)\n        }\n        return\n      }\n      for await (const key of Object.keys(packages)) {\n        (await import(key)).default(this, packages[key])\n      }\n    } catch (error) {\n      this.logger.error(`Error loading scripts from npm package - ${error.stack}`)\n      throw error\n    }\n  }\n\n  // Setup the Express server's defaults.\n  //\n  // Returns Server.\n  async setupExpress () {\n    const user = process.env.EXPRESS_USER\n    const pass = process.env.EXPRESS_PASSWORD\n    const stat = process.env.EXPRESS_STATIC\n    const port = process.env.EXPRESS_PORT || process.env.PORT || 8080\n    const address = process.env.EXPRESS_BIND_ADDRESS || process.env.BIND_ADDRESS || '0.0.0.0'\n    const limit = process.env.EXPRESS_LIMIT || '100kb'\n    const paramLimit = parseInt(process.env.EXPRESS_PARAMETER_LIMIT) || 1000\n\n    const express = (await import('express')).default\n    const basicAuth = (await import('express-basic-auth')).default\n\n    const app = express()\n\n    app.use((req, res, next) => {\n      res.setHeader('X-Powered-By', `hubot/${encodeURI(this.name)}`)\n      return next()\n    })\n\n    if (user && pass) {\n      const authUser = {}\n      authUser[user] = pass\n      app.use(basicAuth({ users: authUser }))\n    }\n\n    app.use(express.json({ limit }))\n    app.use(express.urlencoded({ limit, parameterLimit: paramLimit, extended: true }))\n\n    if (stat) {\n      app.use(express.static(stat))\n    }\n    return new Promise((resolve, reject) => {\n      try {\n        this.server = app.listen(port, address, () => {\n          this.router = app\n          this.emit('listening', this.server)\n          resolve(this.server)\n        })\n      } catch (err) {\n        reject(err)\n      }\n    })\n  }\n\n  // Setup an empty router object\n  //\n  // returns nothing\n  setupNullRouter () {\n    const msg = 'A script has tried registering a HTTP route while the HTTP server is disabled with --disabled-httpd.'\n    const self = this\n    this.router = {\n      get: () => self.logger.info(msg),\n      post: () => self.logger.info(msg),\n      put: () => self.logger.info(msg),\n      delete: () => self.logger.info(msg)\n    }\n  }\n\n  // Load the adapter Hubot is going to use.\n  //\n  // path    - A String of the path to adapter if local.\n  // adapter - A String of the adapter name to use.\n  //\n  // Returns nothing.\n  async loadAdapter (adapterPath = null) {\n    if (this.adapter && this.adapter.use) {\n      this.adapter = await this.adapter.use(this)\n      this.adapterName = this.adapter.name ?? this.adapter.constructor.name\n      return\n    }\n    this.logger.debug(`Loading adapter ${adapterPath ?? 'from npmjs:'} ${this.adapterName}`)\n    const ext = path.extname(adapterPath ?? '')\n    try {\n      if (Array.from(HUBOT_DEFAULT_ADAPTERS).indexOf(this.adapterName) > -1) {\n        this.adapter = await this.requireAdapterFrom(path.resolve(path.join(__dirname, 'adapters', `${this.adapterName}.mjs`)))\n      } else if (['.js', '.cjs'].includes(ext)) {\n        this.adapter = await this.requireAdapterFrom(path.resolve(adapterPath))\n      } else if (['.mjs'].includes(ext)) {\n        this.adapter = await this.importAdapterFrom(path.resolve(adapterPath))\n      } else {\n        this.adapter = await this.importFromRepo(this.adapterName)\n      }\n    } catch (error) {\n      this.logger.error(`Cannot load adapter ${adapterPath ?? '[no path set]'} ${this.adapterName} - ${error}`)\n      throw error\n    }\n\n    this.adapterName = this.adapter.name ?? this.adapter.constructor.name\n  }\n\n  async requireAdapterFrom (adapaterPath) {\n    return await this.importAdapterFrom(adapaterPath)\n  }\n\n  async importAdapterFrom (adapterPath) {\n    const forImport = this.prepareForImport(adapterPath)\n    return await (await import(forImport)).default.use(this)\n  }\n\n  async importFromRepo (adapterPath) {\n    return await (await import(adapterPath)).default.use(this)\n  }\n\n  // Public: Help Commands for Running Scripts.\n  //\n  // Returns an Array of help commands for running scripts.\n  helpCommands () {\n    return this.helpStrings.sort()\n  }\n\n  // Private: load help info from a loaded script.\n  //\n  // filePath - A String path to the file on disk.\n  //\n  // Returns nothing.\n  parseHelp (filePath) {\n    const scriptDocumentation = {}\n    const body = fs.readFileSync(path.resolve(filePath), 'utf-8')\n\n    const useStrictHeaderRegex = /^[\"']use strict['\"];?\\s+/\n    const lines = body.replace(useStrictHeaderRegex, '').split(/(?:\\n|\\r\\n|\\r)/)\n      .reduce(toHeaderCommentBlock, { lines: [], isHeader: true }).lines\n      .filter(Boolean) // remove empty lines\n    let currentSection = null\n    let nextSection\n\n    this.logger.debug(`Parsing help for ${filePath}`)\n\n    for (let i = 0, line; i < lines.length; i++) {\n      line = lines[i]\n\n      if (line.toLowerCase() === 'none') {\n        continue\n      }\n\n      nextSection = line.toLowerCase().replace(':', '')\n      if (Array.from(HUBOT_DOCUMENTATION_SECTIONS).indexOf(nextSection) !== -1) {\n        currentSection = nextSection\n        scriptDocumentation[currentSection] = []\n      } else {\n        if (currentSection) {\n          scriptDocumentation[currentSection].push(line)\n          if (currentSection === 'commands') {\n            this.helpStrings.push(line)\n          }\n        }\n      }\n    }\n\n    if (currentSection === null) {\n      this.logger.info(`${filePath} is using deprecated documentation syntax`)\n      scriptDocumentation.commands = []\n      for (let i = 0, line, cleanedLine; i < lines.length; i++) {\n        line = lines[i]\n        if (line.match('-')) {\n          continue\n        }\n\n        cleanedLine = line.slice(2, +line.length + 1 || 9e9).replace(/^hubot/i, this.name).trim()\n        scriptDocumentation.commands.push(cleanedLine)\n        this.helpStrings.push(cleanedLine)\n      }\n    }\n  }\n\n  // Public: A helper send function which delegates to the adapter's send\n  // function.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more Strings for each message to send.\n  //\n  // Returns whatever the extending adapter returns.\n  async send (envelope, ...strings) {\n    return await this.adapter.send(envelope, ...strings)\n  }\n\n  // Public: A helper reply function which delegates to the adapter's reply\n  // function.\n  //\n  // envelope - A Object with message, room and user details.\n  // strings  - One or more Strings for each message to send.\n  //\n  // Returns whatever the extending adapter returns.\n  async reply (envelope, ...strings) {\n    return await this.adapter.reply(envelope, ...strings)\n  }\n\n  // Public: A helper send function to message a room that the robot is in.\n  //\n  // room    - String designating the room to message.\n  // strings - One or more Strings for each message to send.\n  //\n  // Returns whatever the extending adapter returns.\n  async messageRoom (room, ...strings) {\n    const envelope = { room }\n    return await this.adapter.send(envelope, ...strings)\n  }\n\n  // Public: A wrapper around the EventEmitter API to make usage\n  // semantically better.\n  //\n  // event    - The event name.\n  // listener - A Function that is called with the event parameter\n  //            when event happens.\n  //\n  // Returns nothing.\n  on (event, ...args) {\n    this.events.on(event, ...args)\n  }\n\n  // Public: A wrapper around the EventEmitter API to make usage\n  // semantically better.\n  //\n  // event   - The event name.\n  // args...  - Arguments emitted by the event\n  //\n  // Returns nothing.\n  emit (event, ...args) {\n    this.events.emit(event, ...args)\n  }\n\n  // Public: Kick off the event loop for the adapter\n  //\n  // Returns whatever the adapter returns.\n  async run () {\n    this.setupCommandListeners()\n    if (this.shouldEnableHttpd) {\n      await this.setupExpress()\n    } else {\n      this.setupNullRouter()\n    }\n    await this.adapter.run()\n    this.emit('running')\n  }\n\n  // Public: Gracefully shutdown the robot process\n  //\n  // Returns nothing.\n  shutdown () {\n    if (this.pingIntervalId != null) {\n      clearInterval(this.pingIntervalId)\n    }\n    this.commands.clearPendingProposals()\n    this.adapter?.close()\n    if (this.server) {\n      this.server.close()\n    }\n    this.brain.close()\n    this.events.removeAllListeners()\n  }\n\n  prepareForImport (filePath) {\n    return pathToFileURL(filePath)\n  }\n\n  // Public: The version of Hubot from npm\n  //\n  // Returns a String of the version number.\n  parseVersion () {\n    const pkg = fs.readFileSync(path.join(__dirname, '..', 'package.json'))\n    this.version = pkg.version\n\n    return this.version\n  }\n\n  // Public: Creates a scoped http client with chainable methods for\n  // modifying the request. This doesn't actually make a request though.\n  // Once your request is assembled, you can call `get()`/`post()`/etc to\n  // send the request.\n  //\n  // url - String URL to access.\n  // options - Optional options to pass on to the client\n  //\n  // Examples:\n  //\n  //     robot.http(\"http://example.com\")\n  //       # set a single header\n  //       .header('Authorization', 'bearer abcdef')\n  //\n  //       # set multiple headers\n  //       .headers(Authorization: 'bearer abcdef', Accept: 'application/json')\n  //\n  //       # add URI query parameters\n  //       .query(a: 1, b: 'foo & bar')\n  //\n  //       # make the actual request\n  //       .get() (err, res, body) ->\n  //         console.log body\n  //\n  //       # or, you can POST data\n  //       .post(data) (err, res, body) ->\n  //         console.log body\n  //\n  //    # Can also set options\n  //    robot.http(\"https://example.com\", {rejectUnauthorized: false})\n  //\n  // Returns a ScopedClient instance.\n  http (url, options) {\n    const httpOptions = extend({}, this.globalHttpOptions, options)\n\n    return HttpClient.create(url, httpOptions).header('User-Agent', `Hubot/${this.version}`)\n  }\n\n  herokuKeepalive (server) {\n    let herokuUrl = process.env.HEROKU_URL\n    if (herokuUrl) {\n      if (!/\\/$/.test(herokuUrl)) {\n        herokuUrl += '/'\n      }\n      this.pingIntervalId = setInterval(() => {\n        HttpClient.create(`${herokuUrl}hubot/ping`).post()((_err, res, body) => {\n          this.logger.info('keep alive ping!')\n        })\n      }, 5 * 60 * 1000)\n    }\n  }\n\n  // Private: Install narrow command listeners for confirmation and invocation\n  //\n  // Returns nothing.\n  setupCommandListeners () {\n    // Use receiveMiddleware to intercept addressed messages for commands\n    this.receiveMiddleware(async (context) => {\n      const message = context.response.message\n      \n      // Only process TextMessages addressed to the bot\n      if (message.constructor.name !== 'TextMessage') {\n        return true // continue to other listeners\n      }\n\n      const text = message.text || ''\n      \n      // Check if message is addressed to bot (has bot name or alias at start)\n      const robotPattern = new RegExp(`^[@]?${escapeRegExp(this.name)}[:,]?\\\\s+`, 'i')\n      const aliasPattern = this.alias ? new RegExp(`^[@]?${escapeRegExp(this.alias)}[:,]?\\\\s+`, 'i') : null\n      \n      const isAddressed = robotPattern.test(text) || (aliasPattern && aliasPattern.test(text))\n      \n      if (!isAddressed) {\n        return true // not addressed to bot, continue to other listeners\n      }\n\n      // Strip bot name/alias from message\n      let commandText = text.replace(robotPattern, '').trim()\n\n      const contextData = {\n        user: message.user,\n        room: message.room,\n        message: message,\n        res: context.response\n      }\n\n      // Check for pending confirmation first\n      const confirmationKey = this.commands._getConfirmationKey(message.user.id, message.room)\n      const hasPending = this.commands.pendingProposals.has(confirmationKey)\n      \n      if (hasPending && /^(yes|y|no|n|cancel)$/i.test(commandText)) {\n        const result = await this.commands.confirm(commandText, contextData)\n        \n        if (result) {\n          if (result.executed) {\n            context.response.reply(result.result || 'Command executed successfully')\n          } else if (result.cancelled) {\n            context.response.reply('Command cancelled')\n          }\n          message.done = true\n          return false // stop processing\n        }\n      }\n\n      // Try to parse as command invocation\n      const parsed = this.commands.parse(commandText)\n      \n      if (!parsed) {\n        return true // not a command, continue to other listeners\n      }\n\n      try {\n        const result = await this.commands.invoke(commandText, contextData)\n\n        if (result) {\n          if (result.needsConfirmation) {\n            const proposal = result.proposal\n            context.response.reply(`Preview: ${proposal.preview}\\n\\nRun it? (yes/no)`)\n          } else if (result.ok) {\n            context.response.reply(result.result || 'Command executed successfully')\n          } else {\n            // Validation error\n            let errorMsg = 'Invalid command:\\n'\n            if (result.missing && result.missing.length > 0) {\n              errorMsg += `Missing required arguments: ${result.missing.join(', ')}\\n`\n            }\n            if (result.errors && result.errors.length > 0) {\n              errorMsg += `Errors: ${result.errors.join(', ')}`\n            }\n            context.response.reply(errorMsg)\n          }\n          message.done = true\n          return false // stop processing\n        }\n      } catch (err) {\n        context.response.reply(`Error executing command: ${err.message}`)\n        message.done = true\n        return false\n      }\n\n      return true // continue to other listeners\n    })\n  }\n\n  \n}\n\nfunction isCatchAllMessage (message) {\n  return message instanceof Message.CatchAllMessage\n}\n\nfunction toHeaderCommentBlock (block, currentLine) {\n  if (!block.isHeader) {\n    return block\n  }\n\n  if (isCommentLine(currentLine)) {\n    block.lines.push(removeCommentPrefix(currentLine))\n  } else {\n    block.isHeader = false\n  }\n\n  return block\n}\n\nfunction isCommentLine (line) {\n  return /^(#|\\/\\/)/.test(line)\n}\n\nfunction removeCommentPrefix (line) {\n  return line.replace(/^[#/]+\\s*/, '')\n}\n\nfunction extend (obj, ...sources) {\n  sources.forEach((source) => {\n    if (typeof source !== 'object') {\n      return\n    }\n\n    Object.keys(source).forEach((key) => {\n      obj[key] = source[key]\n    })\n  })\n\n  return obj\n}\n\nfunction escapeRegExp (string) {\n  return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nexport default Robot\n"
  },
  {
    "path": "src/User.mjs",
    "content": "'use strict'\n\nimport { DataStoreUnavailable } from './DataStore.mjs'\n\nclass User {\n  // Represents a participating user in the chat.\n  //\n  // id      - A unique ID for the user.\n  // options - An optional Hash of key, value pairs for this user.\n  constructor (id, options) {\n    this.id = id\n\n    if (options == null) {\n      options = {}\n    }\n\n    // Define a getter method so we don't actually store the\n    // robot itself on the user object, preventing it from\n    // being serialized into the brain.\n    if (options.robot) {\n      const robot = options.robot\n      delete options.robot\n      this._getRobot = function () { return robot }\n    } else {\n      this._getRobot = function () { }\n    }\n\n    Object.keys(options).forEach((key) => {\n      this[key] = options[key]\n    })\n\n    if (!this.name) {\n      this.name = this.id.toString()\n    }\n  }\n\n  set (key, value) {\n    this._checkDatastoreAvailable()\n    return this._getDatastore()._set(this._constructKey(key), value, 'users')\n  }\n\n  get (key) {\n    this._checkDatastoreAvailable()\n    return this._getDatastore()._get(this._constructKey(key), 'users')\n  }\n\n  _constructKey (key) {\n    return `${this.id}+${key}`\n  }\n\n  _checkDatastoreAvailable () {\n    if (!this._getDatastore()) {\n      throw new DataStoreUnavailable('datastore is not initialized')\n    }\n  }\n\n  _getDatastore () {\n    const robot = this._getRobot()\n    if (robot) {\n      return robot.datastore\n    }\n  }\n}\n\nexport default User\n"
  },
  {
    "path": "src/adapters/Campfire.mjs",
    "content": "'use strict'\n\nimport HTTPS from 'node:https'\nimport EventEmitter from 'node:events'\nimport Adapter from '../Adapter.mjs'\nimport { TextMessage, EnterMessage, LeaveMessage, TopicMessage } from '../Message.mjs'\n\nclass Campfire extends Adapter {\n  constructor(robot) {\n    super(robot)\n    this.timeouts = []\n  }\n\n  send (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n\n    if (strings.length === 0) {\n      return\n    }\n\n    const string = strings.shift()\n    if (typeof string === 'function') {\n      string()\n      this.send.apply(this, [envelope].concat(strings))\n      return\n    }\n\n    this.bot.Room(envelope.room).speak(string, (error, data) => {\n      if (error != null) {\n        this.robot.logger.error(`Campfire send error: ${error}`)\n      }\n      this.send.apply(this, [envelope].concat(strings))\n    })\n  }\n\n  emote (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n    this.send.apply(this, [envelope].concat(strings.map(str => `*${str}*`)))\n  }\n\n  reply (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n    this.send.apply(this, [envelope].concat(strings.map(str => `${envelope.user.name}: ${str}`)))\n  }\n\n  topic (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n    this.bot.Room(envelope.room).topic(strings.join(' / '), (err, data) => {\n      if (err != null) {\n        this.robot.logger.error(`Campfire topic error: ${err}`)\n      }\n    })\n  }\n\n  play (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n    this.bot.Room(envelope.room).sound(strings.shift(), (err, data) => {\n      if (err != null) {\n        this.robot.logger.error(`Campfire sound error: ${err}`)\n      }\n      this.play.apply(this, [envelope].concat(strings))\n    })\n  }\n\n  locked (envelope/* , ...strings */) {\n    const strings = [].slice.call(arguments, 1)\n\n    if (envelope.message.private) {\n      this.send.apply(this, [envelope].concat(strings))\n    }\n\n    this.bot.Room(envelope.room).lock(() => {\n      strings.push(() => {\n        // campfire won't send messages from just before a room unlock. 3000\n        // is the 3-second poll.\n        const timeoutId = setTimeout(() => this.bot.Room(envelope.room).unlock(), 3000)\n        this.timeouts.push(timeoutId)\n      })\n\n      this.send.apply(this, [envelope].concat(strings))\n    })\n  }\n\n  async run () {\n    const self = this\n\n    const options = {\n      token: process.env.HUBOT_CAMPFIRE_TOKEN,\n      rooms: process.env.HUBOT_CAMPFIRE_ROOMS,\n      account: process.env.HUBOT_CAMPFIRE_ACCOUNT\n    }\n\n    const bot = new CampfireStreaming(options, this.robot, this)\n\n    function withAuthor (callback) {\n      return function (id, created, room, user, body) {\n        bot.User(user, function (_err, userData) {\n          if (userData.user) {\n            const author = self.robot.brain.userForId(userData.user.id, userData.user)\n            const userId = userData.user.id\n            self.robot.brain.data.users[userId].name = userData.user.name\n            self.robot.brain.data.users[userId].email_address = userData.user.email_address\n            author.room = room\n            return callback(id, created, room, user, body, author)\n          }\n        })\n      }\n    }\n\n    bot.on('TextMessage', withAuthor(function (id, created, room, user, body, author) {\n      if (bot.info.id !== author.id) {\n        const message = new TextMessage(author, body, id)\n        message.private = bot.private[room]\n        self.receive(message)\n      }\n    }))\n\n    bot.on('EnterMessage', withAuthor(function (id, created, room, user, body, author) {\n      if (bot.info.id !== author.id) {\n        self.receive(new EnterMessage(author, null, id))\n      }\n    }))\n\n    bot.on('LeaveMessage', withAuthor(function (id, created, room, user, body, author) {\n      if (bot.info.id !== author.id) {\n        self.receive(new LeaveMessage(author, null, id))\n      }\n    }))\n\n    bot.on('TopicChangeMessage', withAuthor(function (id, created, room, user, body, author) {\n      if (bot.info.id !== author.id) {\n        self.receive(new TopicMessage(author, body, id))\n      }\n    }))\n\n    bot.on('LockMessage', withAuthor((id, created, room, user, body, author) => {\n      bot.private[room] = true\n    }))\n\n    bot.on('UnlockMessage', withAuthor((id, created, room, user, body, author) => {\n      bot.private[room] = false\n    }))\n\n    bot.Me(function (_err, data) {\n      bot.info = data.user\n      bot.name = bot.info.name\n\n      return Array.from(bot.rooms).map(roomId => (roomId => bot.Room(roomId).join((_err, callback) => bot.Room(roomId).listen()))(roomId))\n    })\n\n    bot.on('reconnect', roomId => bot.Room(roomId).join((_err, callback) => bot.Room(roomId).listen()))\n\n    this.bot = bot\n\n    self.emit('connected')\n  }\n\n  close() {\n    // Clear all pending timeouts\n    for (const timeoutId of this.timeouts) {\n      clearTimeout(timeoutId)\n    }\n    this.timeouts = []\n    super.close()\n  }\n}\n\nclass CampfireStreaming extends EventEmitter {\n  constructor (options, robot, adapter) {\n    super()\n\n    this.robot = robot\n    this.adapter = adapter\n    if (options.token == null || options.rooms == null || options.account == null) {\n      this.robot.logger.error('Not enough parameters provided. I need a token, rooms and account')\n      process.exit(1)\n    }\n\n    this.token = options.token\n    this.rooms = options.rooms.split(',')\n    this.account = options.account\n    this.host = this.account + '.campfirenow.com'\n    this.authorization = `Basic ${Buffer.from(`${this.token}:x`).toString('base64')}`\n    this.private = {}\n  }\n\n  Rooms (callback) {\n    return this.get('/rooms', callback)\n  }\n\n  User (id, callback) {\n    return this.get(`/users/${id}`, callback)\n  }\n\n  Me (callback) {\n    return this.get('/users/me', callback)\n  }\n\n  Room (id) {\n    const self = this\n    const logger = this.robot.logger\n\n    return {\n      show (callback) {\n        return self.get(`/room/${id}`, callback)\n      },\n\n      join (callback) {\n        return self.post(`/room/${id}/join`, '', callback)\n      },\n\n      leave (callback) {\n        return self.post(`/room/${id}/leave`, '', callback)\n      },\n\n      lock (callback) {\n        return self.post(`/room/${id}/lock`, '', callback)\n      },\n\n      unlock (callback) {\n        return self.post(`/room/${id}/unlock`, '', callback)\n      },\n\n      // say things to this channel on behalf of the token user\n      paste (text, callback) {\n        return this.message(text, 'PasteMessage', callback)\n      },\n\n      topic (text, callback) {\n        const body = { room: { topic: text } }\n        return self.put(`/room/${id}`, body, callback)\n      },\n\n      sound (text, callback) {\n        return this.message(text, 'SoundMessage', callback)\n      },\n\n      speak (text, callback) {\n        const body = { message: { body: text } }\n        return self.post(`/room/${id}/speak`, body, callback)\n      },\n\n      message (text, type, callback) {\n        const body = { message: { body: text, type } }\n        return self.post(`/room/${id}/speak`, body, callback)\n      },\n\n      // listen for activity in channels\n      listen () {\n        const headers = {\n          Host: 'streaming.campfirenow.com',\n          Authorization: self.authorization,\n          'User-Agent': `Hubot/${this.robot != null ? this.robot.version : undefined} (${this.robot != null ? this.robot.name : undefined})`\n        }\n\n        const options = {\n          agent: false,\n          host: 'streaming.campfirenow.com',\n          port: 443,\n          path: `/room/${id}/live.json`,\n          method: 'GET',\n          headers\n        }\n\n        const request = HTTPS.request(options, function (response) {\n          response.setEncoding('utf8')\n\n          let buf = ''\n\n          response.on('data', function (chunk) {\n            if (chunk === ' ') {\n              // campfire api sends a ' ' heartbeat every 3s\n\n            } else if (chunk.match(/^\\s*Access Denied/)) {\n              return logger.error(`Campfire error on room ${id}: ${chunk}`)\n            } else {\n              // api uses newline terminated json payloads\n              // buffer across tcp packets and parse out lines\n              buf += chunk\n\n              return (() => {\n                let offset\n                const result = []\n                while ((offset = buf.indexOf('\\r')) > -1) {\n                  let item\n                  const part = buf.substr(0, offset)\n                  buf = buf.substr(offset + 1)\n\n                  if (part) {\n                    try {\n                      const data = JSON.parse(part)\n                      item = self.emit(data.type, data.id, data.created_at, data.room_id, data.user_id, data.body)\n                    } catch (error) {\n                      item = logger.error(`Campfire data error: ${error}\\n${error.stack}`)\n                    }\n                  }\n                  result.push(item)\n                }\n\n                return result\n              })()\n            }\n          })\n\n          response.on('end', function () {\n            logger.error(`Streaming connection closed for room ${id}. :(`)\n            const timeoutId = setTimeout(() => self.emit('reconnect', id), 5000)\n            self.adapter.timeouts.push(timeoutId)\n          })\n\n          return response.on('error', err => logger.error(`Campfire listen response error: ${err}`))\n        })\n\n        request.on('error', err => logger.error(`Campfire listen request error: ${err}`))\n\n        return request.end()\n      }\n    }\n  }\n\n  get (path, callback) {\n    return this.request('GET', path, null, callback)\n  }\n\n  post (path, body, callback) {\n    return this.request('POST', path, body, callback)\n  }\n\n  put (path, body, callback) {\n    return this.request('PUT', path, body, callback)\n  }\n\n  request (method, path, body, callback) {\n    const logger = this.robot.logger\n\n    const headers = {\n      Authorization: this.authorization,\n      Host: this.host,\n      'Content-Type': 'application/json',\n      'User-Agent': `Hubot/${this.robot != null ? this.robot.version : undefined} (${this.robot != null ? this.robot.name : undefined})`\n    }\n\n    const options = {\n      agent: false,\n      host: this.host,\n      port: 443,\n      path,\n      method,\n      headers\n    }\n\n    if (method === 'POST' || method === 'PUT') {\n      if (typeof body !== 'string') {\n        body = JSON.stringify(body)\n      }\n\n      body = Buffer.from(body)\n      options.headers['Content-Length'] = body.length\n    }\n\n    const request = HTTPS.request(options, function (response) {\n      let data = ''\n\n      response.on('data', chunk => {\n        data += chunk\n      })\n\n      response.on('end', function () {\n        if (response.statusCode >= 400) {\n          switch (response.statusCode) {\n            case 401:\n              throw new Error('Invalid access token provided')\n            default:\n              logger.error(`Campfire HTTPS status code: ${response.statusCode}`)\n              logger.error(`Campfire HTTPS response data: ${data}`)\n          }\n        }\n\n        if (callback) {\n          try {\n            return callback(null, JSON.parse(data))\n          } catch (_err) {\n            return callback(null, data || {})\n          }\n        }\n      })\n\n      return response.on('error', function (err) {\n        logger.error(`Campfire HTTPS response error: ${err}`)\n        return callback(err, {})\n      })\n    })\n\n    if (method === 'POST' || method === 'PUT') {\n      request.end(body, 'binary')\n    } else {\n      request.end()\n    }\n\n    return request.on('error', err => logger.error(`Campfire request error: ${err}`))\n  }\n}\n\nexport default {\n  use (robot) {\n    return new Campfire(robot)\n  }\n}\n"
  },
  {
    "path": "src/adapters/Shell.mjs",
    "content": "'use strict'\n\nimport { stat, writeFile, unlink, appendFile, readFile } from 'node:fs/promises'\nimport readline from 'node:readline'\nimport Adapter from '../Adapter.mjs'\nimport { TextMessage } from '../Message.mjs'\n\nconst historySize = process.env.HUBOT_SHELL_HISTSIZE != null ? parseInt(process.env.HUBOT_SHELL_HISTSIZE) : 1024\nconst historyPath = '.hubot_history'\n\nconst completer = line => {\n  const completions = '\\\\q exit \\\\? help \\\\c clear'.split(' ')\n  const hits = completions.filter((c) => c.startsWith(line))\n  // Show all completions if none found\n  return [hits.length ? hits : completions, line]\n}\nconst showHelp = () => {\n  console.log('usage:')\n  console.log('\\\\q, exit - close Shell and exit')\n  console.log('\\\\?, help - show this help')\n  console.log('\\\\c, clear - clear screen')\n}\n\nconst bold = str => `\\x1b[1m${str}\\x1b[22m`\nconst green = str => `\\x1b[32m${str}\\x1b[0m`\nconst levelColors = {\n  error: '\\x1b[31m',\n  warn: '\\x1b[33m',\n  debug: '\\x1b[35m',\n  info: '\\x1b[34m',\n  trace: '\\x1b[36m',\n  fatal: '\\x1b[91m'\n}\nconst reset = '\\x1b[0m'\n\nclass Shell extends Adapter {\n  #rl = null\n  #levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']\n  #logLevel = 'info'\n  #levelPriorities = {}\n  constructor (robot) {\n    super(robot)\n    this.name = 'Shell'\n    this.#logLevel = process.env.HUBOT_LOG_LEVEL || this.#logLevel\n    this.#levelPriorities = this.#levels.reduce((acc, current, idx) => {\n      acc[current] = idx\n      return acc\n    }, {})\n\n    this.robot.on('scripts have loaded', () => {\n      this.#rl?.prompt()\n    })\n  }\n\n  async send (envelope, ...strings) {\n    this.#rl?.prompt()\n    Array.from(strings).forEach(str => console.log(bold(str)))\n  }\n\n  async emote (envelope, ...strings) {\n    Array.from(strings).map(str => this.send(envelope, `* ${str}`))\n  }\n\n  async reply (envelope, ...strings) {\n    strings = strings.map((s) => `${envelope.user.name}: ${s}`)\n    await this.send(envelope, ...strings)\n  }\n\n  async run () {\n    try {\n      const stats = await stat(historyPath)\n      if (stats.size > historySize) {\n        await unlink(historyPath)\n        await writeFile(historyPath, '')\n      }\n    } catch (error) {\n      console.log(error)\n      await writeFile(historyPath, '')\n    }\n\n    this.#rl = readline.createInterface({\n      input: this.robot.stdin ?? process.stdin,\n      output: this.robot.stdout ?? process.stdout,\n      prompt: green(`${this.robot.name ?? this.robot.alias}> `),\n      completer\n    })\n    this.#rl.on('line', async (line) => {\n      const input = line.trim()\n      switch (input) {\n        case '\\\\q':\n        case 'exit':\n          this.#rl.close()\n          process.exit(0)\n          break\n        case '\\\\?':\n        case 'help':\n          showHelp()\n          this.#rl.prompt()\n          break\n        case '\\\\c':\n        case 'clear':\n          this.#rl.write(null, { ctrl: true, name: 'l' })\n          this.#rl.prompt()\n          break\n      }\n      if (input.length === 0) {\n        this.#rl.prompt()\n        return\n      }\n      if (input.length > 0) {\n        this.#rl.history.push(input)\n      }\n      let userId = process.env.HUBOT_SHELL_USER_ID || '1'\n      if (userId.match(/A\\d+z/)) {\n        userId = parseInt(userId)\n      }\n      const userName = process.env.HUBOT_SHELL_USER_NAME || 'Shell'\n      const user = this.robot.brain.userForId(userId, { name: userName, room: 'Shell' })\n      const message = new TextMessage(user, input, Date.now())\n      if (!message.text.startsWith(this.robot.name) && !message.text.startsWith(this.robot.alias)) {\n        message.text = `${this.robot.name} ${message.text}`\n      }\n      await this.receive(message)\n      this.#rl.prompt()\n    })\n\n    this.#rl.on('history', async (history) => {\n      if (history.length === 0) return\n      await appendFile(historyPath, `${history[0]}\\n`)\n    })\n\n    const existingHistory = (await readFile(historyPath, 'utf8')).split('\\n')\n    existingHistory.reverse().forEach(line => this.#rl.history.push(line))\n\n    const configuredPriority = this.#levelPriorities[this.#logLevel]\n    const noop = async () => {}\n    this.#levels.forEach(level => {\n      const priority = this.#levelPriorities[level]\n      if (priority >= configuredPriority) {\n        this.robot.logger[level] = async (...args) => {\n          const color = levelColors[level] || ''\n          const msg = `${color}[${level}]${reset} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`\n          await this.send({ user: { name: 'Logger', room: 'Shell' } }, msg)\n        }\n      } else {\n        this.robot.logger[level] = noop\n      }\n    })\n\n    try {\n      this.emit('connected', this)\n    } catch (error) {\n      console.log(error)\n    }\n  }\n\n  close () {\n    super.close()\n    if (this.#rl?.close) {\n      this.#rl.close()\n    }\n  }\n}\n\n// Prevent output buffer \"swallowing\" every other character on OSX / Node version > 16.19.0.\nprocess.stdout._handle.setBlocking(false)\nexport default {\n  use (robot) {\n    return new Shell(robot)\n  }\n}\n"
  },
  {
    "path": "src/datastores/Memory.mjs",
    "content": "'use strict'\n\nimport { DataStore } from '../DataStore.mjs'\n\nclass InMemoryDataStore extends DataStore {\n  constructor (robot) {\n    super(robot)\n    this.data = {\n      global: {},\n      users: {}\n    }\n  }\n\n  async _get (key, table) {\n    return Promise.resolve(this.data[table][key])\n  }\n\n  async _set (key, value, table) {\n    return Promise.resolve(this.data[table][key] = value)\n  }\n}\n\nexport default InMemoryDataStore\n"
  },
  {
    "path": "test/AdapterName_test.mjs",
    "content": "import test from 'node:test'\nimport assert from 'node:assert/strict'\nimport { Robot, Adapter } from '../index.mjs'\n\nclass InMemoryAdapter extends Adapter {\n}\n\nfunction getRobotWithAdapter (adapter) {\n  return new Robot({\n    async use (robot) {\n      adapter.robot = robot\n      return adapter\n    }\n  }, false, 'Hubot', 't-bot')\n}\n\nawait test('Adapter Name', async (t) => {\n  await t.test('Adapter argument is an object with user function', async () => {\n    const adapter = new InMemoryAdapter()\n    const robot = getRobotWithAdapter(adapter)\n    await robot.loadAdapter()\n    assert.equal(robot.adapterName, 'InMemoryAdapter')\n  })\n\n  await t.test('Adapter argument is null', async () => {\n    const robot = new Robot(null, false, 'Hubot', 't-bot')\n    await robot.loadAdapter()\n    assert.equal(robot.adapterName, 'Shell')\n  })\n\n  await t.test('Adapter argument is a file path', async () => {\n    const robot = new Robot('../test/fixtures/MockAdapter.mjs', false, 'Hubot', 't-bot')\n    await robot.loadAdapter()\n    assert.equal(robot.adapterName, 'MockAdapter')\n  })\n})\n"
  },
  {
    "path": "test/Adapter_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport Adapter from '../src/Adapter.mjs'\nimport { TextMessage } from '../src/Message.mjs'\nimport User from '../src/User.mjs'\n\ndescribe('Adapter', () => {\n  let robot = null\n  beforeEach(() => {\n    robot = { receive (msg) {} }\n  })\n\n  describe('Public API', () => {\n    let adapter = null\n    beforeEach(() => {\n      adapter = new Adapter(robot)\n    })\n    afterEach(() => {\n      adapter.close()\n      process.removeAllListeners()\n    })\n\n    it('assigns robot', () => {\n      assert.deepEqual(adapter.robot, robot, 'The adapter should have a reference to the robot.')\n    })\n\n    describe('send', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.send === 'function', 'The adapter should have a send method.')\n      })\n\n      it('does nothing', () => {\n        adapter.send({}, 'nothing')\n      })\n    })\n\n    describe('reply', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.reply === 'function', 'The adapter should have a reply method.')\n      })\n\n      it('does nothing', () => {\n        adapter.reply({}, 'nothing')\n      })\n    })\n    describe('emote', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.emote === 'function', 'The adapter should have a emote method.')\n      })\n\n      it('does nothing', () => {\n        adapter.emote({}, 'nothing')\n      })\n    })\n    describe('topic', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.topic === 'function', 'The adapter should have a topic method.')\n      })\n\n      it('does nothing', () => {\n        adapter.topic({}, 'nothing')\n      })\n    })\n\n    describe('play', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.play === 'function', 'The adapter should have a play method.')\n      })\n\n      it('does nothing', () => {\n        adapter.play({}, 'nothing')\n      })\n    })\n\n    describe('run', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.run === 'function', 'The adapter should have a run method.')\n      })\n\n      it('does nothing', async () => {\n        await adapter.run()\n      })\n    })\n\n    describe('close', () => {\n      it('is a function', () => {\n        assert.ok(typeof adapter.close === 'function', 'The adapter should have a close method.')\n      })\n\n      it('does nothing', () => {\n        adapter.close()\n      })\n    })\n  })\n\n  it('dispatches received messages to the robot', (t, done) => {\n    const adapter = new Adapter(robot)\n    const message = new TextMessage(new User('node'), 'hello', 1)\n    robot.receive = (msg) => {\n      assert.deepEqual(msg, message, 'The message should be passed through.')\n      done()\n    }\n    adapter.receive(message)\n    adapter.close()\n  })\n})\n"
  },
  {
    "path": "test/Brain_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { User, Robot, Brain } from '../index.mjs'\nimport mockAdapter from './fixtures/MockAdapter.mjs'\n\ndescribe('Brain', () => {\n  let mockRobot = null\n  let user1 = null\n  let user2 = null\n  let user3 = null\n  beforeEach(async () => {\n    mockRobot = new Robot(mockAdapter, false, 'TestHubot')\n    await mockRobot.loadAdapter()\n    await mockRobot.run()\n    user1 = mockRobot.brain.userForId('1', { name: 'Guy One' })\n    user2 = mockRobot.brain.userForId('2', { name: 'Guy One Two' })\n    user3 = mockRobot.brain.userForId('3', { name: 'Girl Three' })\n  })\n  afterEach(() => {\n    mockRobot.shutdown()\n    process.removeAllListeners()\n  })\n  describe('Unit Tests', () => {\n    describe('#mergeData', () => {\n      it('performs a proper merge with the new data taking precedent', () => {\n        mockRobot.brain.data = {\n          1: 'old',\n          2: 'old'\n        }\n\n        mockRobot.brain.mergeData({ 2: 'new' })\n\n        assert.deepEqual(mockRobot.brain.data, {\n          1: 'old',\n          2: 'new'\n        }, 'The data should be merged properly.')\n      })\n\n      it('emits a loaded event with the new data', (t, done) => {\n        const loadedListener = (data) => {\n          assert.ok(typeof data === 'object', 'data should be an object.')\n          mockRobot.brain.off('loaded', loadedListener)\n          done()\n        }\n        mockRobot.brain.on('loaded', loadedListener)\n        mockRobot.brain.mergeData({})\n      })\n\n      it('coerces loaded data into User objects', () => {\n        mockRobot.brain.mergeData({ users: { 4: { name: 'new', id: '4' } } })\n        const user = mockRobot.brain.userForId('4')\n        assert.ok(user instanceof User)\n        assert.equal(user.id, '4')\n        assert.equal(user.name, 'new')\n      })\n    })\n\n    describe('#save', () => {\n      it('emits a save event', (t, done) => {\n        const saveListener = (data) => {\n          assert.deepEqual(data, mockRobot.brain.data)\n          mockRobot.brain.off('save', saveListener)\n          done()\n        }\n        mockRobot.brain.on('save', saveListener)\n        mockRobot.brain.save()\n      })\n    })\n\n    describe('#resetSaveInterval', () => {\n      it('updates the auto-save interval', async () => {\n        let wasCalled = false\n        const shouldNotBeCalled = (data) => {\n          assert.fail('save event should not have been emitted')\n        }\n        const shouldBeCalled = (data) => {\n          mockRobot.brain.off('save', shouldBeCalled)\n          wasCalled = true\n        }\n        mockRobot.brain.on('save', shouldNotBeCalled)\n        mockRobot.brain.on('save', shouldBeCalled)\n        // make sure autosave is on\n        mockRobot.brain.setAutoSave(true)\n        // default is 5s\n        mockRobot.brain.resetSaveInterval(6)\n\n        await Promise.all([\n          new Promise((resolve, reject) => {\n            setTimeout(() => {\n              assert.deepEqual(wasCalled, true, 'save event should have been emitted')\n              resolve()\n            }, 1000 * 6)\n          }),\n          new Promise((resolve, reject) => {\n            setTimeout(() => {\n              assert.notEqual(wasCalled, true)\n              mockRobot.brain.off('save', shouldNotBeCalled)\n              resolve()\n            }, 1000 * 5)\n          })\n        ])\n      })\n    })\n\n    describe('#close', () => {\n      it('saves', (t, done) => {\n        const saveListener = data => {\n          mockRobot.brain.off('save', saveListener)\n          assert.ok(data)\n          done()\n        }\n        mockRobot.brain.on('save', saveListener)\n        mockRobot.brain.close()\n      })\n\n      it('emits a close event', (t, done) => {\n        const closeListener = () => {\n          mockRobot.brain.off('close', closeListener)\n          assert.ok(true)\n          done()\n        }\n        mockRobot.brain.on('close', closeListener)\n        mockRobot.brain.close()\n      })\n\n      it('saves before emitting the close event', (t, done) => {\n        let wasSaveCalled = false\n        const saveListener = data => {\n          mockRobot.brain.off('save', saveListener)\n          wasSaveCalled = true\n        }\n        const closeListener = () => {\n          mockRobot.brain.off('close', closeListener)\n          assert.ok(wasSaveCalled)\n          done()\n        }\n        mockRobot.brain.on('save', saveListener)\n        mockRobot.brain.on('close', closeListener)\n        mockRobot.brain.close()\n      })\n\n      it('stops auto-saving', (t, done) => {\n        // make sure autosave is on\n        mockRobot.brain.setAutoSave(true)\n        mockRobot.brain.close()\n\n        // set up the spy after because 'close' calls 'save'\n        const saveListener = data => {\n          assert.fail('save event should not have been emitted')\n        }\n        mockRobot.brain.on('save', saveListener)\n        setTimeout(() => {\n          assert.ok(true)\n          mockRobot.brain.off('save', saveListener)\n          done()\n        }, 1000 * 10)\n      })\n    })\n\n    describe('#get', () => {\n      it('returns the saved value', () => {\n        const brain = new Brain(mockRobot)\n        brain.set('test-key', 'value')\n        assert.equal(brain.get('test-key'), 'value')\n        brain.close()\n      })\n\n      it('returns null if object is not found', () => {\n        const brain = new Brain(mockRobot)\n        assert.equal(brain.get('not a real key'), null)\n        brain.close()\n      })\n    })\n\n    describe('#set', () => {\n      it('sets multiple keys at once if an object is provided', () => {\n        mockRobot.brain.data._private = {\n          key1: 'val1',\n          key2: 'val1'\n        }\n\n        mockRobot.brain.set({\n          key2: 'val2',\n          key3: 'val2'\n        })\n\n        assert.deepEqual(mockRobot.brain.data._private, {\n          key1: 'val1',\n          key2: 'val2',\n          key3: 'val2'\n        })\n      })\n\n      // Unable to understand why this behavior is needed, but adding a test\n      // case to protect it\n      it('emits loaded', (t, done) => {\n        const loadedListener = (data) => {\n          assert.deepEqual(data, mockRobot.brain.data)\n          mockRobot.brain.off('loaded', loadedListener)\n          done()\n        }\n        mockRobot.brain.on('loaded', loadedListener)\n        mockRobot.brain.set('test-key', 'value')\n      })\n\n      it('returns the mockRobot.brain', () => {\n        assert.deepEqual(mockRobot.brain.set('test-key', 'value'), mockRobot.brain)\n      })\n    })\n\n    describe('#remove', () => it('removes the specified key', () => {\n      mockRobot.brain.set('test-key', 'value')\n      mockRobot.brain.remove('test-key')\n      assert.deepEqual(Object.keys(mockRobot.brain.data._private).includes('test-key'), false)\n    }))\n\n    describe('#userForId', () => {\n      it('returns the user object', () => {\n        const brain = new Brain(mockRobot)\n        brain.userForId('1', user1)\n        assert.deepEqual(brain.userForId('1'), user1)\n        brain.close()\n      })\n\n      it('does an exact match', () => {\n        const user4 = mockRobot.brain.userForId('FOUR')\n        assert.notDeepEqual(mockRobot.brain.userForId('four'), user4)\n      })\n\n      // Cannot understand why this behavior is needed, but adding a test case\n      // to protect it\n      it('recreates the user if the room option differs from the user object', () => {\n        assert.equal(mockRobot.brain.userForId(1).room, undefined)\n\n        // undefined -> having a room\n        const newUser1 = mockRobot.brain.userForId(1, { room: 'room1' })\n        assert.notDeepEqual(newUser1, user1)\n\n        // changing the room\n        const newUser2 = mockRobot.brain.userForId(1, { room: 'room2' })\n        assert.notDeepEqual(newUser2, newUser1)\n      })\n\n      describe('when there is no matching user ID', () => {\n        it('creates a new User', () => {\n          assert.notEqual(Object.keys(mockRobot.brain.data.users).includes('all-new-user'), true)\n          const newUser = mockRobot.brain.userForId('all-new-user')\n          assert.ok(newUser instanceof User)\n          assert.equal(newUser.id, 'all-new-user')\n          assert.ok(Object.keys(mockRobot.brain.data.users).includes('all-new-user'))\n        })\n\n        it('passes the provided options to the new User', () => {\n          const brain = new Brain(mockRobot)\n          const newUser = brain.userForId('all-new-user', { name: 'All New User', prop: 'mine' })\n          assert.equal(newUser.name, 'All New User')\n          assert.equal(newUser.prop, 'mine')\n          brain.close()\n        })\n      })\n    })\n\n    describe('#userForName', () => {\n      it('returns the user with a matching name', () => {\n        const user = { id: 'user-for-name-guy-one', name: 'Guy One' }\n        const brain = new Brain(mockRobot)\n        const guy = brain.userForId('user-for-name-guy-one', user)\n        assert.deepEqual(brain.userForName('Guy One'), guy)\n        brain.close()\n      })\n\n      it('does a case-insensitive match', () => {\n        const user = { name: 'Guy One' }\n        const brain = new Brain(mockRobot)\n        const guy = brain.userForId('user-for-name-guy-one-case-insensitive', user)\n        assert.deepEqual(brain.userForName('guy one'), guy)\n        brain.close()\n      })\n\n      it('returns null if no user matches', () => {\n        assert.equal(mockRobot.brain.userForName('not a real user'), null)\n      })\n    })\n\n    describe('#usersForRawFuzzyName', () => {\n      it('does a case-insensitive match', () => {\n        const brain = new Brain(mockRobot)\n        const guy = brain.userForId('1', user1)\n        const guy2 = brain.userForId('2', user2)\n        assert.ok(brain.usersForRawFuzzyName('guy').includes(guy) && brain.usersForRawFuzzyName('guy').includes(guy2))\n        brain.close()\n      })\n\n      it('returns all matching users (prefix match) when there is not an exact match (case-insensitive)', () => {\n        const brain = new Brain(mockRobot)\n        const guy = brain.userForId('1', user1)\n        const guy2 = brain.userForId('2', user2)\n        assert.ok(brain.usersForRawFuzzyName('Guy').includes(guy) && brain.usersForRawFuzzyName('Guy').includes(guy2))\n        brain.close()\n      })\n\n      it('returns all matching users (prefix match) when there is an exact match (case-insensitive)', () => {\n        const brain = new Brain(mockRobot)\n        const girl = brain.userForId('1', user1)\n        const girl2 = brain.userForId('2', user2)\n        // Matched case\n        assert.deepEqual(brain.usersForRawFuzzyName('Guy One'), [girl, girl2])\n        // Mismatched case\n        assert.deepEqual(brain.usersForRawFuzzyName('guy one'), [girl, girl2])\n        brain.close()\n      })\n\n      it('returns an empty array if no users match', () => {\n        const result = mockRobot.brain.usersForRawFuzzyName('not a real user')\n        assert.equal(result.length, 0)\n      })\n    })\n\n    describe('#usersForFuzzyName', () => {\n      it('does a case-insensitive match', () => {\n        const brain = new Brain(mockRobot)\n        const girl = brain.userForId('1', user1)\n        const girl2 = brain.userForId('2', user2)\n        assert.ok(brain.usersForFuzzyName('guy').includes(girl) && brain.usersForFuzzyName('guy').includes(girl2))\n        brain.close()\n      })\n\n      it('returns all matching users (prefix match) when there is not an exact match', () => {\n        const brain = new Brain(mockRobot)\n        const girl = brain.userForId('1', user1)\n        const girl2 = brain.userForId('2', user2)\n        assert.ok(brain.usersForFuzzyName('Guy').includes(girl) && brain.usersForFuzzyName('Guy').includes(girl2))\n        brain.close()\n      })\n\n      it('returns just the user when there is an exact match (case-insensitive)', () => {\n        const brain = new Brain(mockRobot)\n        const girl = brain.userForId('1', user1)\n        brain.userForId('2', user2)\n        // Matched case\n        assert.deepEqual(brain.usersForFuzzyName('Guy One'), [girl])\n        // Mismatched case\n        assert.deepEqual(brain.usersForFuzzyName('guy one'), [girl])\n        brain.close()\n      })\n\n      it('returns an empty array if no users match', () => {\n        const result = mockRobot.brain.usersForFuzzyName('not a real user')\n        assert.equal(result.length, 0)\n      })\n    })\n  })\n\n  describe('Auto-Save', () => {\n    it('is on by default', () => {\n      assert.deepEqual(mockRobot.brain.autoSave, true)\n    })\n\n    it('automatically saves every 5 seconds when turned on', (t, done) => {\n      let wasCalled = false\n      const saveListener = data => {\n        mockRobot.brain.off('save', saveListener)\n        wasCalled = true\n      }\n      mockRobot.brain.on('save', saveListener)\n      mockRobot.brain.setAutoSave(true)\n      setTimeout(() => {\n        mockRobot.brain.off('save', saveListener)\n        assert.deepEqual(wasCalled, true)\n        done()\n      }, 1000 * 5.5)\n    })\n\n    it('does not auto-save when turned off', (t, done) => {\n      let wasCalled = false\n      const saveListener = data => {\n        wasCalled = true\n        assert.fail('save event should not have been emitted')\n      }\n      mockRobot.brain.setAutoSave(false)\n      mockRobot.brain.on('save', saveListener)\n      setTimeout(() => {\n        assert.notEqual(wasCalled, true)\n        mockRobot.brain.off('save', saveListener)\n        done()\n      }, 1000 * 10)\n    })\n  })\n\n  describe('User Searching', () => {\n    it('finds users by ID', () => {\n      assert.deepEqual(mockRobot.brain.userForId('1'), user1)\n    })\n\n    it('finds users by exact name', () => {\n      assert.deepEqual(mockRobot.brain.userForName('Guy One'), user1)\n    })\n\n    it('finds users by fuzzy name (prefix match)', () => {\n      const result = mockRobot.brain.usersForFuzzyName('Guy')\n      assert.ok(result.includes(user1) && result.includes(user2))\n      assert.ok(!result.includes(user3))\n    })\n\n    it('returns User objects, not POJOs', () => {\n      assert.ok(mockRobot.brain.userForId('1') instanceof User)\n      for (const user of mockRobot.brain.usersForFuzzyName('Guy')) {\n        assert.ok(user instanceof User)\n      }\n\n      for (const user of mockRobot.brain.usersForRawFuzzyName('Guy One')) {\n        assert.ok(user instanceof User)\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "test/CommandBus_test.mjs",
    "content": "import { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { CommandBus } from '../src/CommandBus.mjs'\n\n// Fake robot for testing\nclass FakeRobot {\n  constructor() {\n    this.brain = {\n      users: () => ({\n        '1': { id: '1', name: 'alice' },\n        '2': { id: '2', name: 'bob' },\n        '3': { id: '3', name: 'charlie' }\n      })\n    }\n  }\n}\n\ndescribe('CommandBus', () => {\n  let commandBus\n  let robot\n  let logPath\n\n  beforeEach(() => {\n    robot = new FakeRobot()\n    logPath = path.join(process.cwd(), `test-commands-${Date.now()}.ndjson`)\n    commandBus = new CommandBus(robot, { logPath, disableLogging: true })\n  })\n\n  afterEach(async () => {\n    commandBus.clearPendingProposals()\n    try {\n      await fs.promises.unlink(logPath)\n    } catch (err) {\n    }\n  })\n\n  describe('register()', () => {\n    it('should register a command with minimal spec', () => {\n      const spec = {\n        id: 'test.hello',\n        description: 'Say hello',\n        handler: async (ctx) => 'Hello!'\n      }\n\n      commandBus.register(spec)\n      const cmd = commandBus.getCommand('test.hello')\n\n      assert.strictEqual(cmd.id, 'test.hello')\n      assert.strictEqual(cmd.description, 'Say hello')\n    })\n\n    it('should throw error when registering duplicate command id', () => {\n      const spec = {\n        id: 'test.duplicate',\n        description: 'Test',\n        handler: async () => {}\n      }\n\n      commandBus.register(spec)\n      assert.throws(() => {\n        commandBus.register(spec)\n      }, /already registered/)\n    })\n\n    it('should allow updating a command', () => {\n      const spec1 = {\n        id: 'test.update',\n        description: 'First version',\n        handler: async () => 'v1'\n      }\n      const spec2 = {\n        id: 'test.update',\n        description: 'Second version',\n        handler: async () => 'v2'\n      }\n\n      commandBus.register(spec1)\n      commandBus.register(spec2, { update: true })\n\n      const cmd = commandBus.getCommand('test.update')\n      assert.strictEqual(cmd.description, 'Second version')\n    })\n\n    it('should allow unregistering a command', () => {\n      const spec = {\n        id: 'test.remove',\n        description: 'Will be removed',\n        handler: async () => {}\n      }\n\n      commandBus.register(spec)\n      assert.ok(commandBus.getCommand('test.remove'))\n\n      commandBus.unregister('test.remove')\n      assert.strictEqual(commandBus.getCommand('test.remove'), undefined)\n    })\n  })\n\n  describe('parse()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'tickets.create',\n        description: 'Create a ticket',\n        handler: async () => {}\n      })\n      commandBus.register({\n        id: 'deploy.run',\n        description: 'Deploy',\n        handler: async () => {}\n      })\n    })\n\n    it('should parse command with quoted string arguments', () => {\n      const result = commandBus.parse('tickets.create --title \"VPN down\" --priority high')\n\n      assert.ok(result)\n      assert.strictEqual(result.commandId, 'tickets.create')\n      assert.strictEqual(result.args.title, 'VPN down')\n      assert.strictEqual(result.args.priority, 'high')\n    })\n\n    it('should parse backslash-escaped quotes inside quoted strings', () => {\n      const result = commandBus.parse('tickets.create --message \"She said \\\\\\\"hello\\\\\\\"\"')\n\n      assert.ok(result)\n      assert.strictEqual(result.commandId, 'tickets.create')\n      assert.strictEqual(result.args.message, 'She said \"hello\"')\n    })\n\n    it('should parse command with key:value arguments', () => {\n      const result = commandBus.parse('tickets.create title:\"VPN down\" priority:high')\n\n      assert.ok(result)\n      assert.strictEqual(result.commandId, 'tickets.create')\n      assert.strictEqual(result.args.title, 'VPN down')\n      assert.strictEqual(result.args.priority, 'high')\n    })\n\n    it('should parse boolean flags', () => {\n      const result = commandBus.parse('deploy.run --dry-run --force')\n\n      assert.ok(result)\n      assert.strictEqual(result.commandId, 'deploy.run')\n      assert.strictEqual(result.args['dry-run'], true)\n      assert.strictEqual(result.args.force, true)\n    })\n\n    it('should return null for messages not starting with prefix', () => {\n      const result = commandBus.parse('just a normal chat message')\n      assert.strictEqual(result, null)\n    })\n\n    it('should handle mixed argument styles', () => {\n      const result = commandBus.parse('tickets.create --title \"VPN down\" priority:high --assignee matt')\n\n      assert.ok(result)\n      assert.strictEqual(result.args.title, 'VPN down')\n      assert.strictEqual(result.args.priority, 'high')\n      assert.strictEqual(result.args.assignee, 'matt')\n    })\n\n    it('should return null for unknown command or alias', () => {\n      const result = commandBus.parse('unknown.command --arg value')\n      assert.strictEqual(result, null)\n    })\n\n    it('should use schema hints to disambiguate boolean flags', () => {\n      commandBus.register({\n        id: 'test.schema',\n        description: 'Test schema-aware parsing',\n        args: {\n          force: { type: 'boolean' },\n          count: { type: 'number' }\n        },\n        handler: async () => {}\n      })\n\n      const result = commandBus.parse('test.schema --force --count 5')\n\n      assert.ok(result)\n      assert.strictEqual(result.args.force, true)\n      assert.strictEqual(result.args.count, '5')\n    })\n  })\n\n  describe('aliases', () => {\n    it('should normalize aliases and enforce uniqueness', () => {\n      commandBus.register({\n        id: 'test.aliases',\n        description: 'Test aliases',\n        aliases: ['  New  Ticket  ', 'new ticket', 'NEW   TICKET', 'create ticket'],\n        handler: async () => {}\n      })\n\n      const cmd = commandBus.getCommand('test.aliases')\n      assert.deepStrictEqual(cmd.aliases, ['New  Ticket', 'create ticket'])\n      assert.deepStrictEqual(cmd.normalizedAliases, ['new ticket', 'create ticket'])\n    })\n\n    it('should reject invalid aliases', () => {\n      assert.throws(() => {\n        commandBus.register({\n          id: 'test.aliases.invalid',\n          description: 'Invalid aliases',\n          aliases: ['  ', 123],\n          handler: async () => {}\n        })\n      }, /aliases/)\n    })\n\n    it('should not execute or propose for alias invocation', async () => {\n      let executed = false\n      commandBus.register({\n        id: 'test.alias.invoke',\n        description: 'Alias should not invoke',\n        aliases: ['alias.invoke'],\n        sideEffects: ['side effect'],\n        handler: async () => {\n          executed = true\n          return 'done'\n        }\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const result = await commandBus.invoke('alias.invoke --arg value', context)\n      assert.strictEqual(result, null)\n      assert.strictEqual(executed, false)\n    })\n  })\n\n  describe('validate()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.validate',\n        description: 'Test validation',\n        args: {\n          title: { type: 'string', required: true },\n          priority: { type: 'enum', values: ['low', 'medium', 'high'], default: 'medium' },\n          count: { type: 'number', required: false },\n          active: { type: 'boolean', default: false }\n        },\n        handler: async () => {}\n      })\n    })\n\n    it('should apply defaults from schema', async () => {\n      const result = await commandBus.validate('test.validate', { title: 'Test' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.title, 'Test')\n      assert.strictEqual(result.args.priority, 'medium')\n      assert.strictEqual(result.args.active, false)\n    })\n\n    it('should reject missing required args', async () => {\n      const result = await commandBus.validate('test.validate', {}, {})\n\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.missing.includes('title'))\n    })\n\n    it('should reject invalid enum values', async () => {\n      const result = await commandBus.validate('test.validate', { title: 'Test', priority: 'critical' }, {})\n\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.errors.some(e => e.includes('priority')))\n    })\n\n    it('should validate and convert number type', async () => {\n      const result = await commandBus.validate('test.validate', { title: 'Test', count: '42' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.count, 42)\n    })\n\n    it('should validate boolean type', async () => {\n      const result = await commandBus.validate('test.validate', { title: 'Test', active: 'true' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.active, true)\n    })\n  })\n\n  describe('user resolver', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.user',\n        description: 'Test user resolution',\n        args: {\n          assignee: { type: 'user', required: true }\n        },\n        handler: async () => {}\n      })\n    })\n\n    it('should resolve user by name to brain user record', async () => {\n      const result = await commandBus.validate('test.user', { assignee: 'alice' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.assignee.id, '1')\n      assert.strictEqual(result.args.assignee.name, 'alice')\n    })\n\n    it('should fail validation if user not found', async () => {\n      const result = await commandBus.validate('test.user', { assignee: 'nonexistent' }, {})\n\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.errors.some(e => e.includes('assignee')))\n    })\n  })\n\n  describe('room resolver', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.room',\n        description: 'Test room validation',\n        args: {\n          channel: { type: 'room', required: true }\n        },\n        handler: async () => {}\n      })\n    })\n\n    it('should validate room format', async () => {\n      const result = await commandBus.validate('test.room', { channel: '#ops' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.channel, '#ops')\n    })\n\n    it('should reject invalid room format', async () => {\n      const result = await commandBus.validate('test.room', { channel: 'ops' }, {})\n\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.errors.some(e => e.includes('channel')))\n    })\n\n    it('should allow custom room resolver override', async () => {\n      commandBus.registerTypeResolver('room', async (value) => {\n        if (!value.startsWith('room:')) {\n          throw new Error('room must start with room:')\n        }\n        return value\n      })\n\n      const result = await commandBus.validate('test.room', { channel: 'room:ops' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.channel, 'room:ops')\n    })\n  })\n\n  describe('date resolver', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.date',\n        description: 'Test date parsing',\n        args: {\n          due: { type: 'date', required: true }\n        },\n        handler: async () => {}\n      })\n    })\n\n    it('should parse \"today\" keyword', async () => {\n      const result = await commandBus.validate('test.date', { due: 'today' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.ok(result.args.due instanceof Date)\n    })\n\n    it('should parse \"tomorrow\" keyword', async () => {\n      const result = await commandBus.validate('test.date', { due: 'tomorrow' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.ok(result.args.due instanceof Date)\n    })\n\n    it('should parse ISO date string', async () => {\n      const result = await commandBus.validate('test.date', { due: '2026-02-15' }, {})\n\n      assert.strictEqual(result.ok, true)\n      assert.ok(result.args.due instanceof Date)\n    })\n\n    it('should reject invalid date', async () => {\n      const result = await commandBus.validate('test.date', { due: 'invalid-date' }, {})\n\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.errors.some(e => e.includes('due')))\n    })\n  })\n\n  describe('custom type resolvers', () => {\n    it('should allow registering custom type resolver', async () => {\n      commandBus.registerTypeResolver('project_id', async (value) => {\n        if (!value.startsWith('PRJ-')) {\n          throw new Error('must start with PRJ-')\n        }\n        return value.toUpperCase()\n      })\n\n      commandBus.register({\n        id: 'test.custom',\n        description: 'Test custom type',\n        args: {\n          project: { type: 'project_id', required: true }\n        },\n        handler: async () => {}\n      })\n\n      const result = await commandBus.validate('test.custom', { project: 'PRJ-123' }, {})\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.args.project, 'PRJ-123')\n    })\n\n    it('should reject invalid custom type values', async () => {\n      commandBus.registerTypeResolver('project_id', async (value) => {\n        if (!value.startsWith('PRJ-')) {\n          throw new Error('must start with PRJ-')\n        }\n        return value.toUpperCase()\n      })\n\n      commandBus.register({\n        id: 'test.custom.invalid',\n        description: 'Test custom type validation',\n        args: {\n          project: { type: 'project_id', required: true }\n        },\n        handler: async () => {}\n      })\n\n      const result = await commandBus.validate('test.custom.invalid', { project: 'invalid' }, {})\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.errors.some(e => e.includes('must start with PRJ-')))\n    })\n\n    it('should throw error for invalid resolver registration', () => {\n      assert.throws(() => {\n        commandBus.registerTypeResolver('', () => {})\n      }, /non-empty string/)\n\n      assert.throws(() => {\n        commandBus.registerTypeResolver('test', 'not a function')\n      }, /must be a function/)\n    })\n  })\n\n  describe('propose() and confirm()', () => {\n    let executeCalled\n    let executeArgs\n\n    beforeEach(() => {\n      executeCalled = false\n      executeArgs = null\n\n      commandBus.register({\n        id: 'test.sideeffect',\n        description: 'Command with side effects',\n        sideEffects: ['modifies database'],\n        args: {\n          action: { type: 'string', required: true }\n        },\n        handler: async (ctx) => {\n          executeCalled = true\n          executeArgs = ctx.args\n          return 'Command executed!'\n        }\n      })\n    })\n\n    it('should create pending proposal for side-effect command', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const proposal = await commandBus.propose({\n        commandId: 'test.sideeffect',\n        args: { action: 'delete' }\n      }, context)\n\n      assert.ok(proposal)\n      assert.strictEqual(proposal.commandId, 'test.sideeffect')\n      assert.ok(proposal.preview)\n      assert.ok(proposal.confirmationKey)\n    })\n\n    it('should execute on confirm(\"yes\")', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      await commandBus.propose({\n        commandId: 'test.sideeffect',\n        args: { action: 'delete' }\n      }, context)\n\n      const result = await commandBus.confirm('yes', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.executed, true)\n      assert.ok(executeCalled)\n      assert.strictEqual(executeArgs.action, 'delete')\n    })\n\n    it('should cancel on confirm(\"no\")', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      await commandBus.propose({\n        commandId: 'test.sideeffect',\n        args: { action: 'delete' }\n      }, context)\n\n      const result = await commandBus.confirm('no', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.cancelled, true)\n      assert.strictEqual(executeCalled, false)\n    })\n\n    it('should cancel on confirm(\"cancel\")', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      await commandBus.propose({\n        commandId: 'test.sideeffect',\n        args: { action: 'delete' }\n      }, context)\n\n      const result = await commandBus.confirm('cancel', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.cancelled, true)\n      assert.strictEqual(executeCalled, false)\n    })\n\n    it('should return null if no pending confirmation exists', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const result = await commandBus.confirm('yes', context)\n      assert.strictEqual(result, null)\n    })\n  })\n\n  describe('_renderPreview()', () => {\n    it('should escape quotes inside quoted values', () => {\n      const preview = commandBus._renderPreview('test.preview', {\n        message: 'test \"quoted\" text'\n      })\n\n      assert.strictEqual(preview, 'test.preview --message \"test \\\\\\\"quoted\\\\\\\" text\"')\n    })\n  })\n\n  describe('confirm policy', () => {\n    it('should require confirmation when confirm=always', async () => {\n      commandBus.register({\n        id: 'test.always',\n        description: 'Always confirm',\n        confirm: 'always',\n        handler: async () => {}\n      })\n\n      const needsConfirm = commandBus.needsConfirmation('test.always')\n      assert.strictEqual(needsConfirm, true)\n    })\n\n    it('should not require confirmation when confirm=never', async () => {\n      commandBus.register({\n        id: 'test.never',\n        description: 'Never confirm',\n        confirm: 'never',\n        handler: async () => {}\n      })\n\n      const needsConfirm = commandBus.needsConfirmation('test.never')\n      assert.strictEqual(needsConfirm, false)\n    })\n\n    it('should require confirmation for commands with sideEffects', async () => {\n      commandBus.register({\n        id: 'test.effects',\n        description: 'Has side effects',\n        sideEffects: ['deletes data'],\n        handler: async () => {}\n      })\n\n      const needsConfirm = commandBus.needsConfirmation('test.effects')\n      assert.strictEqual(needsConfirm, true)\n    })\n  })\n\n  describe('execute()', () => {\n    it('should pass args and context to handler', async () => {\n      let received\n\n      commandBus.register({\n        id: 'test.execute.ctx',\n        description: 'Execution context shape',\n        handler: async (ctx) => {\n          received = ctx\n          return 'ok'\n        }\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1',\n        message: { id: 'm1' },\n        res: { id: 'r1' }\n      }\n\n      const result = await commandBus.execute('test.execute.ctx', { foo: 'bar' }, context)\n\n      assert.strictEqual(result, 'ok')\n      assert.deepStrictEqual(received.args, { foo: 'bar' })\n      assert.deepStrictEqual(received.context, context)\n    })\n  })\n\n  describe('permissions', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.restricted',\n        description: 'Restricted to specific rooms',\n        permissions: {\n          rooms: ['#ops', '#admin']\n        },\n        handler: async () => 'Success'\n      })\n    })\n\n    it('should allow execution in allowed rooms', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#ops'\n      }\n\n      const result = await commandBus.execute('test.restricted', {}, context)\n      assert.strictEqual(result, 'Success')\n    })\n\n    it('should deny execution in disallowed rooms', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#general'\n      }\n\n      await assert.rejects(\n        async () => await commandBus.execute('test.restricted', {}, context),\n        /Permission denied/\n      )\n    })\n\n    it('should allow execution when user has required role', async () => {\n      const permissionProvider = {\n        hasRole: async (user, roles, context) => {\n          return roles.includes('admin') && user.id === 'user1'\n        }\n      }\n\n      const busCfg = new CommandBus(robot, { permissionProvider })\n      busCfg.register({\n        id: 'test.role.restricted',\n        description: 'Restricted by role',\n        permissions: {\n          roles: ['admin']\n        },\n        handler: async () => 'Success'\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#general'\n      }\n\n      const result = await busCfg.execute('test.role.restricted', {}, context)\n      assert.strictEqual(result, 'Success')\n    })\n\n    it('should deny execution when user lacks required role', async () => {\n      const permissionProvider = {\n        hasRole: async (user, roles, context) => {\n          return false\n        }\n      }\n\n      const busCfg = new CommandBus(robot, { permissionProvider })\n      busCfg.register({\n        id: 'test.role.denied',\n        description: 'Restricted by role',\n        permissions: {\n          roles: ['admin']\n        },\n        handler: async () => 'Success'\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#general'\n      }\n\n      await assert.rejects(\n        async () => await busCfg.execute('test.role.denied', {}, context),\n        /Permission denied: insufficient role/\n      )\n    })\n\n    it('should check multiple roles and allow if user has any required role', async () => {\n      const permissionProvider = {\n        hasRole: async (user, roles, context) => {\n          const userRoles = new Set(['developer', 'reviewer'])\n          return roles.some(role => userRoles.has(role))\n        }\n      }\n\n      const busCfg = new CommandBus(robot, { permissionProvider })\n      busCfg.register({\n        id: 'test.multi.role',\n        description: 'Multiple roles allowed',\n        permissions: {\n          roles: ['admin', 'developer', 'lead']\n        },\n        handler: async () => 'Success'\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#general'\n      }\n\n      const result = await busCfg.execute('test.multi.role', {}, context)\n      assert.strictEqual(result, 'Success')\n    })\n\n    it('should allow execution without permissionProvider when roles defined', async () => {\n      const busCfg = new CommandBus(robot)\n      busCfg.register({\n        id: 'test.no.provider',\n        description: 'Roles but no provider',\n        permissions: {\n          roles: ['admin']\n        },\n        handler: async () => 'Success'\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: '#general'\n      }\n\n      const result = await busCfg.execute('test.no.provider', {}, context)\n      assert.strictEqual(result, 'Success')\n    })\n  })\n\n  describe('invoke()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.invoke',\n        description: 'Test invoke pipeline',\n        args: {\n          name: { type: 'string', required: true }\n        },\n        handler: async (ctx) => `Hello, ${ctx.args.name}!`\n      })\n    })\n\n    it('should parse, validate, and execute in one call', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const result = await commandBus.invoke('test.invoke --name alice', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.result, 'Hello, alice!')\n    })\n\n    it('should return validation errors if invalid', async () => {\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const result = await commandBus.invoke('test.invoke', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.ok, false)\n      assert.ok(result.missing.includes('name'))\n    })\n\n    it('should return help when --help flag is provided', async () => {\n      let executed = false\n\n      commandBus.register({\n        id: 'test.help.invoke',\n        description: 'Help flag test',\n        args: {\n          name: { type: 'string', required: true }\n        },\n        handler: async () => {\n          executed = true\n          return 'executed'\n        }\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      const result = await commandBus.invoke('test.help.invoke --help', context)\n\n      assert.ok(result)\n      assert.strictEqual(result.ok, true)\n      assert.strictEqual(result.helpOnly, true)\n      assert.ok(result.result.includes('Usage:'))\n      assert.strictEqual(executed, false)\n    })\n  })\n\n  describe('listCommands()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'admin.restart',\n        description: 'Restart the bot',\n        handler: async () => {}\n      })\n      commandBus.register({\n        id: 'admin.status',\n        description: 'Check bot status',\n        handler: async () => {}\n      })\n      commandBus.register({\n        id: 'user.profile',\n        description: 'View user profile',\n        handler: async () => {}\n      })\n    })\n\n    it('should list all commands when no filter', () => {\n      const commands = commandBus.listCommands()\n      assert.strictEqual(commands.length, 3)\n    })\n\n    it('should filter commands by prefix', () => {\n      const commands = commandBus.listCommands({ prefix: 'admin' })\n      assert.strictEqual(commands.length, 2)\n      assert.ok(commands.every(c => c.id.startsWith('admin')))\n    })\n  })\n\n  describe('getHelp()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'test.help',\n        description: 'Test command for help',\n        aliases: ['help.test', 'test help'],\n        args: {\n          name: { type: 'string', required: true },\n          count: { type: 'number', default: 1 }\n        },\n        examples: [\n          '/test.help --name alice',\n          '/test.help --name bob --count 5'\n        ],\n        handler: async () => {}\n      })\n    })\n\n    it('should return help text for a command', () => {\n      const help = commandBus.getHelp('test.help')\n\n      assert.ok(help)\n      assert.ok(help.includes('test.help'))\n      assert.ok(help.includes('Test command for help'))\n      assert.ok(help.includes('Intent:'))\n      assert.ok(help.includes('help.test'))\n      assert.ok(help.includes('name'))\n      assert.ok(help.includes('required'))\n    })\n  })\n\n  describe('search()', () => {\n    beforeEach(() => {\n      commandBus.register({\n        id: 'tickets.create',\n        description: 'Create a support ticket',\n        aliases: ['ticket new', 'create ticket'],\n        examples: ['/tickets.create --title \"VPN down\"'],\n        handler: async () => {}\n      })\n      commandBus.register({\n        id: 'tickets.status',\n        description: 'Check ticket status',\n        aliases: ['ticket status'],\n        handler: async () => {}\n      })\n    })\n\n    it('should rank exact alias match highest', () => {\n      const results = commandBus.search('ticket new')\n      assert.ok(results.length > 0)\n      assert.strictEqual(results[0].id, 'tickets.create')\n      assert.strictEqual(results[0].matchedOn, 'alias')\n    })\n\n    it('should rank alias token overlap above description overlap', () => {\n      const results = commandBus.search('ticket')\n      assert.ok(results.length > 0)\n      assert.strictEqual(results[0].matchedOn, 'alias')\n    })\n  })\n\n  describe('aliasCollisions()', () => {\n    it('should return collisions for normalized aliases', () => {\n      commandBus.register({\n        id: 'cmd.one',\n        description: 'First command',\n        aliases: ['My Alias'],\n        handler: async () => {}\n      })\n      commandBus.register({\n        id: 'cmd.two',\n        description: 'Second command',\n        aliases: ['my  alias'],\n        handler: async () => {}\n      })\n\n      const collisions = commandBus.aliasCollisions()\n      assert.deepStrictEqual(collisions['my alias'], ['cmd.one', 'cmd.two'])\n    })\n  })\n\n  describe('event emission', () => {\n    it('should emit commands:registered event', (t, done) => {\n      commandBus.on('commands:registered', (event) => {\n        assert.strictEqual(event.commandId, 'test.event')\n        done()\n      })\n\n      commandBus.register({\n        id: 'test.event',\n        description: 'Test event emission',\n        handler: async () => {}\n      })\n    })\n\n    it('should emit commands:invocation_parsed event', (t, done) => {\n      commandBus.register({\n        id: 'test.parse',\n        description: 'Test parse',\n        handler: async () => {}\n      })\n\n      commandBus.on('commands:invocation_parsed', (event) => {\n        assert.strictEqual(event.commandId, 'test.parse')\n        done()\n      })\n\n      commandBus.parse('test.parse --arg value')\n    })\n  })\n\n  describe('TTL for pending proposals', () => {\n    it('should expire pending proposals after TTL', async (t) => {\n      const shortTTL = 100 // 100ms\n      const busWithShortTTL = new CommandBus(robot, { proposalTTL: shortTTL, disableLogging: true })\n\n      busWithShortTTL.register({\n        id: 'test.ttl',\n        description: 'Test TTL',\n        sideEffects: ['test'],\n        handler: async () => 'Done'\n      })\n\n      const context = {\n        user: { id: 'user1', name: 'alice' },\n        room: 'room1'\n      }\n\n      await busWithShortTTL.propose({\n        commandId: 'test.ttl',\n        args: {}\n      }, context)\n\n      // Wait for TTL to expire\n      await new Promise(resolve => setTimeout(resolve, 150))\n\n      const result = await busWithShortTTL.confirm('yes', context)\n      assert.strictEqual(result, null) // Should be expired\n      \n      // Clean up any remaining timers\n      busWithShortTTL.clearPendingProposals()\n    })\n  })\n})\n"
  },
  {
    "path": "test/Configuration_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { Robot } from '../index.mjs'\nimport { resolve } from 'node:path'\ndescribe('Configuration', () => {\n  describe('#robot', () => {\n    let robot = null\n    beforeEach(() => {\n      robot = new Robot(null, false, 'TestHubot')\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('Load files from configuration folder', async () => {\n      await robot.load(resolve('.', 'configuration'))\n      assert.ok(robot.config !== undefined)\n    })\n  })\n})\n"
  },
  {
    "path": "test/DataStore_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport InMemoryDataStore from '../src/datastores/Memory.mjs'\nimport Brain from '../src/Brain.mjs'\n\ndescribe('Datastore', () => {\n  let robot = null\n  beforeEach(() => {\n    robot = {\n      emit () {},\n      on () {},\n      receive (msg) {}\n    }\n    robot.brain = new Brain(robot)\n    robot.datastore = new InMemoryDataStore(robot)\n    robot.brain.userForId('1', { name: 'User One' })\n    robot.brain.userForId('2', { name: 'User Two' })\n  })\n  afterEach(() => {\n    robot.brain.close()\n    // Getting warning about too many listeners, so remove them all\n    process.removeAllListeners()\n  })\n\n  describe('global scope', () => {\n    it('returns undefined for values not in the datastore', async () => {\n      const value = await robot.datastore.get('blah')\n      assert.deepEqual(value, undefined)\n    })\n\n    it('can store simple values', async () => {\n      await robot.datastore.set('key', 'value')\n      const value = await robot.datastore.get('key')\n      assert.equal(value, 'value')\n    })\n\n    it('can store arbitrary JavaScript values', async () => {\n      const object = {\n        name: 'test',\n        data: [1, 2, 3]\n      }\n      await robot.datastore.set('key', object)\n      const value = await robot.datastore.get('key')\n      assert.equal(value.name, 'test')\n      assert.deepEqual(value.data, [1, 2, 3])\n    })\n\n    it('can dig inside objects for values', async () => {\n      const object = {\n        a: 'one',\n        b: 'two'\n      }\n      await robot.datastore.set('key', object)\n      const value = await robot.datastore.getObject('key', 'a')\n      assert.equal(value, 'one')\n    })\n\n    it('can set individual keys inside objects', async () => {\n      const object = {\n        a: 'one',\n        b: 'two'\n      }\n      await robot.datastore.set('object', object)\n      await robot.datastore.setObject('object', 'c', 'three')\n      const value = await robot.datastore.get('object')\n      assert.equal(value.a, 'one')\n      assert.equal(value.b, 'two')\n      assert.equal(value.c, 'three')\n    })\n\n    it('creates an object from scratch when none exists', async () => {\n      const datastore = new InMemoryDataStore(robot)\n      await datastore.setObject('object', 'key', 'value')\n      const value = await datastore.get('object')\n      assert.deepEqual(value, { key: 'value' })\n    })\n\n    it('can append to an existing array', async () => {\n      await robot.datastore.set('array', [1, 2, 3])\n      await robot.datastore.setArray('array', 4)\n      const value = await robot.datastore.get('array')\n      assert.deepEqual(value, [1, 2, 3, 4])\n    })\n\n    it('creates an array from scratch when none exists', async () => {\n      const datastore = new InMemoryDataStore(robot)\n      await datastore.setArray('array', 4)\n      const value = await datastore.get('array')\n      assert.deepEqual(value, [4])\n    })\n    it('creates an array with an array', async () => {\n      const expected = [1, 2, 3]\n      const datastore = new InMemoryDataStore(robot)\n      await datastore.setArray('array', [1, 2, 3])\n      const actual = await datastore.get('array')\n      assert.deepEqual(actual, expected)\n    })\n  })\n\n  describe('User scope', () => {\n    it('has access to the robot object', () => {\n      const user = robot.brain.userForId('1')\n      assert.deepEqual(user._getRobot(), robot)\n    })\n\n    it('can store user data which is separate from global data', async () => {\n      const user = robot.brain.userForId('1')\n      await user.set('blah', 'blah')\n      const userBlah = await user.get('blah')\n      const datastoreBlah = await robot.datastore.get('blah')\n      assert.notDeepEqual(userBlah, datastoreBlah)\n      assert.equal(userBlah, 'blah')\n      assert.deepEqual(datastoreBlah, undefined)\n    })\n\n    it('stores user data separate per-user', async () => {\n      const userOne = robot.brain.userForId('1')\n      const userTwo = robot.brain.userForId('2')\n      await userOne.set('blah', 'blah')\n      const valueOne = await userOne.get('blah')\n      const valueTwo = await userTwo.get('blah')\n      assert.notDeepEqual(valueOne, valueTwo)\n      assert.equal(valueOne, 'blah')\n      assert.deepEqual(valueTwo, undefined)\n    })\n  })\n})\n"
  },
  {
    "path": "test/Hubot_test.mjs",
    "content": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport path from 'node:path'\nimport { spawn } from 'node:child_process'\nimport { TextMessage, User } from '../index.mjs'\nimport { fileURLToPath } from 'node:url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\nconst root = __dirname.replace(/test$/, '')\n\ndescribe('Running bin/Hubot.mjs', () => {\n  it('should load adapter from HUBOT_FILE environment variable', async () => {\n    process.env.HUBOT_HTTPD = 'false'\n    process.env.HUBOT_FILE = path.resolve(root, 'test', 'fixtures', 'MockAdapter.mjs')\n    const hubot = (await import('../bin/Hubot.mjs')).default\n    await hubot.loadFile(path.resolve(root, 'test', 'fixtures'), 'TestScript.mjs')\n    while (!hubot.adapter) {\n      await new Promise(resolve => setTimeout(resolve, 100))\n    }\n    hubot.adapter.on('reply', (envelope, ...strings) => {\n      assert.equal(strings[0], 'test response from .mjs script')\n      delete process.env.HUBOT_FILE\n      delete process.env.HUBOT_HTTPD\n      hubot.shutdown()\n    })\n    try {\n      await hubot.receive(new TextMessage(new User('mocha', { room: '#mocha' }), '@Hubot test'))\n      assert.deepEqual(hubot.hasLoadedTestMjsScript, true)\n      assert.equal(hubot.name, 'Hubot')\n    } finally {\n      hubot.shutdown()\n    }\n  })\n\n  it('should output a help message when run with --help', (t, done) => {\n    const hubot = process.platform === 'win32' ? spawn('node', ['./bin/Hubot.mjs', '--help']) : spawn('./bin/hubot', ['--help'])\n    const expected = `Usage: hubot [options]\n  -a, --adapter HUBOT_ADAPTER\n  -f, --file HUBOT_FILE\n  -c, --create HUBOT_CREATE\n  -d, --disable-httpd HUBOT_HTTPD\n  -h, --help\n  -l, --alias HUBOT_ALIAS\n  -n, --name HUBOT_NAME\n  -r, --require PATH\n  -t, --config-check\n  -v, --version\n  -e, --execute\n`\n    let actual = ''\n    hubot.stdout.on('data', (data) => {\n      actual += data.toString()\n    })\n    hubot.stderr.on('data', (data) => {\n      actual += data.toString()\n    })\n    hubot.on('close', (code) => {\n      assert.deepEqual(actual, expected)\n      done()\n    })\n  })\n  it('should execute the command when run with --execute or -e', (t, done) => {\n    const expected = \"HELO World! I'm Hubot.\"\n    const commandText = 'helo'\n    const env = Object.assign({}, process.env, { NOLOG: 'off' })\n    const hubot = process.platform === 'win32'\n      ? spawn('node', ['./bin/Hubot.mjs', '-d', '--execute', commandText, '-r', 'test/scripts'], { env })\n      : spawn('./bin/hubot', ['-d', '--execute', commandText, '-r', 'test/scripts'], { env })\n    let actual = ''\n    hubot.stdout.on('data', (data) => {\n      actual += data.toString()\n    })\n    hubot.stderr.on('data', (data) => {\n      actual += data.toString()\n    })\n    hubot.on('close', (code) => {\n      assert.ok(actual.includes(expected))\n      done()\n    })\n  })\n})\n\ndescribe('Running hubot with args', () => {\n  it('should not start web service when --disable-httpd is passed', (t, done) => {\n    const hubot = process.platform === 'win32' ? spawn('node', ['./bin/Hubot.mjs', '--disable-httpd']) : spawn('./bin/hubot', ['--disable-httpd'])\n    let actual = {}\n    const logMessages = []\n    hubot.stdout.on('data', (data) => {\n      console.log(data.toString())\n      logMessages.push(data.toString())\n    })\n    hubot.stderr.on('data', (data) => {\n      console.log(data.toString())\n      logMessages.push(data.toString())\n    })\n    const interval = setInterval(async () => {\n      if (logMessages.some(m => m.includes('EADDRINUSE'))) {\n        clearInterval(interval)\n        assert.fail('Web service started when --disable-httpd was passed')\n        done()\n      }\n      if (logMessages.some(m => m.includes('No external-scripts.json found. Skipping'))) {\n        clearInterval(interval)\n        try {\n          const response = await fetch('http://localhost:8080')\n          actual = await response.text()\n        } catch (e) {\n          actual = e\n        } finally {\n          hubot.kill()\n        }\n        console.log(actual)\n        assert.ok(actual instanceof TypeError)\n        assert.deepEqual(actual.message, 'fetch failed', 'this is an expected failure since the web service should not be running')\n        done()\n      }\n    }, 60)\n  })\n})\n"
  },
  {
    "path": "test/Listener_test.mjs",
    "content": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { EnterMessage, TextMessage, Listener, TextListener, Response, User, Middleware } from '../index.mjs'\n\ndescribe('Listener', () => {\n  const robot = {\n    // Re-throw AssertionErrors for clearer test failures\n    emit (name, err, response) {\n      if (err.constructor.name === 'AssertionError') {\n        return process.nextTick(() => {\n          throw err\n        })\n      }\n    },\n    // Ignore log messages\n    logger: {\n      debug () {},\n      error (...args) {\n        // console.error(...args)\n      }\n    },\n    // Why is this part of the Robot object??\n    Response\n  }\n  const user = new User({\n    id: 1,\n    name: 'hubottester',\n    room: '#mocha'\n  })\n\n  describe('Unit Tests', () => {\n    describe('#call', () => {\n      it('calls the matcher', async () => {\n        const testMessage = new TextMessage(user, 'message')\n        const testMatcher = message => {\n          assert.deepEqual(message, testMessage)\n          return true\n        }\n        const middleware = new Middleware(robot)\n        middleware.register(async context => {\n          assert.deepEqual(context.listener, testListener)\n        })\n        const testListener = new Listener(robot, testMatcher, async response => true)\n        await testListener.call(testMessage, middleware)\n      })\n\n      it('the response object should have the match results so listeners can have access to it', async () => {\n        const matcherResult = {}\n        const testMatcher = message => {\n          return matcherResult\n        }\n        const testMessage = new TextMessage(user, 'response should have match')\n        const listenerCallback = response => assert.deepEqual(response.match, matcherResult)\n        const testListener = new Listener(robot, testMatcher, listenerCallback)\n        await testListener.call(testMessage, null)\n      })\n\n      describe('if the matcher returns true', () => {\n        const createListener = cb => {\n          return new Listener(robot, () => true, cb)\n        }\n\n        it('executes the listener callback', async () => {\n          const listenerCallback = async response => {\n            assert.deepEqual(response.message, testMessage)\n          }\n          const testMessage = {}\n\n          const testListener = createListener(listenerCallback)\n          await testListener.call(testMessage, async (_) => {})\n        })\n\n        it('returns true', () => {\n          const testMessage = {}\n\n          const testListener = createListener(() => {})\n          const result = testListener.call(testMessage)\n          assert.ok(result)\n        })\n\n        it('calls the provided callback with true', (t, done) => {\n          const testMessage = {}\n\n          const testListener = createListener(() => {})\n          testListener.call(testMessage, async result => {\n            assert.ok(result)\n            done()\n          })\n        })\n\n        it('calls the provided callback after the function returns', (t, done) => {\n          const testMessage = {}\n\n          const testListener = createListener(() => {})\n          let finished = false\n          testListener.call(testMessage, async result => {\n            assert.ok(finished)\n            done()\n          })\n          finished = true\n        })\n\n        it('handles uncaught errors from the listener callback', async () => {\n          const testMessage = {}\n          const theError = new Error()\n\n          const listenerCallback = async response => {\n            throw theError\n          }\n\n          robot.emit = (name, err, response) => {\n            assert.equal(name, 'error')\n            assert.deepEqual(err, theError)\n            assert.deepEqual(response.message, testMessage)\n          }\n\n          const testListener = createListener(listenerCallback)\n          await testListener.call(testMessage, async response => {})\n        })\n\n        it('calls the provided callback with true if there is an error thrown by the listener callback', (t, done) => {\n          const testMessage = {}\n          const theError = new Error()\n\n          const listenerCallback = async response => {\n            throw theError\n          }\n\n          const testListener = createListener(listenerCallback)\n          testListener.call(testMessage, async result => {\n            assert.ok(result)\n            done()\n          })\n        })\n\n        it('calls the listener callback with a Response that wraps the Message', async () => {\n          const testMessage = {}\n          const listenerCallback = async response => {\n            assert.deepEqual(response.message, testMessage)\n          }\n          const testListener = createListener(listenerCallback)\n          await testListener.call(testMessage, async response => {})\n        })\n\n        it('passes through the provided middleware stack', async () => {\n          const testMessage = {}\n          const testListener = createListener(async () => {})\n          const testMiddleware = {\n            execute (context, next, done) {\n              assert.deepEqual(context.listener, testListener)\n              assert.ok(context.response instanceof Response)\n              assert.deepEqual(context.response.message, testMessage)\n              assert.ok(typeof next === 'function')\n              assert.ok(typeof done === 'function')\n            }\n          }\n\n          await testListener.call(testMessage, testMiddleware)\n        })\n\n        it('executes the listener callback if middleware succeeds', async () => {\n          let wasCalled = false\n          const listenerCallback = async () => {\n            wasCalled = true\n          }\n          const testMessage = {}\n\n          const testListener = createListener(listenerCallback)\n\n          await testListener.call(testMessage, async result => {\n            assert.ok(result)\n          })\n          assert.deepEqual(wasCalled, true)\n        })\n\n        it('does not execute the listener callback if middleware fails', async () => {\n          let wasCalled = false\n          const listenerCallback = async () => {\n            wasCalled = true\n          }\n          const testMessage = {}\n\n          const testListener = createListener(listenerCallback)\n          const testMiddleware = {\n            async execute (context) {\n              return false\n            }\n          }\n\n          await testListener.call(testMessage, testMiddleware, async result => {\n            assert.ok(result)\n          })\n          assert.deepEqual(wasCalled, false)\n        })\n      })\n\n      describe('if the matcher returns false', () => {\n        const createListener = cb => {\n          return new Listener(robot, () => false, cb)\n        }\n\n        it('does not execute the listener callback', async () => {\n          let wasCalled = false\n          const listenerCallback = async () => {\n            wasCalled = true\n          }\n          const testMessage = {}\n\n          const testListener = createListener(listenerCallback)\n          await testListener.call(testMessage, async context => {\n            assert.deepEqual(wasCalled, false)\n          })\n        })\n\n        it('returns null', async () => {\n          const testMessage = {}\n\n          const testListener = createListener(async () => {})\n          const result = await testListener.call(testMessage)\n          assert.deepEqual(result, null)\n        })\n\n        it('returns null because there is no matched listener', async () => {\n          const testMessage = {}\n          const testListener = createListener(async () => {})\n          const middleware = context => {\n            throw new Error('Should not be called')\n          }\n          const result = await testListener.call(testMessage, middleware)\n          assert.deepEqual(result, null)\n        })\n      })\n    })\n\n    describe('#constructor', () => {\n      it('requires a matcher', () => {\n        assert.throws(() => {\n          return new Listener(robot, undefined, {}, async () => {})\n        }, Error)\n      })\n\n      it('requires a callback', () => {\n        // No options\n        assert.throws(() => {\n          return new Listener(robot, () => {})\n        }, Error)\n        // With options\n        assert.throws(() => {\n          return new Listener(robot, () => {}, {})\n        }, Error)\n      })\n\n      it('gracefully handles missing options', () => {\n        const testMatcher = () => {}\n        const listenerCallback = async () => {}\n        const testListener = new Listener(robot, testMatcher, listenerCallback)\n        // slightly brittle because we are testing for the default options Object\n        assert.deepEqual(testListener.options, { id: null })\n        assert.deepEqual(testListener.callback, listenerCallback)\n      })\n\n      it('gracefully handles a missing ID (set to null)', () => {\n        const testMatcher = () => {}\n        const listenerCallback = async () => {}\n        const testListener = new Listener(robot, testMatcher, {}, listenerCallback)\n        assert.deepEqual(testListener.options.id, null)\n      })\n    })\n\n    describe('TextListener', () =>\n      describe('#matcher', () => {\n        it('matches TextMessages', () => {\n          const callback = async () => {}\n          const testMessage = new TextMessage(user, 'test')\n\n          testMessage.match = regex => {\n            assert.deepEqual(regex, testRegex)\n            return true\n          }\n          const testRegex = /test/\n\n          const testListener = new TextListener(robot, testRegex, callback)\n          const result = testListener.matcher(testMessage)\n\n          assert.ok(result)\n        })\n\n        it('does not match EnterMessages', () => {\n          const callback = async () => {}\n          const testMessage = new EnterMessage(user)\n          const testRegex = /test/\n\n          const testListener = new TextListener(robot, testRegex, callback)\n          const result = testListener.matcher(testMessage)\n\n          assert.deepEqual(result, undefined)\n        })\n\n        it('matches non-TextMessage objects with a match function (duck-typing regression test)', () => {\n          const callback = async () => {}\n          const testRegex = /test/\n          // Simulate a message from a linked module that isn't an instanceof TextMessage\n          const nonTextMessage = {\n            user: 'testuser',\n            text: 'test message',\n            match (regex) {\n              assert.deepEqual(regex, testRegex)\n              return true\n            }\n          }\n\n          const testListener = new TextListener(robot, testRegex, callback)\n          const result = testListener.matcher(nonTextMessage)\n\n          assert.ok(result)\n        })\n      })\n    )\n  })\n})\n"
  },
  {
    "path": "test/Message_test.mjs",
    "content": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { User, Message, TextMessage } from '../index.mjs'\n\ndescribe('Message', () => {\n  const user = new User({\n    id: 1,\n    name: 'hubottester',\n    room: '#mocha'\n  })\n\n  describe('Unit Tests', () => {\n    describe('#finish', () =>\n      it('marks the message as done', () => {\n        const testMessage = new Message(user)\n        assert.deepEqual(testMessage.done, false)\n        testMessage.finish()\n        assert.deepEqual(testMessage.done, true)\n      })\n    )\n\n    describe('TextMessage', () =>\n      describe('#match', () =>\n        it('should perform standard regex matching', () => {\n          const testMessage = new TextMessage(user, 'message123')\n          assert.equal(testMessage.match(/^message123$/)[0], 'message123')\n          assert.deepEqual(testMessage.match(/^does-not-match$/), null)\n        })\n      )\n    )\n  })\n})\n"
  },
  {
    "path": "test/Middleware_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { Robot, TextMessage, Response, Middleware } from '../index.mjs'\nimport mockAdapter from './fixtures/MockAdapter.mjs'\n\ndescribe('Middleware', () => {\n  describe('Unit Tests', () => {\n    let robot = null\n    let middleware = null\n    beforeEach(() => {\n      robot = { emit () {} }\n      middleware = new Middleware(robot)\n    })\n\n    describe('#execute', () => {\n      it('executes synchronous middleware', async () => {\n        let wasCalled = false\n        const testMiddleware = async context => {\n          wasCalled = true\n          return true\n        }\n        middleware.register(testMiddleware)\n        await middleware.execute({})\n        assert.deepEqual(wasCalled, true)\n      })\n\n      it('executes all registered middleware in definition order', async () => {\n        const middlewareExecution = []\n        const testMiddlewareA = async context => {\n          middlewareExecution.push('A')\n        }\n        const testMiddlewareB = async context => {\n          middlewareExecution.push('B')\n        }\n        middleware.register(testMiddlewareA)\n        middleware.register(testMiddlewareB)\n        await middleware.execute({})\n        assert.deepEqual(middlewareExecution, ['A', 'B'])\n      })\n\n      describe('error handling', () => {\n        it('does not execute subsequent middleware after the error is thrown', async () => {\n          const middlewareExecution = []\n\n          const testMiddlewareA = async context => {\n            middlewareExecution.push('A')\n          }\n\n          const testMiddlewareB = async context => {\n            middlewareExecution.push('B')\n            throw new Error()\n          }\n\n          const testMiddlewareC = async context => {\n            middlewareExecution.push('C')\n          }\n\n          middleware.register(testMiddlewareA)\n          middleware.register(testMiddlewareB)\n          middleware.register(testMiddlewareC)\n          await middleware.execute({})\n          assert.deepEqual(middlewareExecution, ['A', 'B'])\n        })\n      })\n    })\n\n    describe('#register', () => {\n      it('adds to the list of middleware', () => {\n        const testMiddleware = async context => {}\n        middleware.register(testMiddleware)\n        assert.ok(middleware.stack.includes(testMiddleware))\n      })\n\n      it('validates the arity of middleware', () => {\n        const testMiddleware = async (context, next, done, extra) => {}\n\n        assert.throws(() => middleware.register(testMiddleware), 'Incorrect number of arguments')\n      })\n    })\n  })\n\n  // Per the documentation in docs/scripting.md\n  // Any new fields that are exposed to middleware should be explicitly\n  // tested for.\n  describe('Public Middleware APIs', () => {\n    let robot = null\n    let user = null\n    let testListener = null\n    let testMessage = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run\n\n      // Re-throw AssertionErrors for clearer test failures\n      robot.on('error', function (err, response) {\n        if (__guard__(err != null ? err.constructor : undefined, x => x.name) === 'AssertionError') {\n          process.nextTick(() => {\n            throw err\n          })\n        }\n      })\n\n      user = robot.brain.userForId('1', {\n        name: 'hubottester',\n        room: '#mocha'\n      })\n      testMessage = new TextMessage(user, 'message123')\n      robot.hear(/^message123$/, async response => {})\n      testListener = robot.listeners[0]\n    })\n\n    afterEach(() => {\n      robot.shutdown()\n    })\n\n    describe('listener middleware context', () => {\n      describe('listener', () => {\n        it('is the listener object that matched, has metadata in options object with id', async () => {\n          robot.listenerMiddleware(async context => {\n            assert.deepEqual(context.listener, testListener)\n            assert.ok(context.listener.options)\n            assert.deepEqual(context.listener.options.id, null)\n            return true\n          })\n          await robot.receive(testMessage)\n        })\n      })\n\n      describe('response', () =>\n        it('is a Response that wraps the message', async () => {\n          robot.listenerMiddleware(async context => {\n            assert.ok(context.response instanceof Response)\n            assert.ok(context.response.message)\n            assert.deepEqual(context.response.message, testMessage)\n            return true\n          })\n          await robot.receive(testMessage)\n        })\n      )\n    })\n\n    describe('receive middleware context', () => {\n      describe('response', () => {\n        it('is a match-less Response object', async () => {\n          robot.receiveMiddleware(async context => {\n            assert.ok(context.response instanceof Response)\n            assert.ok(context.response.message)\n            assert.deepEqual(context.response.message, testMessage)\n            return true\n          })\n\n          await robot.receive(testMessage)\n        })\n      })\n    })\n  })\n})\n\nfunction __guard__ (value, transform) {\n  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined\n}\n"
  },
  {
    "path": "test/OptParse-test.mjs",
    "content": "import { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport OptParse from '../src/OptParse.mjs'\n\ndescribe('CLI Argument Parsing', () => {\n  it('should parse arguments into options', () => {\n    const switches = [\n      ['-a', '--adapter HUBOT_ADAPTER', 'The Adapter to use, e.g. \"Shell\" (to load the default hubot Shell adapter)'],\n      ['-f', '--file HUBOT_FILE', 'Path to adapter file, e.g. \"./adapters/CustomAdapter.mjs\"'],\n      ['-c', '--create HUBOT_CREATE', 'Create a deployable hubot'],\n      ['-d', '--disable-httpd HUBOT_HTTPD', 'Disable the HTTP server'],\n      ['-h', '--help', 'Display the help information'],\n      ['-l', '--alias HUBOT_ALIAS', \"Enable replacing the robot's name with alias\"],\n      ['-n', '--name HUBOT_NAME', 'The name of the robot in chat'],\n      ['-r', '--require PATH', 'Alternative scripts path'],\n      ['-t', '--config-check', \"Test hubot's config to make sure it won't fail at startup\"],\n      ['-v', '--version', 'Displays the version of hubot installed']\n    ]\n\n    const options = {\n      adapter: null,\n      alias: false,\n      create: false,\n      enableHttpd: true,\n      scripts: [],\n      name: 'Hubot',\n      file: null,\n      configCheck: false\n    }\n\n    const Parser = new OptParse(switches)\n    Parser.on('adapter', (opt, value) => {\n      options.adapter = value\n    })\n    Parser.on('disable-httpd', (opt, value) => {\n      options.enableHttpd = false\n    })\n    Parser.on('alias', (opt, value) => {\n      options.alias = value\n    })\n    Parser.parse(['-a', 'Shell', '-d', '--alias', 'bot'])\n    assert.deepEqual(options.adapter, 'Shell')\n    assert.deepEqual(options.enableHttpd, false)\n    assert.deepEqual(options.alias, 'bot')\n  })\n})\n"
  },
  {
    "path": "test/Robot_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport path from 'node:path'\nimport { Robot, CatchAllMessage, EnterMessage, LeaveMessage, TextMessage, TopicMessage, User, Response } from '../index.mjs'\nimport mockAdapter from './fixtures/MockAdapter.mjs'\n\ndescribe('Robot', () => {\n  describe('#http', () => {\n    let robot = null\n    beforeEach(() => {\n      robot = new Robot(null, false, 'TestHubot')\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('API', () => {\n      const agent = {}\n      const httpClient = robot.http('http://example.com', { agent })\n      assert.ok(httpClient.get)\n      assert.ok(httpClient.post)\n    })\n    it('passes options through to the ScopedHttpClient', () => {\n      const agent = {}\n      const httpClient = robot.http('http://example.com', { agent })\n      assert.deepEqual(httpClient.options.agent, agent)\n    })\n    it('sets a user agent', () => {\n      const httpClient = robot.http('http://example.com')\n      assert.ok(httpClient.options.headers['User-Agent'].indexOf('Hubot') > -1)\n    })\n    it('meges global http options', () => {\n      const agent = {}\n      robot.globalHttpOptions = { agent }\n      const httpClient = robot.http('http://localhost')\n      assert.deepEqual(httpClient.options.agent, agent)\n    })\n    it('local options override global http options', () => {\n      const agentA = {}\n      const agentB = {}\n      robot.globalHttpOptions = { agent: agentA }\n      const httpClient = robot.http('http://localhost', { agent: agentB })\n      assert.deepEqual(httpClient.options.agent, agentB)\n    })\n    it('builds the url correctly from a string', () => {\n      const httpClient = robot.http('http://localhost')\n      const options = httpClient.buildOptions('http://localhost:3001')\n      assert.equal(options.host, 'localhost:3001')\n      assert.equal(options.pathname, '/')\n      assert.equal(options.protocol, 'http:')\n      assert.equal(options.port, '3001')\n    })\n  })\n\n  describe('#respondPattern', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot', 't-bot')\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('matches messages starting with robot\\'s name', () => {\n      const testMessage = robot.name + 'message123'\n      const testRegex = /(.*)/\n\n      const pattern = robot.respondPattern(testRegex)\n      assert.match(testMessage, pattern)\n      const match = testMessage.match(pattern)[1]\n      assert.equal(match, 'message123')\n    })\n    it(\"matches messages starting with robot's alias\", () => {\n      const testMessage = robot.alias + 'message123'\n      const testRegex = /(.*)/\n\n      const pattern = robot.respondPattern(testRegex)\n      assert.match(testMessage, pattern)\n      const match = testMessage.match(pattern)[1]\n      assert.equal(match, 'message123')\n    })\n\n    it('does not match unaddressed messages', () => {\n      const testMessage = 'message123'\n      const testRegex = /(.*)/\n\n      const pattern = robot.respondPattern(testRegex)\n      assert.doesNotMatch(testMessage, pattern)\n    })\n\n    it('matches properly when name is substring of alias', () => {\n      robot.name = 'Meg'\n      robot.alias = 'Megan'\n      const testMessage1 = robot.name + ' message123'\n      const testMessage2 = robot.alias + ' message123'\n      const testRegex = /(.*)/\n\n      const pattern = robot.respondPattern(testRegex)\n\n      assert.match(testMessage1, pattern)\n      const match1 = testMessage1.match(pattern)[1]\n      assert.equal(match1, 'message123')\n\n      assert.match(testMessage2, pattern)\n      const match2 = testMessage2.match(pattern)[1]\n      assert.equal(match2, 'message123')\n    })\n\n    it('matches properly when alias is substring of name', () => {\n      robot.name = 'Megan'\n      robot.alias = 'Meg'\n      const testMessage1 = robot.name + ' message123'\n      const testMessage2 = robot.alias + ' message123'\n      const testRegex = /(.*)/\n\n      const pattern = robot.respondPattern(testRegex)\n\n      assert.match(testMessage1, pattern)\n      const match1 = testMessage1.match(pattern)[1]\n      assert.equal(match1, 'message123')\n\n      assert.match(testMessage2, pattern)\n      const match2 = testMessage2.match(pattern)[1]\n      assert.equal(match2, 'message123')\n    })\n  })\n  describe('Listening API', () => {\n    let robot = null\n    beforeEach(() => {\n      robot = new Robot(null, false, 'TestHubot')\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('#listen: registers a new listener directly', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.listen(() => {}, () => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n\n    it('#hear: registers a new listener directly', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.hear(/.*/, () => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n\n    it('#respond: registers a new listener using respond', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.respond(/.*/, () => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n\n    it('#enter: registers a new listener using listen', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.enter(() => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n\n    it('#leave: registers a new listener using listen', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.leave(() => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n    it('#topic: registers a new listener using listen', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.topic(() => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n\n    it('#catchAll: registers a new listener using listen', () => {\n      assert.equal(robot.listeners.length, 0)\n      robot.catchAll(() => {})\n      assert.equal(robot.listeners.length, 1)\n    })\n  })\n  describe('#receive', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      user = new User('1', { name: 'node', room: '#test' })\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('calls all registered listeners', async () => {\n      // Need to use a real Message so that the CatchAllMessage constructor works\n      const testMessage = new TextMessage(user, 'message123')\n      let counter = 0\n      const listener = async message => {\n        counter++\n      }\n      robot.listen(() => true, null, listener)\n      robot.listen(() => true, null, listener)\n      robot.listen(() => true, null, listener)\n      robot.listen(() => true, null, listener)\n      await robot.receive(testMessage)\n      assert.equal(counter, 4)\n    })\n\n    it('sends a CatchAllMessage if no listener matches', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n      robot.listeners = []\n      let actual = null\n      robot.catchAll(async (message) => {\n        actual = message\n      })\n      await robot.receive(testMessage)\n      assert.ok(actual.message instanceof CatchAllMessage)\n      assert.deepEqual(actual.message.message, testMessage)\n    })\n\n    it('calls the catch-all listener with a Response object', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n\n      const listenerCallback = async () => {\n        assert.fail('Should not have called listener')\n      }\n      robot.hear(/^no-matches$/, listenerCallback)\n      let actual = null\n      robot.catchAll(async response => {\n        response.reply('caught by catchAll')\n        actual = response\n      })\n\n      await robot.receive(testMessage)\n      assert.ok(actual instanceof Response)\n    })\n\n    it('does not trigger a CatchAllMessage if a listener matches', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n\n      const matchingListener = async response => {\n        assert.deepEqual(response.message, testMessage)\n      }\n\n      robot.listen(() => true, null, matchingListener)\n      robot.catchAll(null, () => {\n        throw new Error('Should not have triggered catchAll')\n      })\n      await robot.receive(testMessage)\n    })\n\n    it('stops processing if a listener marks the message as done', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n      let spyCalled = false\n\n      const matchingListener = async response => {\n        response.message.finish()\n        assert.equal(response.message.text, testMessage.text)\n        assert.equal(response.message.user.id, testMessage.user.id)\n      }\n      const listenerSpy = async message => {\n        spyCalled = true\n        assert.fail('Should not have triggered listener')\n      }\n      robot.listen(() => true, null, matchingListener)\n      robot.listen(() => true, null, listenerSpy)\n      await robot.receive(testMessage)\n      assert.equal(spyCalled, false)\n    })\n\n    it('gracefully handles listener uncaughtExceptions (move on to next listener)', async () => {\n      const testMessage = {}\n      const theError = new Error('Expected error')\n\n      const badListener = async () => {\n        throw theError\n      }\n\n      let goodListenerCalled = false\n      const goodListener = async message => {\n        goodListenerCalled = true\n      }\n\n      robot.listen(() => true, null, badListener)\n      robot.listen(() => true, null, goodListener)\n      robot.on('error', (err, response) => {\n        assert.deepEqual(err, theError)\n        assert.deepEqual(response.message, testMessage)\n      })\n      await robot.receive(testMessage)\n      assert.ok(goodListenerCalled)\n    })\n  })\n  describe('#load', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n      process.removeAllListeners()\n    })\n    it('should load scripts in the same order that they are in the folder', async () => {\n      await robot.load(path.resolve('./test/ordered-scripts'))\n      assert.deepEqual(robot.loadedScripts, ['01-First', '02-Second', '03-Third'])\n    })\n  })\n  describe('#loadFile', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n      process.removeAllListeners()\n    })\n    it('should require the specified file', async () => {\n      await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js')\n      assert.deepEqual(robot.hasLoadedTestJsScript, true)\n    })\n\n    it('should load an .mjs file', async () => {\n      await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.mjs')\n      assert.deepEqual(robot.hasLoadedTestMjsScript, true)\n    })\n\n    it('should load an .ts file', async () => {\n      await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.ts')\n      assert.deepEqual(robot.hasLoadedTestTsScript, true)\n    })\n\n    describe('proper script', () => {\n      it('should parse the script documentation', async () => {\n        await robot.loadFile(path.resolve('./test/fixtures'), 'TestScript.js')\n        assert.deepEqual(robot.helpCommands(), ['hubot test - Responds with a test response'])\n      })\n    })\n\n    describe('non-Function script', () => {\n      it('logs a warning for a .js file that does not export the correct API', async () => {\n        let wasCalled = false\n        robot.logger.warn = (...args) => {\n          wasCalled = true\n          assert.ok(args)\n        }\n        await robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.js')\n        assert.deepEqual(wasCalled, true)\n      })\n\n      it('logs a warning for a .mjs file that does not export the correct API', async () => {\n        let wasCalled = false\n        robot.logger.warn = (...args) => {\n          wasCalled = true\n          assert.ok(args)\n        }\n        await robot.loadFile(path.resolve('./test/fixtures'), 'TestScriptIncorrectApi.mjs')\n        assert.deepEqual(wasCalled, true)\n      })\n    })\n\n    describe('unsupported file extension', () => {\n      it('should not be loaded by the Robot', async () => {\n        let wasCalled = false\n        robot.logger.debug = (...args) => {\n          wasCalled = true\n          assert.match(args[0], /unsupported file type/)\n        }\n        await robot.loadFile(path.resolve('./test/fixtures'), 'unsupported.yml')\n        assert.deepEqual(wasCalled, true)\n      })\n    })\n  })\n\n  describe('Sending API', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n\n    it('#send: delegates to adapter \"send\" with proper context', async () => {\n      let wasCalled = false\n      robot.adapter.send = async (envelop, ...strings) => {\n        wasCalled = true\n        assert.deepEqual(strings, ['test message'], 'The strings should be passed through.')\n      }\n      await robot.send({}, 'test message')\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('#reply: delegates to adapter \"reply\" with proper context', async () => {\n      let wasCalled = false\n      robot.adapter.reply = async (envelop, ...strings) => {\n        assert.deepEqual(strings, ['test message'], 'The strings should be passed through.')\n        wasCalled = true\n      }\n      await robot.reply({}, 'test message')\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('#messageRoom: delegates to adapter \"send\" with proper context', async () => {\n      let wasCalled = false\n      robot.adapter.send = async (envelop, ...strings) => {\n        assert.equal(envelop.room, 'testRoom', 'The room should be passed through.')\n        assert.deepEqual(strings, ['messageRoom test'], 'The strings should be passed through.')\n        wasCalled = true\n      }\n      await robot.messageRoom('testRoom', 'messageRoom test')\n      assert.deepEqual(wasCalled, true)\n    })\n  })\n  describe('Listener Registration', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      user = new User('1', { name: 'node', room: '#test' })\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('#listen: forwards the matcher, options, and callback to Listener', () => {\n      const callback = async () => {}\n      const matcher = () => {}\n      const options = {}\n\n      robot.listen(matcher, options, callback)\n      const testListener = robot.listeners[0]\n\n      assert.deepEqual(testListener.matcher, matcher)\n      assert.deepEqual(testListener.callback, callback)\n      assert.deepEqual(testListener.options, options)\n    })\n\n    it('#hear: matches TextMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'message123')\n      const testRegex = /^message123$/\n\n      robot.hear(testRegex, callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.ok(result)\n    })\n\n    it('does not match EnterMessages', () => {\n      const callback = async () => {}\n      const testMessage = new EnterMessage(user)\n      const testRegex = /.*/\n\n      robot.hear(testRegex, callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, undefined)\n    })\n\n    it('#respond: matches TextMessages addressed to the robot', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'TestHubot message123')\n      const testRegex = /message123$/\n\n      robot.respond(testRegex, callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.ok(result)\n    })\n\n    it('does not match EnterMessages', () => {\n      const callback = async () => {}\n      const testMessage = new EnterMessage(user)\n      const testRegex = /.*/\n\n      robot.respond(testRegex, callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, undefined)\n    })\n    it('#enter: matches EnterMessages', () => {\n      const callback = async () => {}\n      const testMessage = new EnterMessage(user)\n\n      robot.enter(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.ok(result)\n    })\n\n    it('does not match TextMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.enter(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, false)\n    })\n\n    it('#leave: matches LeaveMessages', () => {\n      const callback = async () => {}\n      const testMessage = new LeaveMessage(user)\n\n      robot.leave(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.ok(result)\n    })\n\n    it('does not match TextMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.leave(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, false)\n    })\n    it('#topic: matches TopicMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TopicMessage(user)\n\n      robot.topic(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, true)\n    })\n\n    it('does not match TextMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.topic(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, false)\n    })\n\n    it('#catchAll: matches CatchAllMessages', () => {\n      const callback = async () => {}\n      const testMessage = new CatchAllMessage(new TextMessage(user, 'message123'))\n\n      robot.catchAll(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, true)\n    })\n\n    it('does not match TextMessages', () => {\n      const callback = async () => {}\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.catchAll(callback)\n      const testListener = robot.listeners[0]\n      const result = testListener.matcher(testMessage)\n\n      assert.deepEqual(result, false)\n    })\n  })\n  describe('Message Processing', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      user = new User('1', { name: 'node', room: '#test' })\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('calls a matching listener', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n      robot.hear(/^message123$/, async response => {\n        assert.deepEqual(response.message, testMessage)\n      })\n      await robot.receive(testMessage)\n    })\n\n    it('calls multiple matching listeners', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n\n      let listenersCalled = 0\n      const listenerCallback = async response => {\n        assert.deepEqual(response.message, testMessage)\n        listenersCalled++\n      }\n\n      robot.hear(/^message123$/, listenerCallback)\n      robot.hear(/^message123$/, listenerCallback)\n\n      await robot.receive(testMessage)\n      assert.equal(listenersCalled, 2)\n    })\n\n    it('calls the catch-all listener if no listeners match', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n\n      const listenerCallback = async () => {\n        assert.fail('Should not have called listener')\n      }\n      robot.hear(/^no-matches$/, listenerCallback)\n\n      robot.catchAll(async response => {\n        assert.deepEqual(response.message.message, testMessage)\n      })\n\n      await robot.receive(testMessage)\n    })\n\n    it('does not call the catch-all listener if any listener matched', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n      let counter = 0\n      const listenerCallback = async () => {\n        counter++\n      }\n      robot.hear(/^message123$/, listenerCallback)\n\n      const catchAllCallback = async () => {\n        assert.fail('Should not have been called')\n      }\n      robot.catchAll(catchAllCallback)\n\n      await robot.receive(testMessage)\n      assert.equal(counter, 1)\n    })\n\n    it('stops processing if message.finish() is called synchronously', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.hear(/^message123$/, async response => response.message.finish())\n      let wasCalled = false\n      const listenerCallback = async () => {\n        wasCalled = true\n        assert.fail('Should not have been called')\n      }\n      robot.hear(/^message123$/, listenerCallback)\n\n      await robot.receive(testMessage)\n      assert.equal(wasCalled, false)\n    })\n\n    it('calls non-TextListener objects', async () => {\n      const testMessage = new EnterMessage(user)\n\n      robot.enter(async response => {\n        assert.deepEqual(response.message, testMessage)\n      })\n\n      await robot.receive(testMessage)\n    })\n\n    it('gracefully handles hearer uncaughtExceptions (move on to next hearer)', async () => {\n      const testMessage = new TextMessage(user, 'message123')\n      const theError = new Error('Expected error to be thrown')\n\n      robot.hear(/^message123$/, async () => {\n        throw theError\n      })\n\n      let goodListenerCalled = false\n      robot.hear(/^message123$/, async response => {\n        goodListenerCalled = true\n      })\n      robot.on('error', (err, response) => {\n        assert.deepEqual(err, theError)\n        assert.deepEqual(response.message, testMessage)\n      })\n\n      await robot.receive(testMessage)\n      assert.deepEqual(goodListenerCalled, true)\n    })\n  })\n  describe('Listener Middleware', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      user = new User('1', { name: 'node', room: '#test' })\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('allows listener callback execution', async () => {\n      let wasCalled = false\n      const listenerCallback = async () => {\n        wasCalled = true\n      }\n      robot.hear(/^message123$/, listenerCallback)\n      robot.listenerMiddleware(async context => true)\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('can block listener callback execution', async () => {\n      let wasCalled = false\n      const listenerCallback = async () => {\n        wasCalled = true\n        assert.fail('Should not have been called')\n      }\n      robot.hear(/^message123$/, listenerCallback)\n      robot.listenerMiddleware(async context => false)\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, false)\n    })\n\n    it('receives the correct arguments', async () => {\n      robot.hear(/^message123$/, async () => {})\n      const testListener = robot.listeners[0]\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.listenerMiddleware(async context => {\n        // Escape middleware error handling for clearer test failures\n        assert.deepEqual(context.listener, testListener)\n        assert.deepEqual(context.response.message, testMessage)\n        return true\n      })\n\n      await robot.receive(testMessage)\n    })\n\n    it('executes middleware in order of definition', async () => {\n      const execution = []\n\n      const testMiddlewareA = async context => {\n        execution.push('middlewareA')\n      }\n\n      const testMiddlewareB = async context => {\n        execution.push('middlewareB')\n      }\n\n      robot.listenerMiddleware(testMiddlewareA)\n      robot.listenerMiddleware(testMiddlewareB)\n\n      robot.hear(/^message123$/, () => execution.push('listener'))\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      execution.push('done')\n      assert.deepEqual(execution, [\n        'middlewareA',\n        'middlewareB',\n        'listener',\n        'done'\n      ])\n    })\n  })\n  describe('Receive Middleware', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      user = new User('1', { name: 'node', room: '#test' })\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('fires for all messages, including non-matching ones', async () => {\n      let middlewareWasCalled = false\n      const middlewareSpy = async () => {\n        middlewareWasCalled = true\n      }\n      let wasCalled = false\n      const listenerCallback = async () => {\n        wasCalled = true\n        assert.fail('Should not have been called')\n      }\n      robot.hear(/^message123$/, listenerCallback)\n      robot.receiveMiddleware(async context => {\n        middlewareSpy()\n      })\n\n      const testMessage = new TextMessage(user, 'not message 123')\n\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, false)\n      assert.deepEqual(middlewareWasCalled, true)\n    })\n\n    it('can block listener execution', async () => {\n      let middlewareWasCalled = false\n      const middlewareSpy = async () => {\n        middlewareWasCalled = true\n      }\n      let wasCalled = false\n      const listenerCallback = async () => {\n        wasCalled = true\n        assert.fail('Should not have been called')\n      }\n      robot.hear(/^message123$/, listenerCallback)\n      robot.receiveMiddleware(async context => {\n        middlewareSpy()\n        return false\n      })\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, false)\n      assert.deepEqual(middlewareWasCalled, true)\n    })\n\n    it('receives the correct arguments', async () => {\n      robot.hear(/^message123$/, () => {})\n      const testMessage = new TextMessage(user, 'message123')\n\n      robot.receiveMiddleware(async context => {\n        assert.deepEqual(context.response.message, testMessage)\n      })\n\n      await robot.receive(testMessage)\n    })\n\n    it('executes receive middleware in order of definition', async () => {\n      const execution = []\n\n      const testMiddlewareA = async context => {\n        execution.push('middlewareA')\n      }\n\n      const testMiddlewareB = async context => {\n        execution.push('middlewareB')\n      }\n\n      robot.receiveMiddleware(testMiddlewareA)\n      robot.receiveMiddleware(testMiddlewareB)\n      robot.hear(/^message123$/, () => execution.push('listener'))\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      execution.push('done')\n      assert.deepEqual(execution, [\n        'middlewareA',\n        'middlewareB',\n        'listener',\n        'done'\n      ])\n    })\n\n    it('allows editing the message portion of the given response', async () => {\n      const testMiddlewareA = async context => {\n        context.response.message.text = 'foobar'\n      }\n\n      const testMiddlewareB = async context => {\n        assert.equal(context.response.message.text, 'foobar')\n      }\n\n      robot.receiveMiddleware(testMiddlewareA)\n      robot.receiveMiddleware(testMiddlewareB)\n      let wasCalled = false\n      const testCallback = () => {\n        wasCalled = true\n      }\n      // We'll never get to this if testMiddlewareA has not modified the message.\n      robot.hear(/^foobar$/, testCallback)\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, true)\n    })\n  })\n  describe('Response Middleware', () => {\n    let robot = null\n    let user = null\n    beforeEach(async () => {\n      robot = new Robot(mockAdapter, false, 'TestHubot')\n      user = new User('1', { name: 'node', room: '#test' })\n      robot.alias = 'Hubot'\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('executes response middleware in order', async () => {\n      let wasCalled = false\n      robot.adapter.send = async (envelope, ...strings) => {\n        assert.deepEqual(strings, ['replaced bar-foo, sir, replaced bar-foo.'])\n        wasCalled = true\n      }\n      robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.'))\n\n      robot.responseMiddleware(async context => {\n        context.strings[0] = context.strings[0].replace(/foobar/g, 'barfoo')\n      })\n\n      robot.responseMiddleware(async context => {\n        context.strings[0] = context.strings[0].replace(/barfoo/g, 'replaced bar-foo')\n      })\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('allows replacing outgoing strings', async () => {\n      let wasCalled = false\n      robot.adapter.send = async (envelope, ...strings) => {\n        wasCalled = true\n        assert.deepEqual(strings, ['whatever I want.'])\n      }\n      robot.hear(/^message123$/, async response => response.send('foobar, sir, foobar.'))\n\n      robot.responseMiddleware(async context => {\n        context.strings = ['whatever I want.']\n      })\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('marks plaintext as plaintext', async () => {\n      robot.adapter.send = async (envelope, ...strings) => {\n        assert.deepEqual(strings, ['foobar, sir, foobar.'])\n      }\n      robot.adapter.play = async (envelope, ...strings) => {\n        assert.deepEqual(strings, ['good luck with that'])\n      }\n\n      robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.'))\n      robot.hear(/^message456$/, async response => await response.play('good luck with that'))\n\n      let method\n      let plaintext\n      robot.responseMiddleware(async context => {\n        method = context.method\n        plaintext = context.plaintext\n      })\n\n      const testMessage = new TextMessage(user, 'message123')\n\n      await robot.receive(testMessage)\n      assert.deepEqual(plaintext, true)\n      assert.equal(method, 'send')\n      const testMessage2 = new TextMessage(user, 'message456')\n      await robot.receive(testMessage2)\n      assert.deepEqual(plaintext, undefined)\n      assert.equal(method, 'play')\n    })\n\n    it('does not send trailing functions to middleware', async () => {\n      let wasCalled = false\n      robot.adapter.send = async (envelope, ...strings) => {\n        wasCalled = true\n        assert.deepEqual(strings, ['foobar, sir, foobar.'])\n      }\n\n      let asserted = false\n      robot.hear(/^message123$/, async response => await response.send('foobar, sir, foobar.'))\n\n      robot.responseMiddleware(async context => {\n        // We don't send the callback function to middleware, so it's not here.\n        assert.deepEqual(context.strings, ['foobar, sir, foobar.'])\n        assert.equal(context.method, 'send')\n        asserted = true\n      })\n\n      const testMessage = new TextMessage(user, 'message123')\n      await robot.receive(testMessage)\n      assert.deepEqual(asserted, true)\n      assert.deepEqual(wasCalled, true)\n    })\n  })\n  describe('Robot ES6', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot('MockAdapter', false, 'TestHubot')\n      robot.alias = 'Hubot'\n      await robot.loadAdapter('./test/fixtures/MockAdapter.mjs')\n      await robot.loadFile(path.resolve('./test/fixtures/'), 'TestScript.js')\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n    })\n    it('should load an ES6 module adapter from a file', async () => {\n      const { MockAdapter } = await import('./fixtures/MockAdapter.mjs')\n      assert.ok(robot.adapter instanceof MockAdapter)\n      assert.equal(robot.adapter.name, 'MockAdapter')\n    })\n    it('should respond to a message', async () => {\n      const sent = async (envelop, strings) => {\n        assert.deepEqual(strings, ['test response'])\n      }\n      robot.adapter.on('send', sent)\n      await robot.receive(new TextMessage('tester', 'hubot test'))\n    })\n  })\n  describe('Robot Defaults', () => {\n    let robot = null\n    beforeEach(async () => {\n      robot = new Robot(null, false, 'TestHubot')\n      robot.alias = 'Hubot'\n      await robot.loadAdapter()\n      await robot.run()\n    })\n    afterEach(() => {\n      robot.shutdown()\n      process.removeAllListeners()\n    })\n    it('should load the builtin Shell adapter by default', async () => {\n      assert.equal(robot.adapter.name, 'Shell')\n    })\n  })\n  describe('Robot HTTP Service', () => {\n    it('should start a web service', async () => {\n      process.env.PORT = 0\n      const robot = new Robot(mockAdapter, true, 'TestHubot')\n      await robot.loadAdapter()\n      await robot.run()\n      const port = robot.server.address().port\n      const res = await fetch(`http://127.0.0.1:${port}/hubot/version`)\n      assert.equal(res.status, 404)\n      assert.match(await res.text(), /Cannot GET \\/hubot\\/version/ig)\n      robot.shutdown()\n      delete process.env.PORT\n    })\n  })\n})\n"
  },
  {
    "path": "test/Shell_test.mjs",
    "content": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { Robot, TextMessage, User } from '../index.mjs'\nimport stream from 'node:stream'\nimport { writeFile, stat } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport path from 'node:path'\n\ndescribe('Shell history file test', () => {\n  it('History file is > 1024 bytes when running does not throw an error', async () => {\n    const robot = new Robot('Shell', false, 'TestHubot')\n    robot.stdin = new stream.Readable()\n    robot.stdin._read = () => {}\n    const __filename = fileURLToPath(import.meta.url)\n    const __dirname = path.dirname(__filename)\n    const historyPath = path.join(__dirname, '..', '.hubot_history')\n    await writeFile(historyPath, 'a'.repeat(1025))\n    await robot.loadAdapter()\n    await robot.run()\n    try {\n      const fileInfo = await stat(historyPath)\n      assert.ok(fileInfo.size <= 1024, 'History file should be less than or equal to 1024 bytes after running the robot')\n    } catch (error) {\n      assert.fail('Should not throw an error when reading history file')\n    } finally {\n      robot.shutdown()\n    }\n  })\n})\n\ndescribe('Shell Adapter Integration Test', () => {\n  let robot = null\n  beforeEach(async () => {\n    robot = new Robot('Shell', false, 'TestHubot')\n    robot.stdin = new stream.Readable()\n    robot.stdin._read = () => {}\n    await robot.loadAdapter()\n    await robot.run()\n  })\n  afterEach(() => {\n    robot.shutdown()\n  })\n  it('responds to a message that starts with the robot name', async () => {\n    let wasCalled = false\n    robot.respond(/helo/, async res => {\n      wasCalled = true\n      await res.reply('hello from the other side')\n    })\n    robot.stdin.push(robot.name + ' helo\\n')\n    robot.stdin.push(null)\n    await new Promise(resolve => setTimeout(resolve, 60))\n    assert.deepEqual(wasCalled, true)\n  })\n  it('responds to a message without starting with the robot name', async () => {\n    let wasCalled = false\n    robot.respond(/helo/, async res => {\n      wasCalled = true\n      await res.reply('hello from the other side')\n    })\n    robot.stdin.push('helo\\n')\n    robot.stdin.push(null)\n    await new Promise(resolve => setTimeout(resolve, 60))\n    assert.deepEqual(wasCalled, true)\n  })\n  it('shows prompt if nothing was entered', async () => {\n    let wasCalled = false\n    robot.respond(/\\n/, async res => {\n      wasCalled = true\n      await res.reply('hello from the other side')\n    })\n    robot.stdin.push('\\n')\n    robot.stdin.push(null)\n    await new Promise(resolve => setTimeout(resolve, 60))\n    assert.deepEqual(wasCalled, false)\n  })\n  it('shows prompt if only spaces were entered', async () => {\n    let wasCalled = false\n    robot.respond(/.*/, async res => {\n      wasCalled = true\n      await res.reply('hello from the other side')\n    })\n    robot.stdin.push('   \\n')\n    robot.stdin.push(null)\n    await new Promise(resolve => setTimeout(resolve, 60))\n    assert.deepEqual(wasCalled, false)\n  })\n  it('shows prompt if only tabs were entered', async () => {\n    let wasCalled = false\n    robot.respond(/.*/, async res => {\n      wasCalled = true\n      await res.reply('hello from the other side')\n    })\n    robot.stdin.push('\\t\\t\\n')\n    robot.stdin.push(null)\n    await new Promise(resolve => setTimeout(resolve, 60))\n    assert.deepEqual(wasCalled, false)\n  })\n})\n\ndescribe('Shell Adapter', () => {\n  let robot = null\n  beforeEach(async () => {\n    robot = new Robot('Shell', false, 'TestHubot')\n    robot.stdin = new stream.Readable()\n    robot.stdin._read = () => {}\n    await robot.loadAdapter()\n    await robot.run()\n  })\n  afterEach(() => {\n    robot.shutdown()\n  })\n\n  describe('Public API', () => {\n    let adapter = null\n    beforeEach(() => {\n      adapter = robot.adapter\n    })\n\n    it('assigns robot', () => {\n      assert.deepEqual(adapter.robot, robot, 'The adapter should have a reference to the robot.')\n    })\n\n    it('sends a message', async () => {\n      const old = console.log\n      let wasCalled = false\n      console.log = (...args) => {\n        console.log = old\n        assert.deepEqual(args[0], '\\x1b[1mhello\\x1b[22m', 'Message should be outputed as bold to the console.')\n        wasCalled = true\n      }\n      await adapter.send({ room: 'general' }, 'hello')\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('emotes a message', async () => {\n      const old = console.log\n      let wasCalled = false\n      console.log = (...args) => {\n        console.log = old\n        assert.deepEqual(args[0], '\\x1b[1m* hello\\x1b[22m', 'Message should be bold and have an * in front.')\n        wasCalled = true\n      }\n      await adapter.emote({ room: 'general' }, 'hello')\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('replies to a message', async () => {\n      const old = console.log\n      let wasCalled = false\n      console.log = (...args) => {\n        console.log = old\n        assert.deepEqual(args[0], '\\x1b[1mnode: hello\\x1b[22m', 'The strings should be passed through.')\n        wasCalled = true\n      }\n      await adapter.reply({ room: 'general', user: { name: 'node' } }, 'hello')\n      assert.deepEqual(wasCalled, true)\n    })\n\n    it('runs the adapter and emits connected', async () => {\n      let wasCalled = false\n      const connected = () => {\n        adapter.off('connected', connected)\n        assert.ok(true, 'The connected event should be emitted.')\n        wasCalled = true\n      }\n      adapter.on('connected', connected)\n      await adapter.run()\n      assert.deepEqual(wasCalled, true)\n      robot.shutdown()\n    })\n\n    it('dispatches received messages to the robot', async () => {\n      const message = new TextMessage(new User('node'), 'hello', 1)\n      let wasCalled = false\n      robot.receive = (msg) => {\n        assert.deepEqual(msg, message, 'The message should be passed through.')\n        wasCalled = true\n      }\n      await adapter.receive(message)\n      assert.deepEqual(wasCalled, true)\n    })\n  })\n})\n\ndescribe('Shell Adapter: Print human readable logging in the console when something is logged with robot.logger', async () => {\n  it('setting HUBOT_LOG_LEVEL to debug prints debug and info log messages to the console', async () => {\n    process.env.HUBOT_LOG_LEVEL = 'debug'\n    const robot = new Robot('Shell', false, 'TestHubot')\n    await robot.loadAdapter()\n    await robot.run()\n\n    const old = console.log\n    const expected = {\n      debug: false,\n      info: false\n    }\n    console.log = (...args) => {\n      old(...args)\n      switch (true) {\n        case args[0].includes('[debug]'):\n          expected.debug = true\n          break\n        case args[0].includes('[info]'):\n          expected.info = true\n          break\n      }\n    }\n    robot.logger.debug('should print debug message to console')\n    robot.logger.info('should print info message to console')\n    delete process.env.HUBOT_LOG_LEVEL\n    console.log = old\n    assert.deepEqual(expected, { debug: true, info: true })\n    robot.shutdown()\n  })\n\n  it('setting HUBOT_LOG_LEVEL to error only prints error log messages to the console', async () => {\n    process.env.HUBOT_LOG_LEVEL = 'error'\n    const robot = new Robot('Shell', false, 'TestHubot')\n    await robot.loadAdapter()\n    await robot.run()\n\n    const old = console.log\n    const expected = {\n      debug: false,\n      info: false,\n      error: false\n    }\n    console.log = (...args) => {\n      old(...args)\n      switch (true) {\n        case args[0].includes('[debug]'):\n          expected.debug = true\n          break\n        case args[0].includes('[info]'):\n          expected.info = true\n          break\n        case args[0].includes('[error]'):\n          expected.error = true\n          break\n      }\n    }\n    robot.logger.debug('should NOT print debug message to console')\n    robot.logger.info('should NOT print info message to console')\n    robot.logger.error('should print error message to console')\n    delete process.env.HUBOT_LOG_LEVEL\n    console.log = old\n    assert.deepEqual(expected, { debug: false, info: false, error: true })\n    robot.shutdown()\n  })\n})\n\ndescribe('Shell Adapter: Logger before adapter run', () => {\n  it('does not throw when logging before the adapter initializes readline', async () => {\n    const robot = new Robot('Shell', false, 'TestHubot')\n    robot.stdin = new stream.Readable()\n    robot.stdin._read = () => {}\n\n    const originalLog = console.log\n    const logMessages = []\n    console.log = (...args) => {\n      logMessages.push(args[0])\n    }\n\n    try {\n      await assert.doesNotReject(async () => {\n        // Before loadAdapter - uses default pino logger\n        robot.logger.info('log before adapter load')\n\n        await robot.loadAdapter()\n\n        // After loadAdapter but before run - still uses pino logger (logger override happens in run())\n        await robot.logger.info('log after load before run')\n\n        await robot.run()\n\n        // After run - uses Shell adapter's custom logger with formatted output\n        await robot.logger.info('log after run')\n      })\n\n      // Verify that logging after run() uses the Shell adapter's custom formatted logger\n      assert.ok(logMessages.some(msg => typeof msg === 'string' && msg.includes('[info]') && msg.includes('log after run')),\n        'Should use Shell adapter formatted logger after run()')\n    } finally {\n      console.log = originalLog\n      robot.shutdown()\n    }\n  })\n})\n"
  },
  {
    "path": "test/User_test.mjs",
    "content": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { User } from '../index.mjs'\n\ndescribe('User', () =>\n  describe('new', function () {\n    it('uses id as the default name', function () {\n      const user = new User('hubot')\n\n      assert.equal(user.name, 'hubot', 'User constructor should set name')\n    })\n\n    it('sets attributes passed in', function () {\n      const user = new User('hubot', { foo: 1, bar: 2 })\n\n      assert.equal(user.foo, 1, 'Passing an object with attributes in the User constructor should set those attributes on the instance.')\n      assert.equal(user.bar, 2, 'Passing an object with attributes in the User constructor should set those attributes on the instance.')\n    })\n\n    it('uses name attribute when passed in, not id', function () {\n      const user = new User('hubot', { name: 'tobuh' })\n\n      assert.equal(user.name, 'tobuh', 'Passing a name attribute in the User constructor should set the name attribute on the instance.')\n    })\n  })\n)\n"
  },
  {
    "path": "test/XampleTest.mjs",
    "content": "import { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\n\n// Replace this with import { Robot } from 'hubot'\nimport { Robot } from '../index.mjs'\n\n// You need a dummy adapter to test scripts\nimport dummyRobot from './doubles/DummyAdapter.mjs'\n\n// Mocks Aren't Stubs\n// https://www.martinfowler.com/articles/mocksArentStubs.html\n\ndescribe('Xample testing Hubot scripts', () => {\n  let robot = null\n  beforeEach(async () => {\n    process.env.EXPRESS_PORT = 0\n    robot = new Robot(dummyRobot, true, 'Dumbotheelephant')\n    await robot.loadAdapter()\n    await robot.run()\n    await robot.loadFile('./test/scripts', 'Xample.mjs')\n  })\n  afterEach(() => {\n    delete process.env.EXPRESS_PORT\n    robot.shutdown()\n  })\n  it('should handle /helo request', async () => {\n    const expected = \"HELO World! I'm Dumbotheelephant.\"\n    const response = await fetch(`http://localhost:${robot.server.address().port}/helo`)\n    const actual = await response.text()\n    assert.strictEqual(actual, expected)\n  })\n  it('should reply with expected message', async () => {\n    const expected = 'HELO World! I\\'m Dumbotheelephant.'\n    const user = robot.brain.userForId('test-user', { name: 'test user' })\n    let actual = ''\n    robot.on('reply', (envelope, ...strings) => {\n      actual = strings.join('')\n    })\n    await robot.adapter.say(user, '@Dumbotheelephant helo', 'test-room')\n    assert.strictEqual(actual, expected)\n  })\n\n  it('should send message to the #general room', async () => {\n    const expected = 'general'\n    const user = robot.brain.userForId('test-user', { name: 'test user' })\n    let actual = ''\n    robot.on('send', (envelope, ...strings) => {\n      actual = envelope.room\n    })\n    await robot.adapter.say(user, '@Dumbotheelephant helo room', 'general')\n    assert.strictEqual(actual, expected)\n  })\n})\n"
  },
  {
    "path": "test/doubles/DummyAdapter.mjs",
    "content": "'use strict'\n// Replace this with import { Adapter, TextMessage } from 'hubot'\nimport { Adapter, TextMessage } from '../../index.mjs'\n\nexport class DummyAdapter extends Adapter {\n  constructor (robot) {\n    super(robot)\n    this.name = 'DummyAdapter'\n    this.messages = new Set()\n  }\n\n  async send (envelope, ...strings) {\n    this.emit('send', envelope, ...strings)\n    this.robot.emit('send', envelope, ...strings)\n  }\n\n  async reply (envelope, ...strings) {\n    this.emit('reply', envelope, ...strings)\n    this.robot.emit('reply', envelope, ...strings)\n  }\n\n  async topic (envelope, ...strings) {\n    this.emit('topic', envelope, ...strings)\n    this.robot.emit('topic', envelope, ...strings)\n  }\n\n  async play (envelope, ...strings) {\n    this.emit('play', envelope, ...strings)\n    this.robot.emit('play', envelope, ...strings)\n  }\n\n  run () {\n    // This is required to get the scripts loaded\n    this.emit('connected')\n  }\n\n  close () {\n    this.emit('closed')\n  }\n\n  async say (user, message, room) {\n    this.messages.add(message)\n    user.room = room\n    await this.robot.receive(new TextMessage(user, message))\n  }\n}\nexport default {\n  async use (robot) {\n    return new DummyAdapter(robot)\n  }\n}\n"
  },
  {
    "path": "test/fixtures/MockAdapter.mjs",
    "content": "'use strict'\n\nimport { Adapter } from '../../index.mjs'\n\nexport class MockAdapter extends Adapter {\n  constructor (robot) {\n    super(robot)\n    this.name = 'MockAdapter'\n  }\n\n  async send (envelope, ...strings) {\n    this.emit('send', envelope, ...strings)\n  }\n\n  async reply (envelope, ...strings) {\n    this.emit('reply', envelope, ...strings)\n  }\n\n  async topic (envelope, ...strings) {\n    this.emit('topic', envelope, ...strings)\n  }\n\n  async play (envelope, ...strings) {\n    this.emit('play', envelope, ...strings)\n  }\n\n  run () {\n    // This is required to get the scripts loaded\n    this.emit('connected')\n  }\n\n  close () {\n    this.emit('closed')\n  }\n}\nexport default {\n  use (robot) {\n    return new MockAdapter(robot)\n  }\n}\n"
  },
  {
    "path": "test/fixtures/TestScript.js",
    "content": "'use strict'\n\n// Description: A test script for the robot to load\n//\n// Commands:\n//   hubot test - Responds with a test response\n//\nmodule.exports = robot => {\n  robot.hasLoadedTestJsScript = true\n  robot.respond('test', async res => {\n    await res.send('test response')\n  })\n}\n"
  },
  {
    "path": "test/fixtures/TestScript.mjs",
    "content": "'use strict'\n\n// Description: A test .mjs script for the robot to load\n//\n// Commands:\n//   hubot test mjs - Responds with a test response from a .mjs script\n//\n\nexport default robot => {\n  robot.hasLoadedTestMjsScript = true\n  robot.respond(/test$/, async res => {\n    await res.reply('test response from .mjs script')\n  })\n}\n"
  },
  {
    "path": "test/fixtures/TestScript.ts",
    "content": "'use strict'\n\n// Description: A test .ts script for the robot to load\n//\n// Commands:\n//   hubot test ts - Responds with a test response from a .ts script\n//\n\nexport default robot => {\n  robot.hasLoadedTestTsScript = true\n  robot.respond(/test$/, async res => {\n    await res.reply('test response from .ts script')\n  })\n}\n"
  },
  {
    "path": "test/fixtures/TestScriptIncorrectApi.js",
    "content": "'use strict'\n\n// Description: A test script for the robot to load\n//\n// Commands:\n//   hubot test - Responds with a test response\n//\n\nmodule.exports = {}\n"
  },
  {
    "path": "test/fixtures/TestScriptIncorrectApi.mjs",
    "content": "'use strict'\n\n// Description: A test .mjs script for the robot to load\n//\n// Commands:\n//   hubot test mjs - Responds with a test response from a .mjs script\n//\n\nexport default {}\n"
  },
  {
    "path": "test/index_test.mjs",
    "content": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport {\n  Adapter, User, Brain, Robot, Response, Listener, TextListener,\n  Message, TextMessage, EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage, loadBot\n} from '../index.mjs'\nimport mockAdapter from './fixtures/MockAdapter.mjs'\n\ndescribe('hubot/index', () => {\n  it('exports User class', () => {\n    class MyUser extends User {}\n    const user = new MyUser('id123', { foo: 'bar' })\n\n    assert.ok(user instanceof User)\n    assert.equal(user.id, 'id123')\n    assert.equal(user.foo, 'bar')\n  })\n\n  it('exports Brain class', () => {\n    class MyBrain extends Brain {}\n    const robotMock = {\n      on () {\n        assert.ok(true)\n      }\n    }\n    const brain = new MyBrain(robotMock)\n\n    assert.ok(brain instanceof Brain)\n    brain.set('foo', 'bar')\n    assert.equal(brain.get('foo'), 'bar')\n  })\n\n  it('exports Robot class', async () => {\n    class MyRobot extends Robot {}\n    const robot = new MyRobot(mockAdapter, false, 'TestHubot')\n    await robot.loadAdapter()\n    assert.ok(robot instanceof Robot)\n    assert.equal(robot.name, 'TestHubot')\n    robot.shutdown()\n  })\n\n  it('exports Adapter class', () => {\n    class MyAdapter extends Adapter {}\n    const adapter = new MyAdapter('myrobot')\n\n    assert.ok(adapter instanceof Adapter)\n    assert.equal(adapter.robot, 'myrobot')\n  })\n\n  it('exports Response class', () => {\n    class MyResponse extends Response {}\n    const robotMock = 'robotMock'\n    const messageMock = {\n      room: 'room',\n      user: 'user'\n    }\n    const matchMock = 'matchMock'\n    const response = new MyResponse(robotMock, messageMock, matchMock)\n\n    assert.ok(response instanceof Response)\n    assert.deepEqual(response.message, messageMock)\n    assert.equal(response.match, matchMock)\n  })\n\n  it('exports Listener class', () => {\n    class MyListener extends Listener {}\n    const robotMock = 'robotMock'\n    const matcherMock = 'matchMock'\n    const callback = () => {}\n    const listener = new MyListener(robotMock, matcherMock, callback)\n\n    assert.ok(listener instanceof Listener)\n    assert.deepEqual(listener.robot, robotMock)\n    assert.equal(listener.matcher, matcherMock)\n    assert.equal(listener.options.id, null)\n    assert.deepEqual(listener.callback, callback)\n  })\n\n  it('exports TextListener class', () => {\n    class MyTextListener extends TextListener {}\n    const robotMock = 'robotMock'\n    const regex = /regex/\n    const callback = () => {}\n    const textListener = new MyTextListener(robotMock, regex, callback)\n\n    assert.ok(textListener instanceof TextListener)\n    assert.deepEqual(textListener.regex, regex)\n  })\n\n  it('exports Message class', () => {\n    class MyMessage extends Message {}\n    const userMock = {\n      room: 'room'\n    }\n    const message = new MyMessage(userMock)\n\n    assert.ok(message instanceof Message)\n    assert.deepEqual(message.user, userMock)\n  })\n\n  it('exports TextMessage class', () => {\n    class MyTextMessage extends TextMessage {}\n    const userMock = {\n      room: 'room'\n    }\n    const textMessage = new MyTextMessage(userMock, 'bla blah')\n\n    assert.ok(textMessage instanceof TextMessage)\n    assert.ok(textMessage instanceof Message)\n    assert.equal(textMessage.text, 'bla blah')\n  })\n\n  it('exports EnterMessage class', () => {\n    class MyEnterMessage extends EnterMessage {}\n    const userMock = {\n      room: 'room'\n    }\n    const enterMessage = new MyEnterMessage(userMock)\n\n    assert.ok(enterMessage instanceof EnterMessage)\n    assert.ok(enterMessage instanceof Message)\n  })\n\n  it('exports LeaveMessage class', () => {\n    class MyLeaveMessage extends LeaveMessage {}\n    const userMock = {\n      room: 'room'\n    }\n    const leaveMessage = new MyLeaveMessage(userMock)\n\n    assert.ok(leaveMessage instanceof LeaveMessage)\n    assert.ok(leaveMessage instanceof Message)\n  })\n\n  it('exports TopicMessage class', () => {\n    class MyTopicMessage extends TopicMessage {}\n    const userMock = {\n      room: 'room'\n    }\n    const topicMessage = new MyTopicMessage(userMock)\n\n    assert.ok(topicMessage instanceof TopicMessage)\n    assert.ok(topicMessage instanceof Message)\n  })\n\n  it('exports CatchAllMessage class', () => {\n    class MyCatchAllMessage extends CatchAllMessage {}\n    const messageMock = {\n      user: {\n        room: 'room'\n      }\n    }\n    const catchAllMessage = new MyCatchAllMessage(messageMock)\n\n    assert.ok(catchAllMessage instanceof CatchAllMessage)\n    assert.ok(catchAllMessage instanceof Message)\n    assert.deepEqual(catchAllMessage.message, messageMock)\n    assert.deepEqual(catchAllMessage.user, messageMock.user)\n  })\n\n  it('exports loadBot function', () => {\n    assert.ok(loadBot && typeof loadBot === 'function')\n    const robot = loadBot('adapter', false, 'botName', 'botAlias')\n    assert.equal(robot.name, 'botName')\n    assert.equal(robot.alias, 'botAlias')\n    robot.shutdown()\n  })\n})\n"
  },
  {
    "path": "test/ordered-scripts/01-PFirst.mjs",
    "content": "// Description:\n//   First one\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot => {\n  robot.loadedScripts = []\n  robot.loadedScripts.push('01-First')\n}\n"
  },
  {
    "path": "test/ordered-scripts/02-SetupBotConfig.mjs",
    "content": "// Description:\n//   Setup bot configuration\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot => {\n  robot.loadedScripts.push('02-Second')\n}\n"
  },
  {
    "path": "test/ordered-scripts/WebSetup.mjs",
    "content": "// Description:\n//   Third one\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot => {\n  robot.loadedScripts.push('03-Third')\n}\n"
  },
  {
    "path": "test/scripts/Xample.mjs",
    "content": "// Description:\n//   Test script\n//\n// Commands:\n//   hubot helo - Responds with HELO World!.\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default (robot) => {\n  robot.respond(/helo$/, async res => {\n    await res.reply(`HELO World! I'm ${robot.name}.`)\n  })\n  robot.respond(/helo (.*)/gi, async res => {\n    await res.send(`Hello World! I'm ${robot.name}.`)\n  })\n  robot.router.get('/helo', async (req, res) => {\n    res.send(`HELO World! I'm ${robot.name}.`)\n  })\n}\n"
  }
]