kairan
by ukwhatn
README.md
# KAIRAN
Claude Code / Codex などの agent が生成した markdown / HTML を、tool call ひとつでブラウザに表示するローカル MCP サーバー。

| リビジョン差分(unified / side-by-side) | ダークモード |
|---|---|
|  |  |
- 何個の agent から接続されても、表示サーバーは 1 つ・port は 1 つ(初回 tool call で自動起動、全員がいなくなると自動停止)
- セッションには**表示名**を付けられる(`start_session` で agent が付け、ブラウザからいつでも変更できる)。ID は日時ベース(`0814-1345`)で自動採番され、URL に出る
- サイドバーの各セッションから**改名・アーカイブ・完全削除**ができる。ファイルは表示中のツールバーから削除できる(どちらも元に戻せない)
- URL は `http://localhost:5766/<セッションID>/<ファイル名>`。全 URL が deep link
- 同じ名前で再 publish すると新リビジョンとして積まれ、リビジョン間の差分(unified / side-by-side)が見られる
- 3 ペイン UI(セッション / ファイル / ビュー)+ SSE live update。新着 publish への自動追従は「新着に追従」トグルで制御
- agent が終了したセッションは自動で archive され、サイドバーの「archived」トグルで表示できる。`kairan restart` を挟んでも、生きている agent のセッションは active のまま残る
- **agent を閉じて `--resume` / `--continue` で開き直すと、同じセッションに戻る**(Claude Code のセッション ID を鍵にしている。この ID を持たない agent では従来どおり毎回新しいセッションになる)。**セッションができるのは最初に kairan を使った時点**なので、agent を立ち上げただけでは何も増えない
- markdown は GFM + shiki シンタックスハイライト + mermaid 図に対応。HTML は iframe でそのまま実行できる
- **publish された HTML は kairan と同一オリジンで動く**。実行中の文書にそのままインラインコメントを付けられるようにするための設計で、引き換えに文書のスクリプトは kairan の API(セッション・ファイルの削除、レビュー送信、ローカルファイルを開く操作)を叩ける。信頼できない HTML を publish しないこと(markdown 側は本体画面の `script-src 'self'` で inline handler を禁止している)
- **タブの favicon がステータスを示す**。あなたの対応待ち(未回答の質問・agent がレビュー送信を待っている)があれば赤バッジ、タブを開いている間に届いた未読の publish があれば青バッジ。タブタイトルにも対応待ちの件数が出る
- **表示中のファイルを「Finder で表示」「エディタで開く」「ダウンロード」できる**。Finder / エディタは `path` で publish されたファイルを localhost から見ているときだけ出る(cloudflare tunnel 等のリモート閲覧ではダウンロードのみ)
- publish 時に macOS 通知センターへ通知(設定で off 可)。[terminal-notifier](https://github.com/julienXX/terminal-notifier) が入っていれば**通知クリックでそのファイルをブラウザで開ける**(`brew install terminal-notifier`。無ければ osascript 通知にフォールバック、クリック遷移なし)
- **人間 → agent のフィードバック**にも対応。文書にインラインコメントを付けて GitHub PR レビューのように一括送信でき、agent からの選択肢つき質問は**その文書の末尾に埋め込まれたフォーム**として出る
- **agent は待たない**。回答・レビューは Stop hook(`kairan hook stop`)がセッションへ注入する。hook を入れていない agent は次の kairan tool call か `list_feedback` で回収する
- 質問は 1 文書に 1 セットだけ。同じ文書に publish し直すと前の未回答の質問は置き換わるので、**答えないまま溜まらない**
- 選択範囲へのインラインコメントは markdown・HTML の**どちらの表示でも**使える(HTML は実行したまま。ソース表示ではファイル全体へのコメントのみ)
- 本文の横に常時並ぶコメントカードは markdown 表示だけ。HTML では iframe が内側でスクロールして位置を追えないため、ハイライトの hover / クリックでカードを出す
## セットアップ
```bash
bun install
bun link # `kairan` コマンドをグローバルに登録
```
### Claude Code
```bash
claude mcp add --scope user kairan -- kairan mcp
```
### Codex CLI
```toml
# ~/.codex/config.toml
[mcp_servers.kairan]
command = "kairan"
args = ["mcp"]
```
### Stop hook(回答をセッションへ届ける・Claude Code)
質問への回答とレビューは、**agent が待つのではなく Stop hook が注入する**。`~/.claude/settings.json` の `Stop` に足す:
```json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "if command -v kairan >/dev/null 2>&1; then kairan hook stop; else exit 0; fi",
"asyncRewake": true,
"timeout": 3900,
"statusMessage": "kairan: 回答を待っています"
}
]
}
]
}
}
```
- `asyncRewake: true` が要る。hook はバックグラウンドで走り、回答が届いた時点でセッションを起こす(`timeout` は秒。`hookWaitMs` より長くしておく)
- **止まっているデーモンを hook が起こすことはない**(デーモンが動いていなければ即座に終わる)。デーモンが動いている場合は、質問もレビューも無いターンでも hook は次の回答を待って `hookWaitMs`(既定 60 分)までバックグラウンドに留まる。agent のターン自体はブロックしない
- **ターン終了時に `Stop hook error occurred · ctrl+o to see` と出るのは正常**。Claude Code は「モデルを起こす」合図に exit code 2 を使うため、成功時もエラー表示になる
- hook を入れない場合も動く(回答は次の kairan tool call か `list_feedback` で回収される)。ただし人が答えたことに気付くのは agent が次に kairan を触ったときになる
**なぜ hook 経由なのか**: MCP の tool call は Claude Code のハーネス側で壁時計 270 秒で打ち切られる(progress 通知では延びない)。人間の応答を tool call の中で待つ設計は、答えるのに数分かかる質問では成立しない。
## tool
### `start_session`
このプロセスのセッションを開始し、人間向けの表示名を付ける。最初の `publish` の前に一度呼ぶと、サイドバーでどの agent のセッションか見分けられる。以後 `session` を省略した tool call はここで始めたセッションに載る。
| 引数 | 説明 |
|---|---|
| `label` | サイドバーに出る表示名。**一意である必要はなく**、ブラウザからいつでも変更できる |
| `id` | セッション ID(URL セグメント)を固定したいときだけ渡す。省略時は日時ベース(`0814-1345`)で自動採番 |
戻り値: `{ sessionId, label, url }`
### `publish`
markdown / HTML をブラウザに表示する。`path`(ファイルパス)か `content`(文字列)のどちらかを渡す。
| 引数 | 説明 |
|---|---|
| `path` | 表示するファイルのパス(`content` と排他) |
| `content` | 本文の直接渡し(`name` 必須) |
| `name` | セッション内のファイル ID(URL セグメント)。省略時は `path` の basename。**同名で再 publish = 上書き = 新リビジョン** |
| `format` | `markdown` / `html`。省略時は拡張子から推定 |
| `session` | publish 先のセッション ID(別プロセスから同じセッションを継続するときに使う)。省略時はこのプロセスのセッション |
| `title` | ファイルリストに表示するタイトル |
| `questions` | この文書について聞きたいこと(最大 8 問)。文書の末尾にフォームとして出る。各問は選択肢 + 自由記述を持つ。**渡さなければ今ある質問に触らない / `[]` を渡すと取り下げ** |
| `open` | `true` で強制オープン / `false` でオープン抑制 |
戻り値: `{ url, sessionId, fileId, revision, pendingFeedback, askId }`(`pendingFeedback` は未受領フィードバック件数、`askId` は文書に出ている質問セットの ID)
`questions` は**ブロックしない**。同じ内容で publish し直しても同じフォームが残り(回答途中の入力も消えない)、内容を変えると前の未回答の質問は取り下げられて新しいものに置き換わる。答えないまま何個も並ぶことはない。
`path` で publish したファイルは元の絶対パスが記録され、ブラウザの「Finder で表示」「エディタで開く」から開ける(`content` で publish し直すと記録は消える)。パス自体は API の応答にも `list_files` にも出ない。
### `list_files`
自セッション(または `session` で指定したセッション ID)の publish 済みファイル一覧。
### `request_review`
人間にブラウザでのレビューを依頼する。**ブロックしない**。人間側はコメントを下書きとして溜め、総評とともに「送信」した時点でまとめて返る(GitHub PR レビューと同じモデル。コメント 0 件 + 総評空の「コメントなしで返す」も可)。送信されたレビューは Stop hook がセッションへ注入する。
各コメントの `commentId`・対象ファイル・引用文(選択範囲)・本文と、総評・スレッド返信・未回収の質問回答が届く。
`wait_seconds` を明示的に渡したときだけ、その秒数だけブロックして待つ(Stop hook を持たない agent 向け)。
### `reply_comment`
`request_review` / `list_feedback` が返した `commentId` へのスレッド返信。`resolve: true` でコメントを解決済みにできる(人間側から再オープン可)。
### `list_feedback`
ブロックせずに、送信済み・未受領のフィードバック(レビュー・質問回答)を回収する。agent が待っていない間に送信されたぶんの回収用。各項目は一度だけ返る。
## CLI
```bash
kairan status # デーモンの稼働確認
kairan restart # デーモンの再起動(コード・設定変更の反映用)
kairan stop # デーモンの停止(通常は不要: 全接続が消えると自動停止する)
kairan daemon # デーモンをフォアグラウンド起動(通常は自動起動されるため不要)
kairan relink # 過去のセッションを agent のセッションに繋ぎ直す(下記)
kairan hook stop # Claude Code の Stop hook 本体(settings.json から呼ばれる。手で叩くものではない)
```
### `kairan relink`
復帰の鍵(agent のセッション ID)を持たない古いセッションに、Claude Code の履歴から鍵を埋め直す。鍵が無いセッションは `--resume` で戻れず、開き直すたびに新しいセッションができてしまうため、その復旧用。
- `~/.claude/projects/*/*.jsonl`(`CLAUDE_CONFIG_DIR` を設定していればそちら)から、kairan の tool 呼び出しの結果だけを拾って対応付ける
- 併せて**畳まれていて中身が空のセッションを削除する**(残したくない場合は `--keep-empty`)
- `--dry-run` で何をするかだけ表示できる
- 適用時はデーモンを止めてから DB をバックアップし、終わったら元の状態に戻す。稼働中のセッションからは鍵を奪わない
### コード変更の反映
- **デーモン側**(Web UI・API・レンダリング・通知など大半のロジック): `kairan restart` で反映される。稼働中の agent は自動で接続し直すため、セッションは active のまま残る
- **stdio ランチャー側**(tool 定義・入力解決): ランチャープロセスは agent が起動・保持しているため kairan 側からは再起動できない。agent の MCP 再接続で反映される(Claude Code は `/mcp` → Reconnect、または新しいセッションを開始)
## 設定
`~/.kairan/config.json`(すべて任意)と環境変数で上書きできる。優先度: 環境変数 > config.json > デフォルト。
| キー | 環境変数 | デフォルト | 説明 |
|---|---|---|---|
| `port` | `KAIRAN_PORT` | `5766` | デーモンの listen port |
| `host` | `KAIRAN_HOST` | `127.0.0.1` | bind アドレス(`127.0.0.1` / `localhost` / `::1` のみ。認証なしのため loopback 限定) |
| `dataDir` | `KAIRAN_DATA_DIR` | `~/.kairan` | SQLite / lock の置き場所 |
| `autoOpen` | `KAIRAN_AUTO_OPEN` | `session-first` | `session-first`(セッション初回のみ自動オープン)/ `always` / `never` |
| `reopenWhenNoTab` | `KAIRAN_REOPEN_WHEN_NO_TAB` | `true` | publish 時にそのセッションを見ているタブが無ければ開き直す |
| `notifications` | `KAIRAN_NOTIFICATIONS` | `true` | macOS 通知センターへの通知 |
| `notifyOn` | `KAIRAN_NOTIFY_ON` | `all` | `all`(上書きも通知)/ `new-file`(新規ファイルのみ) |
| `openCommand` | `KAIRAN_OPEN_COMMAND` | `open` | ブラウザを開くコマンド |
| `editorUrl` | `KAIRAN_EDITOR_URL` | `vscode://file{path}` | 「エディタで開く」の URL テンプレート。`{path}` が publish 元の絶対パスに置換される(Cursor なら `cursor://file{path}`)。空文字にするとボタンを出さない |
| `followDefault` | `KAIRAN_FOLLOW_DEFAULT` | `true` | UI「新着に追従」トグルの初期値 |
| `reuseTab` | `KAIRAN_REUSE_TAB` | `true` | 自動オープン・通知クリック時に既存の kairan タブを再利用する(Chrome 系 / Safari。初回に macOS の自動化許可が必要。`false` で常に新規タブ) |
| `shutdownGraceMs` | `KAIRAN_SHUTDOWN_GRACE_MS` | `5000` | 全接続 0 になってから自動停止するまでの猶予 |
| `archiveGraceMs` | `KAIRAN_ARCHIVE_GRACE_MS` | `10000` | デーモン起動後、生きている agent が接続し直すのを待つ時間。これを過ぎても接続の無い active セッションは archive する |
| `feedbackWaitMs` | `KAIRAN_FEEDBACK_WAIT_MS` | `240000`(4 分) | ブロックして待つ API(`/api/feedback/wait`)で待ち時間の指定が無かったときの既定。MCP tool call はハーネス側で 270 秒で切られるため、それより短くしてある |
| `hookWaitMs` | `KAIRAN_HOOK_WAIT_MS` | `3600000`(60 分) | Stop hook が回答を待つ時間。`settings.json` の hook `timeout`(秒)より短くしておく |
設定ファイルのパス自体は `KAIRAN_CONFIG_PATH` で変更できる。
## 公開する場合の注意
kairan 自体は**認証を持たない**。`/api/*` の POST に入っているのは cross-origin を弾く CSRF 対策であって認証ではなく、Origin ヘッダの無いリクエスト(MCP ランチャー・curl)は意図的に通す。cloudflare tunnel 等で外から届くようにする場合、**tunnel 側で認証をかけること**(到達できる相手はセッションの一覧取得も完全削除もできる)。
publish された HTML も同一オリジンで動く(実行中の文書にコメントを付けるための設計)。つまり**文書のスクリプトは kairan の API を叩ける**ので、信頼できない HTML を publish しない。ローカルでの利便性を優先した割り切りで、agent が生成した文書を自分で見る用途を想定している。
## アーキテクチャ
```
agent (Claude Code / Codex)
│ stdio MCP
▼
kairan mcp(agent ごとに 1 プロセス。port は使わない)
│ HTTP(初回 tool call 時にデーモンを自動 spawn・生存申告の SSE を維持)
▼
kairan daemon(全体で 1 つ・port 1 つ)── SQLite (~/.kairan/kairan.db)
│ HTTP + SSE
▼
browser(3ペイン UI)
```
- stdio プロセス = 1 セッション。プロセス終了(= agent 終了)で TCP が切れ、デーモンがセッションを archive する
- デーモンは「active セッション 0 かつ 閲覧タブ 0」になると自動停止する。データは SQLite に永続化されているため、次回起動時も過去セッションを閲覧できる
- 設計判断の経緯: `.local/docs/adr/0001-stdio-launcher-shared-daemon.md`
## 開発
```bash
bun test # テスト
bun run typecheck # tsc
bun run lint # biome
bun run dev # デーモンをフォアグラウンド起動
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues