This is a TypeScript-based modular Slack bot framework that powers various bot functionalities in the TSG Slack workspace. The system uses a plugin-based architecture where each bot is implemented as a separate plugin. Please follow these guidelines when contributing:
- Plugin System: Modular bot plugins loaded dynamically from individual directories
- Slack Clients: Uses
@slack/web-api,@slack/events-api, and@slack/interactive-messages - Fastify Server: HTTP server handling Slack events and interactive messages
- State Management: Persistent storage system for plugin data using the
lib/statemodule - Logging: Structured logging with Winston logger from
lib/logger
Each plugin should follow this pattern:
import type {SlackInterface} from '../lib/slack';
// Default export for event handling
export default async ({webClient, eventClient, messageClient}: SlackInterface) => {
// Plugin initialization logic
};
// Optional server export for HTTP endpoints
import plugin from 'fastify-plugin';
import type {FastifyPluginCallback} from 'fastify';
export const server = ({webClient, eventClient, messageClient}: SlackInterface) => {
const callback: FastifyPluginCallback = async (fastify, opts, next) => {
// Register HTTP endpoints
next();
};
return plugin(callback);
};index.ts: Main entry point that loads and initializes plugins- Individual plugin directories: Each contains a self-contained bot implementation
bin/: Command-line tools and scriptslib/: Core utilities (state management, logging, etc.)functions/: Firebase functions package
This Slackbot has a shared achievement system across the entire project. When a user achieves something noteworthy in relation to a bot, that bot's implementation can unlock an achievement for them. Achievements unlocked by each user are managed and persisted in a shared Firestore-backed database. Users can also view their earned achievements on the external site at https://achievements.tsg.ne.jp/.
Achievements are defined in achievements/achievements.ts and managed by achievements/index_production.ts, which persists data to Firestore.
Defining an achievement — add an entry to the array in achievements.ts:
{
id: 'my-achievement', // unique identifier
difficulty: 'hard', // baby | easy | medium | hard | professional
title: '実績タイトル',
condition: '解除条件の説明',
category: 'my-bot',
// Optional: auto-unlock when a counter reaches a value
counter: 'my-counter-name',
value: 100,
}Exported functions — import from ../achievements:
| Function | Description |
|---|---|
increment(user, counterName, value?) |
Increment a named counter; auto-unlocks achievements whose counter + value match |
get(user, key) |
Read arbitrary per-user data (returns unknown) |
set(user, key, value) |
Write arbitrary per-user data |
unlock(user, achievementId) |
⚠ Discouraged. Directly unlock an achievement without going through a counter |
Since each achievement's value is visible on the achievements site mentioned above, it is important that every achievement has an associated counter. The preferred way to unlock an achievement is via increment: define a counter and value on the achievement, then call increment(user, counterName) at the appropriate point. Even for one-time achievements, avoid using unlock and instead define the achievement with value: 1 and call increment. Use unlock only when the counter value would carry no meaningful interpretation for the achievement.
get/set store data in the Firestore users collection and are suitable for tracking collection-style progress (e.g., arrays of completed items). Always check the type of values returned by get before use.
- Use TypeScript for new code, with proper type definitions
- Follow existing code patterns and structure
- Use modern JavaScript/TypeScript features whenever possible
- Implement proper error handling and logging using the shared logger
- When adding or editing the dependencies, make sure not to edit
package.jsonorpackage-lock.jsondirectly. Instead, use the propernpmcommands - Use the shared
SlackInterfacetype for Slack client interactions - When adding new environment variables, make sure to document them in the
.env.examplefile - Explicitly using
anytype should be strongly avoided. Instead, use more specific types whenever possible - Type casting with
askeyword should be used judiciously and only when necessary
The code under the helloworld/ directory is not intended to be used in production, but is intended to demonstrate coding and testing best practices for developers.
The code in this repository is a mix of modern and recommended syntax and old and deprecated syntax. The helloworld/ directory is maintained to always be the latest and recommended syntax for slackbots, so if you're not sure what to do, follow the helloworld bot's syntax.
Don't add useless comments that state the obvious or repeat the code. Focus on explaining the "why" behind complex logic. For example:
// Calculate the 10th Fibonacci number
const nextFibonacci = fibonacciCalculator(10);is not necessary because the code is self-explanatory. Instead, you must just remove it, or focus on explaining the purpose of the calculation or any non-obvious logic.
- Write tests using Vitest framework
- Follow existing test patterns and structure
- Use the provided
SlackMockclass for testing Slack interactions - Place tests in
*.test.tsfiles - Run tests with
npx vitest run [<test-file>]
- Run development server:
npm run dev - Run specific plugin only:
npm run dev -- --only [bot-id] - Type-check the project:
npx tsgo --noEmit(the project has migrated fromtsc/ts-nodetotsgo; CI runs this same command, so always usetsgorather thantsc) - Build for production:
npm run build(runstsgo, compiling TypeScript into.build/) - Production mode:
npm start(requiresnpm run buildto have been run first — pm2 runs the compiled.build/index.jsvia plainnode, notindex.tsdirectly)
Note: Local debugging requires
CLAUDE.local.mdto exist in the project root. If it does not exist, local debugging is not configured and these instructions cannot be followed. CopyCLAUDE.local.md.exampletoCLAUDE.local.mdand ask user to fill in the appropriate environment values before proceeding.
See CLAUDE.local.md for environment-specific values (ngrok domain, port, channel IDs, etc.).
Launch the following two commands simultaneously in the background when debugging.
ngrok http --domain=<NGROK_DOMAIN> <PORT> > .logs/ngrok.log 2>&1 & echo "Ngrok PID: $!"Required to forward Slack Events API requests to localhost.
npm run dev -- --only <bot-id> > .logs/<bot-id>.log 2>&1 & echo "App PID: $!"- The
--onlyflag is mandatory. Omitting it starts all plugins simultaneously, making the app extremely slow. <bot-id>matches the plugin's directory name (e.g.,sushi-bot).- Redirect logs to
.logs/<bot-id>.logfor background monitoring.
Confirm startup by checking the log:
tail -f .logs/<bot-id>.logThe app is ready when Server launched at http://0.0.0.0:<PORT> appears.
Use the MCP tools from plugin:slack:slack to interact with the development Slack workspace.
If the plugin:slack:slack MCP tools cannot be found, the likely causes are:
- Authentication for the
plugin:slack:slackplugin has not been completed. - The
plugin:slack:slackplugin has not been set up properly.
Wait about 10 seconds and check again. If the tools are still unavailable, ask the user to install the plugin:slack:slack plugin in Claude Code.
- Send messages:
slack_send_message - Search channels:
slack_search_channels - Search messages:
slack_search_public
After debugging is complete, stop the background processes by PID obtained at startup:
kill <ngrok-pid> <app-pid>Note the PIDs when launching (they are printed after the & command) and use them to stop only the intended processes.
npm run dev uses tsx watch, which automatically detects file changes and restarts the server without any manual intervention. When a .ts file is saved, the output looks like:
[INFO] Restarting: /path/to/changed-file.ts has been modified
[INFO] Server launched at http://0.0.0.0:<PORT>
This means you can edit plugin code and immediately test it in Slack without manually restarting the process. Wait for the Server launched line before sending test messages.
- After startup, it may take a few seconds before the first Slack event arrives.
already_reactederrors are expected when a reaction has already been added to a message.