標準デコレータ(TC39)
@Command.define({...}) は TC39 標準デコレータのみ。 experimentalDecorators も reflect-metadata も不要です。 デコレータは宣言し、ローダーが実行する — 厳密に分離されています。
クラスを置く。フレームワークが見つけ、登録し、つなぐ。
Discord Bot の起動までを、規約と型に任せられます。
bun add @cc-discord-framework/coreauto-discovery — 登録コードを書かずに、配置だけで Bot が組み上がる
01 · 設計思想
発見・登録・ルーティング・型付け。Bot を書くたびに 繰り返してきた「いつもの配線」は、規約と型が肩代わりします。
commands/ に置いたクラスが、そのままスラッシュコマンドになる。 import も登録リストも書きません。名前はクラス名から導出 (UserInfoCommand → /user-info)。
どのコンポーネントからも this.services.audio / this.services.ai / this.services.ui。 宣言マージで型も通ります — 手動のジェネリクス指定はありません。
export * from "discord.js" — 全 API を再エクスポートし、Client は discord.js の Client そのもの。 これまでの知識も、エコシステムも、すべてそのまま使えます。
02 · discord.js との関係
全 API を再エクスポートし(export * from "discord.js")、 その上へ規約と型の構造を足します。同じ /ping を 動かすまでのコードが、これだけ変わります。
discord.js のみ1ファイル · 36行
cc-discord-framework2ファイル · 20行
// src/index.ts — エントリポイントはこれだけ
import {
Client,
GatewayIntentBits,
} from "@cc-discord-framework/core";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
await client.login(); // トークンは DISCORD_TOKEN 環境変数から自動使用
// src/commands/PingCommand.ts — 置くだけ。登録・同期は自動
import {
Command,
type ChatInputCommandInteraction,
} from "@cc-discord-framework/core";
@Command.define({ description: "Botの応答速度を確認します。" })
export class PingCommand extends Command {
override async chatInputRun(interaction: ChatInputCommandInteraction) {
await interaction.reply(`Pong! ${this.client.ws.ping}ms`);
}
}
interaction は discord.js の型そのもの。2つ目のコマンドは ファイルをもう1枚置くだけで、左のコードのように登録行や分岐が増えることはありません。
03 · 主要機能
コアが持つのはサービス・コマンド・リスナー・Precondition だけ。それ以外は、同じ仕組みの上にプラグインとして積み上がります。
@Command.define({...}) は TC39 標準デコレータのみ。 experimentalDecorators も reflect-metadata も不要です。 デコレータは宣言し、ローダーが実行する — 厳密に分離されています。
createClient() が src/config/ を読み、1関心1ファイルの 設定を合成。plugins は priority 順に連結、intents は 合併(union)されます。
プラグインは機能ではなくコンポーネント種別ごと追加できます。utils は tasks/、music は resolvers/+providers/、 ai は ai/ — すべて Public API だけで実現。
ユーザーに見える文言・色・上限は、すべてただの既定値。 変更できないハードコードは設計ルールとして存在しません。 Bot の見せ方は Bot が決めます。
Bun 1.4+ 専用。TypeScript をそのまま実行するので、開発にビルド工程は ありません。500件を超える自動テストを bun test でまとめて実行できます。
リスナーのイベント引数、ストア参照、this.services.*、 Precondition 名 — 宣言マージにより、手動のジェネリクス指定なしで型が通ります。
04 · コード
以下はすべて、リポジトリ同梱の公式リファレンス Bot(client/)で実際に動いているコードです。
オプションはデコレータで宣言し、本体は this.services.ai.reply() の1行。defer・ストリーミング表示・ 長文の分割・失敗時の表示は、すべてサービス側の責務です。
AskCommand → /ask)import {
ApplicationCommandOptionType,
Command,
type ChatInputCommandInteraction,
} from "@cc-discord-framework/core";
@Command.define({
description: "AIに質問します(会話履歴は使いません)。",
options: [
{
type: ApplicationCommandOptionType.String,
name: "prompt",
description: "聞きたいこと",
required: true,
},
],
})
export class AskCommand extends Command {
override async chatInputRun(interaction: ChatInputCommandInteraction) {
// defer・ストリーミング表示・長文の分割・失敗時の表示は reply() の担当。
await this.services.ai.reply(interaction, {
prompt: interaction.options.getString("prompt", true),
});
}
}
コマンドストアを走査して一覧を作り、this.services.ui の テーマ済み埋め込みと paginate() でページ送りに。 他のコンポーネントを import する行は1本もありません。
this.container.stores — 全コンポーネントへ型付きでアクセスimport { chunk, paginate, type Page } from "@cc-discord-framework/utils";
import { Command, type ChatInputCommandInteraction } from "@cc-discord-framework/core";
const PAGE_SIZE = 20;
@Command.define({ description: "コマンド一覧を表示します。" })
export class HelpCommand extends Command {
override async chatInputRun(interaction: ChatInputCommandInteraction) {
const lines = this.container.stores
.get("commands")
.map((command) => `**/${command.name}** — ${command.description}`)
.sort();
const pages: Page[] = chunk(lines, PAGE_SIZE).map((body, page, all) =>
// utils プラグインの this.services.ui — 色は Bot 全体のテーマから。
this.services.ui
.info(body.join("\n"))
.setTitle("コマンド一覧")
.setFooter({ text: `${page + 1}/${all.length}ページ・全${lines.length}コマンド` }),
);
await paginate(interaction, { pages, ephemeral: true });
}
}
tasks/ は utils プラグインが追加するコンポーネント種別。 コアの commands/ と同じように、置くだけで定期実行が始まります。
every: "5m" — 実行間隔も宣言的にrunOnStart: trueimport { ActivityType } from "@cc-discord-framework/core";
import { Task } from "@cc-discord-framework/utils";
/** 公式 utils プラグインが追加する Task 種別のコンポーネント。 */
@Task.define({ every: "5m", runOnStart: true })
export class PresenceTask extends Task {
override run() {
this.client.user?.setPresence({
activities: [{ type: ActivityType.Playing, name: "/help" }],
});
}
}
intents は設定ファイル間で合併(union)されるので、音楽にしか 要らない GuildVoiceStates は music の設定に置けます。 音楽をやめるときはこのファイルを消すだけ — 要らなくなった intent も一緒に消えます。
plugins は priority 順に連結 — 依存の向きで順序を制御createClient() が config/ を合成import { defineConfig, GatewayIntentBits } from "@cc-discord-framework/core";
import { music } from "@cc-discord-framework/music";
import { musicSources } from "@cc-discord-framework/music-sources";
import { env } from "./_env.js";
export default defineConfig({
priority: 50,
intents: [
// 音楽再生に必要(ボイスチャンネルの出入りを追うため)。
GatewayIntentBits.GuildVoiceStates,
],
plugins: [
// キュー・再生制御の this.services.audio(/play などは src/commands/)。
music(),
// YouTube と SoundCloud を音源として追加(music より後に置く)。
musicSources({
soundcloud: { clientId: env.soundcloudClientId },
}),
],
});
src/ai/ に置いたクラスは、モデルが呼び出せるツールになります。 中では this.services.audio(music プラグイン)がそのまま使える — プラグイン横断の合成です。/chat で「いま何の曲?」と聞くと、 モデルがこのツールを呼びます。
guildOnly: true で DM からの呼び出しを制限import { AiTool, type AiToolContext } from "@cc-discord-framework/ai";
import { z } from "zod";
const input = z.object({
キュー: z.boolean().optional().describe("待機中の曲を題名の一覧で返すかどうか"),
});
@AiTool.define({
description:
"このサーバーで再生中の曲と、待機中の曲の状況を返します。音楽の再生状況を聞かれたら使ってください。",
inputSchema: input,
// 再生キューはサーバー単位なので、DM からの呼び出しでは使わせない。
guildOnly: true,
})
export class NowPlayingTool extends AiTool<z.infer<typeof input>> {
override execute({ キュー = false }: z.infer<typeof input>, context: AiToolContext) {
const queue = context.guildId === null ? null : this.services.audio.queue(context.guildId);
if (!queue?.current) return { 再生中: null, 待機中: 0 };
return {
再生中: { 題名: queue.current.title, 演者: queue.current.author },
待機中: キュー ? queue.tracks.map((track) => track.title) : queue.tracks.length,
};
}
}
05 · プラグイン
プラグインが提供するのは、コンポーネント種別の自動ロード・サービス・ イベントの3つだけ。コマンドは登録しません — /play も /ask も Bot の機能なので、src/commands/ に自分で書きます。文言も見せ方も、Bot が決める。
そして種別は横断して合成できます。src/ai/ のツールの中で this.services.audio が普通に動く — 「いま流れている曲」を AI が答えられるのは、この合成のおかげです。
独自のデコレータ・ディレクトリ・ライフサイクルを持つ新しい種別を、 Public API だけで丸ごと追加できます。公式プラグインも、 この同じ拡張点の上に立っています。
置く場所が、そのまま役割。プラグインを入れると、読めるディレクトリが増える。
06 · 公式プラグイン
公式プラグインはそれぞれ独立したパッケージ。すべて npm の @cc-discord-framework スコープで公開されていて、使う分だけ bun add で足せます。
@cc-discord-framework/utilsテーマ済み埋め込みの this.services.ui、確認ダイアログの confirm()、ページ送りの paginate()、 定期実行の tasks/ 種別、時間・文字列の整形ユーティリティ。
tasks/ · confirm() · paginate() · this.services.ui
@cc-discord-framework/musicthis.services.audio で解決・キュー・再生制御。 コマンドは登録しない設計 — /play の文言も見せ方も Bot 側が決めます。音源はプロバイダー機構で差し替え可能。
this.services.audio · resolvers/ · providers/
@cc-discord-framework/music-sourcesmusic プラグインに YouTube と SoundCloud を音源として追加。 Bot 側の /play から検索語や URL を同じ Resolver へ渡せます。 重い音源処理を本体から分離した独立パッケージです。
YouTube · SoundCloud · yt-dlp · ffmpeg
@cc-discord-framework/aiVercel AI SDK ベース。this.services.ai.reply() が defer・ストリーミング・分割まで引き受け、ai/ に置いた クラスはそのまま LLM のツールになります。
Vercel AI SDK+ OpenAI 互換 API
this.services.ai · ai/ · Streaming · Structured Output
07 · Roadmap — WebAssembly
プラグインの次の形として、WebAssembly コンポーネントの ネイティブ対応を構想しています。境界を WIT で定義すれば、 TypeScript 以外の言語で書いたプラグインも、いまと同じ 「置くだけ」で動く — 言語の違いを、フレームワークの 境界にしないための計画です。
設計構想の段階です。API と WIT 定義は未確定で、 仕様は RFC として公開する予定です。
08 · Project Status — Updated 2026-08-25
詳しい状況とサポート方針はステータスページにまとめています。
@cc-discord-framework/core 2.0.0。このサイトと main の コードが説明する現行版で、公式プラグインも同じスコープで npm 公開中。
スコープなしの旧パッケージ cc-discord-framework(1.0.5)は 旧世代です。v2 と API 互換ではないため、新規導入では選ばないでください。
09 · はじめる
最小の Bot まで3ステップ。ボイラープレートの生成も、ビルドの設定もありません。
discord.js は同梱・再エクスポートされるので、個別にインストールする 必要はありません。ランタイムは Bun 1.4+ だけ。
src/index.ts に Client を作って login()。トークンは DISCORD_TOKEN 環境変数から自動で使われます。
src/commands/ にコマンドのクラスを1枚。ビルド工程なしで bun run src/index.ts — もう /ping が動いています。
bun add @cc-discord-framework/core
bun run src/index.ts
Built in Japan
cc-discord-framework は日本発のオープンソースです。 ドキュメントもコードコメントもエラーメッセージも、翻訳ではなく 最初から日本語で書かれています。MIT License。