ツール別の呼び出し回数とエラー件数を一覧にする(読み取り専用)

Claude Code の Mods(関数フック型プラグイン)を作って配布するリポジトリ。1リポジトリ = 1マーケットプレイス、mods/<name>/ が1 mod。
| mod | 内容 | コマンド |
|---|---|---|
context-doctor | コンテキストの内訳を分析し、課題点と改善点を出す(読み取り専用) | /ctx |
session-recap | ツール別の呼び出し回数とエラー報告件数を一覧にする(読み取り専用・tool 名の集計のみ、input は出力しない) | /recap |
repeat-call-notice | 1 ターンで同じツールを同じ入力で N 回以上呼んだら、ターン終了時にトーストで知らせる(読み取り専用・tool 名と回数のみ、input は出力しない。N=3 は仮置き) | トースト |
このリポジトリは現在 private です。リポジトリへのアクセス権(GitHub 上で閲覧できる権限)がある人だけが取得できます。公開するかどうかは未判断で、判断の経緯は docs/design/open-questions.md の Q-001 を参照してください。
以下の手順は、リポジトリへのアクセス権があり、GitHub 認証済みの利用者向けです。
/plugin install context-doctor --marketplace dtakamiya/claude-code-mods
y でマーケットプレイスを追加 → スコープを選択。terminal セッションのプロンプトで実行する。
Claude Code 2.1.296(validate / test / tsc 通過。UI の目視確認は未実施)。Mods API は early access のため、バージョンが変わったら再検証する。
Mods はサンプルボックスなしで利用者の権限のまま動く。このリポジトリの mod は既定で読み取り専用。$.process.run・$.fs 書込・外部ネットワークを使う mod は、使う理由を各 mod の README に書く。
claude plugin validate mods/<name>
claude plugin test mods/<name>
claude --plugin-dir mods/<name> # 目視確認
詳細は CLAUDE.md。
hooks/register.ts 19 lines1import type { Register } from 'claude-code'
2
3import { render, summarize } from './recap'
4
5export const register: Register = on => {
6 on('session.start', async ($, e, next) => {
7 await $.command.register({
8 name: 'recap',
9 description: 'ツール別の呼び出し回数とエラー件数を一覧にする',
10 })
11 return next(e)
12 })
13
14 on('command.run', { command: 'recap' }, async $ => {
15 const messages = await $.session.messages()
16 return { text: render(summarize(messages)) }
17 })
18}
19hooks/recap.ts 40 lines1// 判定ロジックは $ に触れない純粋関数。tool 名の集計だけを行い、input / 結果本文は読まない。
2export type ToolUseLike = { tool: string; isError?: true }
3export type MessageLike = { toolUses: readonly ToolUseLike[] }
4
5export type ToolTally = { tool: string; calls: number; errors: number }
6export type Recap = { tools: readonly ToolTally[]; totalCalls: number; totalErrors: number; messageCount: number }
7
8/** `$.session.messages()` が返す件数の上限(型の記述)。 */
9export const MESSAGE_LIMIT = 4096
10
11export function summarize(messages: readonly MessageLike[]): Recap {
12 const byTool = new Map<string, ToolTally>()
13 for (const m of messages) {
14 for (const use of m.toolUses) {
15 const prev = byTool.get(use.tool) ?? { tool: use.tool, calls: 0, errors: 0 }
16 byTool.set(use.tool, { ...prev, calls: prev.calls + 1, errors: prev.errors + (use.isError ? 1 : 0) })
17 }
18 }
19 const tools = [...byTool.values()].sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool))
20 return {
21 tools,
22 totalCalls: tools.reduce((n, t) => n + t.calls, 0),
23 totalErrors: tools.reduce((n, t) => n + t.errors, 0),
24 messageCount: messages.length,
25 }
26}
27
28export function render(recap: Recap): string {
29 const lines: string[] = ['ツール利用の振り返り(観察のみ。原因の推測はしません)']
30 if (recap.totalCalls === 0) {
31 lines.push('ツール呼び出しはまだありません。')
32 } else {
33 lines.push(`合計: ${recap.totalCalls} 回 / エラー報告 ${recap.totalErrors} 件`)
34 for (const t of recap.tools) lines.push(` ${t.tool}: ${t.calls} 回(エラー報告 ${t.errors} 件)`)
35 }
36 lines.push(`注記: 集計対象は直近 ${recap.messageCount} 件のメッセージ(返却は最大 ${MESSAGE_LIMIT} 件まで。それより古い分は含みません)。`)
37 lines.push('注記: 「エラー報告」はツールが isError を返した件数で、失敗の原因や重大さは示しません。')
38 return lines.join('\n')
39}
40