Full Code of hubotio/hubot for AI

main 9e76a4f9bdc8 cached
96 files
405.7 KB
102.8k tokens
273 symbols
1 requests
Download .txt
Showing preview only (430K chars total). Download the full file or copy to clipboard to get everything.
Repository: hubotio/hubot
Branch: main
Commit: 9e76a4f9bdc8
Files: 96
Total size: 405.7 KB

Directory structure:
gitextract_19fif895/

├── .editorconfig
├── .github/
│   ├── stale.yml
│   └── workflows/
│       ├── nodejs-macos.yml
│       ├── nodejs-ubuntu.yml
│       ├── nodejs-windows.yml
│       ├── pipeline.yml
│       └── sfab-gh-pages.yml
├── .gitignore
├── .npmignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── bin/
│   ├── Hubot.mjs
│   ├── e2e-test.sh
│   └── hubot
├── configuration/
│   └── Config.mjs
├── docs/
│   ├── adapters/
│   │   ├── campfire.md
│   │   ├── development.md
│   │   └── shell.md
│   ├── adapters.md
│   ├── assets/
│   │   ├── fonts/
│   │   │   └── otf/
│   │   │       ├── octicons-regular-webfont.otf
│   │   │       ├── style_154042.otf
│   │   │       ├── style_154045.otf
│   │   │       ├── style_154046.otf
│   │   │       ├── style_154048.otf
│   │   │       ├── style_154051.otf
│   │   │       └── style_154053.otf
│   │   └── stylesheets/
│   │       └── application.css
│   ├── deploying/
│   │   ├── azure.md
│   │   ├── bluemix.md
│   │   ├── unix.md
│   │   └── windows.md
│   ├── deploying.md
│   ├── designs/
│   │   └── commands.md
│   ├── docs.md
│   ├── implementation.md
│   ├── index.html
│   ├── layouts/
│   │   ├── docs.html
│   │   └── main.html
│   ├── patterns.md
│   └── scripting.md
├── examples/
│   ├── hubot-start.ps1
│   └── hubot.service
├── index.mjs
├── package.json
├── script/
│   ├── bootstrap
│   ├── release
│   ├── server
│   ├── simple-lint.mjs
│   ├── smoke-test
│   └── test
├── sfab-hooks/
│   └── SfabHook.mjs
├── src/
│   ├── Adapter.mjs
│   ├── Brain.mjs
│   ├── CommandBus.mjs
│   ├── DataStore.mjs
│   ├── GenHubot.mjs
│   ├── HttpClient.mjs
│   ├── Listener.mjs
│   ├── Message.mjs
│   ├── Middleware.mjs
│   ├── OptParse.mjs
│   ├── Response.mjs
│   ├── Robot.mjs
│   ├── User.mjs
│   ├── adapters/
│   │   ├── Campfire.mjs
│   │   └── Shell.mjs
│   └── datastores/
│       └── Memory.mjs
└── test/
    ├── AdapterName_test.mjs
    ├── Adapter_test.mjs
    ├── Brain_test.mjs
    ├── CommandBus_test.mjs
    ├── Configuration_test.mjs
    ├── DataStore_test.mjs
    ├── Hubot_test.mjs
    ├── Listener_test.mjs
    ├── Message_test.mjs
    ├── Middleware_test.mjs
    ├── OptParse-test.mjs
    ├── Robot_test.mjs
    ├── Shell_test.mjs
    ├── User_test.mjs
    ├── XampleTest.mjs
    ├── doubles/
    │   └── DummyAdapter.mjs
    ├── fixtures/
    │   ├── MockAdapter.mjs
    │   ├── TestScript.js
    │   ├── TestScript.mjs
    │   ├── TestScript.ts
    │   ├── TestScriptIncorrectApi.js
    │   └── TestScriptIncorrectApi.mjs
    ├── index_test.mjs
    ├── ordered-scripts/
    │   ├── 01-PFirst.mjs
    │   ├── 02-SetupBotConfig.mjs
    │   └── WebSetup.mjs
    └── scripts/
        └── Xample.mjs

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: http://EditorConfig.org

# Top-most EditorConfig file
root = true

# Match and apply these rules for all file
# types you open in your code editor
[*]
# Unix-style newlines
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true


================================================
FILE: .github/stale.yml
================================================
# Configuration for probot-stale - https://github.com/probot/stale

# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before a stale Issue or Pull Request is closed
daysUntilClose: 7
# Issues or Pull Requests with these labels will never be considered stale
exemptLabels:
  - pinned
  - security
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
  This issue has been automatically marked as stale because it has [not had
  recent activity](https://github.com/github/hubot/blob/main/CONTRIBUTING.md#stale-issue-and-pull-request-policy).
  It will be closed if no further activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label. Set to `false` to disable
unmarkComment: false
# Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable
closeComment: false
# Limit to only `issues` or `pulls`
# only: issues


================================================
FILE: .github/workflows/nodejs-macos.yml
================================================
name: Node.js (macOS) CI
permissions:
  contents: read
  issues: read

on:
  push:
    branches: [ "main" ]
  schedule:
    - cron:  '5 4 * * 0'

jobs:
  npm-test:

    runs-on: macos-latest

    strategy:
      matrix:
        node-version: [23.x]

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm test --experimental-strip-types


================================================
FILE: .github/workflows/nodejs-ubuntu.yml
================================================
name: Node.js (Ubuntu) CI
permissions:
  contents: read
  issues: read

on:
  push:
    branches: [ "main" ]
  schedule:
    - cron:  '5 4 * * 0'

jobs:
  npm-test:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [23.x, latest]

    steps:
    - uses: actions/checkout@v4
    - name: Install expect
      run: sudo apt-get install expect
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm test --experimental-strip-types
    - run: npm run test:e2e


================================================
FILE: .github/workflows/nodejs-windows.yml
================================================
name: Node.js (Windows) CI
permissions:
  contents: read
  issues: read
on:
  push:
    branches:
      - main
  schedule:
    - cron: '5 4 * * 0'
jobs:
  npm-test:
    runs-on: windows-latest
    strategy:
      matrix:
        node-version: [23.x]
    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - name: Run Tests
      env:
        HUBOT_LOG_LEVEL: debug
      run: npm test --experimental-strip-types


================================================
FILE: .github/workflows/pipeline.yml
================================================
name: Build and release pipeline
permissions:
  contents: write
  issues: write
  pull-requests: write
  id-token: write
on:
  push:
    branches:
      - main
      - next
  pull_request:
    branches:
      - main
      - next
jobs:
  build:
    name: Build and Verify
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version:
          - 20.x
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
        run: npm audit signatures
  test:
    name: Tests
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        node-version:
          - 24.x
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --experimental-strip-types
  e2etest:
    name: E2E Test
    needs:
      - build
      - test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version:
          - 20.x
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install expect
        run: sudo apt-get install expect
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run test:e2e
  release:
    name: Release
    if: github.ref == 'refs/heads/main' && success()
    needs: [build, test, e2etest]
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version:
          - 22.14.0
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - name: Semantic Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release


================================================
FILE: .github/workflows/sfab-gh-pages.yml
================================================
# Sample workflow for building and deploying a sfab site to GitHub Pages
name: Deploy sfab with GitHub Pages dependencies preinstalled

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Pages
        uses: actions/configure-pages@v4
      - name: Use Node.js and Build with sfab
        uses: actions/setup-node@v4
        with:
          node-version: latest
      - run: npm run build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .gitignore
================================================
node_modules
.hubot_history
.node-version
.nyc_output/
npm-debug.log
coverage/
_site
.env
users.md
.data

================================================
FILE: .npmignore
================================================
.editorconfig
.github
.hubot_history
bin/e2e-test.sh
test
docs
examples
script
www
_site
*.tgz
configuration


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In 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.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project 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.

Project 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.

## Scope

This 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.

## Enforcement

Instances 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.

Project 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.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Contributions 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).

Everyone 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.

No 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.

We love pull requests. Here's a quick guide:

1. If you're adding a new feature or changing user-facing APIs, check out the [Hubot Evolution](https://github.com/hubotio/evolution) process.
1. 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)
1. Fork the repo, and clone it locally
1. `npm link` to make your cloned repo available to npm
1. Follow [Getting Started](docs/index.md) to generate a testbot
1. `npm link hubot` in your newly created bot to use your hubot fork
1. Create a new branch for your contribution
1. Add [tests](test/) (run with `npm test`)
1. Push to your fork and submit a pull request

At this point you're waiting on us. We like to at least comment on, if not
accept, pull requests within a few days. We may suggest some changes or improvements or alternatives.

Some things that will increase the chance that your pull request is accepted:

* Make sure the tests pass
* Update the documentation: code comments, example code, guides. Basically,
  update everything affected by your contribution.
* Include any information that would be relevant to reproducing bugs, use cases for new features, etc.

* 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)
  * 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?
* Your commits are associated with your GitHub user: https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/
* Make pull requests against a feature branch,
* Follow our commit message conventions:
  * use imperative, present tense: “change” not “changed” nor “changes”
  * Commit test files with `test: …` or `test(scope): …` prefix.
  * Commit bug fixes with `fix: …` or `fix(scope): …` prefix
  * Commit features with `feat: …` or `feat(scope): …` prefix
  * Commit breaking changes by adding `BREAKING CHANGE:` in the commit body.
    The commit subject does not matter. A commit can have multiple `BREAKING CHANGE:`
    sections
  * Commit changes to `package.json`, `.gitignore` and other meta files with
  `chore(filenamewithout.ext): …`
  * Commit changes to README files or comments with `docs(README): …`
  * Cody style changes with `style: standard`
  * see [Angular’s Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)
    for a full list of recommendations.

# Stale issue and pull request policy

Issues 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.

The 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.

# Pull Request Reviews & releasing

Releasing `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

* `fix: …` will bump the fix version, e.g. 1.2.3 → 1.2.4
* `feat: …` will bump the feature version, e.g. 1.2.3 → 1.3.0
* `BREAKING CHANGE: …` in the commit body will bump the breaking change version, e.g. 1.2.3 → 2.0.0


================================================
FILE: LICENSE.md
================================================
Copyright (c) 2011-2024 GitHub Inc.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
![Pipeline Status](https://github.com/hubotio/hubot/actions/workflows/pipeline.yml/badge.svg)

![Build Status: MacOS](https://github.com/hubotio/hubot/actions/workflows/nodejs-macos.yml/badge.svg)
![Build Status: Ubuntu](https://github.com/hubotio/hubot/actions/workflows/nodejs-ubuntu.yml/badge.svg)
![Build Status: Window](https://github.com/hubotio/hubot/actions/workflows/nodejs-windows.yml/badge.svg)

# Hubot

**Note: v10.0.4 accidentally contains the removal of CoffeeScript; v10.0.5 puts it back in**
**Note: v11 removes CoffeeScript and converts this codebase to ESM**

Hubot is a framework to build chat bots, modeled after GitHub's Campfire bot of the same name, hubot.
He's pretty cool. He's [extendable with scripts](https://hubotio.github.io/hubot/docs#scripts) and can work
on [many different chat services](https://hubotio.github.io/hubot/adapters.html).

This repository provides a library that's distributed by `npm` that you
use for building your own bots.  See the [documentation](https://hubotio.github.io/hubot/docs.html)
for details on getting up and running with your very own robot friend.

In most cases, you'll probably never have to hack on this repo directly if you
are building your own bot. But if you do, check out [CONTRIBUTING.md](CONTRIBUTING.md)

# Create your own Hubot instance

This will create a directory called `myhubot` in the current working directory.

```sh
npx hubot --create myhubot --adapter @hubot-friends/hubot-slack
npx hubot --create myhubot --adapter @hubot-friends/hubot-discord
npx hubot --create myhubot --adapter @hubot-friends/hubot-ms-teams
npx hubot --create myhubot --adapter @hubot-friends/hubot-irc
```

Review `scripts/example.mjs`. Create more scripts in the `scripts` folder.

## Command bus (robot.commands)

Hubot includes a deterministic command subsystem for slash-style commands. It is safe by default and does not interfere with legacy `hear` and `respond` listeners.

### Basic Command Registration

```mjs
export default (robot) => {
	robot.commands.register({
		id: 'tickets.create',
		description: 'Create a ticket',
		aliases: ['ticket new', 'new ticket'],
		args: {
			title: { type: 'string', required: true },
			priority: { type: 'enum', values: ['low', 'medium', 'high'], default: 'medium' }
		},
		sideEffects: ['creates external ticket'],
		handler: async (ctx) => {
			return `Created ticket: ${ctx.args.title}`
		}
	})
}
```

Invoke with addressing the bot:

- `@hubot tickets.create --title "VPN down" --priority high`
- `@hubot tickets.create title:"VPN down" priority:high`

Commands that declare side effects will require confirmation before execution.

The user is asked to confirm. They do so like so:
```sh
@hubot yes
@hubot no
@hubot cancel
```

Aliases are for discovery and search only. They do not execute commands or create proposals. They are intent utterances.

### Built-in Help Command

Hubot automatically registers a `help` command that provides command discovery and documentation:

```
@hubot help                          # List all commands
@hubot help tickets                  # Filter commands by prefix
@hubot help search "create ticket"   # Search by keyword, alias, description, or example
```

### Search for Commands

```mjs
const results = robot.commands.search('ticket new')
// [{ id: 'tickets.create', score: 100, matchedOn: 'alias' }, ...]
```

### Custom Type Resolvers

Extend validation with custom argument types:

```mjs
export default (robot) => {
	// Register custom type resolver
	robot.commands.registerTypeResolver('project_id', async (value, schema, context) => {
		if (!value.startsWith('PRJ-')) {
			throw new Error('must start with PRJ-')
		}
		return value.toUpperCase()
	})

	// Use it in a command
	robot.commands.register({
		id: 'projects.deploy',
		description: 'Deploy a project',
		args: {
			projectId: { type: 'project_id', required: true }
		},
		handler: async (ctx) => {
			return `Deploying ${ctx.args.projectId}`
		}
	})
}
```

### Configuration Options

When creating a CommandBus instance, you can configure:

- `prefix` - Command prefix (default: '')
- `proposalTTL` - Timeout for pending confirmations in milliseconds (default: 300000 = 5 minutes)
- `logPath` - Path to NDJSON event log file (default: `.data/commands-events.ndjson`)
- `disableLogging` - Disable event logging to disk (default: true - logging is disabled by default)
- `permissionProvider` - Custom permission checking handler (optional)

### Permissions

Control who can execute commands using room-based and role-based permissions.

#### Room-Based Permissions

Restrict command execution to specific chat rooms:

```mjs
robot.commands.register({
	id: 'sensitive.action',
	description: 'Admin-only action',
	permissions: {
		rooms: ['#admin', '#ops']  // Only allowed in these rooms
	},
	handler: async (ctx) => {
		return 'Action executed!'
	}
})
```

Users in other rooms get: `Permission denied: command not allowed in this room`

#### Role-Based Permissions

Restrict command execution to users with specific roles:

```mjs
robot.commands.register({
	id: 'deploy.production',
	description: 'Deploy to production',
	permissions: {
		roles: ['admin', 'devops']  // Only users with these roles
	},
	handler: async (ctx) => {
		return 'Deploying...'
	}
})
```

To enable role checking, provide a `permissionProvider` when creating CommandBus:

```mjs
const commandBus = new CommandBus(robot, {
	permissionProvider: {
		hasRole: async (user, requiredRoles, context) => {
			// Custom logic to check if user has any of the required roles
			const userRoles = await fetchUserRoles(user.id)
			return requiredRoles.some(role => userRoles.includes(role))
		}
	}
})
```

Without a permission provider, role-based permissions are ignored (allow by default). Room-based permissions are always enforced.

## License

See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).

# Hubot History

[Say hello to Hubot](https://github.blog/2011-10-25-say-hello-to-hubot/)

[Cartoon with Hubot](https://www.youtube.com/watch?v=vq2jYFZVMDA&t=129s)

[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/)

[The Story of Hubot](https://www.youtube.com/watch?v=Je4TjjtFDNU)

[Hubot by Hubotics](https://www.theoldrobots.com/hubot.html)

[Automating Inefficiencies](https://zachholman.com/2011/01/automating-inefficiencies/)

[Getting Started with Hubot](https://www.youtube.com/watch?v=A7fh6RIzGrw)

================================================
FILE: bin/Hubot.mjs
================================================
'use strict'

import fs from 'node:fs'
import { resolve as pathResolve } from 'node:path'
import OptParse from '../src/OptParse.mjs'
import Hubot from '../index.mjs'
import create from '../src/GenHubot.mjs'

const switches = [
  ['-a', '--adapter HUBOT_ADAPTER', 'The Adapter to use, e.g. "Shell" (to load the default hubot Shell adapter)'],
  ['-f', '--file HUBOT_FILE', 'Path to adapter file, e.g. "./adapters/CustomAdapter.mjs"'],
  ['-c', '--create HUBOT_CREATE', 'Create a deployable hubot'],
  ['-d', '--disable-httpd HUBOT_HTTPD', 'Disable the HTTP server'],
  ['-h', '--help', 'Display the help information'],
  ['-l', '--alias HUBOT_ALIAS', "Enable replacing the robot's name with alias"],
  ['-n', '--name HUBOT_NAME', 'The name of the robot in chat'],
  ['-r', '--require PATH', 'Alternative scripts path'],
  ['-t', '--config-check', "Test hubot's config to make sure it won't fail at startup"],
  ['-v', '--version', 'Displays the version of hubot installed'],
  ['-e', '--execute', 'Runs the command as if it were a hubot command']
]

const options = {
  adapter: process.env.HUBOT_ADAPTER,
  alias: process.env.HUBOT_ALIAS || false,
  create: process.env.HUBOT_CREATE || false,
  enableHttpd: process.env.HUBOT_HTTPD !== 'false',
  scripts: process.env.HUBOT_SCRIPTS || [],
  name: process.env.HUBOT_NAME || 'Hubot',
  file: process.env.HUBOT_FILE,
  configCheck: false
}

const Parser = new OptParse(switches)
Parser.banner = 'Usage: hubot [options]'

Parser.on('adapter', (opt, value) => {
  options.adapter = value
})

Parser.on('file', (opt, value) => {
  options.file = value
})

Parser.on('create', function (opt, value) {
  options.path = value
  options.create = true
})

Parser.on('disable-httpd', opt => {
  options.enableHttpd = false
})

Parser.on('help', function (opt, value) {
  console.log(Parser.toString())
  return process.exit(0)
})

Parser.on('alias', function (opt, value) {
  if (!value) {
    value = '/'
  }
  options.alias = value
})

Parser.on('name', (opt, value) => {
  options.name = value
})

Parser.on('execute', (opt, value) => {
  options.execute = value
})

Parser.on('require', (opt, value) => {
  options.scripts.push(value)
})

Parser.on('config-check', opt => {
  options.configCheck = true
})

Parser.on('version', (opt, value) => {
  options.version = true
})

Parser.on(undefined, (opt, value) => {
  console.warn(`Unknown option: ${opt}`)
})

Parser.parse(process.argv)

if (options.create) {
  options.hubotInstallationPath = process.env.HUBOT_INSTALLATION_PATH ?? 'hubot'
  create(options.path, options)
  process.exit(0)
}

if (options.file) {
  options.adapter = options.file.split('/').pop().split('.')[0]
}

const robot = Hubot.loadBot(options.adapter, options.enableHttpd, options.name, options.alias)
export default robot

async function loadScripts () {
  await robot.load(pathResolve('.', 'scripts'))
  await robot.load(pathResolve('.', 'src', 'scripts'))

  await loadExternalScripts()

  const tasks = options.scripts.map((scriptPath) => {
    if (scriptPath[0] === '/') {
      return robot.load(scriptPath)
    }

    return robot.load(pathResolve('.', scriptPath))
  })
  await Promise.all(tasks)
}

async function loadExternalScripts () {
  const externalScripts = pathResolve('.', 'external-scripts.json')
  try {
    const data = await fs.promises.readFile(externalScripts)
    try {
      robot.loadExternalScripts(JSON.parse(data))
    } catch (error) {
      console.error(`Error parsing JSON data from external-scripts.json: ${error}`)
      process.exit(1)
    }
  } catch (e) {
    robot.logger.info('No external-scripts.json found. Skipping.')
  }
}

(async () => {
  await robot.load(pathResolve('.', 'configuration'))
  await robot.loadAdapter(options.file)
  if (options.version) {
    console.log(robot.version)
    process.exit(0)
  }

  if (options.configCheck) {
    await loadScripts()
    console.log('OK')
    process.exit(0)
  }

  robot.adapter.once('connected', async () => {
    await loadScripts()
    if (options.execute) {
      await robot.receive(new Hubot.TextMessage(new Hubot.User('shell', { room: '#shell' }), `@${robot.name} ${options.execute.trim()}`))
      robot.shutdown()
    }
    robot.emit('scripts have loaded', robot)
  })
  await robot.run()
})()


================================================
FILE: bin/e2e-test.sh
================================================
#!/bin/bash
HUBOT_FOLDER=$(pwd)
TEMP_ROOT=$(mktemp -d)

echo "$ pushd $TEMP_ROOT"
pushd $TEMP_ROOT
trap "{ CODE=$?; popd; rm -rf $TEMP_ROOT; exit $CODE; }" EXIT

## https://github.com/hubotio/hubot/blob/main/docs/index.md

## use this hubot version
echo "Creating hubot in $TEMP_ROOT"
echo " and installing Hubot from $HUBOT_FOLDER"
npm init -y
npm i $HUBOT_FOLDER

export HUBOT_INSTALLATION_PATH=$HUBOT_FOLDER
./node_modules/.bin/hubot --create .

# npm install /path/to/hubot will create a symlink in npm 5+ (http://blog.npmjs.org/post/161081169345/v500).
# As the require calls for app-specific scripts happen inside hubot, we have to
# set NODE_PATH to the app’s node_modules path so they can be found
echo "$ Update NODE_PATH=$TEMP_ROOT/node_modules so everything can be found correctly."
export NODE_PATH=$TEMP_ROOT/node_modules
export PATH=$PATH:$TEMP_ROOT/node_modules/.bin

## start, but have to sleep 1 second to wait for hubot to start and the scripts to load
expect <<EOL
  set timeout 30
  spawn hubot --name e2etest
  expect "e2etest> "
  sleep 1
  send "e2etest PING\r"
  expect {
    "PONG" {}
    timeout {exit 1}
  }
  send "e2etest adapter\r"
  expect {
    "Shell" {}
    "shell" {}
    timeout {exit 1}
  }
EOL


================================================
FILE: bin/hubot
================================================
#!/usr/bin/env node

import('./Hubot.mjs').then(async ({ default: robot }) => {})

================================================
FILE: configuration/Config.mjs
================================================
// Description:
//   Configuration
//
// Commands:
//
// Notes:
//   This is a test script.
//

export default async robot => {
  robot.config = {}
}


================================================
FILE: docs/adapters/campfire.md
================================================
---
title: Campfire adapter
layout: layouts/docs.html
permalink: /adapters/campfire.html
---

# Campfire adapter

[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.

## Getting Started

You will need a Campfire account to start.

Next, 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.

Hubot defaults to using its [Shell](./shell.html), so to use Campfire instead, you can run hubot with `-a Campfire`:

    % bin/hubot -a campfire

If you are using foreman, you need to make sure the hubot is called with `-a Campfire` in the `Procfile`:

    web: bin/hubot -a campfire -n Hubot

## Configuring

The adapter requires the following environment variables.

* `HUBOT_CAMPFIRE_ACCOUNT`
* `HUBOT_CAMPFIRE_TOKEN`
* `HUBOT_CAMPFIRE_ROOMS`

### Campfire API Token

This can be found by logging in with your hubot's account click the **My Info**
link and make a note of the API token.

### Campfire Room IDs

If you join the rooms you want your hubot to join will see notice a numerical
ID for the room in the URL. Make a note of each ID for the rooms you want your
hubot to join.

### Campfire Account

This is simply the first part of the domain you visit for your Campfire
account. For example if your Campfire was at `hubot.campfirenow.com` your
subdomain is `hubot`. Make a note of the subdomain.

### Configuring the variables on UNIX

    % export HUBOT_CAMPFIRE_TOKEN="..."

    % export HUBOT_CAMPFIRE_ROOMS="123,321"

    % export HUBOT_CAMPFIRE_ACCOUNT="..."

### Configuring the variables on Windows

Using PowerShell:

    setx HUBOT_CAMPFIRE_TOKEN "..." /m

    setx HUBOT_CAMPFIRE_ROOMS "123,321" /m

    setx HUBOT_CAMPFIRE_ACCOUNT "..." /m


================================================
FILE: docs/adapters/development.md
================================================
---
title: Development adapter
layout: layouts/docs.html
permalink: /adapters/development.html
---

# Development adapter

## Adapter Basics

All adapters inherit from the Adapter class in the `src/Adapter.mjs` file.

```javascript
const Adapter = require('hubot/index.mjs').Adapter;
```

There are certain methods that you will want to override.  Here is a basic stub of what an extended Adapter class would look like:

```javascript
const Adapter = require('../adapter')
const User = require('../user')
const TextMessage = require('../message').TextMessage
class Sample extends Adapter {
    constructor(robot) {
        super(robot)
        this.robot.logger.info('Constructor')
    }
    send(envelope, ...strings) {
        this.robot.logger.info('Send')
    }
    reply(envelope, ...strings) {
        this.robot.logger.info('Reply')
    }
    run() {
        this.robot.logger.info('Run')
        this.emit('connected') // The 'connected' event is required to trigger loading of Hubot scripts.
        const user = new User(1001, 'Sample User')
        const message = new TextMessage(user, 'Some Sample Message', 'MSG-001')
        this.robot.receive(message)
    }
}
exports.use = (robot) => new Sample(robot)
```

## Option 1. Setting Up Your Development Environment

1. Create a new folder for your adapter `hubot-sample`
  - `mkdir hubot-sample`
2. Change your working directory to `hubot-sample`
  - `cd hubot-sample`
3. Run `npm init` to create your package.json
  - make sure the entry point is `src/sample.js`
4. Add your `.gitignore` to include `node_modules`
5. Edit the `src/sample.js` file to include the above stub for your adapter
6. Edit the `package.json` to add a peer dependency on `hubot`

  ```json
  "dependencies": {
  },
  "peerDependencies": {
    "hubot": ">=11"
  }
  ```

7. Generate your Hubot using the `npx hubot --create myhubot`
8. Change working directories to the `hubot` you created in step 7.
9. Now perform an `npm link` to add your adapter to `hubot`
  - `npm link ../hubot-sample`
10. Run `hubot -a sample`

## Gotchas

There 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.

1. For the import in your `hubot-sample` adapter, add the following code

  ```javascript
  let  {Robot,Adapter,TextMessage,User} = {}
  try {
    {Robot,Adapter,TextMessage,User} = require('hubot')
  } catch {
    const prequire = require('parent-require')
    {Robot,Adapter,TextMessage,User} = prequire('hubot')
  }
  ```
2. In your `hubot-sample` folder, modify the `package.json` to include the following dependency so this custom import mechanism will work

  ```json
  "dependencies": {
    "parent-require": "^1.0.0"
  }
  ```
3. Now try running `hubot -a sample` again and see that the imports are properly loaded.
4. 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.
  - 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.

## Option 2. Setting Up Your Development Environment

Another option is to load the file from local disk.

1. Create a new folder for your adapter `hubot-sample`
  - `mkdir hubot-sample`
2. Change your working directory to `hubot-sample`
  - `cd hubot-sample`
3. Run `npm init` to create your package.json
  - make sure the entry point is `src/sample.js`
4. Add your `.gitignore` to include `node_modules`
5. Edit the `src/sample.js` file to include the above stub for your adapter
6. Edit the `package.json` to add a peer dependency on `hubot`

  ```json
  "dependencies": {
  },
  "peerDependencies": {
    "hubot": ">=11"
  }
  ```

7. Run `npx hubot -p ./src -a sample.js`


================================================
FILE: docs/adapters/shell.md
================================================
---
title: Shell adapter
layout: layouts/docs.html
permalink: /adapters/shell.html
---

# Shell adapter

The shell adapter provides a simple read-eval-print loop for interacting with a hubot locally.
It can be useful for testing scripts before using them on a live hubot.

## Getting Started

To use the Shell adapter you can simply omit the `-a` option when running
hubot as it will use the Shell adapter by default.

    % bin/hubot

## Configuring

This adapter doesn't require any configuration.

It supports two environment variables to make it possible to test scripts as different users:

* HUBOT_SHELL_USER_ID: default is 1
* HUBOT_SHELL_USER_NAME: default is Shell


================================================
FILE: docs/adapters.md
================================================
---
title: Adapters
layout: layouts/docs.html
permalink: /adapters.html
---

# Adapters

Adapters are the interface to the service you want your hubot to run on.

Hubot includes two official adapters:

* [Shell](./adapters/shell.html), i.e. for use with development
* [Campfire](./adapters/campfire.html)

There are Third-party adapters available for most chat services. Here are the most popular ones:

* [Discord](https://github.com/hubot-friends/hubot-discord)
* [IRC](https://github.com/hubot-friends/hubot-irc)
* [Slack](https://github.com/hubot-friends/hubot-slack)
* [MS Teams](https://github.com/hubot-friends/hubot-ms-teams)
* [Gitter](https://github.com/huafu/hubot-gitter2)
* [HipChat](https://github.com/hipchat/hubot-hipchat)
* [Rocket.Chat](https://github.com/RocketChat/hubot-rocketchat)
* [XMPP](https://github.com/markstory/hubot-xmpp)

Browse 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.

## Writing Your Own Adapter

Interested in adding your own adapter? Check out our documentation for [developing adapters](./adapters/development.html)


================================================
FILE: docs/assets/stylesheets/application.css
================================================
@font-face {
    font-family: 'octicons';
    src: url("../vendors/octicons/octicons/octicons.eot");
    src: url("../vendors/octicons/octicons/octicons.eot?#iefix") format("embedded-opentype"),
        url("../vendors/octicons/octicons/octicons.woff") format("woff"),
        url("../vendors/octicons/octicons/octicons.ttf") format("truetype"),
        url("../vendors/octicons/octicons/octicons.svg#svgFontName") format("svg");
}

* {
    box-sizing: border-box;
}

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section {
    display: block;
}

audio,
canvas,
video {
    display: inline-block;
}

audio:not([controls]) {
    display: none;
}

[hidden] {
    display: none;
}

html {
    font-size: 21px;
}

body {
    margin: 0;
    font-size: 0.8rem;
    line-height: 1.5;
}

body,
button,
input,
select,
textarea {
    color: #222;
}

::-moz-selection {
    background: #4793bd;
    color: #fff;
    text-shadow: none;
}

::selection {
    background: #4793bd;
    color: #fff;
    text-shadow: none;
}

a {
    color: #00e;
}

a:visited {
    color: #551a8b;
}

a:hover {
    color: #06e;
}

a:focus {
    outline: thin dotted;
}

a:hover,
a:active {
    outline: 0;
}

abbr[title] {
    border-bottom: 1px dotted;
}

b,
strong {
    font-weight: bold;
}

blockquote {
    margin: 1em 40px;
}

dfn {
    font-style: italic;
}

hr {
    display: block;
    height: 1px;
    border: 0;
    border-top: 1px solid #ccc;
    margin: 1em 0;
    padding: 0;
}

ins {
    background: #ff9;
    color: #000;
    text-decoration: none;
}

mark {
    background: #ff0;
    color: #000;
    font-style: italic;
    font-weight: bold;
}
code {
    white-space: pre-wrap;
    word-break: break-word;
}
pre,
code,
kbd,
samp {
    font-family: monospace, monospace;
    font-size: 1em;
}

q {
    quotes: none;
}

q:before,
q:after {
    content: "";
    content: none;
}

small {
    font-size: 0.5rem;
}

sub,
sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}

sup {
    top: -0.5em;
}

sub {
    bottom: -0.25em;
}

ul,
ol {
    margin: 1em 0;
    padding: 0 0 0 40px;
}

dd {
    margin: 0 0 0 40px;
}

nav ul,
nav ol {
    list-style: none;
    list-style-image: none;
    margin: 0;
    padding: 0;
}

img {
    border: 0;
    -ms-interpolation-mode: bicubic;
    vertical-align: middle;
}

svg:not(:root) {
    overflow: hidden;
}

figure {
    margin: 0;
}

form {
    margin: 0;
}

fieldset {
    border: 0;
    margin: 0;
    padding: 0;
}

label {
    cursor: pointer;
}

legend {
    border: 0;
    padding: 0;
}

button,
input,
select,
textarea {
    font-size: 100%;
    margin: 0;
    vertical-align: baseline;
}

button,
input {
    line-height: normal;
}

button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
    cursor: pointer;
    appearance: button;
}

input[type="checkbox"],
input[type="radio"] {
    box-sizing: border-box;
}

input[type="search"] {
    appearance: textfield;
    box-sizing: content-box;
}

input[type="search"]::-webkit-search-decoration {
    appearance: none;
}

button::-moz-focus-inner,
input::-moz-focus-inner {
    border: 0;
    padding: 0;
}

textarea {
    overflow: auto;
    vertical-align: top;
    resize: vertical;
}

input:invalid,
textarea:invalid {
    background-color: #f0dddd;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

td {
    vertical-align: top;
}

@font-face {
    font-family: HandOfSeanRegular;
    src: url("../fonts/eot/handsean-webfont.eot");
    src: local("☺"), url("../fonts/otf/handsean-webfont.ttf") format("truetype"),
        url("../fonts/svg/handsean-webfont.svg#webfontV6q8jOTr") format("svg"),
        url("../fonts/woff/handsean-webfont.woff") format("woff");
    font-weight: normal;
    font-style: normal;
}

@font-face {
    font-family: PoliticaThin;
    src: url("../fonts/eot/style_154053.eot");
    src: local("☺"), url("../fonts/otf/style_154053.otf") format("opentype"),
        url("../fonts/svg/style_154053.svg#PoliticaThin") format("svg"),
        url("../fonts/woff/style_154053.woff") format("woff");
}

@font-face {
    font-family: PoliticaThin-Italic;
    src: url("../fonts/eot/style_154049.eot");
    src: local("☺"), url("../fonts/svg/style_154049.svg#PoliticaThin-Italic") format("svg"),
        url("../fonts/woff/style_154049.woff") format("woff");
}

@font-face {
    font-family: PoliticaLight;
    src: url("../fonts/eot/style_154051.eot");
    src: local("☺"), url("../fonts/otf/style_154051.otf") format("opentype"),
        url("../fonts/svg/style_154051.svg#PoliticaLight") format("svg"),
        url("../fonts/woff/style_154051.woff") format("woff");
}

@font-face {
    font-family: PoliticaLight-Italic;
    src: url("../fonts/eot/style_154044.eot");
    src: local("☺"), url("../fonts/svg/style_154044.svg#PoliticaLight-Italic") format("svg"),
        url("../fonts/woff/style_154044.woff") format("woff");
}

@font-face {
    font-family: Politica;
    src: url("../fonts/eot/style_154046.eot");
    src: local("☺"), url("../fonts/otf/style_154046.otf") format("opentype"),
        url("../fonts/svg/style_154046.svg#Politica") format("svg"),
        url("../fonts/woff/style_154046.woff") format("woff");
}

@font-face {
    font-family: Politica-Italic;
    src: url("../fonts/eot/style_154048");
    src: local("☺"), url("../fonts/otf/style_154048.otf") format("opentype"),
        url("../fonts/svg/style_154048.svg#Politica-Italic") format("svg"),
        url("../fonts/woff/style_154048.woff") format("woff");
}

@font-face {
    font-family: Politica-Bold;
    src: url("../fonts/eot/style_154045.eot");
    src: local("☺"), url("../fonts/otf/style_154045.otf") format("opentype"),
        url("../fonts/svg/style_154045.svg#Politica-Bold") format("svg"),
        url("../fonts/woff/style_154045.woff") format("woff");
}

@font-face {
    font-family: Politica-BoldItalic;
    src: url("../fonts/eot/style_154042.eot");
    src: local("☺"), url("../fonts/otf/style_154042.otf") format("opentype"),
        url("../fonts/svg/style_154042.svg#Politica-BoldItalic") format("svg"),
        url("../fonts/woff/style_154042.woff") format("woff");
}

@font-face {
    font-family: 'Octicons Regular';
    src: url("../fonts/eot/octicons-regular-webfont.eot");
    src: local("☺"), url("../fonts/eot/octicons-regular-webfont.eot#iefix") format("embedded-opentype"),
        url("../fonts/otf/octicons-regular-webfont.otf") format("opentype"),
        url("../fonts/svg/octicons-regular-webfont.svg#newFontRegular") format("svg"),
        url("../fonts/woff/octicons-regular-webfont.woff") format("woff");
}

a,
a:visited {
    color: #5f8faf;
    text-decoration: none;
}

body {
    background: url("../images/layout/project-paper.png");
    font-family: 'Helvetica Neue', Helvetica, arial, freesans, clean, sans-serif;
    padding: 1rem;
}

.container {
    background: #fff;
    margin: 0 auto;
    box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.05), 0 0 0 12px rgba(0, 0, 0, 0.02) inset;
}

.frame {
    background: url("../images/layout/header-field-emblem.png") no-repeat left top;
    border: solid 1px #eee;
}
.hubot-avatar {
    background-color: #fff;
    background-image: linear-gradient(#fff 10%, #f7f7f7 90%);
    background-position: center center;
    background-repeat: no-repeat;
    border-radius: 4px;
    height: 106px;
    left: 1px;
    top: 1px;
    width: 106px;
    transform: rotate(-3deg);
    box-shadow: 0 0 1px transparent, 0 2px 2px rgba(0, 0, 0, 0.2);
}

.hubot-avatar:before {
    background: url("../images/layout/tape.png") no-repeat left top;
    content: '';
    display: block;
    height: 26px;
    position: absolute;
    top: -12px;
    left: 4px;
    width: 99px;
    z-index: 1;
}

.hubot-avatar .hubot-avatar-img {
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    margin: auto;
}

header.clearfix {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    gap: 1rem;
}

header.clearfix h1 {
    background: url("../images/layout/header-field-1.png") no-repeat left bottom;
    color: #333;
    font-family: Politica;
    font-size: 72px;
    margin: 0 12px;
    position: relative;
    text-transform: uppercase;
    width: 252px;
}
header.clearfix h1 span {
    color: #7880a7;
    display: block;
    font-family: 'HandOfSeanRegular';
    font-size: 14px;
    font-weight: normal;
    left: 156px;
    letter-spacing: -1px;
    position: absolute;
    top: 32px;
    text-transform: lowercase;
    width: 125px;
    transform: rotate(-3deg);
}

header.clearfix h1 span b {
    font-size: 18px;
    font-weight: normal;
}

header.clearfix h2 {
    background: url("../images/layout/header-field-2.png") no-repeat left bottom;
    color: #7880a7;
    float: left;
    font-size: 16px;
    font-weight: normal;
    height: 132px;
    line-height: 18px;
    margin: 0 12px;
    padding-top: 52px;
    text-transform: uppercase;
    width: 252px;
}

header.clearfix p {
    background: url("../images/layout/header-field-3.png") no-repeat left bottom;
    color: #bebebe;
    float: left;
    font-size: 16px;
    font-weight: normal;
    height: 132px;
    line-height: 18px;
    margin: 0 0 0 12px;
    padding-top: 34px;
    text-transform: uppercase;
    width: 252px;
}

.download {
    line-height: 24px;
}

.insides {
    margin: 12px 0 12px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    gap: 1rem;
}


.insides .button {
    background: url("../images/layout/checkbawx.png") no-repeat 12px 12px;
    border: 1px solid #ddd;
    margin: 0;
    padding: 10px 12px 10px 30px;
    text-transform: uppercase;
    display: block;
    width: 40%;
}

@media (max-width: 920px) {
    .insides .button {
        width: 80%;
    }
}

.insides .button:hover {
    background-position: 12px -44px;
    border: 1px solid #ccc;
    background-color: rgba(254, 174, 40, 0.1);
}

.insides .button:first-child {
    margin-left: 0;
}

.insides span {
    color: #ccc;
    display: block;
    font-size: 10px;
}

.main {
    position: relative;
}

.schematics {
    background: url("../images/layout/schematic-shadow.png") no-repeat center bottom;
    height: 444px;
    z-index: 5;
    transform: rotate(-1deg);
}

.schematic {
    background: url("../images/layout/old-mathematics.png");
    background-size: auto, 100px;
    height: 432px;
    position: relative;
    box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);
}

.schematic .schematic-img {
    margin: 10px auto 0;
    display: block;
    padding-top: 25px;
    width: 100%;
    height: 100%;
}

.schematic p {
    background: #fff;
    border: 1px solid rgba(0, 0, 0, 0.1);
    color: #999;
    bottom: 12px;
    font-size: 10px;
    left: 24px;
    padding: 6px 12px;
    position: absolute;
    text-transform: uppercase;
    width: 172px;
}

.about {
    color: #666;
    line-height: 24px;
    padding: 1rem 5rem;
    width: 100%;
    display: flex;
    flex-direction: row;
    gap: 1rem;
    justify-content: space-evenly;
    align-items: center;
}
.about article {
    width: 70%;
}
.about aside {
    width: 30%;
}

@media (max-width: 920px) {
    .about {
        flex-direction: column;
        padding: 1rem 3rem;
    }
    .about aside,
    .about article {
        width: 100%;
    }
}

.about h2 {
    border-bottom: 1px solid #eee;
    padding-bottom: 12px;
    color: #222;
    font-weight: normal;
    margin-top: 24px;
}

.letter {
    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;
    line-height: 24px;
    padding: 48px 24px 106px 48px;
    box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2);
    position: relative;
    margin-bottom: 2rem;
}

.letter:before,
.letter:after {
    background: url("../images/layout/tape.png") no-repeat left top;
    content: '';
    position: absolute;
    width: 99px;
    height: 26px;
}

.letter:before {
    top: -12px;
}

.letter:after {
    top: -12px;
    right: 32px;
}

.screenshot {
    width: 100%;
    text-align: center;
    z-index: 60;
    position: relative;
}
.screenshot img {
    width: 100%;
    box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25);
}

.screenshot:before {
    background: url("../images/layout/tape.png") no-repeat left top;
    content: '';
    display: block;
    height: 26px;
    position: absolute;
    top: -12px;
    left: 50%;
    margin-left: -49px;
    width: 99px;
}

.footer {
    color: #4c82a5;
    margin: 24px auto 0;
    padding: 0 24px;
}

.footer a:visited {
    color: #5f8faf;
}

.footer .right {
    margin: 0;
    float: right;
}

.mega-icon {
    font-family: 'Octicons Regular';
    font-weight: normal;
    font-style: normal;
    display: inline-block;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    text-decoration: none;
}

.mega-icon-invertocat {
    color: #4c82a5;
    position: absolute;
    left: 50%;
    height: 24px;
    width: 24px;
    margin-top: -4px;
    margin-left: -12px;
    font-size: 24px;
}

.mega-icon-invertocat:before {
    content: "\f20a";
}

.docs .container .main {
    display: flex;
    padding: 0 2rem;
}
.docs .container .main h2 {
    border-bottom: 1px solid #eee;
    padding-bottom: 0.8rem;
    color: #222;
    font-weight: normal;
}
.docs .container header.clearfix {
    display: block;
    margin-left: 170px;
    position: relative;
}
.docs .container header.clearfix a {
    position: absolute;
    top: 0;
    left: -135px;
}
.docs header h1 {
    background: none;
    width: auto;
    border-bottom: 1px solid #ccc;
    margin-bottom: 30px;
}

.docs-nav {
    margin-right: 20px;
    margin-top: 20px;
    border: 1px solid #ccc;
    font-size: 14px;
}

.docs-nav li {
    border-top: 1px solid #ccc;
}

.docs-nav .subpage .docs-link {
    padding-left: 24px;
}

.docs-nav .docs-list>li:first-child {
    border-top: none;
}

.docs-nav .docs-link {
    display: block;
    padding: 8px 10px;
}

.docs-nav .docs-link:hover,
.docs-nav .docs-link.current {
    background-color: rgba(254, 174, 40, 0.1);
    color: #333;
    border-right: 4px solid #feae28;
    padding-right: 6px;
}

@media (max-width: 600px) {
    .mega-icon-invertocat {
        position: static;
    }
    .docs-nav {
        width: 100%;
        margin: 0;
    }
    .docs .container .main {
        display: flex;
        flex-direction: column;
        padding: 0 1rem;
    }
    .docs header h1 {
        font-size: 2.5rem;
    }
    .docs .container header.clearfix {
        margin-left: 0;
    }
    .docs .container header.clearfix a {
        position: relative;
        top: 0;
        left: 0;
    }
}
@font-face {
    font-family: 'octicons';
    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");
    font-weight: normal;
    font-style: normal;
}

.octicon,
.mega-octicon {
    font: normal normal normal 16px/1 octicons;
    display: inline-block;
    text-decoration: none;
    text-rendering: auto;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    user-select: none;
}

.mega-octicon {
    font-size: 32px;
}

.octicon-alert:before {
    content: '\f02d';
}

.octicon-alignment-align:before {
    content: '\f08a';
}

.octicon-alignment-aligned-to:before {
    content: '\f08e';
}

.octicon-alignment-unalign:before {
    content: '\f08b';
}

.octicon-arrow-down:before {
    content: '\f03f';
}

.octicon-arrow-left:before {
    content: '\f040';
}

.octicon-arrow-right:before {
    content: '\f03e';
}

.octicon-arrow-small-down:before {
    content: '\f0a0';
}

.octicon-arrow-small-left:before {
    content: '\f0a1';
}

.octicon-arrow-small-right:before {
    content: '\f071';
}

.octicon-arrow-small-up:before {
    content: '\f09f';
}

.octicon-arrow-up:before {
    content: '\f03d';
}

.octicon-beer:before {
    content: '\f069';
}

.octicon-book:before {
    content: '\f007';
}

.octicon-bookmark:before {
    content: '\f07b';
}

.octicon-briefcase:before {
    content: '\f0d3';
}

.octicon-broadcast:before {
    content: '\f048';
}

.octicon-browser:before {
    content: '\f0c5';
}

.octicon-bug:before {
    content: '\f091';
}

.octicon-calendar:before {
    content: '\f068';
}

.octicon-check:before {
    content: '\f03a';
}

.octicon-checklist:before {
    content: '\f076';
}

.octicon-chevron-down:before {
    content: '\f0a3';
}

.octicon-chevron-left:before {
    content: '\f0a4';
}

.octicon-chevron-right:before {
    content: '\f078';
}

.octicon-chevron-up:before {
    content: '\f0a2';
}

.octicon-circle-slash:before {
    content: '\f084';
}

.octicon-circuit-board:before {
    content: '\f0d6';
}

.octicon-clippy:before {
    content: '\f035';
}

.octicon-clock:before {
    content: '\f046';
}

.octicon-cloud-download:before {
    content: '\f00b';
}

.octicon-cloud-upload:before {
    content: '\f00c';
}

.octicon-code:before {
    content: '\f05f';
}

.octicon-color-mode:before {
    content: '\f065';
}

.octicon-comment-add:before,
.octicon-comment:before {
    content: '\f02b';
}

.octicon-comment-discussion:before {
    content: '\f04f';
}

.octicon-credit-card:before {
    content: '\f045';
}

.octicon-dash:before {
    content: '\f0ca';
}

.octicon-dashboard:before {
    content: '\f07d';
}

.octicon-database:before {
    content: '\f096';
}

.octicon-device-camera:before {
    content: '\f056';
}

.octicon-device-camera-video:before {
    content: '\f057';
}

.octicon-device-desktop:before {
    content: '\f27c';
}

.octicon-device-mobile:before {
    content: '\f038';
}

.octicon-diff:before {
    content: '\f04d';
}

.octicon-diff-added:before {
    content: '\f06b';
}

.octicon-diff-ignored:before {
    content: '\f099';
}

.octicon-diff-modified:before {
    content: '\f06d';
}

.octicon-diff-removed:before {
    content: '\f06c';
}

.octicon-diff-renamed:before {
    content: '\f06e';
}

.octicon-ellipsis:before {
    content: '\f09a';
}

.octicon-eye-unwatch:before,
.octicon-eye-watch:before,
.octicon-eye:before {
    content: '\f04e';
}

.octicon-file-binary:before {
    content: '\f094';
}

.octicon-file-code:before {
    content: '\f010';
}

.octicon-file-directory:before {
    content: '\f016';
}

.octicon-file-media:before {
    content: '\f012';
}

.octicon-file-pdf:before {
    content: '\f014';
}

.octicon-file-submodule:before {
    content: '\f017';
}

.octicon-file-symlink-directory:before {
    content: '\f0b1';
}

.octicon-file-symlink-file:before {
    content: '\f0b0';
}

.octicon-file-text:before {
    content: '\f011';
}

.octicon-file-zip:before {
    content: '\f013';
}

.octicon-flame:before {
    content: '\f0d2';
}

.octicon-fold:before {
    content: '\f0cc';
}

.octicon-gear:before {
    content: '\f02f';
}

.octicon-gift:before {
    content: '\f042';
}

.octicon-gist:before {
    content: '\f00e';
}

.octicon-gist-secret:before {
    content: '\f08c';
}

.octicon-git-branch-create:before,
.octicon-git-branch-delete:before,
.octicon-git-branch:before {
    content: '\f020';
}

.octicon-git-commit:before {
    content: '\f01f';
}

.octicon-git-compare:before {
    content: '\f0ac';
}

.octicon-git-merge:before {
    content: '\f023';
}

.octicon-git-pull-request-abandoned:before,
.octicon-git-pull-request:before {
    content: '\f009';
}

.octicon-globe:before {
    content: '\f0b6';
}

.octicon-graph:before {
    content: '\f043';
}

.octicon-heart:before {
    content: '\2665';
}

.octicon-history:before {
    content: '\f07e';
}

.octicon-home:before {
    content: '\f08d';
}

.octicon-horizontal-rule:before {
    content: '\f070';
}

.octicon-hourglass:before {
    content: '\f09e';
}

.octicon-hubot:before {
    content: '\f09d';
}

.octicon-inbox:before {
    content: '\f0cf';
}

.octicon-info:before {
    content: '\f059';
}

.octicon-issue-closed:before {
    content: '\f028';
}

.octicon-issue-opened:before {
    content: '\f026';
}

.octicon-issue-reopened:before {
    content: '\f027';
}

.octicon-jersey:before {
    content: '\f019';
}

.octicon-jump-down:before {
    content: '\f072';
}

.octicon-jump-left:before {
    content: '\f0a5';
}

.octicon-jump-right:before {
    content: '\f0a6';
}

.octicon-jump-up:before {
    content: '\f073';
}

.octicon-key:before {
    content: '\f049';
}

.octicon-keyboard:before {
    content: '\f00d';
}

.octicon-law:before {
    content: '\f0d8';
}

.octicon-light-bulb:before {
    content: '\f000';
}

.octicon-link:before {
    content: '\f05c';
}

.octicon-link-external:before {
    content: '\f07f';
}

.octicon-list-ordered:before {
    content: '\f062';
}

.octicon-list-unordered:before {
    content: '\f061';
}

.octicon-location:before {
    content: '\f060';
}

.octicon-gist-private:before,
.octicon-mirror-private:before,
.octicon-git-fork-private:before,
.octicon-lock:before {
    content: '\f06a';
}

.octicon-logo-github:before {
    content: '\f092';
}

.octicon-mail:before {
    content: '\f03b';
}

.octicon-mail-read:before {
    content: '\f03c';
}

.octicon-mail-reply:before {
    content: '\f051';
}

.octicon-mark-github:before {
    content: '\f00a';
}

.octicon-markdown:before {
    content: '\f0c9';
}

.octicon-megaphone:before {
    content: '\f077';
}

.octicon-mention:before {
    content: '\f0be';
}

.octicon-microscope:before {
    content: '\f089';
}

.octicon-milestone:before {
    content: '\f075';
}

.octicon-mirror-public:before,
.octicon-mirror:before {
    content: '\f024';
}

.octicon-mortar-board:before {
    content: '\f0d7';
}

.octicon-move-down:before {
    content: '\f0a8';
}

.octicon-move-left:before {
    content: '\f074';
}

.octicon-move-right:before {
    content: '\f0a9';
}

.octicon-move-up:before {
    content: '\f0a7';
}

.octicon-mute:before {
    content: '\f080';
}

.octicon-no-newline:before {
    content: '\f09c';
}

.octicon-octoface:before {
    content: '\f008';
}

.octicon-organization:before {
    content: '\f037';
}

.octicon-package:before {
    content: '\f0c4';
}

.octicon-paintcan:before {
    content: '\f0d1';
}

.octicon-pencil:before {
    content: '\f058';
}

.octicon-person-add:before,
.octicon-person-follow:before,
.octicon-person:before {
    content: '\f018';
}

.octicon-pin:before {
    content: '\f041';
}

.octicon-playback-fast-forward:before {
    content: '\f0bd';
}

.octicon-playback-pause:before {
    content: '\f0bb';
}

.octicon-playback-play:before {
    content: '\f0bf';
}

.octicon-playback-rewind:before {
    content: '\f0bc';
}

.octicon-plug:before {
    content: '\f0d4';
}

.octicon-repo-create:before,
.octicon-gist-new:before,
.octicon-file-directory-create:before,
.octicon-file-add:before,
.octicon-plus:before {
    content: '\f05d';
}

.octicon-podium:before {
    content: '\f0af';
}

.octicon-primitive-dot:before {
    content: '\f052';
}

.octicon-primitive-square:before {
    content: '\f053';
}

.octicon-pulse:before {
    content: '\f085';
}

.octicon-puzzle:before {
    content: '\f0c0';
}

.octicon-question:before {
    content: '\f02c';
}

.octicon-quote:before {
    content: '\f063';
}

.octicon-radio-tower:before {
    content: '\f030';
}

.octicon-repo-delete:before,
.octicon-repo:before {
    content: '\f001';
}

.octicon-repo-clone:before {
    content: '\f04c';
}

.octicon-repo-force-push:before {
    content: '\f04a';
}

.octicon-gist-fork:before,
.octicon-repo-forked:before {
    content: '\f002';
}

.octicon-repo-pull:before {
    content: '\f006';
}

.octicon-repo-push:before {
    content: '\f005';
}

.octicon-rocket:before {
    content: '\f033';
}

.octicon-rss:before {
    content: '\f034';
}

.octicon-ruby:before {
    content: '\f047';
}

.octicon-screen-full:before {
    content: '\f066';
}

.octicon-screen-normal:before {
    content: '\f067';
}

.octicon-search-save:before,
.octicon-search:before {
    content: '\f02e';
}

.octicon-server:before {
    content: '\f097';
}

.octicon-settings:before {
    content: '\f07c';
}

.octicon-log-in:before,
.octicon-sign-in:before {
    content: '\f036';
}

.octicon-log-out:before,
.octicon-sign-out:before {
    content: '\f032';
}

.octicon-split:before {
    content: '\f0c6';
}

.octicon-squirrel:before {
    content: '\f0b2';
}

.octicon-star-add:before,
.octicon-star-delete:before,
.octicon-star:before {
    content: '\f02a';
}

.octicon-steps:before {
    content: '\f0c7';
}

.octicon-stop:before {
    content: '\f08f';
}

.octicon-repo-sync:before,
.octicon-sync:before {
    content: '\f087';
}

.octicon-tag-remove:before,
.octicon-tag-add:before,
.octicon-tag:before {
    content: '\f015';
}

.octicon-telescope:before {
    content: '\f088';
}

.octicon-terminal:before {
    content: '\f0c8';
}

.octicon-three-bars:before {
    content: '\f05e';
}

.octicon-tools:before {
    content: '\f031';
}

.octicon-trashcan:before {
    content: '\f0d0';
}

.octicon-triangle-down:before {
    content: '\f05b';
}

.octicon-triangle-left:before {
    content: '\f044';
}

.octicon-triangle-right:before {
    content: '\f05a';
}

.octicon-triangle-up:before {
    content: '\f0aa';
}

.octicon-unfold:before {
    content: '\f039';
}

.octicon-unmute:before {
    content: '\f0ba';
}

.octicon-versions:before {
    content: '\f064';
}

.octicon-remove-close:before,
.octicon-x:before {
    content: '\f081';
}

.octicon-zap:before {
    content: '\26A1';
}

================================================
FILE: docs/deploying/azure.md
================================================
---
title: Deploying to Azure
layout: layouts/docs.html
permalink: /deploying/azure.html
---

# Deploying to Azure

If 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.
[Azure](http://azure.microsoft.com/) is a way to deploy hubot.

You will need to install the azure-cli via npm after you have follow the initial instructions for your hubot.

    % npm install -g azure-cli

Inside your new hubot directory, make sure you've created a git repository, and that your work is committed:

    % git init
    % git add .
    % git commit -m "Initial commit"

Then [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.

    % git remote add origin _your GitHub repo_
    % git push -u origin main

Once 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.

    % $creds = Get-Credential
    % New-AzureWebsite mynewhubot -github -githubrepository yourgithubaccount/yourhubotreponame -githubcredentials $creds

Once 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.

First, 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.

    % azure site deploymentscript --node

Then, edit this file and look for the sections that give you steps 1, 2 and 3. You're going to add a 4th step:

    :: 4. Create Hubot file with a js extension
    copy /Y "%DEPLOYMENT_TARGET%\node_modules\hubot\bin\hubot" "%DEPLOYMENT_TARGET%\node_modules\hubot\bin\Hubot.mjs"

Now, create a new file in the base directory of hubot called `server.js` and put these two lines into it:

    module.exports = await import('hubot/bin/Hubot.mjs');

Finally 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).

    % $settings = New-Object Hashtable
    % $settings["HUBOT_ADAPTER"] = "Slack"
    % $settings["HUBOT_SLACK_TOKEN"] = "yourslackapikey"
    % Set-AzureWebsite -AppSettings $settings mynewhubot

Commit your changes in git and push to GitHub and Azure will automatically pick up the changes and deploy them to your website.

    % git commit -m "Add Azure settings for hubot"
    % git push

Azure 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.

    % npm install hubot-azure-brain --save

Then add the following line in `external-scripts.json` in the list with the other external scripts

    "hubot-azure-brain"

Finally, add one more environment variables to your website. You can do this either via the GUI or the following PowerShell commands.

    % $settings = New-Object Hashtable
    % $settings["HUBOT_BRAIN_AZURE_CONNSTRING"] = "your Azure blob storage connection string"
    % Set-AzureWebsite -AppSettings $settings mynewhubot

Now 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.

### Troubleshooting tips and tricks

Due 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.

If 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.

If 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`

An example of a startup script:

```
let retrys=0
while : ; do
    STATUSCODE=$(curl --silent --output /dev/stderr --write-out "%{http_code}" https://${WEBSITE_SITE_NAME}.azurewebsites.net/hubot/keepalive)
    echo $STATUSCODE
    [[ $retrys -ne 5 ]] || break
    echo $retrys
    ((retrys++))
    [[ $STATUSCODE -ne 200 ]] || break
done
```

### Slack Integration
Currently 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.


================================================
FILE: docs/deploying/bluemix.md
================================================
---
title: Deploying to Bluemix
layout: layouts/docs.html
permalink: /deploying/bluemix.html
---

# Deploying to Bluemix

If 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.
[IBM Bluemix](http://bluemix.net) is a way to deploy hubot. It is built on the open-source project
[Cloud Foundry](https://www.cloudfoundry.org/), so we'll be using the `cf cli`
throughout these examples.

Hubot was originally very closely coupled to Heroku, so there are a couple of
things to clean up first that we don't need or that might get in the way on
another platform:
* remove `Procfile` as we'll create the `manifest.yml` that Bluemix needs in a
 moment
* remove the `hubot-heroku-keepalive` line from `external_scripts.json` and also
 remove the related npm module (it causes errors on other platforms):

  npm uninstall --save hubot-heroku-keepalive

In preparation for working with Bluemix, install the [Cloud Foundry
CLI](https://github.com/cloudfoundry/cli/releases), and create a [Bluemix
Account](http://bluemix.net).

First we need to define a `manifest.yml` file in the root directory. The
contents of the manifest at the bare minimum should look like:

```yml
applications:
- name: myVeryOwnHubot
  command: ./bin/hubot --adapter slack
  instances: 1
  memory: 512M
```

In this example, we're using the slack adapter, if you choose slack as your
adapter when creating a hubot this will work, otherwise add the `hubot-slack`
module to your `package.json`.  **Change the name of your hubot in the
`manifest.yml` file** because otherwise your application will clash with someone
else's who already deployed an app called this!  There are many more useful
things you can change about your hubot using the manifest file, so check out
[these docs](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html)
for more information.

You then need to connect your hubot project to Bluemix:

```sh
$ cd your_hubot_project
$ cf api https://api.ng.bluemix.net
$ cf login
```

Note that the `cf api` command changes per Bluemix region so to deploy somewhere
other than "US South", replace this api as appropriate.  The `cf login` command
will prompt you with your login credentials.

Next, we need to set up our environment variables, but we need to create the app
first.  It won't work properly without the environment variables it needs, so
we'll first of all use the `--no-start` flag to deploy but not attempt to start
it.

```sh
$ cf push NAME_OF_YOUR_HUBOT_APP --no-start
```

Now the app exists, we can set its environment variables.  To access slack,
you'll need a slack token from the "Apps and Integrations" page; it's visible
when you go to create a slackbot.  Copy that token and set it as an environment
variable called `HUBOT_SLACK_TOKEN`, like this:

```sh
$ cf set-env NAME_OF_YOUR_HUBOT_APP HUBOT_SLACK_TOKEN TOKEN_VALUE
```

If you have other environment variables to set, such as configuring the
`REDIS_URL` for `hubot-redis-brain`, this is a good time to do that.

Finally, we're ready to go!  Deploy "for real" this time:

```sh
$ cf push NAME_OF_YOUR_HUBOT_APP
```

You should see your bot connect to slack!

### Further Reading

  - [Deploying Cloud Foundry Apps To Bluemix](https://www.ng.bluemix.net/docs/cfapps/runtimes.html)
  - [Neploying Node.js Apps to Bluemix](https://www.ng.bluemix.net/docs/starters/nodejs/index.html)
  - [Setting up your manifest](https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html)
  - [Understanding the CF CLI](https://www.ng.bluemix.net/docs/cli/reference/cfcommands/index.html)
  - [Setting up a Build Pipleline in Bluemix](https://www.ng.bluemix.net/docs/#services/DeliveryPipeline/index.html#getstartwithCD)

### Troubleshooting

**Bot doesn't connect**

Check your logs for more information using the command `cf logs YOUR_APP_NAME
--recent`.  If you have NodeJS installed locally, you can also try running the
bot on your local machine to inspect any output: simply do `bin/hubot` from the
top level of the project.

**Bot crashes repeatedly**

It is sometimes necessary to to assign more memory to your hubot, depending
which plugins you are using (if your app crashes with error 137, try increasing
the memory limit).



================================================
FILE: docs/deploying/unix.md
================================================
---
title: Deploying to Unix
layout: layouts/docs.html
permalink: /deploying/unix.html
---

# Deploying to Unix

Because there are so many variations of Linux, and more generally UNIX, it's
difficult for the hubot team to have canonical documentation for installing and
deploying it to every version out there. So, this is an attempt to give an
overview of what's needed to get deploying.

There are 3 primary things to deploying and running hubot:

  * node and npm
  * a way to get source code updated on the server
  * a way to start hubot, start it up if it crashes, and restart it when code
    updates

## node and npm

To start, your UNIX server will need node and npm. Check out the node.js wiki
for [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).

## Updating code on the server

The simplest way to update your hubot's code is going to be to have a git
checkout 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
feel a dirty hack, but it works when you are starting out.

If you have a Ruby background, you might be more comfortable using
[capistrano](https://github.com/capistrano/capistrano).

If you have a [Chef](http://www.chef.io/chef/) background, there's a
[deploy](https://docs.chef.io/resource_deploy.html) resource for managing
deployments.

## Starting, stopping, and restarting hubot

Every hubot install has a `bin/hubot` script to handle starting up the hubot.
You can run this command from your git checkout on the server, but there are some problems you can encounter:

* you disconnect, and hubot dies
* hubot dies, for any reason, and doesn't start again
* it doesn't start up at boot automatically

For handling you disconnecting, you can start with running `bin/hubot` in
[screen session](http://www.gnu.org/software/screen/) or with
[nohup](http://linux.die.net/man/1/nohup).

For handling hubot dying, and restarting it automatically, you can imagine
running `bin/hubot` in a
[bash while loop](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-7.html#ss7.3). But
really, you probably want some process monitoring using tools like
[monit](http://mmonit.com/monit/),
[god](http://godrb.com/),
[bluepill](https://github.com/arya/bluepill),
[upstart](http://upstart.ubuntu.com/),
[runit](http://smarden.org/runit/),
[systemd](http://freedesktop.org/wiki/Software/systemd/).

For starting at boot, you can create an init script appropriate for your UNIX
distribution. If you are using one of the process monitoring tools above, make
sure it boots at startup. See the [examples](https://github.com/github/hubot/tree/main/examples)
for configuration examples.

## Recommendations

This document has been deliberately light on strong recommendations. At a high
level though, it's strongly recommended to avoid anything that is overly manual
and non-repeatable. That would mean using your OS's packages and tools whenever
possible, and having a proper deploy tool to update hubot, and process
management to keep hubot running.


================================================
FILE: docs/deploying/windows.md
================================================
---
title: Deploying to Windows
layout: layouts/docs.html
permalink: /deploying/windows.html
---

# Deploying to Windows

Hasn't been fully tested - YMMV

There are 4 primary steps to deploying and running hubot on a Windows machine:

* node and npm
* a way to get source code updated on the server
* setting up environment variables for hubot
* a way to start hubot, start it up if it crashes, and restart it when code updates

## node and npm

To start, your windows server will need node and npm.
The best way to do this is with [chocolatey](http://chocolatey.org) using the [nodejs.install](http://chocolatey.org/packages/nodejs.install) package.
I'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\"`

Your 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.

## Updating code on the server

To 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.

## Setting up environment vars

You 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:

    [Environment]::SetEnvironmentVariable("HUBOT_ADAPTER", "Campfire", "Machine")

This 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.

## Starting, stopping, and restarting hubot

Every hubot install has a `bin/hubot` script to handle starting up the hubot.
You can run this command directly from your hubot folder by typing the following:

    .\bin\hubot –adapter campfire

There are a few issues if you call it manually, though.

* you disconnect, and hubot dies
* hubot dies, for any reason, and doesn't start again
* it doesn't start up at boot automatically

To 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:

    Write-Host "Starting Hubot Watcher"
    While (1)
    {
        Write-Host "Starting Hubot"
        Start-Process powershell -ArgumentList ".\bin\hubot –adapter slack" -wait
    }

Remember to allow local unsigned PowerShell scripts if you are using the .ps1 file to run hubot. Run this command in an Administrator PowerShell window.

    Set-ExecutionPolicy RemoteSigned

You can set this .ps1 as scheduled task on boot if you like or some other way to start your process.

## Expanding the documentation

Not yet fleshed out. [Help contribute by submitting a pull request, please?](https://github.com/github/hubot/pull/new/main)


================================================
FILE: docs/deploying.md
================================================
---
title: Deploying
layout: layouts/docs.html
permalink: /deploying.html
---

# Deploying

- [Azure](./deploying/azure.html)
- [Bluemix](./deploying/bluemix.html)
- [Unix](./deploying/unix.html)
- [Windows](./deploying/windows.html)


================================================
FILE: docs/designs/commands.md
================================================
You are an expert javascript and Node.js engineer implementing a backwards-compatible command subsystem for Hubot.

IMPORTANT: 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.

High-level goal:
Add 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.

Constraints:
- Minimal dependencies (prefer none).
- Defensive coding: invalid input must not crash the bot.
- Design for adapter variability (Slack/Discord/etc), but implement renderer as simple text initially.
- Existing scripts must run unchanged.

Behavioral requirements (what must be true):
1.	Backwards compatibility

- A bot with legacy listeners (respond/hear) behaves the same after adding robot.commands.
- The new subsystem must not consume arbitrary messages. It only intercepts:
a) confirmation replies (yes/no/cancel) when a pending confirmation exists for that user+room

2. New API surface
implement robot.commands with these capabilities.
- Registration with spec and optional opts - handle unregister and update
- get command by id, list commands with a filter, get help for a command by id
- parse the text into a ParsedInvocation that is nullable
- validate with command id, rawArgs and context meta
- execute the command by command id, raw args and execution context asynchronously
- invoke the invocation text with execution context asynchronously that will parse -> validate -> execute pipeline
- propse a given propsal with execution context asynchronously
- confirm the users reply text with the execution context asynchronously
- prending proposal store keyed by userid + roomid with a TTL

# 3. Command Shape
A command is registered as:
- id - string, uniqueue, required)
- description - string
- aliases - string[], optional (alternate names that resolve to this command id)
- examples - string[], optional
- args schema (optional):
    - arg name: {type, required?, default?, values?}
- side effects - string[], optional
- confirm policy - "never" | "if_ambiguous" | "always" (optional)
- permissions - { rooms?: string[], roles?: string[]} (optional)
- handler async function with `{ args, context }` (required)

# 4. Canonical invocation parsing
Parse messages like:
- tickets.create -- title "VPN down" --priority high --assignee matt --room #ops
    - also support key:value tokens
        - tickets.create title:"VPN down" prioriy:high
    - also support command aliases (resolved by parse() to their canonical id)
        - if tickets.create has alias 'ticket.new', then ticket.new also works
- Must support quoted strings, bare words, boolean flags

# 5. Validation and normalization

- Apply defaults from schema
- Enforce required args
- Validate enums
- Support types: string, number, boolean, enum, user, room, date
- Built-in resolvers:
    - user: attempt to map a string to a Hubot user from robot.brain.users()
    - room: validate #room format
    - date: support today, tomorrow, ISO, and “YYYY-MM-DD HH:mm” (keep it light); Return ValidationResult with:
        - ok true + normalized args
        - ok false + missing + errors + (if possible) one clarifyingQuestion

# 6. Permissions
- If permissions.rooms exists, deny execution outside allowed rooms.
- Roles are optional; implement a pluggable permission provider hook. Default allow if not configured.

# 7. Confirmation
- If sideEffects exist OR confirm=always, require confirmation before executing.
- Confirmation uses propose→confirm pipeline:
    - propose() returns a preview of the canonical invocation and asks “Run it? (yes/no)”
    - confirm() executes on yes, cancels on no/cancel
- confirm() only triggers if there is a pending confirmation for that user+room key.

# 8. Middleware Integration

The command subsystem integrates with Hubot's receive middleware to intercept and handle commands:

- **Confirmation Middleware**: Intercepts confirmation replies (yes/no/cancel) when a pending proposal exists for that user+room
- **Invocation Middleware**: Intercepts command invocations (text starting with command ID or alias) to parse, validate, and execute

These 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.

# 9. Bridging (optional but useful)

Allow 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).

# 10. Observability

Emit events through an EventEmitter on robot.commands:
- commands:registered
- commands:updated
- commands:alias_collision_detected
- commands:invocation_parsed
- commands:validation_failed
- commands:proposal_created
- commands:proposal_confirm_requested
- commands:proposal_confirmed
- commands:proposal_cancelled
- commands:executed
- commands:permission_denied
- commands:error

**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.

# TDD plan you must follow:
A) Write tests first. Include at least these test scenarios:
- parse() correctly parses quoted args and key:value args- parse() resolves command aliases to canonical command ids- validate() applies defaults and rejects invalid enum
- user resolver maps a name to a brain user record (use a stub brain)
- propose() creates pending confirmation for side-effect command
- confirm(“yes”) executes, confirm(“no”) cancels
- confirmation listener does nothing if no pending exists

B) Only after tests exist, implement the smallest code to pass them.

C) Refactor as needed to keep code clean, depulication, software design that provides affordances.

Environment for tests:
- Use Node’s built-in test runner (node:test) unless you have a strong reason not to.
- Create minimal “fake robot” and “fake message” objects as needed; do not require a real chat adapter. You can create a dumby one.
- Because we're in an asynchronous environment, tests often "hange". so make sure to use --test-timeout when running node --test or npm test.

Output format:
- Provide the full repository output (tests + implementation + a short README).
- Do not over-engineer; build a clean minimal core with extension hooks.


================================================
FILE: docs/docs.md
================================================
---
title: Getting Started With Hubot
layout: layouts/docs.html
published: 2023-10-10T19:25:22.000Z
permalink: /docs.html
---

# Getting Started With Hubot

You 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.

```sh
npx hubot --create myhubot
```

Now open `package.json` in your code editor and add a `start` property to the `scripts` property:

```json
{
...
    "scripts": {
        "start": "hubot"
    }
...
}
```

Start 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.

Your terminal should look like:

```sh
Hubot>
```

Typing `help` will list some default commands that Hubot's default adapter, Shell, can handle.

```sh
Hubot> help
usage:
history 
exit, \q - close Shell and exit
help, \? - print this usage
clear, \c - clear the terminal screen
Hubot>
```

Changing your Hubot instances name will reduce confusion down the road, so set the `--name` argument in the `hubot` command:

```json

{
...
    "scripts": {
        "start": "hubot --name sam"
    }
...
}
```

Your 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.

```sh
sam> SAM help
sam> sam help
sam> @sam help
sam> sam: help
```

## <a name="scripts">Scripts</a>

Hubot'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:

```sh
$ npm search hubot-scripts github
NAME                  DESCRIPTION
hubot-deployer        Giving Hubot the ability to deploy GitHub repos to PaaS providers hubot hubot-scripts hubot-gith
hubot-gh-release-pr   A hubot script to create GitHub's PR for release
hubot-github          Giving Hubot the ability to be a vital member of your github organization
```

To use a script from an NPM package:

1. Run `npm install <package-name>` in the codebase directory to install it.
2. Add the package name to a file called `external-scripts.json`.

```json
["hubot-diagnostics", "hubot-help"]
```

3. 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.

You 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).

## Adapters

Hubot 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).

## Deploying

You are able to deploy hubot to a UNIX-like system or Windows. Please note the support for deploying to Windows isn't officially supported.

* [Deploying Hubot onto Azure](./deploying/azure.html)
* [Deploying Hubot onto Bluemix](./deploying/bluemix.html)
* [Deploying Hubot onto Unix](./deploying/unix.html)
* [Deploying Hubot onto Windows](./deploying/windows.html)

## Redis

Hubot 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.

An example `external-scripts.json` file might look like the following:

```json
["hubot-redis-brain", "hubot-help", "hubot-diagnostics"]
```

or

```json
{
    "hubot-redis-brain": "some arbitrary value",
    "hubot-help": "this value will be sent to the hubot-help module",
    "hubot-diagnostics": {
        "name": "test",
        "age": "21"
    }
}
```

## Patterns

Using 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.

================================================
FILE: docs/implementation.md
================================================
---
title: Implementation Notes
layout: layouts/docs.html
permalink: /implementation.html
---

# Implementation

For the purpose of maintainability, several internal flows are documented here.

## Message Processing

When 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.

`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).

`middleware.execute` `next` returns to `listener.call`, which executes the matched Listener's callback and then calls the `robot.receive` callback.

Inside 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).

If 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.

## Listeners

Listeners are registered using several functions on the `robot` object: `hear`, `respond`, `enter`, `leave`, `topic`, and `catchAll`.

A 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.

Listener callbacks are assumed to be synchronous.

## Middleware

There are two primary entry points for middleware:

1. `robot.listenerMiddleware` - registers a new piece of middleware in a global array
2. `middleware.execute` - executes all registered middleware in order

## Persistence

### Brain

Hubot has a memory exposed as the `robot.brain` object that can be used to store and retrieve data.
Furthermore, Hubot scripts exist to enable persistence across Hubot restarts.
`hubot-redis-brain` is such a script and uses a backend Redis server.

By default, the brain contains a list of all users seen by Hubot.
Therefore, 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.

### Datastore

Hubot's optional datastore, exposed as the `robot.datastore` object, provides a more robust persistence model. Compared to the brain, the datastore:

1. Is always (instead of optionally) backed by a database
2. Fetches data from the database and stores data in the database on every request, instead of periodically persisting the entire in-memory brain.

The 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.


================================================
FILE: docs/index.html
================================================
{{#> layouts/main.html}}
<meta name="title" content="Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency."/>
<meta name="canonical" content="https://hubotio.github.io/hubot"/>
<section class="main">
    <header>
        <div class="insides">
            <a href="docs.html" class="button"><strong>View Hubot's Documentation</strong>
                <span>(Learn about getting started, etc.)</span>
            </a>
            <a href="https://github.com/hubotio/hubot/" class="button">
                <strong>View Hubot's Source Code</strong><span>(via https://github.com/hubotio/hubot/.)</span>
            </a>
        </div>
        <div class="schematics">
            <div class="schematic">
                <img src="assets/images/layout/schematic.svg" alt="hubot schematic" width="960" class="schematic-img" />
                <p><b>Fig. 1</b> &mdash; Hubot Schematics</p>
            </div>
        </div>
    </header>
    <section class="about">
        <article>
            <h2>What is Hubot?</h2>
            <p><strong>Hubot is your friendly robot sidekick.</strong> Install him in your company to dramatically improve employee efficiency.</p>

            <h2>No seriously, what is Hubot?</h2>
            <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>
            <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>

            <h2>What can Hubot do?</h2>
            <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>
            <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>

            <h2>How do I write my own Hubot scripts?</h2>
            <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>
            <p>If you write a Hubot script for taking over the world, please let us know.</p>
        </article>
        <aside>
            <div class="frame">
                <div class="letter">
                    <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>
                </div>
            </div>
            <div class="screenshot">
                <img src="assets/images/screenshots/dangerroom-full.png" alt="Example of Hubot in action" />
            </div>
        </aside>
    </section>
</section>
{{/layouts/main.html}}


================================================
FILE: docs/layouts/docs.html
================================================
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <base href="{{ base.href }}"/>
        <link rel="stylesheet" href="assets/stylesheets/application.css"/>
        <meta name="viewport" content="initial-scale=1"/>
        <title>{{ title }}</title>
        <meta name="generator" content="Hubot" />
        <meta property="og:title" content="{{ title }}" />
        <meta property="og:locale" content="en_US" />
        <meta name="description" content="{{ description }}" />
        <meta property="og:description" content="{{ description }}" />
        <link rel="canonical" href="{{ permalink }}" />
        <meta property="og:url" content="{{ permalink }}" />
        <meta property="og:site_name" content="Hubot" />
        <meta property="og:image" content="https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
        <script type="application/ld+json">
            {"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"}
        </script>
    </head>
    <body class="docs">
        <div class="container">
            <header class="clearfix">
                <a href="{{ base.href }}">
                    <div class="hubot-avatar">
                        <img src="assets/images/layout/hubot-avatar@2x.png" alt="hubot logo" class="hubot-avatar-img" width="106" />
                    </div>
                </a>
                <h1>Hubot Documentation</h1>
            </header>
            <section class="main">
                <nav class="docs-nav">
                    <ul class="docs-list">
                        <li><a class="docs-link{{current permalink '/docs.html'}}" href="docs.html">Overview</a></li>
                        <li><a class="docs-link{{current permalink '/scripting.html'}}" href="scripting.html">Scripting</a></li>
                        <li><a class="docs-link{{current permalink '/patterns.html'}}" href="patterns.html">Patterns</a></li>
                        <li><a class="docs-link{{current permalink '/implementation.html'}}" href="implementation.html">Implementation</a></li>
                        <li>
                            <a class="docs-link{{current permalink '/adapters.html'}}" href="adapters.html">Adapters</a>
                            <ul class="subnav">
                                <li class="subpage"><a href="adapters/campfire.html" class="docs-link{{current permalink '/adapters/campfire.html'}}">Campfire</a></li>
                                <li class="subpage"><a href="adapters/shell.html" class="docs-link{{current permalink '/adapters/shell.html'}}">Shell</a></li>
                                <li class="subpage"><a href="adapters/development.html" class="docs-link{{current permalink '/adapters/development.html'}}">Development</a></li>
                            </ul>
                        </li>
                        <li>
                            <a class="docs-link{{current permalink '/deploying.html'}}" href="deploying.html">Deploying</a>
                            <ul class="subnav">
                                <li class="subpage"><a href="deploying/unix.html" class="docs-link{{current permalink '/deploying/unix.html'}}">Unix</a></li>
                                <li class="subpage"><a href="deploying/windows.html" class="docs-link{{current permalink '/deploying/windows.html'}}">Windows</a></li>
                                <li class="subpage"><a href="deploying/azure.html" class="docs-link{{current permalink '/deploying/azure.html'}}">Azure</a></li>
                            </ul>
                        </li>
                    </ul>
                </nav>
                <main>
{{> @partial-block }}
                </main>
            </section>
        </div>
        <footer class="footer clearfix">
            <span class="mega-icon mega-icon-invertocat"></span>
            <p class="right">Built with &lt;3 by friends of Hubot</p>
        </footer>
    </body>
    <script>hljs.highlightAll()</script>
</html>


================================================
FILE: docs/layouts/main.html
================================================
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <base href="{{ base.href }}"/>
        <link rel="stylesheet" href="assets/stylesheets/application.css"/>
        <meta name="viewport" content="initial-scale=1"/>
        <title>{{ title }}</title>
        <meta name="generator" content="Hubot" />
        <meta property="og:title" content="Hubot" />
        <meta property="og:locale" content="en_US" />
        <meta name="description" content="Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency." />
        <meta property="og:description" content="Hubot is your friendly robot sidekick. Install him in your company to dramatically improve employee efficiency." />
        <link rel="canonical" href="{{ canonical }}" />
        <meta property="og:url" content="{{ canonical }}" />
        <meta property="og:site_name" content="Hubot" />
        <meta property="og:image" content="https://hubotio.github.io/hubot/assets/images/screenshots/dangerroom-full.png" />
        <script type="application/ld+json">
            {"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"}
        </script>
    </head>
    <body class="home">
        <div class="container">
            <header class="clearfix">
                <div class="frame">
                    <div class="hubot-avatar">
                        <img src="assets/images/layout/hubot-avatar@2x.png" alt="hubot logo" class="hubot-avatar-img" width="106" />
                    </div>
                </div>
                <h1>Hubot <span>(note: it's prounounced <b>hew-bot</b>)</span></h1>
                <h2>A Customizable,<br />Life Embetterment Robot</h2>
                <p>Commissioned by <a href="http://github.com" class="logo">GitHub</a></p>
            </header>
            {{> @partial-block }}
        </div>
        <footer class="footer clearfix">
            <span class="mega-icon mega-icon-invertocat"></span>
            <p class="right">Built with &lt;3 by friends of Hubot</p>
        </footer>
    </body>
</html>


================================================
FILE: docs/patterns.md
================================================
---
title: Patterns
layout: layouts/docs.html
should_publish: yes
published: 2023-10-10T19:25:22.000Z
permalink: /patterns.html
---

# Patterns

Shared patterns for dealing with common Hubot scenarios.

## Renaming the Hubot instance

When 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:

* listen to all messages that start with the old name
* reply to the user letting them know about the new name

Setting this up is very easy:

1. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `rename-hubot.js`
2. Add the following code, modified for your needs:

```javascript
//  Description:
//    Tell people hubot's new name if they use the old one

//  Commands:
//    None

export default async (robot) => {
  robot.hear(/^hubot:? (.+)/i, async (res) => {
    let response = `Sorry, I'm a diva and only respond to ${robot.name}`
    response += robot.alias ? ` or ${robot.alias}` : ''
    return await res.reply(response)
  })
}
```

In the above pattern, modify both the hubot listener and the response message to suit your needs.

Also, 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.

## Deprecating or Renaming Listeners

If 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.

This pattern is similar to the Renaming the Hubot Instance pattern above:

* listen to all messages that match the old command
* reply to the user letting them know that it's been deprecated

Here is the setup:

1. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `deprecations.js`
2. 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:

```javascript
// Description:
//   Tell users when they have used commands that are deprecated or renamed
//
// Commands:
//   None
//
export default async (robot) => {
  robot.respond(/help\s*(.*)?$/i, async (res) => {
    return await res.reply('That means nothing to me anymore. Perhaps you meant "docs" instead?')
  })
}

```

## Preventing Hubot from Running Scripts Concurrently

Sometimes 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.

To 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.

Setting up the lock looks something like this:

```javascript
export default async (robot) => {
  robot.brain.on('loaded', () => {
    // Clear the lock on startup in case Hubot has restarted and Hubot's brain has persistence (e.g. redis).
    // We don't want any orphaned locks preventing us from running commands.
    robot.brain.remove('yourLockName')
  })

  robot.respond(/longrunningthing/i, async (msg) => {
    const lock = robot.brain.get('yourLockName')
    if (lock) {
      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}.`)
    }

    robot.brain.set('yourLockName', msg.message)  // includes user, room, etc about who locked

    try {
      await yourLongClobberingAsyncThing()
      // Clear the lock
      robot.brain.remove('yourLockName')
      await msg.reply('Finally Done')
    } catch (e) {
      console.error(e)
    }
  })
}
```

## Forwarding all HTTP requests through a proxy

In 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.

Due 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.

1. Install ProxyAgent. `npm install proxy-agent`
2. Create a [bundled script](scripting.html) in the `scripts/` directory of your Hubot instance called `proxy.js`
3. Add the following code, modified for your needs:

```javascript
import proxy from 'proxy-agent'
export default async (robot) => {
  robot.globalHttpOptions.httpAgent  = proxy('http://my-proxy-server.internal', false)
  robot.globalHttpOptions.httpsAgent = proxy('http://my-proxy-server.internal', true)
}
```

## Dynamic matching of messages

In 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.

In 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.

For example, the [factoid lookup command](https://github.com/github/hubot-scripts/blob/bd810f99f9394818a9dcc2ea3729427e4101b96d/src/scripts/factoid.coffee#L95-L99) could be reimplemented as:

```javascript
// use case: Hubot>fact1
// This listener doesn't require you to type the bot's name first

import {TextMessage} from '../src/message.mjs'
export default async (robot) => {
    // Dynamically populated list of factoids
    const facts = {
        fact1: 'stuff',
        fact2: 'other stuff'
    }
    robot.listen(
        // Matcher
        (message) => {
            // Check that message is a TextMessage type because
            // if there is no match, this matcher function will 
            // be called again but the message type will be CatchAllMessage
            // which doesn't have a `match` method.
            if(!(message instanceof TextMessage)) return false
            const match = message.match(/^(.*)$/)
            // Only match if there is a matching factoid
            if (match && match[1] in facts) {
                return match[1]
            } else {
                return false 
            }
        },
        // Callback
        async (res) => {
            const fact = res.match
            await res.reply(`${fact} is ${facts[fact]}`)
        }
    )
}
```

## Restricting access to commands

One 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.

There are a variety of different patterns for restricting access that you can follow depending on your specific needs:

* Two buckets of access: full and restricted with include/exclude list
* Specific access rules for every command (Role-based Access Control)
* Include/exclude listing commands in specific rooms

### Simple per-listener access

In 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".

Once you have segregated the listeners, you need to make some tradeoff decisions around include/exclude users and listeners.

The 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.

* 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).
* 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.

The 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.

* 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).
* 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).

As 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.

Once 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.

Example: inclusion list of users given access to selectively restricted power commands

```javascript
const POWER_COMMANDS = [
    'deploy.web' // String that matches the listener ID
]

// Change name to something else to see it reject the command.
const POWER_USERS = [
    'Shell' // String that matches the user ID set by the adapter
]
  
export default async (robot) => {
  robot.listenerMiddleware(async (context) => {
      if (POWER_COMMANDS.indexOf(context.listener.options.id) > -1) {
          if (POWER_USERS.indexOf(context.response.message.user.name) > -1){
              // User is allowed access to this command
              return true
          } else {
              // Restricted command, but user isn't in whitelist
              await context.response.reply(`I'm sorry, @${context.response.message.user.name}, but you don't have access to do that.`)
              return false
          }
      } else {
          // This is not a restricted command; allow everyone
          return true
      }
  })

  robot.listen(message => {
      return true
  }, {id: 'deploy.web'},
  async res => {
      await res.reply('Deploying web...')
  })
}
```

Remember that middleware executes for ALL listeners that match a given message (including `robot.hear(/.+/)`), so make sure you include them when categorizing your listeners.

### Specific access rules per listener

For larger organizations, a binary categorization of access is usually insufficient and more complex access rules are required.

Example access policy:
* Each development team has access to cut releases and deploy their service
* The Operations group has access to deploy all services (but not cut releases)
* The front desk cannot cut releases nor deploy services

Complex policies like this are currently best implemented in code directly.

### Specific access rules per room

Organizations 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.

Work on generalized exlusion list solution is [ongoing](https://github.com/kristenmills/hubot-command-blacklist). An inclusive list soultion could take a similar approach.

## Use scoped npm packages as adapter

It is possible to [install](https://docs.npmjs.com/cli/v7/commands/npm-install) package under a custom alias:

```bash
npm install <alias>@npm:<name>
```

So for example to use `@foo/hubot-adapter` package as the adapter, you can:

```bash
npm install hubot-foo@npm:@foo/hubot-adapter

bin/hubot --adapter foo
```


================================================
FILE: docs/scripting.md
================================================
---
title: Scripting
layout: layouts/docs.html
should_publish: yes
published: 2023-10-10T19:25:22.000Z
permalink: /scripting.html
---

# Scripting

Hubot 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!

## Anatomy of a script

When 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:

* live in a directory on the Hubot script load path (`src/scripts` and `scripts` by default)
* be a `.js` or `.mjs` file
* export a function whos signature takes 1 parameter (`robot`)

By export a function, we just mean:

```javascript
// .mjs
export default async robot => {
  // your code here
}
```

```javascript
// .js
module.exports = async robot => {
  // your code here
}
```

The `robot` parameter is an instance of your robot friend. At this point, we can start scripting up some awesomeness.

## Adding Configuration

The loading code loads files as the following and in this order:

- ./configuration <- so you can add configuration options to the `robot` instance that the Adapters can then use.
- Then it loads the adapter
- ./scripts
- ./src/scripts
- Then the modules defined in `external-scripts.json`

## Hearing and responding

Since 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:

```javascript
// .mjs
export default async robot => {
  robot.hear(/badger/i, async res => {
    // your code here
  })

  robot.respond(/open the pod bay doors/i, async res => {
    // your code here
  }
}
```

The `robot.hear(/badger/)` callback is called anytime a message's text matches. For example:

* Stop badgering the witness
* badger me
* what exactly is a badger anyways

The `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:

*  hal open the pod bay doors
* HAL: open the pod bay doors
* @HAL open the pod bay doors
* /open the pod bay doors

It wouldn't be called for:

* HAL: please open the pod bay doors
   *  because its `respond` is expecting the text to be prefixed with the robots name
*  has anyone ever mentioned how lovely you are when you open the pod bay doors?
   * because it lacks the robot's name at the beginning

## Send & reply

The `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:

```javascript
// .mjs
export default async robot => {
  robot.hear(/badger/i, async res => {
    res.send(`Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS`)
  }

  robot.respond(/open the pod bay doors/i, async res => {
    res.reply(`I'm afraid I can't let you do that.`)
  }

  robot.hear(/I like pie/i, async res => {
    res.emote('makes a freshly baked pie')
  }
}
```

The `robot.hear(/badgers/)` callback sends a message exactly as specified regardless of who said it, "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS".

If 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."

## Messages to a room or user

Messages can be sent to a specified room or user using the messageRoom function.

```javascript
// .mjs
export default async robot => {
  robot.hear(/green eggs/i, async res => {    
    const room = 'mytestroom'
    await robot.messageRoom(room, 'I do not like green eggs and ham.  I do not like them Sam-I-Am.')
  }
}
```

User 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.

```javascript
  robot.respond(/I don't like sam-i-am/i, async res => {
    const room = 'joemanager'
    await robot.messageRoom(room, 'Someone does not like Dr. Seus')
    await res.reply('That Sam-I-Am\nThat Sam-I-Am\nI do not like\nthat Sam-I-Am')
  }

  robot.hear(/Sam-I-Am/i, async res => {
    const room = res.envelope.user.name
    await robot.messageRoom(room, 'That Sam-I-Am\nThat Sam-I-Am\nI do not like\nthat Sam-I-Am')
  }
```

## Capturing data

So 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:

```javascript
  robot.respond(/open the (.*) doors/i, async res => {
    // your code here
  }
```

If 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:

```javascript
  robot.respond(/open the (.*) doors/i, async res => {
    const doorType = res.match[1]
    if (doorType == 'pod bay') {
      await res.reply(`I'm afraid I can't let you do that.`)
    } else {
      await res.reply(`Opening ${doorType} doors`)
    }
  }
```

## Making HTTP calls (please use `fetch` instead)

Hubot 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:


```javascript
  robot.http('https://midnight-train').get()((err, res, body) => {
      // your code here
  })
```

A post looks like:

```javascript
  const data = JSON.stringify({
    foo: 'bar'
  })
  robot.http('https://midnight-train')
    .header('Content-Type', 'application/json')
    .post(data)((err, res, body) => {
      // your code here
    })
```


`err` is an error encountered on the way, if one was encountered. You'll generally want to check for this and handle accordingly:

```javascript
  robot.http('https://midnight-train')
    .get()((err, res, body) => {
      if (err){
        return res.send `Encountered an error :( ${err}`
      }
      // your code here, knowing it was successful
    })
```

`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:

```javascript
  robot.http('https://midnight-train')
    .get() ((err, res, body) => {
      // pretend there's error checking code here
      if (res.statusCode <> 200)
        return res.send(`Request didn't come back HTTP 200 :(`)

      const rateLimitRemaining = res.getHeader('X-RateLimit-Limit') ?  parseInt(res.getHeader('X-RateLimit-Limit')) : 1
      if (rateLimitRemaining && rateLimitRemaining < 1)
        return res.send('Rate Limit hit, stop believing for awhile')

      // rest of your code
    }
```

`body` is the response's body as a string, the thing you probably care about the most:

```javascript
  robot.http('https://midnight-train')
    .get()((err, res, body) => {
      // error checking code here
      res.send(`Got back ${body}`)
    })
```

### JSON

If 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`:

```javascript
  robot.http('https://midnight-train')
    .header('Accept', 'application/json')
    .get()((err, res, body) => {
      // error checking code here
      const data = JSON.parse(body)
      res.send(`${data.passenger} taking midnight train going ${data.destination}`)
    })
```

It'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.

```javascript
  robot.http('https://midnight-train')
    .header('Accept', 'application/json')
    .get()((err, res, body) => {
      // err & res status checking code here
      if (res.getHeader('Content-Type') != 'application/json'){
        return res.send(`Didn't get back JSON :(`)
      }
      let data = null
      try {
        data = JSON.parse(body)
      } catch (error) {
        res.send(`Ran into an error parsing JSON :(`)
      }

      // your code here
    })
```

### XML

XML 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:

* [xml2json](https://github.com/buglabs/node-xml2json) (simplest to use, but has some limitations)
* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)
* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)

### Screen scraping

For 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:

* [cheerio](https://github.com/MatthewMueller/cheerio) (familiar syntax and API to jQuery)
* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)


### Advanced HTTP and HTTPS settings

As 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.

If 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`:

```javascript
  const options = {
    rejectUnauthorized: false // don't verify server certificate against a CA, SCARY!
  }
  robot.http('https://midnight-train', options)
```

In 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.

## Random

A 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:

```javascript
const lulz = ['lol', 'rofl', 'lmao']
res.send(res.random(lulz))
```

## Topic

Hubot can react to a room's topic changing, assuming that the adapter supports it.

```javascript
// .mjs
export default async robot => {
  robot.topic(async res => {
    await res.send()`${res.message.text}? That's a Paddlin'`)
  })
}
```

## Entering and leaving

Hubot can see users entering and leaving, assuming that the adapter supports it.

```javascript
// .mjs
const enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
const leaveReplies = ['Are you still there?', 'Target lost', 'Searching']

export default async robot => {
  robot.enter(async res => {
    await res.send(res.random(enterReplies))
  })
  robot.leave(async res => {
    await res.send(res.random(leaveReplies))
  })
}
```

## Custom Listeners

While 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.

The 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`.

```javascript
// .mjs
export default async robot =>{
  robot.listen(
    (message) => {
      // Match function
      // only match messages with text (ie ignore enter and other events)
      if(!message?.text) return

      // Occassionally respond to things that Steve says
      return message.user.name == 'Steve' && Math.random() > 0.8
    },
    async res => {
      // Standard listener callback
      // Let Steve know how happy you are that he exists
      await res.reply(`HI STEVE! YOU'RE MY BEST FRIEND! (but only like ${res.match * 100}% of the time)`)
    }
  )
}
```

See [the design patterns document](patterns.html#dynamic-matching-of-messages) for examples of complex matchers.

## Environment variables

Hubot 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.

```javascript
// .mjs
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING

export default async robot => {
  robot.respond(/what is the answer to the ultimate question of life/, async res => {
    await res.send(`${answer}, but what is the question?`)
  })
}
```

Take 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.

Here we can default to something:

```javascript
// .mjs
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING ?? 42

export default async robot => {
  robot.respond(/what is the answer to the ultimate question of life/, async res => {
    await res.send(`${answer}, but what is the question?`)
  })
}
```

Here we exit if it's not defined:

```javascript
// .mjs
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
if(!answer) {
  console.log(`Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again`)
  process.exit(1)
}

export default async robot => {
  robot.respond(/what is the answer to the ultimate question of life/, async res => {
    await res.send(`${answer}, but what is the question?`)
  })
}
```

And lastly, we update the `robot.respond` to check it:

```javascript
// .mjs
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING

export default async robot => {
  robot.respond(/what is the answer to the ultimate question of life/, async res => {
    if(!answer) {
      return await res.send('Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again')
    }
    await res.send(`${answer}, but what is the question?`)
  })
}
```

## Dependencies

Hubot 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:

```json
  "dependencies": {
    "hubot": "2.5.5",
    "lolimadeupthispackage": "1.2.3"
  },
```

by executing `npm i lolimadeupthispackage@1.2.3`.

If 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.

# Timeouts and Intervals

Hubot 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:

```javascript
// .mjs
export default async robot => {
  robot.respond(/you are a little slow/, async res => {
    setTimeout(async () => {
      await res.send(`Who you calling 'slow'?`)
    }, 60 * 1000)
  })
}
```

Additionally, 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:

```javascript
// .mjs
export default async robot => {
  robot.respond(/annoy me/, async res => {
    await res.send('Hey, want to hear the most annoying sound in the world?')
    setInterval(async () => {
      await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
    }, 1000)
  })
}
```

Both `setTimeout` and `setInterval` return the ID of the timeout or interval it created. This can be used to to `clearTimeout` and `clearInterval`.

```javascript
// .mjs
export default async robot => {
  let annoyIntervalId = null

  robot.respond(/annoy me/, async res => {
    if (annoyIntervalId) {
      return await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
    }

    await res.send('Hey, want to hear the most annoying sound in the world?')
    annoyIntervalId = setInterval(async () => {
      await res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
    }, 1000)
  }

  robot.respond(/unannoy me/, async res => {
    if (annoyIntervalId) {
      await res.send('GUYS, GUYS, GUYS!')
      clearInterval(annoyIntervalId)
      annoyIntervalId = null
    } else {
      await res.send('Not annoying you right now, am I?')
    }
  }
}
```

## HTTP Listener

Hubot 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`.

You 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`.

The 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.


```javascript
// .mjs
export default async robot => {
  // the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value
  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.
    const room = req.params.room
    const data = req.body?.payload ? JSON.parse(req.body.payload) : req.body
    const secret = data.secret

    await robot.messageRoom(room, `I have a secret: ${secret}`)

    res.send('OK')
  })
}
```

Test it with curl; also see section on [error handling](#error-handling) below.

```sh
# raw json, must specify Content-Type: application/json
curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general

# defaults Content-Type: application/x-www-form-urlencoded, must st payload=...
curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general
```

All 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).

## Events

Hubot 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`.

One 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.

```javascript
// src/scripts/github-commits.mjs
export default async robot => {
  robot.router.post('/hubot/gh-commits', (req, res) => {
    robot.emit('commit', {
        user: {}, //hubot user object
        repo: 'https://github.com/github/hubot',
        hash: '2e1951c089bd865839328592ff673d2f08153643'
    })
  })
}
```

```javascript
export default async robot => {
  robot.on('commit', async (commit) => {
    await robot.send(commit.user, `Will now deploy ${commit.hash} from ${commit.repo}!`)
    // deploy code goes here
  })
}
```

If 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.

## <a name="error-handling">Error Handling</a>

No 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.

```javascript
// src/scripts/does-not-compute.mjs
export default async robot => {
  robot.error(async (err, res) => {
    robot.logger.error('DOES NOT COMPUTE')

    if(res) {
      await res.reply('DOES NOT COMPUTE')
    }
  }
}
```

You 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.

Under 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.

Using previous examples:

```javascript
  robot.router.post()'/hubot/chatsecrets/:room', (req, res) => {
    const room = req.params.room
    let data = null
    try {
      data = JSON.parse(req.body.payload)
    } catch(err) {
      robot.emit('error', err)
    }

    // rest of the code here
  }

  robot.hear(/midnight train/i, (res) => {
    robot.http('https://midnight-train')
      .get()((err, res, body) => {
        if (err) {
          res.reply('Had problems taking the midnight train')
          robot.emit('error', err, res)
          return
        }
        // rest of code here
      })
  })
```

For 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.

## Documenting Scripts

Hubot scripts can be documented with comments at the top of their file, for example:

```javascript
// Description:
//   <description of the scripts functionality>
//
// Dependencies:
//   "<module name>": "<module version>"
//
// Configuration:
//   LIST_OF_ENV_VARS_TO_SET
//
// Commands:
//   hubot <trigger> - <what the respond trigger does>
//   <trigger> - <what the hear trigger does>
//
// Notes:
//   <optional notes required for the script>
//
// Author:
//   <github username of the original script author>
```

The 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.

When documenting commands, here are some best practices:

* 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.
* 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.
* 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
* Check out how man pages document themselves. In particular, brackets indicate optional parts, '...' for any number of parameters, etc.

The 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.

## <a name="persistence">Persistence</a>

Hubot 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`.

### Brain

```javascript
robot.respond(/have a soda/i, async res => {
  // Get number of sodas had (coerced to a number).
  const sodasHad = robot.brain.get('totalSodas') * 1 ?? 0

  if (sodasHad > 4) {
    await res.reply(`I'm too fizzy..`)
  } else {
    await res.reply('Sure!')
    robot.brain.set('totalSodas', sodasHad + 1)
  }
})

robot.respond(/sleep it off/i, async res => {
  robot.brain.set('totalSodas', 0)
  await res.reply('zzzzz')
}
```

If 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`.

```javascript
export default async robot => {
  robot.respond(/who is @?([\w .\-]+)\?*$/i, async res => {
    const name = res.match[1].trim()

    const users = robot.brain.usersForFuzzyName(name)
    if (users.length == 1) {
      const user = users[0]
      // Do something interesting here..
    }
    await res.send(`${name} is user - ${user}`)
  })
}
```

### Datastore

Unlike 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:

```javascript
robot.respond(/have a soda/i, async res => {
  // Get number of sodas had (coerced to a number).
  robot.datastore.get('totalSodas').then((value) => {
    const sodasHad = value * 1 ?? 0

    if (sodasHad > 4) {
      await res.reply(`I'm too fizzy..`)
    } else {
      await res.reply('Sure!')
      robot.brain.set('totalSodas', sodasHad + 1)
    }
  })
})

robot.respond(/sleep it off/i, async res => {
  await robot.datastore.set('totalSodas', 0)
  await res.reply('zzzzz')
})
```

The datastore also allows setting and getting values which are scoped to individual users:

```javascript
export default async robot ->

  robot.respond(/who is @?([\w .\-]+)\?*$/i, async res => {
    const name = res.match[1].trim()

    const users = robot.brain.usersForFuzzyName(name)
    if (users.length == 1) {
      const user = users[0]
      const roles = await user.get('roles')
      await res.send(`${name} is ${roles.join(', ')}`)
    }
  })
```

## Script Loading

There are three main sources to load scripts from:

* all scripts __bundled__ with your hubot installation under `scripts/` directory
* __community scripts__ specified in `hubot-scripts.json` and shipped in the `hubot-scripts` npm package
* scripts loaded from external __npm packages__ and specified in `external-scripts.json`

Scripts loaded from the `scripts/` directory are loaded in alphabetical order, so you can expect a consistent load order of scripts. For example:

* `scripts/1-first.js`
* `scripts/_second.js`
* `scripts/third.js`

# Sharing Scripts

Once 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.

## See if a script already exists

Start 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`.

## <a name="creating-a-script-package">Creating A Script Package</a>

Creating a script package for hubot is very simple. Start by running `npx hubot --create myhubot` to create your own instance.

`cd myhubot` and create a script. For example, if we wanted to create a hubot script called "my-awesome-script":

```sh
% npm hubot --create my-awesome-script
% cd my-awesome-script
% mkdir src
% touch src/AwesomeScript.mjs
```

Open `package.json` and add:

```json
"peerDependencies": {
  "hubot": ">=9"
},
```

If you are using git, the generated directory includes a .gitignore, so you can initialize and add everything:

```sh
% git init
% git add .
% git commit -m "Initial commit"
```

You 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)!

You'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.

# <a name="listener-metadata">Listener Metadata</a>

In 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.

The 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.

Additional extensions may define and handle additional metadata keys. For more information, see the [Listener Middleware section](#listener-middleware).

Returning to an earlier example:

```javascript
export default async robot => {
  robot.respond(/annoy me/, id:'annoyance.start', async res => {
    // code to annoy someone
  })

  robot.respond(/unannoy me/, id:'annoyance.stop', async res => {
    // code to stop annoying someone
  })
}
```

These scoped identifiers allow you to externally specify new behaviors like:
- authorization policy: "allow everyone in the `annoyers` group to execute `annoyance.*` commands"
- rate limiting: "only allow executing `annoyance.start` once every 30 minutes"

# Middleware

There are three kinds of middleware: Receive, Listener and Response.

Receive middleware runs once, before listeners are checked.
Listener middleware runs for every listener that matches the message.
Response middleware runs for every response sent to a message.

## Execution Process and API

Similar 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).

Middleware is called with:

- `context`
  - See the each middleware type's API to see what the context will expose.

`return true` to allow the message to continue; `return false` to stop it from continuing.

Every middleware receives the same API signature of `context`. Different kinds of middleware may receive different information in the
`context` object. For more details, see the API for each type of middleware.

### Error Handling

Asynchronous middleware should catch its own exceptions, emit an `error` event, and return `true` or `false`. Any uncaught exceptions will interrupt all execution of middleware.

## Listener Middleware

Listener 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`.

## Listener Middleware Examples

A 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).

A simple example of middleware logging command executions:

```javascript
export default async robot => {
  robot.listenerMiddleware(async context => {
    // Log commands
    robot.logger.info(`${context.response.message.user.name} asked me to ${context.response.message.text}`)
    // Continue executing middleware
    return true
  })
}
```

In this example, a log message will be written for each chat message that matches a Listener.

A more complex example making a rate limiting decision:

```javascript
export default async robot => {
  // Map of listener ID to last time it was executed
  let lastExecutedTime = {}

  robot.listenerMiddleware(async context => {
    try {
      // Default to 1s unless listener provides a different minimum period
      const minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs ?? 1000

      // See if command has been executed recently
      if (lastExecutedTime.hasOwnProperty(context.listener.options.id) &&
         lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs) {
           // Command is being executed too quickly!
           return false
      } else {
        lastExecutedTime[context.listener.options.id] = Date.now()
        return true
      }
    } catch(err) {
      robot.emit('error', err, context.response)
    }
  })
}
```

In 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`.

This 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.

```javascript
// .mjs
export default async robot => {
  robot.hear(/hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, async res => {
    // This will execute no faster than once every ten seconds
    await res.reply('Why, hello there!')
  })
}
```

## Listener Middleware API

Listener middleware callbacks receive 1 argument, `context`. Listener middleware context includes these fields:
  - `listener`
    - `options`: a simple Object containing options set when defining the listener. See [Listener Metadata](#listener-metadata).
    - all other properties should be considered internal
  - `response`
    - all parts of the standard response API are included in the middleware API. See [Send & Reply](#send--reply).
    - 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)
    - note: the textual message (`response.message.text`) should be considered immutable in listener middleware

# Receive Middleware

Receive middleware runs before any listeners have executed. It's suitable for
excluded commands that have not been updated to add an ID, metrics, and more.

## Receive Middleware Example

This 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.

```javascript
const EXCLUDED_USERS = [
  '12345' // Restrict access for a user ID for a contractor
]

robot.receiveMiddleware(async context => {
  if (EXCLUDED_USERS.some( id => context.response.message.user.id == id)) {
    // Don't process this message further.
    context.response.message.finish()

    // If the message starts with 'hubot' or the alias pattern, this user was
    // explicitly trying to run a command, so respond with an error message.
    if (context.response.message.text?.match(robot.respondPattern(''))) {
      await context.response.reply(`I'm sorry @${context.response.message.user.name}, but I'm configured to ignore your commands.`)
    }

    // Don't process further middleware.
    return false
  } else {
    return true
  }
})
```

## Receive Middleware API

Receive middleware callbacks receive 1 argument, `context`. Receive middleware context includes these fields:
  - `response`
    - this response object will not have a `match` property, as no listeners have been run yet to match it.
    - middleware may decorate the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)
    - middleware may modify the `response.message` object

# Response Middleware

Response middleware runs against every message hubot sends to a chat room. It's helpful for message formatting, preventing password leaks, metrics, and more.

## Response Middleware Example

Response 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.

This 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>:

```javascript
// .mjs
export default async robot => {
  robot.responseMiddleware(async context => {
    // Only process plaintext messages (send, reply, etc.)
    if (!context.plaintext) return true
    
    // Modify each string in the response
    context.strings = context.strings.map(string => {
      // Convert markdown links to Slack format
      return string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>")
    })
    
    return true
  })
}
```

## How to Use Response Middleware

Response middleware is called every time a message is sent from a listener. Key points:

- Access the outgoing message strings via `context.strings` - this is an array of strings being sent
- Modify the strings by reassigning `context.strings` or mapping over the array
- The `context.method` tells you how the message was sent (`send`, `reply`, `emote`, etc.)
- Return `true` to allow the message to continue to the adapter, or `false` to stop it
- Be careful not to create infinite loops by sending new messages from middleware, as they will also trigger middleware
- Use `context.plaintext` to distinguish between regular messages and other message types

## Response Middleware API

Response middleware callbacks receive 1 parameters, `context` and are Promises/async/await. Receive middleware context includes these fields:
  - `response`
    - 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.
  - `strings`
    - An array of strings being sent to the chat room adapter. You can edit these, or use `context.strings = ["new strings"]` to replace them.
  - `method`
    - A string representing which type of response message the listener sent, such as `send`, `reply`, `emote` or `topic`.
  - `plaintext`
    - `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.

# Testing Hubot Scripts

I use [Node's Test Runner](https://nodejs.org/dist/latest-v20.x/docs/api/test.html) for writing and running tests for Hubot.

[package.json](../package.json)

```json
"scripts": {
  "test": "node --test",
}
```

```sh
npm t
```

Checkout [Xample.mjs](../test/XampleTest.mjs) for an example that tests the [Xample.mjs](../test/scripts/Xample.mjs) script.

In 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.

Please feel free to create Github issues if you have questions or comments. I'm happy to collaborate.

If you created your bot with `npx hubot --create xample-bot`, then the `DummyAdapter` is already there. Along with an example test.


================================================
FILE: examples/hubot-start.ps1
================================================
#Hubot PowerShell Start Script
#Invoke from the PowerShell prompt or start via automated tools 

$HubotPath = "drive:\path\to\hubot"
$HubotAdapter = "Hubot adapter"

Write-Host "Starting Hubot Watcher"
While (1)
{
    Write-Host "Starting Hubot"
    Start-Process powershell -ArgumentList "$HubotPath\bin\hubot –adapter $HubotAdapter" -wait
}

================================================
FILE: examples/hubot.service
================================================
; Hubot systemd service unit file
; Place in e.g. `/etc/systemd/system/hubot.service`, then `systemctl daemon-reload` and `service hubot start`.

[Unit]
Description=Hubot
Requires=network.target
After=network.target

[Service]
Type=simple
WorkingDirectory=/path/to/hubot
User=change-to-hubot-user

Restart=always
RestartSec=10

; Configure Hubot environment variables, use quotes around vars with whitespace as shown below.
Environment="HUBOT_aaa=xxx"
Environment="HUBOT_bbb='yyy yyy'"

; Alternatively multiple environment variables can loaded from an external file
;EnvironmentFile=/etc/hubot-environment

ExecStart=/path/to/hubot/bin/hubot --adapter zzz

[Install]
WantedBy=multi-user.target


================================================
FILE: index.mjs
================================================
'use strict'

import User from './src/User.mjs'
import Brain from './src/Brain.mjs'
import Robot from './src/Robot.mjs'
import Adapter from './src/Adapter.mjs'
import Response from './src/Response.mjs'
import Middleware from './src/Middleware.mjs'
import { Listener, TextListener } from './src/Listener.mjs'
import { TextMessage, EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage, Message } from './src/Message.mjs'
import { DataStore, DataStoreUnavailable } from './src/DataStore.mjs'
import { CommandBus } from './src/CommandBus.mjs'

const loadBot = (adapter, enableHttpd, name, alias) => new Robot(adapter, enableHttpd, name, alias)
export {
  Adapter,
  User,
  Brain,
  Robot,
  Response,
  Listener,
  TextListener,
  Message,
  TextMessage,
  EnterMessage,
  LeaveMessage,
  TopicMessage,
  CatchAllMessage,
  DataStore,
  DataStoreUnavailable,
  Middleware,
  CommandBus,
  loadBot
}

export default {
  Adapter,
  User,
  Brain,
  Robot,
  Response,
  Listener,
  TextListener,
  Message,
  TextMessage,
  EnterMessage,
  LeaveMessage,
  TopicMessage,
  CatchAllMessage,
  DataStore,
  DataStoreUnavailable,
  Middleware,
  CommandBus,
  loadBot
}


================================================
FILE: package.json
================================================
{
  "name": "hubot",
  "version": "0.0.0-development",
  "author": "hubot",
  "keywords": [
    "github",
    "hubot",
    "campfire",
    "bot"
  ],
  "description": "A simple helpful robot for your Company",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/hubotio/hubot.git"
  },
  "engines": {
    "node": ">= 18",
    "npm": ">= 9"
  },
  "main": "./index.mjs",
  "bin": {
    "hubot": "./bin/hubot"
  },
  "scripts": {
    "start": "bin/hubot",
    "gen": "bin/hubot --create myhubot",
    "pretest": "node script/simple-lint.mjs",
    "test": "node --test --test-timeout=40000",
    "test:smoke": "node src/**/*.js",
    "test:e2e": "bin/e2e-test.sh",
    "build:local": "npx @hubot-friends/sfab --folder ./docs --destination ./_site --verbose --serve /hubot/ --watch-path ./docs --scripts ./sfab-hooks",
    "build": "npx @hubot-friends/sfab --folder ./docs --destination ./_site --verbose --scripts ./sfab-hooks"
  },
  "release": {
    "branches": [
      "main",
      "next"
    ],
    "dryRun": false
  },
  "dependencies": {
    "express": "^5.2.1",
    "express-basic-auth": "^1.2.1",
    "pino": "^10.3.1"
  }
}


================================================
FILE: script/bootstrap
================================================
#!/usr/bin/env bash

npm install


================================================
FILE: script/release
================================================
#!/usr/bin/env bash
# Tag and push a release.

set -e

# Make sure we're in the project root.

cd $(dirname "$0")/..

# Make sure the darn thing works

npm update && script/smoke-test

# Make sure we're on the main branch.

(git branch | grep -q '* main') || {
  echo "Only release from the main branch."
  exit 1
}

# Figure out what version we're releasing.

tag=v`node -e 'console.log(require("./package.json").version)'`

# Ensure there's a line in the CHANGELOG

grep "$tag" CHANGELOG.md || {
  echo "No entry for '$tag' found in the CHANGELOG."
  exit 1
}

# Make sure we haven't released this version before.

git fetch -t origin

(git tag -l | grep -q "$tag") && {
  echo "Whoops, there's already a '${tag}' tag."
  exit 1
}

# Tag it and bag it.

npm publish && git tag "$tag" &&
  git push origin main --tags


================================================
FILE: script/server
================================================
#!/usr/bin/env bash

npm start -- "$@"


================================================
FILE: script/simple-lint.mjs
================================================
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')

const targets = [
  'src',
  'test',
  'bin',
  'configuration',
  'sfab-hooks',
  'script'
]

const ignoreDirs = new Set([
  'node_modules',
  '.git',
  '_site',
  'docs'
])

const extensions = new Set(['.mjs', '.js'])

const issues = []

const isIgnoredDir = (dirName) => ignoreDirs.has(dirName)

const walk = async (dir) => {
  const entries = await fs.readdir(dir, { withFileTypes: true })
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)
    if (entry.isDirectory()) {
      if (!isIgnoredDir(entry.name)) {
        await walk(fullPath)
      }
      continue
    }
    if (entry.isFile()) {
      const ext = path.extname(entry.name)
      if (extensions.has(ext)) {
        await lintFile(fullPath)
      }
    }
  }
}

const lintFile = async (filePath) => {
  const content = await fs.readFile(filePath, 'utf8')
  const normalizedContent = content.replace(/\r\n/g, '\n')
  const lines = normalizedContent.split('\n')
  const hasFinalNewline = normalizedContent.endsWith('\n')

  lines.forEach((line, index) => {
    const lineNumber = index + 1
    if (line.includes('\t')) {
      issues.push({ filePath, lineNumber, message: 'tab character found' })
    }
    if (/[ \t]+$/.test(line)) {
      issues.push({ filePath, lineNumber, message: 'trailing whitespace' })
    }
  })

  if (!hasFinalNewline) {
    issues.push({ filePath, lineNumber: lines.length, message: 'missing final newline' })
  }
}

const run = async () => {
  for (const target of targets) {
    const dir = path.join(projectRoot, target)
    try {
      const stat = await fs.stat(dir)
      if (stat.isDirectory()) {
        await walk(dir)
      }
    } catch (error) {
      if (error?.code !== 'ENOENT') {
        throw error
      }
    }
  }

  if (issues.length > 0) {
    for (const issue of issues) {
      const relPath = path.relative(projectRoot, issue.filePath)
      console.error(`${relPath}:${issue.lineNumber} ${issue.message}`)
    }
    console.error(`\nFound ${issues.length} lint issue(s)`)
    process.exitCode = 1
    return
  }

  console.log('Lint OK')
}

run().catch((error) => {
  console.error(error)
  process.exitCode = 1
})


================================================
FILE: script/smoke-test
================================================
#!/usr/bin/env bash

npm run test:smoke


================================================
FILE: script/test
================================================
#!/usr/bin/env bash

npm test -- "$@"


================================================
FILE: sfab-hooks/SfabHook.mjs
================================================
export default () => {
  return {
    model (file, model) {
      return {
        base: {
          href: '/hubot/'
        }
      }
    }
  }
}


================================================
FILE: src/Adapter.mjs
================================================
'use strict'

import EventEmitter from 'node:events'

class Adapter extends EventEmitter {
  // An adapter is a specific interface to a chat source for robots.
  //
  // robot - A Robot instance.
  constructor (robot) {
    super()
    this.robot = robot
  }

  // Public: Raw method for sending data back to the chat source. Extend this.
  //
  // envelope - A Object with message, room and user details.
  // strings  - One or more Strings for each message to send.
  //
  // Returns results from adapter.
  async send (envelope, ...strings) {}

  // Public: Raw method for sending emote data back to the chat source.
  // Defaults as an alias for send
  //
  // envelope - A Object with message, room and user details.
  // strings  - One or more Strings for each message to send.
  //
  // Returns results from adapter.
  async emote (envelope, ...strings) {
    return this.send(envelope, ...strings)
  }

  // Public: Raw method for building a reply and sending it back to the chat
  // source. Extend this.
  //
  // envelope - A Object with message, room and user details.
  // strings  - One or more Strings for each reply to send.
  //
  // Returns results from adapter.
  async reply (envelope, ...strings) {}

  // Public: Raw method for setting a topic on the chat source. Extend this.
  //
  // envelope - A Object with message, room and user details.
  // strings  - One more more Strings to set as the topic.
  //
  // Returns results from adapter.
  async topic (envelope, ...strings) {}

  // Public: Raw method for playing a sound in the chat source. Extend this.
  //
  // envelope - A Object with message, room and user details.
  // strings  - One or more strings for each play message to send.
  //
  // Returns results from adapter.
  async play (envelope, ...strings) {}

  // Public: Raw method for invoking the bot to run. Extend this.
  //
  // Returns whatever the extended adapter returns.
  async run () {}

  // Public: Raw method for shutting the bot down. Extend this.
  //
  // Returns nothing.
  close () {
    this.removeAllListeners()
  }

  // Public: Dispatch a received message to the robot.
  //
  // Returns nothing.
  async receive (message) {
    await this.robot.receive(message)
  }

  // Public: Get an Array of User objects stored in the brain.
  //
  // Returns an Array of User objects.
  // @deprecated Use @robot.brain
  users () {
    this.robot.logger.warn('@users() is going to be deprecated in 11.0.0 use @robot.brain.users()')
    return this.robot.brain.users()
  }

  // Public: Get a User object given a unique identifier.
  //
  // Returns a User instance of the specified user.
  // @deprecated Use @robot.brain
  userForId (id, options) {
    this.robot.logger.warn('@userForId() is going to be deprecated in 11.0.0 use @robot.brain.userForId()')
    return this.robot.brain.userForId(id, options)
  }

  // Public: Get a User object given a name.
  //
  // Returns a User instance for the user with the specified name.
  // @deprecated Use @robot.brain
  userForName (name) {
    this.robot.logger.warn('@userForName() is going to be deprecated in 11.0.0 use @robot.brain.userForName()')
    return this.robot.brain.userForName(name)
  }

  // Public: Get all users whose names match fuzzyName. Currently, match
  // means 'starts with', but this could be extended to match initials,
  // nicknames, etc.
  //
  // Returns an Array of User instances matching the fuzzy name.
  // @deprecated Use @robot.brain
  usersForRawFuzzyName (fuzzyName) {
    this.robot.logger.warn('@userForRawFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForRawFuzzyName()')
    return this.robot.brain.usersForRawFuzzyName(fuzzyName)
  }

  // Public: If fuzzyName is an exact match for a user, returns an array with
  // just that user. Otherwise, returns an array of all users for which
  // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
  //
  // Returns an Array of User instances matching the fuzzy name.
  // @deprecated Use @robot.brain
  usersForFuzzyName (fuzzyName) {
    this.robot.logger.warn('@userForFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForFuzzyName()')
    return this.robot.brain.usersForFuzzyName(fuzzyName)
  }

  // Public: Creates a scoped http client with chainable methods for
  // modifying the request. This doesn't actually make a request though.
  // Once your request is assembled, you can call `get()`/`post()`/etc to
  // send the request.
  //
  // Returns a ScopedClient instance.
  // @deprecated Use node.js fetch.
  http (url) {
    this.robot.logger.warn('@http() is going to be deprecated in 11.0.0 use @robot.http()')
    return this.robot.http(url)
  }
}

export default Adapter


================================================
FILE: src/Brain.mjs
================================================
'use strict'

import EventEmitter from 'node:events'
import User from './User.mjs'

// If necessary, reconstructs a User object. Returns either:
//
// 1. If the original object was falsy, null
// 2. If the original object was a User object, the original object
// 3. If the original object was a plain JavaScript object, return
//    a User object with all of the original object's properties.
const reconstructUserIfNecessary = function (user, robot) {
  if (!user) {
    return null
  }

  if (!user.constructor || (user.constructor && user.constructor.name !== 'User')) {
    const id = user.id
    delete user.id
    // Use the old user as the "options" object,
    // populating the new user with its values.
    // Also add the `robot` field so it gets a reference.
    user.robot = robot
    const newUser = new User(id, user)
    delete user.robot

    return newUser
  } else {
    return user
  }
}

class Brain extends EventEmitter {
  // Represents somewhat persistent storage for the robot. Extend this.
  //
  // Returns a new Brain with no external storage.
  constructor (robot) {
    super()
    this.data = {
      users: {},
      _private: {}
    }
    this.getRobot = function () {
      return robot
    }

    this.autoSave = true

    robot.on('running', () => {
      this.resetSaveInterval(5)
    })
  }

  // Public: Store key-value pair under the private namespace and extend
  // existing @data before emitting the 'loaded' event.
  //
  // Returns the instance for chaining.
  set (key, value) {
    let pair
    if (key === Object(key)) {
      pair = key
    } else {
      pair = {}
      pair[key] = value
    }

    Object.keys(pair).forEach((key) => {
      this.data._private[key] = pair[key]
    })

    this.emit('loaded', this.data)

    return this
  }

  // Public: Get value by key from the private namespace in @data
  // or return null if not found.
  //
  // Returns the value.
  get (key) {
    return this.data._private[key] != null ? this.data._private[key] : null
  }

  // Public: Remove value by key from the private namespace in @data
  // if it exists
  //
  // Returns the instance for chaining.
  remove (key) {
    if (this.data._private[key] != null) {
      delete this.data._private[key]
    }

    return this
  }

  // Public: Emits the 'save' event so that 'brain' scripts can handle
  // persisting.
  //
  // Returns nothing.
  save () {
    this.emit('save', this.data)
  }

  // Public: Emits the 'close' event so that 'brain' scripts can handle closing.
  //
  // Returns nothing.
  close () {
    clearInterval(this.saveInterval)
    this.save()
    this.emit('close')
    this.removeAllListeners()
  }

  // Public: Enable or disable the automatic saving
  //
  // enabled - A boolean whether to autosave or not
  //
  // Returns nothing
  setAutoSave (enabled) {
    this.autoSave = enabled
  }

  // Public: Reset the interval between save function calls.
  //
  // seconds - An Integer of seconds between saves.
  //
  // Returns nothing.
  resetSaveInterval (seconds) {
    if (this.saveInterval) {
      clearInterval(this.saveInterval)
    }
    this.saveInterval = setInterval(() => {
      if (this.autoSave) {
        this.save()
      }
    }, seconds * 1000)
  }

  // Public: Merge keys loaded from a DB against the in memory representation.
  //
  // Returns nothing.
  //
  // Caveats: Deeply nested structures don't merge well.
  mergeData (data) {
    for (const k in data || {}) {
      this.data[k] = data[k]
    }

    // Ensure users in the brain are still User objects.
    if (data && data.users) {
      for (const k in data.users) {
        const user = this.data.users[k]
        this.data.users[k] = reconstructUserIfNecessary(user, this.getRobot())
      }
    }

    this.emit('loaded', this.data)
  }

  // Public: Get an object of User objects stored in the brain.
  //
  // Returns an object of User objects.
  users () {
    return this.data.users
  }

  // Public: Get a User object given a unique identifier.
  //
  // Returns a User instance of the specified user.
  userForId (id, options) {
    let user = this.data.users[id]
    if (!options) {
      options = {}
    }
    options.robot = this.getRobot()

    if (!user) {
      user = new User(id, options)
      this.data.users[id] = user
    }

    if (options && options.room && (!user.room || user.room !== options.room)) {
      user = new User(id, options)
      this.data.users[id] = user
    }
    delete options.robot

    return user
  }

  // Public: Get a User object given a name.
  //
  // Returns a User instance for the user with the specified name.
  userForName (name) {
    let result = null
    const lowerName = name.toLowerCase()

    for (const k in this.data.users || {}) {
      const userName = this.data.users[k].name
      if (userName != null && userName.toString().toLowerCase() === lowerName) {
        result = this.data.users[k]
      }
    }

    return result
  }

  // Public: Get all users whose names match fuzzyName. Currently, match
  // means 'starts with', but this could be extended to match initials,
  // nicknames, etc.
  //
  // Returns an Array of User instances matching the fuzzy name.
  usersForRawFuzzyName (fuzzyName) {
    const lowerFuzzyName = fuzzyName.toLowerCase()

    const users = this.data.users || {}

    return Object.keys(users).reduce((result, key) => {
      const user = users[key]
      if (user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) === 0) {
        result.push(user)
      }
      return result
    }, [])
  }

  // Public: If fuzzyName is an exact match for a user, returns an array with
  // just that user. Otherwise, returns an array of all users for which
  // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
  //
  // Returns an Array of User instances matching the fuzzy name.
  usersForFuzzyName (fuzzyName) {
    const matchedUsers = this.usersForRawFuzzyName(fuzzyName)
    const lowerFuzzyName = fuzzyName.toLowerCase()
    const fuzzyMatchedUsers = matchedUsers.filter(user => user.name.toLowerCase() === lowerFuzzyName)

    return fuzzyMatchedUsers.length > 0 ? fuzzyMatchedUsers : matchedUsers
  }
}

export default Brain


================================================
FILE: src/CommandBus.mjs
================================================
import { EventEmitter } from 'node:events'
import fs from 'node:fs'
import path from 'node:path'

/**
 * CommandBus provides deterministic command handling for Hubot with safe-by-default behavior.
 * 
 * Logging Strategy:
 * - Event logging to disk is disabled by default
 * - To enable: pass `disableLogging: false` in constructor options
 * - When enabled, events are written asynchronously (fire-and-forget) to avoid blocking
 * - Writes happen individually as events occur
 */
export class CommandBus extends EventEmitter {
  constructor(robot, options = {}) {
    super()
    this.robot = robot
    this.commands = new Map()
    this.pendingProposals = new Map()
    this.typeResolvers = new Map()
    this.prefix = options.prefix ?? ''
    this.proposalTTL = options.proposalTTL || 300000 // 5 minutes default
    this.logPath = options.logPath || path.join(process.cwd(), '.data', 'commands-events.ndjson')
    this.disableLogging = options.disableLogging ?? true
    this.permissionProvider = options.permissionProvider || null
  }

  register(spec, opts = {}) {
    if (!spec.id) {
      throw new Error('Command spec must have an id')
    }
    if (!spec.handler || typeof spec.handler !== 'function') {
      throw new Error('Command spec must have a handler function')
    }

    const aliases = this._normalizeAliases(spec.aliases)

    const existing = this.commands.get(spec.id)
    if (existing && !opts.update) {
      throw new Error(`Command ${spec.id} is already registered`)
    }

    const command = {
      id: spec.id,
      description: spec.description || '',
      aliases: aliases.original,
      normalizedAliases: aliases.normalized,
      examples: spec.examples || [],
      args: spec.args || {},
      sideEffects: spec.sideEffects || [],
      confirm: spec.confirm || 'if_ambiguous',
      permissions: spec.permissions || {},
      handler: spec.handler
    }

    this.commands.set(spec.id, command)
    const eventPayload = { commandId: spec.id, aliases: command.normalizedAliases, timestamp: Date.now() }
    if (existing && opts.update) {
      this.emit('commands:updated', eventPayload)
      this._log({ event: 'commands:updated', ...eventPayload })
    } else {
      this.emit('commands:registered', eventPayload)
      this._log({ event: 'commands:registered', ...eventPayload })
    }

    const collisions = this.aliasCollisions()
    if (Object.keys(collisions).length > 0) {
      this.emit('commands:alias_collision_detected', { collisions, timestamp: Date.now() })
      this._log({ event: 'commands:alias_collision_detected', collisions, timestamp: Date.now() })
    }

    return command
  }

  /**
   * Register a custom type resolver for argument validation.
   * Resolvers are called during validation and can transform/validate values.
   * 
   * @param {string} typeName - The type name to register (e.g., 'project_id')
   * @param {Function} resolver - Async function(value, schema, context) that returns validated value or throws
   * @throws {Error} If typeName is empty or resolver is not a function
   * @public
   * 
   * @example
   * robot.commands.registerTypeResolver('project_id', async (value, schema, context) => {
   *   if (!value.startsWith('PRJ-')) throw new Error('must start with PRJ-')
   *   return value.toUpperCase()
   * })
   */
  registerTypeResolver(typeName, resolver) {
    if (typeof typeName !== 'string' || !typeName) {
      throw new Error('Type name must be a non-empty string')
    }
    if (typeof resolver !== 'function') {
      throw new Error('Resolver must be a function')
    }
    this.typeResolvers.set(typeName, resolver)
  }

  unregister(commandId) {
    return this.commands.delete(commandId)
  }

  getCommand(commandId) {
    return this.commands.get(commandId)
  }

  listCommands(filter = {}) {
    let commands = Array.from(this.commands.values())

    if (filter.prefix) {
      commands = commands.filter(c => c.id.startsWith(filter.prefix))
    }

    return commands
  }

  aliasCollisions() {
    const collisions = {}
    const aliasMap = new Map()

    for (const command of this.commands.values()) {
      for (const alias of command.normalizedAliases || []) {
        if (!aliasMap.has(alias)) {
          aliasMap.set(alias, [])
        }
        aliasMap.get(alias).push(command.id)
      }
    }

    for (const [alias, ids] of aliasMap.entries()) {
      if (ids.length > 1) {
        collisions[alias] = ids
      }
    }

    return collisions
  }

  search(query, opts = {}) {
    if (!query || typeof query !== 'string') {
      return []
    }

    const normalizedQuery = this._normalizeAlias(query)
    const queryTokens = this._tokenizeQuery(normalizedQuery)
    const results = []

    for (const command of this.commands.values()) {
      const aliasMatches = this._scoreAliases(command, normalizedQuery, queryTokens)
      const descriptionMatches = this._scoreText(command.description, queryTokens)
      const exampleMatches = this._scoreExamples(command.examples, queryTokens)

      const bestAliasScore = aliasMatches.score
      const bestDescScore = descriptionMatches.score
      const bestExampleScore = exampleMatches.score

      const bestScore = Math.max(bestAliasScore, bestDescScore, bestExampleScore)
      if (bestScore === 0) {
        continue
      }

      let matchedOn = 'description'
      if (bestAliasScore >= bestDescScore && bestAliasScore >= bestExampleScore) {
        matchedOn = 'alias'
      } else if (bestExampleScore >= bestDescScore) {
        matchedOn = 'example'
      }

      results.push({
        id: command.id,
        score: bestScore,
        matchedOn
      })
    }

    results.sort((a, b) => b.score - a.score)
    return results
  }

  getHelp(commandId) {
    const command = this.getCommand(commandId)
    if (!command) {
      return null
    }

    let help = `Command: ${command.id}\n`
    help += `Description: ${command.description}\n`
    help += `Usage: ${this.prefix}${command.id} [options]\n`

    if (command.aliases.length > 0) {
      help += `Intent: ${command.aliases.join(', ')}\n`
    }

    if (Object.keys(command.args).length > 0) {
      help += '\nArguments:\n'
      for (const [name, schema] of Object.entries(command.args)) {
        const required = schema.required ? ' (required)' : ''
        const defaultVal = schema.default !== undefined ? ` [default: ${schema.default}]` : ''
        const values = schema.values ? ` [values: ${schema.values.join(', ')}]` : ''
        help += `  --${name} (${schema.type})${required}${defaultVal}${values}\n`
      }
    }

    if (command.examples.length > 0) {
      help += '\nExamples:\n'
      command.examples.forEach(ex => {
        help += `  ${ex}\n`
      })
    }

    return help
  }

  parse(text) {

    if (!text || typeof text !== 'string') {
      return null
    }

    // Strip prefix if present (optional)
    const withoutPrefix = text.startsWith(this.prefix) 
      ? text.slice(this.prefix.length).trim()
      : text.trim()

    const parts = this._tokenize(withoutPrefix)
    if (parts.length === 0) {
      return null
    }

    const commandId = parts[0]
    if (!this.commands.has(commandId)) {
      return null
    }

    const command = this.commands.get(commandId)
    const args = {}

    for (let i = 1; i < parts.length; i++) {
      const token = parts[i]

      // Handle -- key value pattern
      if (token === '--') {
        const key = parts[i + 1]
        const valueToken = parts[i + 2]
        if (key && valueToken && !valueToken.startsWith('--') && !valueToken.includes(':')) {
          args[key] = valueToken
          i += 2
        } else if (key) {
          args[key] = true
          i += 1
        }
        continue
      }

      // Handle --key value or --key "quoted value"
      if (token.startsWith('--')) {
        const key = token.slice(2)
        const nextToken = parts[i + 1]
        const schema = command.args[key]

        // Use schema hint: boolean type = flag, others expect value
        if (schema && schema.type === 'boolean') {
          args[key] = true
        } else if (nextToken && !nextToken.startsWith('--') && !nextToken.includes(':')) {
          args[key] = nextToken
          i++ // Skip next token
        } else {
          // No schema or ambiguous: default to boolean flag
          args[key] = true
        }
      }
      // Handle key:value or key:"quoted value"
      else if (token.includes(':')) {
        const colonIndex = token.indexOf(':')
        const key = token.slice(0, colonIndex)
        const value = token.slice(colonIndex + 1)
        args[key] = value
      }
    }

    const parsed = {
      commandId,
      args,
      rawText: text
    }

    this.emit('commands:invocation_parsed', { commandId, args, timestamp: Date.now() })
    this._log({ event: 'commands:invocation_parsed', commandId, args, timestamp: Date.now() })

    return parsed
  }

  _tokenize(text) {
    const tokens = []
    let current = ''
    let inQuotes = false
    let quoteChar = null
    let escapeNext = false

    for (let i = 0; i < text.length; i++) {
      const char = text[i]

      if (escapeNext) {
        current += char
        escapeNext = false
        continue
      }

      if (inQuotes && char === '\\') {
        escapeNext = true
        continue
      }

      if ((char === '"' || char === '\'') && !inQuotes) {
        inQuotes = true
        quoteChar = char
        continue
      }

      if (char === quoteChar && inQuotes) {
        inQuotes = false
        quoteChar = null
        continue
      }

      if (char === ' ' && !inQuotes) {
        if (current) {
          tokens.push(current)
          current = ''
        }
        continue
      }

      current += char
    }

    if (current) {
      tokens.push(current)
    }

    return tokens
  }

  _normalizeAliases(aliases) {
    if (aliases === undefined || aliases === null) {
      return { original: [], normalized: [] }
    }
    if (!Array.isArray(aliases)) {
      throw new Error('Command aliases must be an array of strings')
    }

    const original = []
    const normalized = []
    const seen = new Set()

    for (const alias of aliases) {
      if (typeof alias !== 'string') {
        throw new Error('Command aliases must be an array of strings')
      }
      const trimmed = alias.trim()
      if (!trimmed) {
        throw new Error('Command aliases must be non-empty strings')
      }

      const normalizedAlias = this._normalizeAlias(trimmed)
      if (seen.has(normalizedAlias)) {
        continue
      }

      seen.add(normalizedAlias)
      original.push(trimmed)
      normalized.push(normalizedAlias)
    }

    return { original, normalized }
  }

  _normalizeAlias(alias) {
    return alias.trim().replace(/\s+/g, ' ').toLowerCase()
  }

  _tokenizeQuery(text) {
    return text.split(/\s+/).filter(Boolean)
  }

  _scoreAliases(command, normalizedQuery, queryTokens) {
    const aliases = command.normalizedAliases || []
    if (aliases.length === 0) {
      return { score: 0 }
    }

    if (aliases.includes(normalizedQuery)) {
      return { score: 100 }
    }

    const bestOverlap = aliases.reduce((best, alias) => {
      const tokens = this._tokenizeQuery(alias)
      const overlap = queryTokens.filter(t => tokens.includes(t)).length
      return Math.max(best, overlap)
    }, 0)

    return { score: bestOverlap * 10 }
  }

  _scoreText(text, queryTokens) {
    if (!text) {
      return { score: 0 }
    }

    const tokens = this._tokenizeQuery(this._normalizeAlias(text))
    const overlap = queryTokens.filter(t => tokens.includes(t)).length
    return { score: overlap * 5 }
  }

  _scoreExamples(examples, queryTokens) {
    if (!examples || examples.length === 0) {
      return { score: 0 }
    }

    const bestOverlap = examples.reduce((best, example) => {
      const tokens = this._tokenizeQuery(this._normalizeAlias(example))
      const overlap = queryTokens.filter(t => tokens.includes(t)).length
      return Math.max(best, overlap)
    }, 0)

    return { score: bestOverlap * 5 }
  }

  async validate(commandId, rawArgs, context) {
    const command = this.getCommand(commandId)
    if (!command) {
      return {
        ok: false,
        errors: [`Command ${commandId} not found`],
        missing: []
      }
    }

    const args = { ...rawArgs }
    const errors = []
    const missing = []

    // Apply defaults and validate each arg
    for (const [name, schema] of Object.entries(command.args)) {
      const value = args[name]

      // Check required
      if (schema.required && (value === undefined || value === null)) {
        missing.push(name)
        continue
      }

      // Apply default
      if (value === undefined && schema.default !== undefined) {
        args[name] = schema.default
        continue
      }

      // Skip validation if not provided and not required
      if (value === undefined) {
        continue
      }

      // Type validation and conversion
      try {
        args[name] = await this._validateType(name, value, schema, context)
      } catch (err) {
        errors.push(err.message)
      }
    }

    if (missing.length > 0 || errors.length > 0) {
      const result = {
        ok: false,
        errors,
        missing,
        args
      }

      this.emit('commands:validation_failed', { commandId, errors, missing, timestamp: Date.now() })
      this._log({ event: 'commands:validation_failed', commandId, errors, missing, timestamp: Date.now() })

      return result
    }

    return {
      ok: true,
      args
    }
  }

  async _validateType(name, value, schema, context) {
    // Check custom type resolvers first
    if (this.typeResolvers.has(schema.type)) {
      const resolver = this.typeResolvers.get(schema.type)
      try {
        return await resolver(value, schema, context)
      } catch (err) {
        throw new Error(`Argument ${name}: ${err.message}`)
      }
    }

    // Built-in types
    switch (schema.type) {
      case 'string':
        return String(value)

      case 'number': {
        const num = Number(value)
        if (isNaN(num)) {
          throw new Error(`Argument ${name} must be a number`)
        }
        return num
      }

      case 'boolean': {
        return coerceToBoolean(value)
      }

      case 'enum': {
        if (!Array.isArray(schema.values) || schema.values.length === 0) {
          throw new Error(`Argument ${name}: enum values must be a non-empty array`)
        }
        if (!schema.values.includes(value)) {
          throw new Error(`Argument ${name} must be one of: ${schema.values.join(', ')}`)
        }
        return value
      }

      case 'user': {
        const users = this.robot.brain.users()
        const user = Object.values(users).find(u => u.name === value || u.id === value)
        if (!user) {
          throw new Error(`Argument ${name}: user "${value}" not found`)
        }
        return user
      }

      case 'room': {
        if (!value.startsWith('#')) {
          throw new Error(`Argument ${name}: room must start with #`)
        }
        return value
      }

      case 'date': {
        let date

        if (value === 'today') {
          date = new Date()
          date.setHours(0, 0, 0, 0)
        } else if
Download .txt
gitextract_19fif895/

├── .editorconfig
├── .github/
│   ├── stale.yml
│   └── workflows/
│       ├── nodejs-macos.yml
│       ├── nodejs-ubuntu.yml
│       ├── nodejs-windows.yml
│       ├── pipeline.yml
│       └── sfab-gh-pages.yml
├── .gitignore
├── .npmignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── bin/
│   ├── Hubot.mjs
│   ├── e2e-test.sh
│   └── hubot
├── configuration/
│   └── Config.mjs
├── docs/
│   ├── adapters/
│   │   ├── campfire.md
│   │   ├── development.md
│   │   └── shell.md
│   ├── adapters.md
│   ├── assets/
│   │   ├── fonts/
│   │   │   └── otf/
│   │   │       ├── octicons-regular-webfont.otf
│   │   │       ├── style_154042.otf
│   │   │       ├── style_154045.otf
│   │   │       ├── style_154046.otf
│   │   │       ├── style_154048.otf
│   │   │       ├── style_154051.otf
│   │   │       └── style_154053.otf
│   │   └── stylesheets/
│   │       └── application.css
│   ├── deploying/
│   │   ├── azure.md
│   │   ├── bluemix.md
│   │   ├── unix.md
│   │   └── windows.md
│   ├── deploying.md
│   ├── designs/
│   │   └── commands.md
│   ├── docs.md
│   ├── implementation.md
│   ├── index.html
│   ├── layouts/
│   │   ├── docs.html
│   │   └── main.html
│   ├── patterns.md
│   └── scripting.md
├── examples/
│   ├── hubot-start.ps1
│   └── hubot.service
├── index.mjs
├── package.json
├── script/
│   ├── bootstrap
│   ├── release
│   ├── server
│   ├── simple-lint.mjs
│   ├── smoke-test
│   └── test
├── sfab-hooks/
│   └── SfabHook.mjs
├── src/
│   ├── Adapter.mjs
│   ├── Brain.mjs
│   ├── CommandBus.mjs
│   ├── DataStore.mjs
│   ├── GenHubot.mjs
│   ├── HttpClient.mjs
│   ├── Listener.mjs
│   ├── Message.mjs
│   ├── Middleware.mjs
│   ├── OptParse.mjs
│   ├── Response.mjs
│   ├── Robot.mjs
│   ├── User.mjs
│   ├── adapters/
│   │   ├── Campfire.mjs
│   │   └── Shell.mjs
│   └── datastores/
│       └── Memory.mjs
└── test/
    ├── AdapterName_test.mjs
    ├── Adapter_test.mjs
    ├── Brain_test.mjs
    ├── CommandBus_test.mjs
    ├── Configuration_test.mjs
    ├── DataStore_test.mjs
    ├── Hubot_test.mjs
    ├── Listener_test.mjs
    ├── Message_test.mjs
    ├── Middleware_test.mjs
    ├── OptParse-test.mjs
    ├── Robot_test.mjs
    ├── Shell_test.mjs
    ├── User_test.mjs
    ├── XampleTest.mjs
    ├── doubles/
    │   └── DummyAdapter.mjs
    ├── fixtures/
    │   ├── MockAdapter.mjs
    │   ├── TestScript.js
    │   ├── TestScript.mjs
    │   ├── TestScript.ts
    │   ├── TestScriptIncorrectApi.js
    │   └── TestScriptIncorrectApi.mjs
    ├── index_test.mjs
    ├── ordered-scripts/
    │   ├── 01-PFirst.mjs
    │   ├── 02-SetupBotConfig.mjs
    │   └── WebSetup.mjs
    └── scripts/
        └── Xample.mjs
Download .txt
SYMBOL INDEX (273 symbols across 27 files)

FILE: bin/Hubot.mjs
  function loadScripts (line 105) | async function loadScripts () {
  function loadExternalScripts (line 121) | async function loadExternalScripts () {

FILE: sfab-hooks/SfabHook.mjs
  method model (line 3) | model (file, model) {

FILE: src/Adapter.mjs
  class Adapter (line 5) | class Adapter extends EventEmitter {
    method constructor (line 9) | constructor (robot) {
    method send (line 20) | async send (envelope, ...strings) {}
    method emote (line 29) | async emote (envelope, ...strings) {
    method reply (line 40) | async reply (envelope, ...strings) {}
    method topic (line 48) | async topic (envelope, ...strings) {}
    method play (line 56) | async play (envelope, ...strings) {}
    method run (line 61) | async run () {}
    method close (line 66) | close () {
    method receive (line 73) | async receive (message) {
    method users (line 81) | users () {
    method userForId (line 90) | userForId (id, options) {
    method userForName (line 99) | userForName (name) {
    method usersForRawFuzzyName (line 110) | usersForRawFuzzyName (fuzzyName) {
    method usersForFuzzyName (line 121) | usersForFuzzyName (fuzzyName) {
    method http (line 133) | http (url) {

FILE: src/Brain.mjs
  class Brain (line 33) | class Brain extends EventEmitter {
    method constructor (line 37) | constructor (robot) {
    method set (line 58) | set (key, value) {
    method get (line 80) | get (key) {
    method remove (line 88) | remove (key) {
    method save (line 100) | save () {
    method close (line 107) | close () {
    method setAutoSave (line 119) | setAutoSave (enabled) {
    method resetSaveInterval (line 128) | resetSaveInterval (seconds) {
    method mergeData (line 144) | mergeData (data) {
    method users (line 163) | users () {
    method userForId (line 170) | userForId (id, options) {
    method userForName (line 194) | userForName (name) {
    method usersForRawFuzzyName (line 213) | usersForRawFuzzyName (fuzzyName) {
    method usersForFuzzyName (line 232) | usersForFuzzyName (fuzzyName) {

FILE: src/CommandBus.mjs
  class CommandBus (line 14) | class CommandBus extends EventEmitter {
    method constructor (line 15) | constructor(robot, options = {}) {
    method register (line 28) | register(spec, opts = {}) {
    method registerTypeResolver (line 90) | registerTypeResolver(typeName, resolver) {
    method unregister (line 100) | unregister(commandId) {
    method getCommand (line 104) | getCommand(commandId) {
    method listCommands (line 108) | listCommands(filter = {}) {
    method aliasCollisions (line 118) | aliasCollisions() {
    method search (line 140) | search(query, opts = {}) {
    method getHelp (line 181) | getHelp(commandId) {
    method parse (line 215) | parse(text) {
    method _tokenize (line 294) | _tokenize(text) {
    method _normalizeAliases (line 345) | _normalizeAliases(aliases) {
    method _normalizeAlias (line 379) | _normalizeAlias(alias) {
    method _tokenizeQuery (line 383) | _tokenizeQuery(text) {
    method _scoreAliases (line 387) | _scoreAliases(command, normalizedQuery, queryTokens) {
    method _scoreText (line 406) | _scoreText(text, queryTokens) {
    method _scoreExamples (line 416) | _scoreExamples(examples, queryTokens) {
    method validate (line 430) | async validate(commandId, rawArgs, context) {
    method _validateType (line 493) | async _validateType(name, value, schema, context) {
    method needsConfirmation (line 573) | needsConfirmation(commandId) {
    method propose (line 591) | async propose(proposal, context) {
    method confirm (line 627) | async confirm(replyText, context) {
    method execute (line 685) | async execute(commandId, args, context) {
    method invoke (line 743) | async invoke(text, context) {
    method _getConfirmationKey (line 786) | _getConfirmationKey(userId, room) {
    method _renderPreview (line 790) | _renderPreview(commandId, args) {
    method clearPendingProposals (line 811) | clearPendingProposals() {
    method _log (line 820) | _log(event) {
    method _writeLog (line 829) | async _writeLog(event) {
  function coerceToBoolean (line 841) | function coerceToBoolean(value) {

FILE: src/DataStore.mjs
  class DataStore (line 3) | class DataStore {
    method constructor (line 7) | constructor (robot) {
    method set (line 16) | async set (key, value) {
    method setObject (line 23) | async setObject (key, objectKey, value) {
    method setArray (line 33) | async setArray (key, value) {
    method get (line 48) | async get (key) {
    method getObject (line 55) | async getObject (key, objectKey) {
    method _set (line 69) | _set (key, value, table) {
    method _get (line 81) | _get (key, table) {
  class DataStoreUnavailable (line 86) | class DataStoreUnavailable extends Error {}

FILE: src/GenHubot.mjs
  function runCommands (line 5) | function runCommands (hubotDirectory, options) {

FILE: src/HttpClient.mjs
  class ScopedClient (line 47) | class ScopedClient {
    method constructor (line 48) | constructor (url, options) {
    method request (line 53) | request (method, reqBody, callback) {
    method fullPath (line 144) | fullPath (p) {
    method scope (line 151) | scope (url, options, callback) {
    method join (line 167) | join (suffix) {
    method path (line 180) | path (p) {
    method query (line 185) | query (key, value) {
    method host (line 199) | host (h) {
    method port (line 204) | port (p) {
    method protocol (line 211) | protocol (p) {
    method encoding (line 216) | encoding (e) {
    method timeout (line 222) | timeout (time) {
    method auth (line 227) | auth (user, pass) {
    method header (line 238) | header (name, value) {
    method headers (line 243) | headers (h) {
    method buildOptions (line 248) | buildOptions () {
  method create (line 312) | create (url, options) {

FILE: src/Listener.mjs
  class Listener (line 6) | class Listener {
    method constructor (line 18) | constructor (robot, matcher, options, callback) {
    method call (line 52) | async call (message, middleware) {
  class TextListener (line 87) | class TextListener extends Listener {
    method constructor (line 97) | constructor (robot, regex, options, callback) {

FILE: src/Message.mjs
  class Message (line 3) | class Message {
    method constructor (line 8) | constructor (user, done) {
    method finish (line 17) | finish () {
  class TextMessage (line 22) | class TextMessage extends Message {
    method constructor (line 28) | constructor (user, text, id) {
    method match (line 39) | match (regex) {
    method toString (line 46) | toString () {
  class EnterMessage (line 54) | class EnterMessage extends Message {}
  class LeaveMessage (line 59) | class LeaveMessage extends Message {}
  class TopicMessage (line 66) | class TopicMessage extends TextMessage {}
  class CatchAllMessage (line 72) | class CatchAllMessage extends Message {
    method constructor (line 76) | constructor (message) {

FILE: src/Middleware.mjs
  class Middleware (line 3) | class Middleware {
    method constructor (line 4) | constructor (robot) {
    method execute (line 18) | async execute (context) {
    method register (line 37) | register (middleware) {

FILE: src/OptParse.mjs
  class OptParse (line 2) | class OptParse extends EventEmitter {
    method constructor (line 3) | constructor (switches) {
    method mappings (line 8) | mappings (switches) {
    method parse (line 16) | parse (args) {
    method toString (line 42) | toString () {

FILE: src/Response.mjs
  class Response (line 3) | class Response {
    method constructor (line 11) | constructor (robot, message, match) {
    method send (line 28) | async send (...strings) {
    method emote (line 38) | async emote (...strings) {
    method reply (line 48) | async reply (...strings) {
    method topic (line 58) | async topic (...strings) {
    method play (line 68) | async play (...strings) {
    method locked (line 78) | async locked (...strings) {
    method #runWithMiddleware (line 84) | async #runWithMiddleware (methodName, opts, ...strings) {
    method random (line 105) | random (items) {
    method finish (line 112) | finish () {
    method http (line 122) | http (url, options) {

FILE: src/Robot.mjs
  constant HUBOT_DEFAULT_ADAPTERS (line 17) | const HUBOT_DEFAULT_ADAPTERS = ['Campfire', 'Shell']
  constant HUBOT_DOCUMENTATION_SECTIONS (line 18) | const HUBOT_DOCUMENTATION_SECTIONS = ['description', 'dependencies', 'co...
  class Robot (line 23) | class Robot {
    method constructor (line 31) | constructor (adapter, httpd, name, alias) {
    method registerHelpCommand (line 84) | registerHelpCommand() {
    method listen (line 188) | listen (matcher, options, callback) {
    method hear (line 201) | hear (regex, options, callback) {
    method respond (line 215) | respond (regex, options, callback) {
    method respondPattern (line 225) | respondPattern (regex) {
    method enter (line 260) | enter (options, callback) {
    method leave (line 271) | leave (options, callback) {
    method topic (line 282) | topic (options, callback) {
    method error (line 292) | error (callback) {
    method invokeErrorHandlers (line 303) | invokeErrorHandlers (error, res) {
    method catchAll (line 322) | catchAll (options, callback) {
    method listenerMiddleware (line 344) | listenerMiddleware (middleware) {
    method responseMiddleware (line 358) | responseMiddleware (middleware) {
    method receiveMiddleware (line 371) | receiveMiddleware (middleware) {
    method receive (line 382) | async receive (message) {
    method processListeners (line 395) | async processListeners (context) {
    method loadmjs (line 430) | async loadmjs (filePath) {
    method loadts (line 442) | async loadts (filePath) {
    method loadjs (line 446) | async loadjs (filePath) {
    method loadFile (line 464) | async loadFile (filepath, filename) {
    method load (line 489) | async load (path) {
    method loadExternalScripts (line 515) | async loadExternalScripts (packages) {
    method setupExpress (line 537) | async setupExpress () {
    method setupNullRouter (line 584) | setupNullRouter () {
    method loadAdapter (line 601) | async loadAdapter (adapterPath = null) {
    method requireAdapterFrom (line 627) | async requireAdapterFrom (adapaterPath) {
    method importAdapterFrom (line 631) | async importAdapterFrom (adapterPath) {
    method importFromRepo (line 636) | async importFromRepo (adapterPath) {
    method helpCommands (line 643) | helpCommands () {
    method parseHelp (line 652) | parseHelp (filePath) {
    method send (line 709) | async send (envelope, ...strings) {
    method reply (line 720) | async reply (envelope, ...strings) {
    method messageRoom (line 730) | async messageRoom (room, ...strings) {
    method on (line 743) | on (event, ...args) {
    method emit (line 754) | emit (event, ...args) {
    method run (line 761) | async run () {
    method shutdown (line 775) | shutdown () {
    method prepareForImport (line 788) | prepareForImport (filePath) {
    method parseVersion (line 795) | parseVersion () {
    method http (line 834) | http (url, options) {
    method herokuKeepalive (line 840) | herokuKeepalive (server) {
    method setupCommandListeners (line 857) | setupCommandListeners () {
  function isCatchAllMessage (line 950) | function isCatchAllMessage (message) {
  function toHeaderCommentBlock (line 954) | function toHeaderCommentBlock (block, currentLine) {
  function isCommentLine (line 968) | function isCommentLine (line) {
  function removeCommentPrefix (line 972) | function removeCommentPrefix (line) {
  function extend (line 976) | function extend (obj, ...sources) {
  function escapeRegExp (line 990) | function escapeRegExp (string) {

FILE: src/User.mjs
  class User (line 5) | class User {
    method constructor (line 10) | constructor (id, options) {
    method set (line 37) | set (key, value) {
    method get (line 42) | get (key) {
    method _constructKey (line 47) | _constructKey (key) {
    method _checkDatastoreAvailable (line 51) | _checkDatastoreAvailable () {
    method _getDatastore (line 57) | _getDatastore () {

FILE: src/adapters/Campfire.mjs
  class Campfire (line 8) | class Campfire extends Adapter {
    method constructor (line 9) | constructor(robot) {
    method send (line 14) | send (envelope/* , ...strings */) {
    method emote (line 36) | emote (envelope/* , ...strings */) {
    method reply (line 41) | reply (envelope/* , ...strings */) {
    method topic (line 46) | topic (envelope/* , ...strings */) {
    method play (line 55) | play (envelope/* , ...strings */) {
    method locked (line 65) | locked (envelope/* , ...strings */) {
    method run (line 84) | async run () {
    method close (line 158) | close() {
  class CampfireStreaming (line 168) | class CampfireStreaming extends EventEmitter {
    method constructor (line 169) | constructor (options, robot, adapter) {
    method Rooms (line 187) | Rooms (callback) {
    method User (line 191) | User (id, callback) {
    method Me (line 195) | Me (callback) {
    method Room (line 199) | Room (id) {
    method get (line 321) | get (path, callback) {
    method post (line 325) | post (path, body, callback) {
    method put (line 329) | put (path, body, callback) {
    method request (line 333) | request (method, path, body, callback) {
  method use (line 405) | use (robot) {

FILE: src/adapters/Shell.mjs
  class Shell (line 36) | class Shell extends Adapter {
    method constructor (line 41) | constructor (robot) {
    method send (line 55) | async send (envelope, ...strings) {
    method emote (line 60) | async emote (envelope, ...strings) {
    method reply (line 64) | async reply (envelope, ...strings) {
    method run (line 69) | async run () {
    method close (line 157) | close () {
  method use (line 168) | use (robot) {

FILE: src/datastores/Memory.mjs
  class InMemoryDataStore (line 5) | class InMemoryDataStore extends DataStore {
    method constructor (line 6) | constructor (robot) {
    method _get (line 14) | async _get (key, table) {
    method _set (line 18) | async _set (key, value, table) {

FILE: test/AdapterName_test.mjs
  class InMemoryAdapter (line 5) | class InMemoryAdapter extends Adapter {
  function getRobotWithAdapter (line 8) | function getRobotWithAdapter (adapter) {

FILE: test/Adapter_test.mjs
  method receive (line 12) | receive (msg) {}

FILE: test/CommandBus_test.mjs
  class FakeRobot (line 8) | class FakeRobot {
    method constructor (line 9) | constructor() {

FILE: test/DataStore_test.mjs
  method emit (line 12) | emit () {}
  method on (line 13) | on () {}
  method receive (line 14) | receive (msg) {}

FILE: test/Listener_test.mjs
  method emit (line 10) | emit (name, err, response) {
  method debug (line 19) | debug () {}
  method error (line 20) | error (...args) {
  method execute (line 151) | execute (context, next, done) {
  method execute (line 187) | async execute (context) {
  method match (line 308) | match (regex) {

FILE: test/Middleware_test.mjs
  method emit (line 13) | emit () {}
  function __guard__ (line 162) | function __guard__ (value, transform) {

FILE: test/doubles/DummyAdapter.mjs
  class DummyAdapter (line 5) | class DummyAdapter extends Adapter {
    method constructor (line 6) | constructor (robot) {
    method send (line 12) | async send (envelope, ...strings) {
    method reply (line 17) | async reply (envelope, ...strings) {
    method topic (line 22) | async topic (envelope, ...strings) {
    method play (line 27) | async play (envelope, ...strings) {
    method run (line 32) | run () {
    method close (line 37) | close () {
    method say (line 41) | async say (user, message, room) {
  method use (line 48) | async use (robot) {

FILE: test/fixtures/MockAdapter.mjs
  class MockAdapter (line 5) | class MockAdapter extends Adapter {
    method constructor (line 6) | constructor (robot) {
    method send (line 11) | async send (envelope, ...strings) {
    method reply (line 15) | async reply (envelope, ...strings) {
    method topic (line 19) | async topic (envelope, ...strings) {
    method play (line 23) | async play (envelope, ...strings) {
    method run (line 27) | run () {
    method close (line 32) | close () {
  method use (line 37) | use (robot) {

FILE: test/index_test.mjs
  class MyUser (line 13) | class MyUser extends User {}
  class MyBrain (line 22) | class MyBrain extends Brain {}
  method on (line 24) | on () {
  class MyRobot (line 36) | class MyRobot extends Robot {}
  class MyAdapter (line 45) | class MyAdapter extends Adapter {}
  class MyResponse (line 53) | class MyResponse extends Response {}
  class MyListener (line 68) | class MyListener extends Listener {}
  class MyTextListener (line 82) | class MyTextListener extends TextListener {}
  class MyMessage (line 93) | class MyMessage extends Message {}
  class MyTextMessage (line 104) | class MyTextMessage extends TextMessage {}
  class MyEnterMessage (line 116) | class MyEnterMessage extends EnterMessage {}
  class MyLeaveMessage (line 127) | class MyLeaveMessage extends LeaveMessage {}
  class MyTopicMessage (line 138) | class MyTopicMessage extends TopicMessage {}
  class MyCatchAllMessage (line 149) | class MyCatchAllMessage extends CatchAllMessage {}
Condensed preview — 96 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (435K chars).
[
  {
    "path": ".editorconfig",
    "chars": 314,
    "preview": "# EditorConfig is awesome: http://EditorConfig.org\n\n# Top-most EditorConfig file\nroot = true\n\n# Match and apply these ru"
  },
  {
    "path": ".github/stale.yml",
    "chars": 1041,
    "preview": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pu"
  },
  {
    "path": ".github/workflows/nodejs-macos.yml",
    "chars": 525,
    "preview": "name: Node.js (macOS) CI\npermissions:\n  contents: read\n  issues: read\n\non:\n  push:\n    branches: [ \"main\" ]\n  schedule:\n"
  },
  {
    "path": ".github/workflows/nodejs-ubuntu.yml",
    "chars": 629,
    "preview": "name: Node.js (Ubuntu) CI\npermissions:\n  contents: read\n  issues: read\n\non:\n  push:\n    branches: [ \"main\" ]\n  schedule:"
  },
  {
    "path": ".github/workflows/nodejs-windows.yml",
    "chars": 589,
    "preview": "name: Node.js (Windows) CI\npermissions:\n  contents: read\n  issues: read\non:\n  push:\n    branches:\n      - main\n  schedul"
  },
  {
    "path": ".github/workflows/pipeline.yml",
    "chars": 2548,
    "preview": "name: Build and release pipeline\npermissions:\n  contents: write\n  issues: write\n  pull-requests: write\n  id-token: write"
  },
  {
    "path": ".github/workflows/sfab-gh-pages.yml",
    "chars": 1417,
    "preview": "# Sample workflow for building and deploying a sfab site to GitHub Pages\nname: Deploy sfab with GitHub Pages dependencie"
  },
  {
    "path": ".gitignore",
    "chars": 104,
    "preview": "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",
    "chars": 109,
    "preview": ".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",
    "chars": 3215,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4614,
    "preview": "# Contributing\n\nContributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6"
  },
  {
    "path": "LICENSE.md",
    "chars": 1060,
    "preview": "Copyright (c) 2011-2024 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of thi"
  },
  {
    "path": "README.md",
    "chars": 6514,
    "preview": "![Pipeline Status](https://github.com/hubotio/hubot/actions/workflows/pipeline.yml/badge.svg)\n\n![Build Status: MacOS](ht"
  },
  {
    "path": "bin/Hubot.mjs",
    "chars": 4265,
    "preview": "'use strict'\n\nimport fs from 'node:fs'\nimport { resolve as pathResolve } from 'node:path'\nimport OptParse from '../src/O"
  },
  {
    "path": "bin/e2e-test.sh",
    "chars": 1232,
    "preview": "#!/bin/bash\nHUBOT_FOLDER=$(pwd)\nTEMP_ROOT=$(mktemp -d)\n\necho \"$ pushd $TEMP_ROOT\"\npushd $TEMP_ROOT\ntrap \"{ CODE=$?; popd"
  },
  {
    "path": "bin/hubot",
    "chars": 81,
    "preview": "#!/usr/bin/env node\n\nimport('./Hubot.mjs').then(async ({ default: robot }) => {})"
  },
  {
    "path": "configuration/Config.mjs",
    "chars": 150,
    "preview": "// Description:\n//   Configuration\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async rob"
  },
  {
    "path": "docs/adapters/campfire.md",
    "chars": 1901,
    "preview": "---\ntitle: Campfire adapter\nlayout: layouts/docs.html\npermalink: /adapters/campfire.html\n---\n\n# Campfire adapter\n\n[Campf"
  },
  {
    "path": "docs/adapters/development.md",
    "chars": 3937,
    "preview": "---\ntitle: Development adapter\nlayout: layouts/docs.html\npermalink: /adapters/development.html\n---\n\n# Development adapte"
  },
  {
    "path": "docs/adapters/shell.md",
    "chars": 674,
    "preview": "---\ntitle: Shell adapter\nlayout: layouts/docs.html\npermalink: /adapters/shell.html\n---\n\n# Shell adapter\n\nThe shell adapt"
  },
  {
    "path": "docs/adapters.md",
    "chars": 1406,
    "preview": "---\ntitle: Adapters\nlayout: layouts/docs.html\npermalink: /adapters.html\n---\n\n# Adapters\n\nAdapters are the interface to t"
  },
  {
    "path": "docs/assets/stylesheets/application.css",
    "chars": 25505,
    "preview": "@font-face {\n    font-family: 'octicons';\n    src: url(\"../vendors/octicons/octicons/octicons.eot\");\n    src: url(\"../ve"
  },
  {
    "path": "docs/deploying/azure.md",
    "chars": 5635,
    "preview": "---\ntitle: Deploying to Azure\nlayout: layouts/docs.html\npermalink: /deploying/azure.html\n---\n\n# Deploying to Azure\n\nIf y"
  },
  {
    "path": "docs/deploying/bluemix.md",
    "chars": 4273,
    "preview": "---\ntitle: Deploying to Bluemix\nlayout: layouts/docs.html\npermalink: /deploying/bluemix.html\n---\n\n# Deploying to Bluemix"
  },
  {
    "path": "docs/deploying/unix.md",
    "chars": 3290,
    "preview": "---\ntitle: Deploying to Unix\nlayout: layouts/docs.html\npermalink: /deploying/unix.html\n---\n\n# Deploying to Unix\n\nBecause"
  },
  {
    "path": "docs/deploying/windows.md",
    "chars": 3295,
    "preview": "---\ntitle: Deploying to Windows\nlayout: layouts/docs.html\npermalink: /deploying/windows.html\n---\n\n# Deploying to Windows"
  },
  {
    "path": "docs/deploying.md",
    "chars": 234,
    "preview": "---\ntitle: Deploying\nlayout: layouts/docs.html\npermalink: /deploying.html\n---\n\n# Deploying\n\n- [Azure](./deploying/azure."
  },
  {
    "path": "docs/designs/commands.md",
    "chars": 6760,
    "preview": "You are an expert javascript and Node.js engineer implementing a backwards-compatible command subsystem for Hubot.\n\nIMPO"
  },
  {
    "path": "docs/docs.md",
    "chars": 5063,
    "preview": "---\ntitle: Getting Started With Hubot\nlayout: layouts/docs.html\npublished: 2023-10-10T19:25:22.000Z\npermalink: /docs.htm"
  },
  {
    "path": "docs/implementation.md",
    "chars": 3653,
    "preview": "---\ntitle: Implementation Notes\nlayout: layouts/docs.html\npermalink: /implementation.html\n---\n\n# Implementation\n\nFor the"
  },
  {
    "path": "docs/index.html",
    "chars": 3780,
    "preview": "{{#> layouts/main.html}}\n<meta name=\"title\" content=\"Hubot is your friendly robot sidekick. Install him in your company "
  },
  {
    "path": "docs/layouts/docs.html",
    "chars": 4688,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <base href=\"{{ base.href }}\"/>\n     "
  },
  {
    "path": "docs/layouts/main.html",
    "chars": 2535,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\"/>\n        <base href=\"{{ base.href }}\"/>\n     "
  },
  {
    "path": "docs/patterns.md",
    "chars": 13087,
    "preview": "---\ntitle: Patterns\nlayout: layouts/docs.html\nshould_publish: yes\npublished: 2023-10-10T19:25:22.000Z\npermalink: /patter"
  },
  {
    "path": "docs/scripting.md",
    "chars": 42616,
    "preview": "---\ntitle: Scripting\nlayout: layouts/docs.html\nshould_publish: yes\npublished: 2023-10-10T19:25:22.000Z\npermalink: /scrip"
  },
  {
    "path": "examples/hubot-start.ps1",
    "chars": 342,
    "preview": "#Hubot PowerShell Start Script\n#Invoke from the PowerShell prompt or start via automated tools \n\n$HubotPath = \"drive:\\pa"
  },
  {
    "path": "examples/hubot.service",
    "chars": 695,
    "preview": "; Hubot systemd service unit file\n; Place in e.g. `/etc/systemd/system/hubot.service`, then `systemctl daemon-reload` an"
  },
  {
    "path": "index.mjs",
    "chars": 1166,
    "preview": "'use strict'\n\nimport User from './src/User.mjs'\nimport Brain from './src/Brain.mjs'\nimport Robot from './src/Robot.mjs'\n"
  },
  {
    "path": "package.json",
    "chars": 1167,
    "preview": "{\n  \"name\": \"hubot\",\n  \"version\": \"0.0.0-development\",\n  \"author\": \"hubot\",\n  \"keywords\": [\n    \"github\",\n    \"hubot\",\n "
  },
  {
    "path": "script/bootstrap",
    "chars": 33,
    "preview": "#!/usr/bin/env bash\n\nnpm install\n"
  },
  {
    "path": "script/release",
    "chars": 819,
    "preview": "#!/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# "
  },
  {
    "path": "script/server",
    "chars": 39,
    "preview": "#!/usr/bin/env bash\n\nnpm start -- \"$@\"\n"
  },
  {
    "path": "script/simple-lint.mjs",
    "chars": 2362,
    "preview": "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst projectRo"
  },
  {
    "path": "script/smoke-test",
    "chars": 40,
    "preview": "#!/usr/bin/env bash\n\nnpm run test:smoke\n"
  },
  {
    "path": "script/test",
    "chars": 38,
    "preview": "#!/usr/bin/env bash\n\nnpm test -- \"$@\"\n"
  },
  {
    "path": "sfab-hooks/SfabHook.mjs",
    "chars": 147,
    "preview": "export default () => {\n  return {\n    model (file, model) {\n      return {\n        base: {\n          href: '/hubot/'\n   "
  },
  {
    "path": "src/Adapter.mjs",
    "chars": 4727,
    "preview": "'use strict'\n\nimport EventEmitter from 'node:events'\n\nclass Adapter extends EventEmitter {\n  // An adapter is a specific"
  },
  {
    "path": "src/Brain.mjs",
    "chars": 6193,
    "preview": "'use strict'\n\nimport EventEmitter from 'node:events'\nimport User from './User.mjs'\n\n// If necessary, reconstructs a User"
  },
  {
    "path": "src/CommandBus.mjs",
    "chars": 23646,
    "preview": "import { EventEmitter } from 'node:events'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\n/**\n * CommandBus prov"
  },
  {
    "path": "src/DataStore.mjs",
    "chars": 3178,
    "preview": "'use strict'\n\nexport class DataStore {\n  // Represents a persistent, database-backed storage for the robot. Extend this."
  },
  {
    "path": "src/GenHubot.mjs",
    "chars": 7523,
    "preview": "import { spawnSync } from 'node:child_process'\nimport File from 'node:fs'\nimport path from 'node:path'\n\nfunction runComm"
  },
  {
    "path": "src/HttpClient.mjs",
    "chars": 8689,
    "preview": "/*\nCopyright (c) 2014 rick\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this softwar"
  },
  {
    "path": "src/Listener.mjs",
    "chars": 3528,
    "preview": "'use strict'\n\nimport { inspect } from 'node:util'\nimport Middleware from './Middleware.mjs'\n\nclass Listener {\n  // Liste"
  },
  {
    "path": "src/Message.mjs",
    "chars": 2111,
    "preview": "'use strict'\n\nexport class Message {\n  // Represents an incoming message from the chat.\n  //\n  // user - A User instance"
  },
  {
    "path": "src/Middleware.mjs",
    "chars": 1402,
    "preview": "'use strict'\n\nclass Middleware {\n  constructor (robot) {\n    this.robot = robot\n    this.stack = []\n  }\n\n  // Public: Ex"
  },
  {
    "path": "src/OptParse.mjs",
    "chars": 1352,
    "preview": "import EventEmitter from 'node:events'\nclass OptParse extends EventEmitter {\n  constructor (switches) {\n    super()\n    "
  },
  {
    "path": "src/Response.mjs",
    "chars": 3748,
    "preview": "'use strict'\n\nclass Response {\n  // Public: Responses are sent to matching listeners. Messages know about the\n  // conte"
  },
  {
    "path": "src/Robot.mjs",
    "chars": 31837,
    "preview": "'use strict'\n\nimport EventEmitter from 'node:events'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { path"
  },
  {
    "path": "src/User.mjs",
    "chars": 1463,
    "preview": "'use strict'\n\nimport { DataStoreUnavailable } from './DataStore.mjs'\n\nclass User {\n  // Represents a participating user "
  },
  {
    "path": "src/adapters/Campfire.mjs",
    "chars": 11704,
    "preview": "'use strict'\n\nimport HTTPS from 'node:https'\nimport EventEmitter from 'node:events'\nimport Adapter from '../Adapter.mjs'"
  },
  {
    "path": "src/adapters/Shell.mjs",
    "chars": 4997,
    "preview": "'use strict'\n\nimport { stat, writeFile, unlink, appendFile, readFile } from 'node:fs/promises'\nimport readline from 'nod"
  },
  {
    "path": "src/datastores/Memory.mjs",
    "chars": 424,
    "preview": "'use strict'\n\nimport { DataStore } from '../DataStore.mjs'\n\nclass InMemoryDataStore extends DataStore {\n  constructor (r"
  },
  {
    "path": "test/AdapterName_test.mjs",
    "chars": 1075,
    "preview": "import test from 'node:test'\nimport assert from 'node:assert/strict'\nimport { Robot, Adapter } from '../index.mjs'\n\nclas"
  },
  {
    "path": "test/Adapter_test.mjs",
    "chars": 2838,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/Brain_test.mjs",
    "chars": 14231,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/CommandBus_test.mjs",
    "chars": 29855,
    "preview": "import { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert'\nimport fs from 'node:fs"
  },
  {
    "path": "test/Configuration_test.mjs",
    "chars": 581,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/DataStore_test.mjs",
    "chars": 4279,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/Hubot_test.mjs",
    "chars": 4288,
    "preview": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport path from 'node:pa"
  },
  {
    "path": "test/Listener_test.mjs",
    "chars": 10661,
    "preview": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { EnterMessage, Te"
  },
  {
    "path": "test/Message_test.mjs",
    "chars": 940,
    "preview": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { User, Message, T"
  },
  {
    "path": "test/Middleware_test.mjs",
    "chars": 5246,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/OptParse-test.mjs",
    "chars": 1726,
    "preview": "import { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport OptParse from '../src/OptParse.m"
  },
  {
    "path": "test/Robot_test.mjs",
    "chars": 34107,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/Shell_test.mjs",
    "chars": 9435,
    "preview": "'use strict'\n\nimport { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\nim"
  },
  {
    "path": "test/User_test.mjs",
    "chars": 1028,
    "preview": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport { User } from '../"
  },
  {
    "path": "test/XampleTest.mjs",
    "chars": 1861,
    "preview": "import { describe, it, beforeEach, afterEach } from 'node:test'\nimport assert from 'node:assert/strict'\n\n// Replace this"
  },
  {
    "path": "test/doubles/DummyAdapter.mjs",
    "chars": 1210,
    "preview": "'use strict'\n// Replace this with import { Adapter, TextMessage } from 'hubot'\nimport { Adapter, TextMessage } from '../"
  },
  {
    "path": "test/fixtures/MockAdapter.mjs",
    "chars": 737,
    "preview": "'use strict'\n\nimport { Adapter } from '../../index.mjs'\n\nexport class MockAdapter extends Adapter {\n  constructor (robot"
  },
  {
    "path": "test/fixtures/TestScript.js",
    "chars": 280,
    "preview": "'use strict'\n\n// Description: A test script for the robot to load\n//\n// Commands:\n//   hubot test - Responds with a test"
  },
  {
    "path": "test/fixtures/TestScript.mjs",
    "chars": 327,
    "preview": "'use strict'\n\n// Description: A test .mjs script for the robot to load\n//\n// Commands:\n//   hubot test mjs - Responds wi"
  },
  {
    "path": "test/fixtures/TestScript.ts",
    "chars": 322,
    "preview": "'use strict'\n\n// Description: A test .ts script for the robot to load\n//\n// Commands:\n//   hubot test ts - Responds with"
  },
  {
    "path": "test/fixtures/TestScriptIncorrectApi.js",
    "chars": 154,
    "preview": "'use strict'\n\n// Description: A test script for the robot to load\n//\n// Commands:\n//   hubot test - Responds with a test"
  },
  {
    "path": "test/fixtures/TestScriptIncorrectApi.mjs",
    "chars": 180,
    "preview": "'use strict'\n\n// Description: A test .mjs script for the robot to load\n//\n// Commands:\n//   hubot test mjs - Responds wi"
  },
  {
    "path": "test/index_test.mjs",
    "chars": 4947,
    "preview": "'use strict'\n\nimport { describe, it } from 'node:test'\nimport assert from 'node:assert/strict'\nimport {\n  Adapter, User,"
  },
  {
    "path": "test/ordered-scripts/01-PFirst.mjs",
    "chars": 192,
    "preview": "// Description:\n//   First one\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot ="
  },
  {
    "path": "test/ordered-scripts/02-SetupBotConfig.mjs",
    "chars": 180,
    "preview": "// Description:\n//   Setup bot configuration\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default"
  },
  {
    "path": "test/ordered-scripts/WebSetup.mjs",
    "chars": 165,
    "preview": "// Description:\n//   Third one\n//\n// Commands:\n//\n// Notes:\n//   This is a test script.\n//\n\nexport default async robot ="
  },
  {
    "path": "test/scripts/Xample.mjs",
    "chars": 475,
    "preview": "// Description:\n//   Test script\n//\n// Commands:\n//   hubot helo - Responds with HELO World!.\n//\n// Notes:\n//   This is "
  }
]

// ... and 7 more files (download for full content)

About this extraction

This page contains the full source code of the hubotio/hubot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 96 files (405.7 KB), approximately 102.8k tokens, and a symbol index with 273 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!