Thanks for helping out. This guide covers setup, how we work, and what a good PR looks like.
We're taking part in Hacktoberfest this year, and first-time contributors are very welcome.
- Pick an open issue labeled
hacktoberfestorgood first issuethat nobody is assigned to. - Comment on it to ask for it. I'll assign it to you, usually within a day. Please don't start on an issue that's assigned to someone else.
- Take one issue at a time. Once your PR is merged, grab the next one. After 3 merged PRs with us you can hold two issues at once.
- If you're assigned and there's no PR or update for 5 days, I'll free the issue up for someone else. Just comment if you need more time.
- Open your PR against
devhere in codercops/chatcops and putCloses #<issue number>in the description. A PR merged inside your own fork never reaches us, so it doesn't count. A check on the PR ("Claim check") fails if that issue isn't assigned to you. Another one ("Description check") fails while the "How I tested this" or "AI assistance" section of the description is missing or empty.
PRs count for Hacktoberfest once they're merged or labeled hacktoberfest-accepted. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the spam label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code. The full rules are under Ground rules below.
Stuck? Ask on the issue, or in #oss-help on our Discord. Questions are welcome. Claims still happen on the issue, so the assignment stays visible to everyone.
You can use AI tools (Copilot, ChatGPT, Claude, Cursor and so on). Two conditions:
- Say so in the PR. If a tool wrote or rewrote more than autocomplete-sized pieces, fill in the "AI assistance" line in the PR template: which tool, and what you used it for.
- You own every line. Run it, test it, and be ready to explain it in review. If you can't explain a change, the PR gets closed.
PRs that are unreviewed AI output (code that doesn't run, invented APIs, a description that doesn't match the diff, unrelated rewrites) are treated as spam.
These get closed and labeled spam:
- a PR for an issue that isn't assigned to you, or that isn't linked to an issue at all
- formatting, whitespace or typo-only changes nobody asked for
- one fix split into several PRs
- a copy of someone else's PR
- claiming a pile of issues at once, or bulk and automated submissions
- untested generated code
Two spam PRs and you won't be assigned more issues this month, and you're out of the CODERCOPS goodies.
Every PR needs a "How I tested this" line: what you ran or clicked through to check it works.
For a bug fix, check that your new test fails without the fix: undo the change in the source, run the tests, then put it back. A test that passes either way doesn't catch anything.
I review most PRs within a few days. Please don't ping repeatedly or DM maintainers about a review. If three days pass with no reply, one follow-up comment on the PR is fine.
The top 5 contributors this October get a CODERCOPS T-shirt and stickers. You need at least 5 merged PRs labeled hacktoberfest-accepted across chatcops, ogcops, toolbelt and discord-bot, merged between Oct 1 and Oct 31 (UTC), and ties go to whoever got there first. To keep it fair:
- only PRs for issues that were assigned to you count
- a fix split into several PRs counts once
- what counts is the maintainers' call, and that call is final
These goodies come from CODERCOPS through our own open source program. They're separate from Hacktoberfest, so anything Hacktoberfest itself gives out is a different thing with its own rules.
- Node.js 22 or newer (CI runs on 22)
- pnpm 9. The easiest way is
corepack enable, which picks up the version pinned inpackage.json.
This is a pnpm workspace. npm install does not work here (it fails on workspace:* dependencies).
# Fork the repo on GitHub, then clone your fork
git clone https://github.com/<your-username>/chatcops.git
cd chatcops
# Add upstream remote
git remote add upstream https://github.com/codercops/chatcops.git
# Install dependencies
pnpm install
# Build all packages (do this before running tests)
pnpm -r build
# Run tests
pnpm test
# Run tests with coverage, as CI does (fails if a package drops below its minimum)
pnpm test:coverage
# Typecheck
pnpm -r typecheckBuild before you test. The server tests import @chatcops/core from its dist folder, so on a fresh clone pnpm test fails until you've run pnpm -r build. If you change packages/core, rebuild it before running the server tests again.
pnpm -r build also builds the website. To build only the packages, run pnpm --filter "./packages/*" build.
packages/
core/ @chatcops/core (AI providers, tools, knowledge base)
widget/ @chatcops/widget (embeddable chat UI)
server/ @chatcops/server (server handler + adapters)
store-*/ conversation, counter and analytics stores, one package per backend
website/ Marketing site + Starlight docs (Astro)
- Sync your fork's
devbranch with upstream:git fetch upstream git checkout dev git merge upstream/dev
- Create a branch for your change. Don't work directly on
dev, or every PR you open will include commits from the others.git checkout -b fix/express-cors-example
- Run tests:
pnpm test - Run typecheck:
pnpm -r typecheck - Add a changeset if your change affects a published package (
core,widgetorserver):pnpm changeset. Docs and website changes don't need one. - Push your branch and open a PR against upstream
dev.
devis the default branch. All development happens here.productionis the release branch, merged fromdevwhen cutting a release.- PRs should target
devunless you're doing a production release.
flowchart LR
A[PRs merged into dev<br/>with changesets] --> B[release PR<br/>dev to production]
B --> C[Release workflow opens<br/>chore: version packages]
C --> D[merge it: publish to npm]
D --> E[sync PR<br/>production into dev]
E --> A
-
Every PR to
devthat changes package source (packages/*/src) needs a changeset; thechangesetcheck in CI fails without one. Create it with:pnpm changeset
For a change that shouldn't be released (an internal refactor, for example), use
pnpm changeset --empty. -
When
devis ready to ship, a maintainer opens a PR fromdevtoproductionand merges it with a merge commit. -
The Release workflow runs on every push to
production:- It builds, typechecks and tests everything.
- If there are unreleased changesets, the Changesets action opens a "chore: version packages" PR on
productionthat bumps versions and updates changelogs. - Merging that PR runs the workflow again, which publishes to npm through OIDC trusted publishing.
-
After a publish,
devhas to catch up with the version commit onproduction. The workflow opens a "sync: production into dev" PR when the release bot is set up (below). Merge it with a merge commit, not a squash. Without the bot, the job summary shows the command to run instead:git fetch upstream && git push upstream upstream/production:dev -
The website is deployed to Cloudflare Workers by Workers Builds (the
chatcops-websiteWorker, config inwebsite/wrangler.jsonc). No manual step needed.
Note: Only maintainers can merge into
production. If you're a contributor, make sure your PR todevincludes a changeset when it changes a package.
The sync PR has to be opened by a GitHub App: dev requires CI to pass, and a PR opened with the workflow's own GITHUB_TOKEN doesn't trigger CI.
- In the organization settings, open Developer settings > GitHub Apps > New GitHub App. Any name works (ours is
chatcops-release-bot). Turn off the webhook. GitHub asks you to confirm your identity first. - Under Repository permissions, give it Contents: Read and write and Pull requests: Read and write. Nothing else.
- Create it, then Install App on
codercops/chatcopsonly. - Copy the App ID into a repository variable named
RELEASE_BOT_APP_ID(Settings > Secrets and variables > Actions > Variables). - In the app settings, open Credentials > Key pairs and click New key. GitHub downloads the
.pemfile once. Save it as a repository secret, then delete the file:gh secret set RELEASE_BOT_PRIVATE_KEY -R codercops/chatcops < path/to/key.pem
The next publish opens the sync PR on its own.
cd packages/widget
# Rebuild the widget on every change
pnpm dev
# In a second terminal, serve the folder
pnpm exec viteThen open http://localhost:5173/dev.html. Opening dev.html straight from disk doesn't work, because browsers block module scripts on file:// URLs.
The page starts in popup mode. Open http://localhost:5173/dev.html?mode=inline to render the widget inside a 400×600 container instead (any other mode value falls back to popup). The links at the top switch modes, and the Open and Close buttons call ChatCops.open() and ChatCops.close().
To test with a real backend, build the packages first, then run the Express example:
pnpm --filter "./packages/*" build
cd packages/server
ANTHROPIC_API_KEY=sk-... npx tsx src/examples/express-server.tsnpx downloads tsx the first time.
The widget has a size budget: 14.5 kB gzipped for the script tag bundle (dist/chatcops.min.js) and 18 kB for the ESM build, checked in CI by pnpm size after a build (limits in .size-limit.json).
The unit tests run in happy-dom. The tests in e2e/ load the built widget in a real Chromium and run an axe accessibility check. e2e/server.mjs serves the pages and runs the real @chatcops/server handler, with a stand-in provider that answers by the message text (no API keys needed). A second, cross-origin server stands in for a 0.5 server:
pnpm --filter @chatcops/widget --filter "@chatcops/server..." build
pnpm exec playwright install chromium # once
pnpm e2eThey cover starting from the script tag, streaming, Escape and the tab order, the phone layout, accessibility, a reply that fails or is cut off, a slow tool kept alive by pings, session tokens, and a chat API on another origin. CI runs them in the e2e job. Add a test there when you change how the widget behaves in a browser.
- Create
packages/core/src/providers/{name}.ts - Implement the
AIProviderinterface - Add a format converter in
base.ts - Register in the
createProviderfactory - Export from
index.ts - Add tests
Stores live in their own packages (packages/store-<name>), so a Cloudflare user never installs a Postgres driver. Each one implements the interfaces in packages/core/src/storage/types.ts: conversations is required, and counters, kv and events are what make rate limits and analytics work across server instances. memoryStorage() in packages/core/src/storage/memory.ts is the reference.
A new package needs:
-
package.jsonwith"private": trueuntil a maintainer publishes it for the first time,"type": "module", anexportsentry like@chatcops/core's,"@chatcops/core": "workspace:^"inpeerDependencies(and"workspace:*"indevDependencies), the database client as a peer dependency where there is one, and thebuild,typecheckandtestscripts. -
tsconfig.jsonextending../../tsconfig.json, like the other packages. -
tests/conformance.test.tsrunning the shared suite, which is the contract every store has to meet:import { describe } from 'vitest'; import { runStorageConformance } from '@chatcops/core/testing'; import { redisStorage } from '../src/index.js'; const url = process.env.CHATCOPS_TEST_REDIS_URL; if (url) runStorageConformance('redis', () => redisStorage({ url })); else describe.skip('redis conformance (set CHATCOPS_TEST_REDIS_URL to run)', () => {});
CI runs Redis, the Upstash REST emulator and Postgres next to the tests and sets CHATCOPS_TEST_REDIS_URL, CHATCOPS_TEST_UPSTASH_URL with CHATCOPS_TEST_UPSTASH_TOKEN, and CHATCOPS_TEST_POSTGRES_URL (see .github/workflows/ci.yml). Locally, start the same containers with Docker and export the variables, or let those tests skip. The root vitest.config.ts and pnpm publint pick up a new package by themselves, and store packages need 90% line and 80% branch coverage, checked in CI (locally the database tests skip, so the store floor only applies there).
There are two sets of strings:
- Core strings (
packages/core/src/i18n/):- Create
packages/core/src/i18n/{code}.ts - Export all
LocaleStringsfields - Register in
packages/core/src/i18n/index.ts - Add to the test in
packages/core/tests/i18n/locales.test.ts
- Create
- Widget UI strings (
packages/widget/src/i18n.ts). The widget is a zero-dependency bundle and doesn't import core, so a language has to be added here too before the widget UI shows it.
Use proper accents and native script (for example Devanagari for Hindi).
pnpm --filter "./packages/*" build # the site imports the built widget
cd website
pnpm devThe site runs without any secrets. Only the live chat demo on the landing page needs OPENAI_API_KEY, and most contributors can skip it. If you want the demo or the visitor counter locally, put the values in website/.dev.vars:
OPENAI_API_KEY=sk-...
UPSTASH_REDIS_REST_URL=https://your-endpoint.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
VISITOR_HASH_SALT=any-long-random-stringThe visitor counter shows - without Upstash credentials, and counts each visitor once per day only when VISITOR_HASH_SALT is set. Without it, every call increments the count. Google Analytics (G-GLYL9J6QYX) is hardcoded in the layout and Starlight config.
Use conventional commits: feat:, fix:, chore:, docs:, test:, refactor: