github-twin について
github-twinは、GitHubのコミット、コード、レビューを索引化し、コーディングエージェント向けに個人または組織単位で検索できるようにします。BM25とベクトル検索を組み合わせ、ローカルで索引を作成し、埋め込みとLLMのバックエンドを設定できます。
主な機能
- 01コミット、コード、レビューの索引化
- 02BM25とベクトル検索の併用
- 03AST対応のコード検索
- 04リポジトリまたは作成者による絞り込み
- 05GitHubデータの差分同期
- 06MCP経由で検索ツールを提供
活用シーン
- 01新しいdiffに関連する過去のレビュー検索
- 02コーディング中に個人のコード例を検索
- 03組織のコードとレビュー履歴の検索
- 04コーディング作業に関係するルールの取得
MCP 対応
stdio MCPサーバーは、検索、レビュー結果予測、パターン要約、同期のツールをMCPクライアントに公開します。検索にはローカルのSQLiteインデックスを使用します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
uvx github-twinGITHUB_TOKENシークレットGitHub personal access token. Optional: github-twin can also pick the token up from `gh auth token` or from its own OAuth device-flow login (`gt auth login`).ANTHROPIC_API_KEYシークレットOptional. Enables the Claude backend for `gt summarize`, `gt distill`, and `gt eval`. Defaults to local Ollama when unset.GEMINI_API_KEYシークレットOptional. Enables the Gemini backend for embedding and the LLM seam. Vertex AI ADC via `GT_GEMINI_PROJECT` is also supported.GT_PATHS__DATA_DIROptional. Override the on-disk data directory holding the SQLite index, embeddings, and cached GitHub responses. Defaults to `$XDG_DATA_HOME/github-twin`.ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
github-twin
6か月前に権限チェックをレビューしました。Claude はそれを覚えていません。 github-twin は覚えています — コミットとレビューコメントをインデックス化し、エージェントがあなたのスタイルで新しいコードを書いたりレビューしたりするたびに、検索結果として提示します。
今すぐ Claude Code で試すには、これを ~/.claude.json に追加して再読み込みしてください。
{
"mcpServers": {
"github-twin": { "command": "uvx", "args": ["github-twin", "serve"] }
}
}
コードはお使いのマシンから外に出ません。 埋め込みはローカルで計算されます (Ollama または sentence-transformers)。LLM との接続部分(
gt summarize、gt distill、gt eval)のみ、必要に応じてホスト型プロバイダーを呼び出します。また、 これもローカルの Ollama に切り替えられます。gemini埋め込み器だけは例外で、 明示的にオプトインした場合にのみ使用されます。
GitHub の履歴を対象とした個人用 RAG です。Claude Code(または任意の MCP クライアント)に stdio サーバーとして提供されます。2 つのスコープを 1 つのコードベースで扱います。
- ユーザーモード — 自分のコミットとレビューコメントをインデックス化します。エージェントが新しいコードを書くときやレビューするときに、過去のコードをスタイル例として、過去のコメントをレビューのヒントとして提示します。
- 組織モード — GitHub 組織全体の全メンバーを対象に、ファイルの HEAD 時点の内容、コミット、PR レビューをインデックス化します。クエリのスコープは、リポジトリ、言語、レビュアーのログイン名で絞り込めます。
検索にはハイブリッド方式(BM25 + RRF によるベクトル検索)を採用しています。tree-sitter を使って AST を考慮し、python/scala/javascript/typescript/go/rust に対応しています。また、埋め込み時にチャンクごとのヘッダーと、任意で LLM が生成する要約を付与して、コンテキストを補います。
インストール
最も手軽なのは uvx を使う方法です。仮想環境の管理は不要で、ツールごとに分離されます。
# One-shot
uvx github-twin --help
# Pinned version
uvx [email protected] --help
# With sentence-transformers for the alt embedder
uvx --with 'github-twin[st]' github-twin --help
プロジェクト内にインストールする場合は、次の方法を使ってください。
uv add github-twin # or: pip install github-twin
gt --help
gt と github-twin は同じ Typer アプリです。使い慣れている方をお使いください。
認証
最も手間の少ない方法を選んでください。github-twin は次の順序で試します。
- OAuth デバイスフロー(
ghのインストールは不要):
トークンは OS のキーチェーン(macOS Keychain/Linux Secret Service/Windows Credential Manager)に保存されます。利用できない場合は、データディレクトリ内の 0600 ファイルに保存されます。uvx github-twin auth login # opens browser, persists token uvx github-twin auth status # show which source is active - 既存の
ghCLI:すでにgh auth loginを実行している場合、gtはgh auth token経由でトークンを取得します。追加の作業は不要です。 GITHUB_TOKEN環境変数:従来型の PAT も使用できます。CI/ヘッドレス環境/Docker で便利です。必要なスコープはrepo、read:org、user:emailです。
Claude Code との接続
MCP サーバーは github-twin serve(または gt serve)を使い、stdio 経由で動作します。
サーバーをホストするマシンで、uvx github-twin auth login を一度実行してください。
オプション A — Claude Code のプラグインマーケットプレイス経由(最も手軽):
/plugin marketplace add ChristopherDavenport/christopherdavenport-marketplace
/plugin install github-twin@christopherdavenport
これにより、MCP サーバーのエントリーが自動的に登録されます。環境変数、またはこのサーバーの ~/.claude.json 内の
env ブロックで GT_PATHS__DATA_DIR を設定し、DB ディレクトリを指定してください。
オプション B — 手動で接続:~/.claude.json(または
mcp_servers.json)にエントリーを追加してください。
{
"mcpServers": {
"github-twin": {
"command": "uvx",
"args": ["github-twin", "serve"],
"env": {
"GT_PATHS__DATA_DIR": "/path/to/your/github-twin-data"
}
}
}
}
トークンを保存せずにインラインで指定したい場合(CI、
エフェメラルコンテナなど)は、その env ブロックに
"GITHUB_TOKEN": "ghp_..." を追加してください。これは最も優先度の低いフォールバックとして使用されます。
Claude Code を再起動すると、find_*、predict_review_outcome、
summarize_review_patterns、sync の各ツールを使用できるようになります。
クイックスタート
SQLite DB、設定、取り込み済みキャッシュを格納するディレクトリを選びます。 データディレクトリごとのすべてのデータは、このルートの下に置かれます。
export GT_PATHS__DATA_DIR=~/github-twin-data
uvx github-twin auth login # one-time OAuth (or set GITHUB_TOKEN)
# user mode (your own GitHub history)
uvx github-twin init # discover identity via /user
uvx github-twin sync # ingest + summarize + embed
uvx github-twin serve # MCP server over stdio
# layer an org into the SAME DB
uvx github-twin init --kind org --org http4s
uvx github-twin sync
# OR keep the org in its own DB by switching data dirs
GT_PATHS__DATA_DIR=~/twin-http4s \
uvx github-twin init --kind org --org http4s
GT_PATHS__DATA_DIR=~/twin-http4s uvx github-twin sync
以降の実行では、gt sync は差分のみを処理します。
config.toml は DB と同じ場所の <data_dir>/config.toml にあり、
最初の gt init --embed-backend ... の呼び出し時に作成されます。GT_PATHS__DATA_DIR が設定されていない場合、デフォルトの
<data_dir> は $XDG_DATA_HOME/github-twin(または ~/.local/share/github-twin)です。
LLM プロバイダーの一覧
検索機能(find_*、predict_review_outcome)は常に SQLite インデックス上でローカル実行され、 API 呼び出しは行いません。LLM の呼び出しが発生するのは、次の 3 つの場合のみです。
gt distill— レビューコメント/コミットをクラスタリングしてルールにします。gt summarize— 埋め込み時のプレフィックスに使用する、チャンクごとの自然言語要約を生成します。gt eval reviews/eval predictions— 保留したデータを使って、RAG とベースラインのスコアを評価します。
それぞれ、優先順位 Claude → Gemini → Ollama(設定されている API キーに応じて)でバックエンドを選択します。または、明示的に指定できます。
| プロバイダー | 環境変数 | 対応範囲 |
|---|---|---|
| Anthropic(Claude) | ANTHROPIC_API_KEY | ルール抽出/要約/評価用の LLM。最高品質です。 |
| Google(Gemini、API キー) | GEMINI_API_KEY または GOOGLE_API_KEY | ルール抽出/要約/評価用の LLM。無料枠が充実しています。 |
| Google(Gemini、Vertex / ADC) | GT_GEMINI_PROJECT(+任意の GT_GEMINI_LOCATION、デフォルトは us-central1) | 同じバックエンドですが、認証には gcloud auth application-default login を使用します。シェルにキーを設定する必要はありません。両方設定されている場合は API キーが優先されます。 |
| Ollama(ローカル) | OLLAMA_HOST(デフォルトは http://127.0.0.1:11434) | ルール抽出/要約/評価用の LLM。完全にオフラインで利用できます。 |
Vertex / ADC の経路では、プロジェクトで aiplatform.googleapis.com API を有効にする必要があります。また、「無料」の Gemini モデルでも課金が発生します。
AI Studio の無料枠は Vertex には適用されません。プロジェクト ID は秘密情報ではありません。認証情報は
~/.config/gcloud/application_default_credentials.json に保存され、gcloud によって更新されます。
埋め込みバックエンド
埋め込みバックエンドは LLM バックエンドとは別に選択します。
- デフォルト — Ollama(
nomic-embed-text、768 次元、チャンクあたり約 50 ミリ秒)。 Ollama デーモンの起動が必要です。費用はかからず、完全にローカルで動作します。 - 代替 — sentence-transformers(
uv add 'github-twin[st]'、torchを取得します)。Ollama デーモンを利用できない場合や、特定の HuggingFace モデルを使いたい場合に便利です。ローカルで動作します。 - 代替 — Gemini(デフォルトでは 3072 次元の
gemini-embedding-001)。 すでにインストール済みのgoogle-genai依存関係を使用します。認証にはGEMINI_API_KEY/GOOGLE_API_KEY、またはGT_GEMINI_PROJECT+ ADC(gcloud auth application-default login)を使い、キーを管理せずに Vertex AI 経由で接続できます。リモートです。チャンクのテキストをローカル環境の外に送信する埋め込みバックエンドはこれだけです。Gemini の認証情報はあるものの、Ollama や[st]のインストールがなく、コーパスを Google と共有しても問題ない場合に選択してください。
埋め込みバックエンドは DB ごとに固定されます。sqlite-vec はテーブルの初回作成時にベクトルの次元を設定します。後続のすべてのコマンドで選択内容が使われるよう、初期化時に
<data_dir>/config.toml に設定を記録してください。
gt init --embed-backend gemini # gemini-embedding-001, 3072
gt init --embed-backend gemini --embed-dim 1536 # request shorter output
gt init --embed-backend sentence_transformers \
--embed-model BAAI/bge-small-en-v1.5 --embed-dim 384
同じ値で再実行しても何も起こりません。既存の config.toml に対して異なる値で実行すると、コーパスが意図せず変更されないよう、エラーが明示的に発生します。単発の上書きや CI では、引き続き GT_EMBED__* 環境変数を使用できます。
「クラウド LLM のみ」の構成では、埋め込み処理用のプロセス(Ollama / [st])が必要です。そうでなければ、リモートの Gemini 埋め込みバックエンドを選択する必要があります。
必要な GitHub トークンのスコープ
gt init を実行するとき、GH クライアントには次の権限が必要です。
repo— プライベートリポジトリと、そのリポジトリ上の PR コメントへのアクセスuser:email— ユーザーモードの ID 確認処理で使用する、検証済みメールアドレスへのアクセスread:org— 組織メンバーの一覧表示と、プライベートな組織リポジトリの検出
きめ細かい PAT を使用できます。クラシックトークンも使用できます。
検索
デフォルトではハイブリッド検索を使用します。BM25(SQLite FTS5)とベクトル類似度検索を並行して実行し、その後、Reciprocal Rank Fusion(k=60)で統合します。ベクトル検索は意味的な意図を捉え、BM25 検索はベクトル検索では見落とされがちな正確な識別子(getUserById、SQLITE_OPEN_READWRITE)を検出します。設計の参考資料:Anthropic — Contextual
Retrieval。
埋め込みの作成時、各チャンクの先頭に決定的なヘッダーが追加されます。ヘッダーには # path :: symbol_name (node_kind)、存在する場合は関数の先頭の docstring/コメント、さらに任意で LLM が生成した要約(gt summarize を参照)が含まれます。このヘッダーにより、本文が識別子しか含まないチャンクにもベクトル検索でたどり着けます(例:VaultSecretEq 関数に対する自然言語のクエリ)。
BM25 クエリ拡張はデフォルトで有効です(cfg.retrieval.query_expansion = "rule")。ルールベースのコード形式の同義語は、BM25 検索にのみ適用されます。埋め込みはすでに同義語を捉えるため、拡張によってベクトル検索のクエリが変更されることはありません。"ollama" に切り替えると、ルールに加えて LLM が生成した代替語を追加し、トークンごとにディスクへキャッシュします。
predict_review_outcome は、逆距離による投票の重み付けが調整済みの L2 距離に依存するため、純粋なベクトル検索を引き続き使用します。
MCP ツール
すべての検索ツールでは、repo= と author_login= のフィルターを任意で指定できます。
| ツール | 戻り値 |
|---|---|
find_review_comments(diff_hunk, language?, repo?, author_login?, k=5) | 入力に類似した差分に対する過去のレビューコメント。 |
find_style_examples(query, language?, repo?, author_login?, k=5) | 説明に一致する過去のコードチャンク。 |
find_code(query, language?, repo?, path_glob?, node_kind?, k=5) | HEAD 時点のファイルから取得したソーススニペット(組織モード)。 |
find_applicable_rules(query, language?, repo?, author_login?, k=5) | コーディングタスクに関連する、抽出済みのコードパターンルール。 |
predict_review_outcome(diff_or_summary, language?, repo?, author_login?, k=20) | 最も近い過去の PR に基づく加重予測:{approved, changes_requested, commented}。 |
summarize_review_patterns(language?, limit=20) | クラスタリングしたレビューコメントから抽出したルール(最初に gt distill を実行してください)。 |
sync(since?) | 増分取り込み+要約+埋め込み。 |
CLI
gt init [--kind user|org|repo] [--org N] [--repo owner/name]
gt repos # list discovered org repos
gt ingest # backfill
gt summarize [--limit N] [--backend ...] # LLM NL summaries per chunk
gt embed # embed pending chunks
gt sync [--skip-summarize] # incremental: ingest → summarize → embed
gt stats # corpus counts
gt distill [--backend ...] [--author ...] # rule extraction
gt clones prune [--older-than-days N] # GC the persistent clone cache
gt eval reviews --since DATE [...] # held-out RAG-vs-baseline eval
gt eval predictions --since DATE [...]
gt eval search evals/queries/default.yaml # retrieval-quality dogfood
gt serve # MCP stdio server
github-twin <command> と gt <command> は同じように使用できます。
切り替え可能なバックエンド
| 対象 | 環境変数/設定キー | デフォルト | 代替 |
|---|---|---|---|
LLM(cfg.distill.backend、cfg.summarize.backend) | ANTHROPIC_API_KEY/GEMINI_API_KEY(または GT_GEMINI_PROJECT + ADC)/Ollama | auto(クラウド優先、次にローカル) | claude/gemini/ollama を指定 |
Embedder(cfg.embed.backend) | —/GEMINI_API_KEY(または GT_GEMINI_PROJECT + ADC) | ollama(nomic-embed-text) | [st] エクストラ経由の sentence_transformers、または gemini(gemini-embedding-001、リモート) |
ベクトルストア(cfg.vector_store.backend) | — | sqlite-vec(ブルートフォース KNN) | [faiss] エクストラ経由の faiss |
BM25 クエリ拡張(cfg.retrieval.query_expansion) | — | rule(決定的) | ollama(LLM、キャッシュあり)または off |
すべての設定は次の順に適用されます:デフォルト → <data_dir>/config.toml(または明示的に指定した --config PATH)→ GT_ をプレフィックスとする環境変数(__ でネストします。例:GT_EMBED__BACKEND=sentence_transformers)。
保留評価
gt eval は、同じプロンプトを検索あり/なしで実行し、実際の保留データに対する RAG の精度向上を測定します。
# Review-comment voice match (cosine distance to ground truth)
uvx github-twin eval reviews --since 2025-01-01 --limit 100
# Org-mode: scope to one reviewer (and optionally one repo)
uvx github-twin eval reviews --since 2025-01-01 --author alice --repo http4s/http4s
uvx github-twin eval predictions --since 2025-01-01 --author alice
# Retrieval-quality dogfood (per-tier, per-backend pass rates)
uvx github-twin eval search evals/queries/default.yaml --mode all
ハーネスは適格件数を事前確認するため、スペルミスのある --author や --repo が指定された場合は、LLM 呼び出しを消費する前にすぐに失敗します。検索結果を使った評価が、その検索結果自体をどの程度クラスタリングできているかの測定にならないよう、評価用 Embedder はデフォルトで検索用とは異なるモデルを使用します([st] エクストラをインストールした場合は sentence-transformers BGE-small)。
オブザーバビリティ(OpenTelemetry)
すべての MCP ツール呼び出し、埋め込み生成器の呼び出し、検索の各段階についてスパンを作成し、OTLP 経由でエクスポートします。自動検出されるため、環境が設定されていない限り、何も実行されません。具体的には、次のとおりです。
-
[otel]エクストラをインストールします(SDK と HTTP OTLP エクスポーターが含まれます)。uvx --with 'github-twin[otel]' github-twin serve -
環境変数で OTLP HTTP コレクターを指定します。
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 export OTEL_SERVICE_NAME=github-twin # optional
OTEL_SDK_DISABLED=true を設定すると、エンドポイントが指定されていても無効になります。
[otel] がない場合、またはエンドポイントの環境変数がない場合でも、コードパスは実行されますが、すべてのスパンは opentelemetry-api に組み込まれたトレーサーによるコストのかからない no-op になります。テレメトリーが有効な場合でも、stdout は一切使用しません。MCP は stdin/stdout 経由で JSON をやり取りするため、コンソールエクスポーターが出力すると通信チャネルが破損するためです。OTLP HTTP エクスポーターはコレクターにデータを送信し、SDK の警告は Python の logging(stderr)経由で出力されます。
Claude Code での設定:
{
"mcpServers": {
"github-twin": {
"command": "uvx",
"args": ["--with", "github-twin[otel]", "github-twin", "serve"],
"env": {
"GITHUB_TOKEN": "ghp_...",
"GT_PATHS__DATA_DIR": "/path/to/twin-data",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318",
"OTEL_SERVICE_NAME": "github-twin"
}
}
}
}
スパン名と、条件を絞り込む際に使える主な属性:
| スパン | 便利な属性 |
|---|---|
mcp.tool.{find_review_comments,find_style_examples,find_code,find_applicable_rules,predict_review_outcome,summarize_review_patterns,sync} | gh_twin.tool.k、gh_twin.filter.*、gh_twin.result.count(predict の場合は .prediction/.confidence) |
embedder.embed | gh_twin.embed.input_chars、gh_twin.embed.model |
retrieval.hybrid_search | gh_twin.retrieval.{chunk_kind,k,expander,hits,top_distance} |
retrieval.vector_search(predict_review_outcome) | 同じ形式ですが、expander はありません |
コレクターが壊れている、または到達不能な場合、フラッシュを試みるたびに Failed to export span batch というログ行が 1 行だけ出力され、ツールハンドラーには一切影響しません。この動作は tests/test_observability.py で固定されています。
gRPC を使う場合:[otel] エクストラと一緒に opentelemetry-exporter-otlp-proto-grpc をインストールすると、SDK は OTEL_EXPORTER_OTLP_PROTOCOL=grpc に基づいて自動的に検出します。
ストレージ
1 つの DB に複数の対象(ユーザー + N 個の組織 + N 個のリポジトリ)を格納できます。対象を分離する場合は、DB ごとに異なる GT_PATHS__DATA_DIR を使用してください。環境変数に応じて、データディレクトリは次のように決まります。
GT_PATHS__DATA_DIRが設定されている場合は、その値- それ以外で
XDG_DATA_HOMEが設定されている場合は$XDG_DATA_HOME/github-twin/ - それ以外の場合は
~/.local/share/github-twin/
現在の作業ディレクトリは参照されません。
レイアウト — DB ごとのすべてのデータは 1 つのルートディレクトリ配下に置かれます。
<data_dir>/
db.sqlite # artifacts + chunks + vectors + FTS5 index
config.toml # written by `gt init --embed-backend ...`
raw/ # on-disk cache of raw GitHub responses
clones/ # persistent shallow clones (if cache_clones=true)
wiki/ # `gt wiki export` default output
auth/token.json # OAuth file fallback
query_expansion_cache.sqlite # only when retrieval.query_expansion=ollama
以前のリリースを実行して、現在の作業ディレクトリに ./config.toml または ./data/ が作成されている場合、CLI を実行するたびに、移行に使う正確な mv コマンドを含む WARN が 1 回だけログに記録されます。
リリース
バージョンは「hatch-vcs」を介して Git タグから取得します。 リリースの作成は、次の 1 つのコマンドで行えます。
git tag v0.2.0 # PEP 440 forms: v0.2.0, v0.2.0a1, v0.2.0rc1, v0.2.0.post1
git push --tags
v* タグへのプッシュによって .github/workflows/release.yml がトリガーされ、次の処理が実行されます。
- Python 3.12 / 3.13 で pytest、ruff、
uv buildを実行します。 - Trusted Publishing
(OIDC) を介して、wheel と sdist を PyPI に公開します。リポジトリのシークレットに PyPI トークンは保存されません。PyPI は、プロジェクトページで登録した Trusted Publisher(ワークフローファイル名
release.yml、環境pypi)に対して、GitHub が署名した OIDC トークンを検証します。 - 自動生成されたリリースノート(前回のタグ以降の PR)を含む GitHub Release を作成し、wheel と sdist を添付します。プレリリースタグ(
a/b/rc)にはフラグが付けられ、「Latest」に置き換わらないようにします。 main上の.claude-plugin/plugin.jsonをタグに合わせて更新します。versionを設定し、MCP サーバーの呼び出しをuvx [email protected] serveに固定します。マーケットプレイスは HEAD からマニフェストを取得するため、マーケットプレイスのキャッシュが更新された際にユーザーが取得する内容はこれです。
リポジトリごとに 1 回だけ行う初回セットアップ:
- プロジェクトを GitHub にプッシュします(アカウント/組織は問いません)。
- PyPI で Trusted Publisher を登録します。
- https://pypi.org/manage/account/publishing/
- Owner:GitHub のユーザー/組織、Repository:
github-twin、 Workflow:release.yml、Environment:pypi。 - (または、プロジェクトがまだ PyPI に存在しない場合は「Pending Publisher」のフローを使用します。)
- GitHub で Settings → Environments → New environment
pypiに移動します。公開の前に追加の承認ステップを設けるには、自分を Required Reviewer として追加します(任意ですが推奨)。
.github/workflows/ci.yml は、すべての PR と main へのプッシュで実行されます。リリースワークフローでは公開前に同じチェックを再実行するため、壊れた main からリリースが作成されることはありません。
設計上の注意
- 埋め込み時のプレフィックス(
embed.prefix.prefix_chunk):種類ごとのヘッダーを埋め込み時に各チャンクのテキストの前に挿入します。chunk.textには書き戻しません。形式が変わるたびにEMBED_TEXT_VERSIONを更新し、次回のgt embedでベクトルを再生成します。 - AST チャンク分割(
process.chunkers):tree-sitter の走査によって、宣言可能な単位ごとにチャンクを生成します。未対応の言語や解析に失敗した場合は、行ウィンドウ方式にフォールバックします。 - 非対称なクエリ拡張
(
store.query_expansion):BM25 側だけに適用し、ベクトル側では常に未加工の埋め込みを使用します。test_hybrid_search.pyによって固定されています。
元の設計計画は、詳細な手順とともに
getting_started.md にあります。
ライセンス
MIT — 詳細は LICENSE を参照してください。
mcp-name: io.github.ChristopherDavenport/github-twin
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。