Repository instructions for AI coding agents working on TripBot. This file applies to the entire repository unless a
more specific AGENTS.md exists below the file being edited.
- Follow the user's current request.
- Follow the nearest applicable
AGENTS.md. - Preserve established repository behavior and conventions.
- Prefer the smallest complete change that solves the requested problem.
If instructions conflict or a requested operation could affect production data, Discord users, credentials, or an external service, stop and ask before taking the risky action.
- Use Node.js with npm for dependency installation and package command execution. Do not use Bun, Yarn, or pnpm.
- Prefer existing
package.jsonscripts when they perform the required task without creating unrelated changes. - Add or update Vitest tests for new or changed behavior as part of the change, without waiting to be asked. Follow
the existing layout and mocks in
src/vitest/, and run the relevant tests before handing off. - Do not edit generated Prisma clients under
src/prisma/*/generated/. - Do not edit
archive,legacy, placeholder, Matrix, or Telegram code (including the archived IRC bot undersrc/irc/archive/) unless the task specifically targets it. - Never commit
.env, tokens, passwords, private URLs, production identifiers, database dumps, or user data. - Never run a production migration, deploy Discord commands, push a branch, or mutate an external service unless the user explicitly requested that operation.
- Preserve unrelated user changes. Never discard, reset, overwrite, or reformat files outside the requested scope.
- Do not use broad auto-fix commands against the whole repository. They can create large unrelated diffs.
TripBot is a strict TypeScript application whose primary runtime is Node.js. It contains:
- a Discord.js bot;
- an Express API;
- a PostgreSQL TripBot database accessed through Prisma;
- a read-oriented Moodle MariaDB integration with its own Prisma schema;
- Docker-based local services;
- Vitest tests, covering the API, Discord command, global business-logic, and IRC layers.
TripBot provides harm-reduction and community tooling. Treat changes to drug information, interactions, dosage text, crisis resources, moderation, privacy, and user records as high-impact. Preserve established wording and data sources; do not invent medical claims or silently weaken safety checks.
- Required Node version:
22.14.0from.nvmrc;package.jsonallows Node>=22.14.0. - Package manager: npm.
- Application runtime: Node.js.
- TypeScript target: ES2022.
- Module format: CommonJS for application code.
- Type checking: strict.
- Style: ESLint with Airbnb TypeScript and SonarJS rules.
- Database toolkit: Prisma 7 with PostgreSQL and MariaDB adapters.
- Test runner: Vitest.
- Dependency lockfile:
package-lock.json. - Package scripts, Docker files, hooks, and CI workflows use npm/npx.
Before editing:
- Run
git status --short --branchand note pre-existing changes. - Read this file and any more specific
AGENTS.mdin the target subtree. - Read the relevant implementation, its callers, adjacent types, and existing tests before choosing a fix.
- Check
package.json,tsconfig.json,.eslintrc, and the relevant Prisma schema only when the task touches them. - Keep the requested scope narrow. Do not turn a targeted fix into cleanup or modernization work.
After editing:
- Review
git diff -- <changed paths>for correctness and accidental churn. - Run the smallest relevant verification set from this document.
- Run
git diff --check. - Confirm
git status --shortcontains only intended files or clearly identify pre-existing changes. - Report what changed, what was verified, and any verification that could not be run.
For an unchanged checkout, perform the reproducible lockfile install used by CI and Docker:
npm ciAvoid dependency changes unless the task requires them. If a dependency must be added, removed, or upgraded:
- use
npm install <package>,npm install --save-dev <package>, ornpm uninstall <package>; - commit both
package.jsonandpackage-lock.jsonwhen both change; - do not hand-edit dependency versions in only one of those files;
- run
npm ciafter the change to confirm the lockfile is reproducible.
Do not create or commit bun.lock, yarn.lock, or pnpm-lock.yaml.
Use npm run <script> for repository scripts and npx <binary> for locally installed command-line tools.
# Type-check without writing build output
npx tsc --noEmit --pretty false
# Lint only files touched by the task
npx eslint --ext .ts,.js path/to/file.ts
# Lint the repository without auto-fixing
npx eslint --ext .ts,.js .
# Run the active Vitest suite
npm run tripbot:test
# Run one existing test file (also works outside the active include globs)
npx vitest run ./path/to/file.test.ts
# Run with coverage
npx vitest run --coverageUse targeted lint and tests first. Repository-wide lint may expose unrelated pre-existing failures.
Prisma clients must exist before a full type-check or build:
npx prisma generate --schema ./src/prisma/tripbot/schema.prisma
npx prisma generate --schema ./src/prisma/moodle/schema.prisma
npx tsc
npx tsc-alias
npx tsc --project tsconfig.prisma.jsonBuild output goes to build/ and must not be committed.
Create a local .env from .env.example, fill in only the values needed for the task, and never commit it.
# Start only the local PostgreSQL service
npm run db:start
# Inspect its logs
npm run db:logs
# Validate and format the default TripBot schema
npx prisma validate
npx prisma format
# Apply already-committed TripBot migrations to the configured local database
npx prisma migrate deployprisma.config.ts selects the TripBot schema by default. It selects Moodle only when PRISMA_SCHEMA=moodle. In
PowerShell, scope that variable carefully and remove it after the command:
$env:PRISMA_SCHEMA = 'moodle'
npx prisma generate
Remove-Item Env:PRISMA_SCHEMADatabase commands can mutate data. Use only a disposable/local database unless the user explicitly authorizes another
target. Do not run prisma db push, prisma migrate reset, or destructive SQL as routine verification.
- The development bot runs inside the
tripbotdevcontainer (.devcontainer/devcontainer.json, thetripbotservice indocker-compose.yml), not on the host. ItspostStartCommandrunsnpm run tripbot:startin a tmux session. Hostnames such astripbot_databaseonly resolve on the Docker network, so starting the bot on the host fails to reach the database. Run bot scripts from the devcontainer terminal, or from the host withdocker exec -it tripbot npm run tripbot:restart(likewisetripbot:logs,tripbot:stop). - The container keeps its own
node_modulesvolume. After a dependency change, runnpm ciinside the container (docker exec tripbot npm ci) or rebuild the devcontainer; a host install does not reach it. - The dev database uses the named volume
tripbot_database_data, so it persists across rebuilds. Removing that volume (docker volume rm tripbot_database_data) wipes local data; only do so when the user asks. - Use
npm run <script>for the scripts defined inpackage.json. tripbot:start,tripbot:dev, andtripbot:restartassumetmux, so they are not native Windows development commands.db:generateand related scripts use POSIX inline environment-variable syntax. On Windows PowerShell, use explicitnpx prismacommands and$env:PRISMA_SCHEMAas shown above.- Docker Compose is the preferred portable way to start the database service.
Deploying Discord commands changes the registered commands for a real application. Only when explicitly requested and the target environment is confirmed, use:
npm run tripbot:deployCommandssrc/start.ts Process entry point; starts API, database setup, and Discord
src/api/ Express application, middleware, v1 and v2 routes
src/discord/discord.ts Discord client startup
src/discord/commands/global/ Commands available across supported Discord contexts
src/discord/commands/guild/ Guild-oriented Discord commands
src/discord/commands/archive/ Archived commands; leave untouched unless requested
src/discord/events/ Discord event handlers
src/discord/utils/ Command deployment, context, embeds, and Discord helpers
src/irc/irc.ts IRC connection to TripSit's Ergo servers
src/irc/bridge.ts IRC side of the chat bridge (RELAYMSG, send queue)
src/global/bridge/ Platform-independent chat bridge core and channel list
src/discord/@types/ Discord command contracts and type augmentation
src/global/commands/ Platform-independent business logic, conventionally g.*
src/global/utils/ Shared utilities, environment, logging, experience, and database updates
src/global/@types/ Global declarations and shared ambient types
src/prisma/tripbot/schema.prisma Primary PostgreSQL schema
src/prisma/tripbot/migrations/ Primary database migration history
src/prisma/tripbot/client.ts PostgreSQL Prisma client adapter
src/prisma/moodle/schema.prisma Moodle MariaDB schema
src/prisma/moodle/client.ts Moodle Prisma client adapter
src/prisma/*/generated/ Generated clients; ignored and never edited manually
vitest.config.mts Active Vitest configuration
src/vitest/ Shared test infrastructure (Discord + Prisma mocks)
src/discord/tests/ Discord command tests (mirrors commands/{global,guild})
src/global/tests/ Global business-logic (g.*) tests
src/irc/tests/ IRC connection and bridge tests
src/docker/ Container build, startup, and wait scripts
assets/ Runtime data, fonts, and images
build/ Generated TypeScript output; ignored
Inactive or partially maintained areas such as Matrix, Telegram, archive, legacy, placeholder, and any
not-yet-migrated flat files directly under src/discord/tests/ are excluded by TypeScript, ESLint, CI, or Vitest
patterns. Do not assume a repository-wide command validates those areas. src/irc/ (outside archive/) is active
code: it is type-checked, linted, tested, and covered by CI.
The intended command flow is:
Discord interaction -> src/discord/commands/.../d.* -> src/global/commands/g.* -> Prisma/external service
- Keep parsing, embeds, buttons, modals, permissions, and interaction lifecycle in
d.*command files. - Put reusable business logic in
src/global/commands/g.*when it can be platform-independent. - Do not make global command logic depend on Discord interaction objects without a strong existing precedent.
- Reuse existing helpers rather than duplicating database or formatting logic.
src/discord/commands/index.ts dynamically loads .ts/.js modules from the global and guild command folders and
registers an exported object that has a .data property.
When adding or changing a command:
- start from the nearest real command, not only a template;
- use the
SlashCommand,MessageCommand, orUserCommandcontract fromsrc/discord/@types/commandDef.ts; - keep the command builder name and exported command object coherent;
- place it in
global/orguild/according to its intended Discord contexts; - set contexts and integration types deliberately; do not broaden availability accidentally;
- preserve permission, role, guild, and environment checks;
- log command context using the established
commandContextpattern where adjacent commands do so; - defer or reply exactly once, then use
editReply/followUpconsistently; - use
MessageFlags.Ephemeralwhere the surrounding code does, especially for private or sensitive responses; - return the boolean required by the command type;
- redeploy command definitions only when the user explicitly requests it.
If options, subcommands, contexts, integration types, or descriptions change, state in the handoff that Discord command registration must be redeployed.
TripSit's IRC network runs Ergo (ergochat): irc.tripsit.io for development and irc.tripsit.me for
production, selected by NODE_ENV unless IRC_SERVER overrides it. Ergo is a single daemon with built-in services, so
do not give Anope, Atheme, or other separate-services instructions; confirm commands with /msg NickServ HELP or
/msg ChanServ HELP on the server when unsure.
- The bot uses
irc-framework(src/irc/irc.ts), connects with TLS on 6697, and authenticates with SASL PLAIN usingIRC_USERNAME/IRC_PASSWORD. It only connects whenIRC_PASSWORDis set. WhenIRC_OPER_NAME/IRC_OPER_PASSWORDare set it sendsOPERafter registering. - The Ergo server config (
ircd.yaml,ircd.db) lives on the IRC hosts, not in a repository; thetripsit/ircrepo only holds itsdocker-compose.yml. Give the user config snippets to apply rather than assuming file access. - Relevant server capabilities on
irc.tripsit.io:draft/message-redaction(deletes, viaREDACT),draft/relaymsgwith separator/(RELAYMSGlets the bridge post asname/d-style nicks),message-tags,echo-message,draft/multiline, anddraft/chathistory. There is no message-edit capability, so IRC cannot receive true edits. Re-check withCAP LS 302before relying on a capability. - Ergo supports the IRCv3 capabilities the bridge relies on (
sasl,account-tag,extended-join,server-time). Use the NickServ account fromaccount-tag/extended-joinas the IRC identity, never the nick alone. - Registering an account: while using the nick,
/msg NickServ REGISTER <password> [email]. If email verification is enabled, finish with/msg NickServ VERIFY <account> <code>. An oper with theaccregcapability can instead run/msg NickServ SAREGISTER <account> <password>, which skips verification. - Channel privileges come from ChanServ, not from oper status:
/msg ChanServ REGISTER #channel, then persistent modes with/msg ChanServ AMODE #channel +o <account>(Ergo usesAMODE, notFLAGS/ACCESS). - Do not give the bot a full oper block. It uses the narrow
tripbotoper class (nofakelag,relaymsg); add a capability only when a feature needs it (for examplebanfor network bans) and have a human apply the change.
TripBot replaces Matterbridge for relaying chat between platforms.
src/global/bridge/bridge.tsroutes aBridgeMessageto every other platform with a registeredBridgeTarget(registerBridgeTarget). Platform adapters own formatting and delivery:src/discord/utils/chatBridge.ts(webhook namedTripSit Chat Bridge, separate from the guild-to-guildTripsit Bridgeinutils/bridge.ts) andsrc/irc/bridge.ts. Matrix has room IDs in the channel list but is deliberately not bridged yet.src/global/bridge/bridge.gateways.tsis the channel list ported from Matterbridge. Discord entries name anenv.config.tskey, never a raw ID, so production and development resolve separately; an empty key value means the channel is not bridged in that environment. IRC joins every listed channel unlessIRC_CHANNELSoverrides it.- Behavior intentionally mirrors the old Matterbridge config: relayed IRC nicks are
name/<label>viaRELAYMSG(falling back to<name/label> text), 400-byte lines sent one second apart with a 30-line queue, markdown stripped for IRC, server nicknames on the Discord side,(edited)appended to relayed edits,@everyone/@hereneutralized, and the old bot nicks ignored. Keep loop prevention intact: adapters must ignore their own output. - Discord deletes are relayed to IRC with
REDACT, using the msgids fromecho-messageechoes of the relayed lines. Each IRC channel has its own send queue. - Relayed message content is user content: never log it.
The codebase intentionally exposes values such as db, log, f, environment data, and Discord state through global
declarations. Check src/global/@types/ and startup/logging code before replacing an established global with a new import
or dependency-injection pattern.
Source aliases are defined in tsconfig.json:
@global/*->src/global/*@discord/*->src/discord/*@database/*->src/prisma/*@db/tripbot-> generated TripBot client@db/moodle-> generated Moodle client
Application builds require tsc-alias because runtime aliases point into build/. Follow the import style of nearby
files and do not introduce a second alias scheme.
src/api/app.tsconfigures middleware and mounts/api/tripsit,/api/v1, and/api/v2.- Put route handling in
*.routes.tsand database/query helpers in*.queries.tswhere that pattern already exists. - Preserve rate limiting, Helmet, body-size limits, authentication middleware, centralized not-found handling, and error handling.
- Validate and normalize request input at the boundary.
- Do not return secrets, raw internal errors, private moderation data, or more user data than the existing contract.
- Forward unexpected async errors to the established error handler rather than swallowing them.
- Keep v1 compatibility unless the task explicitly authorizes a breaking API change.
- TripBot uses PostgreSQL; Moodle uses MariaDB/MySQL. Do not apply one schema's assumptions to the other.
- Generated clients live beside each schema and are gitignored. Regenerate; never hand-edit them.
- Import the primary client through the existing
dbglobal orsrc/prisma/tripbot/client.tspattern. - Import Moodle through
src/prisma/moodle/client.ts. - For a TripBot schema change, update
src/prisma/tripbot/schema.prisma, create a named migration insrc/prisma/tripbot/migrations/, format/validate, regenerate, and inspect the SQL. - Migration names must describe the change; never leave
CHANGE_ME. - Do not rewrite or delete committed migrations unless the user explicitly asks and understands the compatibility cost.
- Avoid
db pushfor changes intended to ship; use migrations. - Preserve unique constraints, indexes, nullability, defaults, relation behavior, and existing data compatibility.
- Treat the large Moodle schema as externally governed. Do not create TripBot migrations for Moodle or casually format the entire schema.
- Keep TypeScript strict; do not weaken
tsconfig.jsonor add broad suppressions to make a change compile. - Prefer precise types and narrow unions over
any, non-null assertions, and unsafe casts. - Follow nearby code style: two-space indentation, single quotes, semicolons, trailing commas in multiline structures, and a preferred maximum line length of 120.
- Use async/await consistently and await operations whose completion affects behavior.
- Handle nullable Discord/API/database values explicitly.
- Keep exported names, default exports, filenames, and command names consistent with adjacent modules.
- Preserve existing public behavior unless the request is explicitly a behavior change.
- Avoid drive-by renames, import sorting, comment cleanup, or formatting unrelated code.
- Add comments only for non-obvious constraints or reasoning; do not narrate straightforward code.
- Do not introduce a new abstraction for one call site unless it materially improves correctness.
- Prefer the existing logger over
console.log; never log tokens, authorization headers, raw credentials, or sensitive user content.
- Harm-reduction output must be conservative, factual, and consistent with the project's established data source.
- Do not invent dose ranges, interaction severities, contraindications, crisis contacts, or medical conclusions.
- Do not remove disclaimers, escalation language, privacy controls, moderation checks, or rate limits as “cleanup.”
- Treat moderation actions, bans, appeals, reports, AI moderation, and role checks as security-sensitive.
- Treat Discord IDs and public configuration differently from secrets, but do not expose identifiers unnecessarily.
- Preserve user privacy in logs, API responses, errors, tests, fixtures, and screenshots.
Choose checks proportional to the change; do not claim checks you did not run.
| Change | Minimum verification |
|---|---|
| Documentation only | Review rendered/plain Markdown, git diff --check |
| TypeScript logic | Targeted ESLint, npx tsc --noEmit --pretty false, new or updated tests for the change |
| Discord command | Type-check, targeted ESLint, relevant existing test if configured, inspect interaction lifecycle |
| Command definition/options | Discord-command checks plus note that deployment is required; do not deploy automatically |
| API route/query | Type-check, targeted ESLint, targeted API Vitest file |
| Prisma schema | prisma format, prisma validate, generate both affected clients, type-check, inspect migration SQL |
| Migration | Schema checks plus apply to a disposable local database and verify data compatibility |
| Dependency/tooling | npm ci, type-check, relevant tests, and confirm package-lock.json is coherent |
| Docker/runtime | Build or start only the affected service, inspect logs, stop only containers started for the task |
vitest.config.mts (repo root) matches and collects coverage across the API, Discord command, and global
business-logic layers — see its test.include/test.exclude and test.coverage blocks for exact scope. A handful of
legacy Discord test files directly under src/discord/tests/ predate the current mock scaffold and are excluded until
migrated; treat them as reference only. Run a relevant existing test with npx vitest run <path> and report failures
honestly. New tests go in the matching tests/ folder so the existing include globs pick them up. Test folders
(src/vitest/, src/discord/tests/, src/global/tests/, src/irc/tests/) each hold a small tsconfig.json that
extends tsconfig.vitest.json so editors type-check them with Vitest globals; add one when creating a new test folder.
If dependencies or generated Prisma clients are unavailable, say so clearly. Do not disguise an environment/setup failure as a source-code failure.
- Work on the current branch unless the user asks for a new one.
- Do not commit, amend, rebase, merge, push, force-push, or open a pull request unless explicitly asked.
- Never use destructive Git commands to clean the workspace.
- Keep commits, when requested, focused and use the repository's concise imperative style.
- Check both
git diffandgit diff --cachedbefore committing. - Do not include generated
build/, coverage,.env, generated Prisma clients, logs, caches, or local database data. - For PR conflict work, inspect the actual canonical PR and its real base before resolving conflicts; do not assume a fork's
originis the authoritative upstream. - Before opening a pull request from a feature branch, pull the latest
uat(the usual PR base) into it first so the branch is up to date and the push won't be rejected as non-fast-forward. Prefer branching fresh fromuatover rebasing an existing shared/pushed branch, since rewriting history on a branch others may have pulled is riskier.
A task is complete only when:
- the requested behavior or artifact is fully implemented;
- the diff is scoped and contains no accidental edits;
- new or changed behavior has tests, and applicable types, lint rules, tests, schema checks, or runtime checks pass, or limitations are reported;
- no secrets, generated output, local state, or unrelated lockfile changes were introduced;
- externally visible follow-up work, such as a Discord command deployment or production migration, is called out but not performed without authorization;
- the final handoff states the changed files, verification performed, and any remaining risk or blocker.