claude-spotter 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.2
4
+
5
+ **Windows で npm-global の `.cmd` 配布 MCP サーバ (例: `claude-mermaid`) が investigate 時に ENOENT で落ちる回帰を修正**。`spotter db refresh` の MCP investigate 経路で `spawn error: spawn claude-mermaid ENOENT` が出て、当該 MCP のツールがカタログに投入されない症状。`.exe` 配布の MCP (例: `openai-image`) や `claude-mermaid.cmd` のように拡張子を明示した登録は影響を受けない、ピンポイントな bug。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `buildStdioSpawn` の Windows ブランチ条件を `/\.(cmd|bat)$/i` から「絶対 `.exe` パス以外は `cmd.exe /c` で包む」に拡張。`export` を追加してユニットテスト可能に
10
+ - **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: 4 件追加 — POSIX パススルー / Windows 裸名の wrap (`claude-mermaid` で v1.2.1 の症状を直接再現) / Windows `.cmd`・`.bat` の wrap / Windows 絶対 `.exe` パスは un-wrap (空白入りパスでの cmd.exe quoting リスク回避)
11
+
12
+ ### 背景
13
+
14
+ #### 何が起きていたか
15
+
16
+ Windows の npm global は CLI を `<name>.cmd` バッチラッパーとして配布する (npm 標準仕様)。Node.js の `child_process.spawn(name, args)` は `shell: true` 無しでは Windows `CreateProcess` をそのまま呼び、`CreateProcess` は `.exe` を直接実行するが PATHEXT で `.cmd` を解決しない。結果、`spawn('claude-mermaid', [])` は `claude-mermaid.cmd` が PATH 上にあっても ENOENT で即落ちる。これは Spotter v0.7.0 → v0.8.0 で claude CLI 起動時に踏んで自分で直した bug の再来 (own caveat: `windows-node-spawn-claude-fails-with-enoent-because-claude-is-a-cmd-wrapper`) で、修正パターン (Windows なら `cmd.exe /c <command>` で包む) は既に Spotter 内に存在していた (`execClaude` / haiku-caller の `buildSpawnArgs`)。**穴は MCP サーバ起動経路にこのパターンが横展開されていなかったこと**。
17
+
18
+ #### v1.2.1 までのコード
19
+
20
+ ```js
21
+ function buildStdioSpawn(command, args) {
22
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)) {
23
+ return { cmd: 'cmd.exe', cmdArgs: ['/c', command, ...args] };
24
+ }
25
+ return { cmd: command, cmdArgs: args };
26
+ }
27
+ ```
28
+
29
+ `/\.(cmd|bat)$/i` は「コマンド名が `.cmd`/`.bat` で**終わっている**」場合のみ wrap する。ところが MCP サーバの登録名 (`claude mcp add` から `~/.claude.json` 直下 `mcpServers` に書かれる、または `.mcp.json` に書かれる) は通常**拡張子を付けない裸名** (`claude-mermaid`)。そのため Windows ブランチが発火せず、wrap 抜きで `spawn('claude-mermaid')` してENOENT。`.exe` 配布だと `CreateProcess` が直接実行できるので症状が出ない。`.cmd` を明示的に書いた登録 (`claude-mermaid.cmd`) も既存ロジックでセーフ。**裸名 + `.cmd` 実体の組合せだけが silent に脱落していた**。
30
+
31
+ ### 設計判断
32
+
33
+ - **絶対 `.exe` パスは un-wrap のまま**: `C:\Program Files\nodejs\node.exe` のような空白入りパスを `cmd.exe /c` で包むと cmd.exe の `/c` 引数解釈ルール (最初の char が `"` のときの outer-quote strip 等) のリスクに晒される。`.exe` は `CreateProcess` が直接実行できるので包む必要がないし、包まないことで quoting リスクをゼロにできる。`.cmd` パスはどのみち cmd.exe 経由でしか走らないので包む必要があり、quoting リスクは織り込み済み (既存挙動から退化なし)
34
+ - **`shell: true` を使わない理由**: Node 24+ で DEP0190 が出るし、引数 quoting が cmd.exe の rules に丸投げされて `&` `|` `>` 等の metacharacter 処理リスクが復活する。caveat の Resolution と既存 (`execClaude` / `buildSpawnArgs`) の選択を踏襲
35
+ - **裸名を全部 wrap する選択 (= 「`.exe` 以外は wrap」)**: `node` のような既知の `.exe` 配布も Windows では cmd.exe 経由になるが、cmd.exe が PATHEXT で正しく解決するので動作影響なし。コードのシンプルさ (extension の case sensitivity / 配布形態の事前知識を不要にする) を優先
36
+ - **patch bump (fix)**: API 変更なし、`buildStdioSpawn` の export 追加は純粋に additive。Windows での挙動が「ENOENT で死ぬ」→「正しく動く」方向の修正なので破壊変更なし
37
+
38
+ ### 自動追従の経路
39
+
40
+ 既に Spotter を導入済みのプロジェクトは npm global update (`npm i -g claude-spotter@1.2.2`) 後、次の Claude Code SessionStart で v1.1.0 機構の `spawnRefreshDetached` が走り、新 `buildStdioSpawn` で MCP investigate を再実行。これまで wrap 抜きで spawn して即 ENOENT だった `.cmd` 配布 MCP が live fetch に成功し、**次の次のセッション**から該当ツールがカタログに復活する (detached の仕様)。即時反映したい場合は `spotter db refresh` を手動実行 (環境によっては既にカタログに残っている古いエントリは prune される / live fetch 成功で description が上書きされる)。
41
+
42
+ ## 1.2.1
43
+
44
+ **Claude Code 公式の MCP scope 3 段 (User / Project / Local) に完全対応**。v1.2.0 までの `readMcpServers` は project スコープ (`<projectRoot>/.mcp.json`) と非公式の legacy `~/.claude/.mcp.json` しか読んでおらず、公式 3 スコープのうち 2 つ (User: `~/.claude.json` 直下 `mcpServers` / Local: `~/.claude.json` `projects[<root>].mcpServers`) を読み損ねていた。結果、`claude mcp add -s user -e KEY=val -- ...` で登録した MCP サーバーは `claude mcp list` で発見されるが env が拾えず、HTTP 系は 401、stdio 系は API キー無しで spawn → tools/list が空 → `resolveAll` の prune ループでカタログから削除、という silent な脱落が発生していた。
45
+
46
+ ### 変更点
47
+
48
+ - **編集 [src/tool-db/mcp-config.mjs](src/tool-db/mcp-config.mjs)**: `readMcpServers` を 4 ソース merge に拡張 — `legacy < user < project < local` の優先順 (公式仕様 Local > Project > User と整合、legacy `~/.claude/.mcp.json` は最下位の互換扱い)。新規 export: `userClaudeJsonPath`、`legacyUserMcpConfigPath`、`normalizeProjectPath`、`extractUserScopeServers`、`findLocalScopeServers`。`readMcpServers` に `claudeJsonPath` / `legacyUserPath` の DI パラメータを追加 (テスト用、デフォルトは実 homedir)。冒頭コメントを公式仕様 ([https://code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp)) に揃えて書き直し、legacy ソースの位置付けを明記
49
+ - **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: 13 件追加 — user/local 単独 / 4 段優先順位 / `~/.claude.json` 不在・malformed・キー欠損 / `extractUserScopeServers` の null 耐性 / `normalizeProjectPath` (separator・trailing slash・Windows case) / `findLocalScopeServers` (exact / 正規化マッチ / 非マッチ時の no-fuzzy / null 入力)
50
+
51
+ ### 設計判断
52
+
53
+ - **`projects[]` キーの照合に正規化を入れた理由**: Claude Code が `~/.claude.json` に書く絶対パスの表記は環境と書き込みタイミングで揺れる (Windows ではドライブレターの大小、separator (`\` vs `/`)、末尾スラッシュ)。正確一致が外れると Local スコープが silent に脱落する = 今回直そうとしている bug の Windows 版が再発する。`normalizeProjectPath` で separator 統一 + 末尾 `/` 除去 + Windows 限定の lower-case を施し、exact 一致が無いときだけ正規化フォールバックする 2 段照合に
54
+ - **fuzzy / prefix マッチを意図的に外した理由**: `/home/u/proj` のキーから `/home/u/other-project` の照合に `mcpServers` を引き渡すと、別プロジェクトの secrets を spotter の audit に混ぜることになる。projectRoot は識別子であって階層ではないので、normalize 後の完全一致のみ採用 (タスク指示の「投機的なファジーマッチはしない」と整合)
55
+ - **`~/.claude.json` の malformed を throw せず空扱い**: このファイルは Claude Code 本体が管理する状態ファイルで、書き込み中に他プロセスが読みに行けば transient corruption に見える可能性がある。Spotter が落ちる方が実害が大きいので寛容に扱う (`.mcp.json` 系 = ユーザー手書きはこれまで通り throw、bug 表面化を優先)
56
+ - **fix 扱いの patch bump**: public API (`readMcpServers` のシグネチャ) は引数が optional 追加のみで後方互換、merge 結果が「漏れてた scope を拾う」方向に増えるだけで既存挙動の縮退なし。programmatic API の破壊変更ではないので minor bump 不要
57
+ - **legacy `~/.claude/.mcp.json` を残した理由**: 仕様外であることは認めるが、現に Spotter で投入されているサンプルや既存ユーザー環境の依存を切ると静かにツールが消える。最下位優先で残しつつ、コメントで「legacy / 公式仕様外 / 互換維持」を明記。撤去は別 PR
58
+
59
+ ### 自動追従の経路
60
+
61
+ 既に Spotter を導入済みのプロジェクトは npm の global update 後、次の Claude Code SessionStart で v1.1.0 機構の `spawnRefreshDetached` が走り、その refresh が新 `readMcpServers` で User / Local スコープの env を拾い直す。これまで env 抜きで spawn して tools/list が空だった MCP サーバーが live fetch に成功するようになり、**次の次のセッション**から該当ツールがカタログに復活する (detached の仕様)。即時反映したい場合は `spotter db refresh` を手動実行。
62
+
3
63
  ## 1.2.0
4
64
 
5
65
  **当該プロジェクトで使えないツールが提案される回帰を構造的に修正**。daemon が監査に使うカタログを **ローカル DB のみ** に変更し、グローバル DB は他プロジェクトでの description 再利用のためのキャッシュ層に役割を限定。同時に `resolveAll` で「現プロジェクトの discovery 結果に含まれないローカルエントリ」を prune するよう変更し、過去にインストールされていた MCP / スキル / サブエージェントが local に居座って Haiku 視野に残る経路を塞いだ。
package/README.ja.md ADDED
@@ -0,0 +1,123 @@
1
+ <p align="center">
2
+ <img src=".github/og.svg" alt="Spotter — Audit agent for Claude Code" width="100%">
3
+ </p>
4
+
5
+ # Spotter
6
+
7
+ [![npm version](https://img.shields.io/npm/v/claude-spotter.svg?style=flat-square)](https://www.npmjs.com/package/claude-spotter)
8
+ [![CI](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml/badge.svg)](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml)
9
+ [![Node.js](https://img.shields.io/node/v/claude-spotter.svg?style=flat-square)](https://nodejs.org)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
11
+
12
+ **[English](README.md) · 日本語**
13
+
14
+ > **気づく役と実行する役を分離する。** Claude Code の横で並走し、Bell (主役の Claude) が**ツールを呼び忘れたとき**だけ静かに指摘する監査役。
15
+
16
+ Claude には「使えるツールがあるのに、使うべきタイミングで使わない」という構造的な弱点があります。現在時刻を推測で答える、`web_search` を呼ばずに古い情報で応答する、`read_file` を使わずにファイルの中身を推測する — **「分からないと自覚できない」から、ツールを取りに行けない**。
17
+
18
+ Spotter はツールカタログを完全に把握した別エージェント (Claude Haiku 4.5) をセッション毎に常駐させ、Bell の発話予定と応答を並走監査します。見落としを検出すると透明化された指摘として Bell に届け、補正応答を促します。**Bell が自覚して呼ぶ**設計は本プロダクトの存在意義を破壊するため、Bell から呼ぶのではなく hook 経由で Bell の意思と独立に検出する構造を取っています。
19
+
20
+ ## 30 秒で見るポイント
21
+
22
+ Spotter が拾うのは、たとえばこういう瞬間です。
23
+
24
+ | 状況 | Bell の応答 | Spotter の指摘 |
25
+ |---|---|---|
26
+ | 「今日の天気を教えて」 | 推測で答えようとする | `web_search` の使用機会 |
27
+ | 「この設定ファイルの中身は?」 | 名前から推測で説明 | `read_file` の使用機会 |
28
+ | 「今何時?」 | 学習時点の情報で答える | `current_time` の使用機会 |
29
+ | 事実の断定 | 裏付けなしで「〜です」 | 検証用ツールの差し込み余地 |
30
+
31
+ 判定軸は 2 段階:
32
+
33
+ - **入力時 (`stage=user_input`)**: ユーザー要請に対し、`when_to_use` の条件に明確に該当するツールを列挙する **要請充足チェック**
34
+ - **応答後 (`stage=turn_end`)**: Bell の最終応答に対し、事実の断定 / 記録すべき新情報 / 既知情報の参照それぞれに、カタログ上のツール (検証 / 登録 / 照会) を差し込める余地がないかを問う **ツール適用機会の監査**
35
+
36
+ ## インストール
37
+
38
+ ```bash
39
+ npm install -g claude-spotter
40
+ cd your-project
41
+ spotter install
42
+ ```
43
+
44
+ `v0.3.0` 以降は**プロジェクト単位の明示的 install** を採用しています (v0.2 までの `postinstall` 自動登録はデーモン増殖の主因だったため撤回)。各プロジェクトの `.claude/settings.json` に hook を登録し、そのプロジェクトでの Claude Code セッションのみで有効になります。
45
+
46
+ ```bash
47
+ spotter uninstall # このプロジェクトの hook 登録を解除
48
+ ```
49
+
50
+ ## 動作要件
51
+
52
+ - **Node.js 22.5 以上**
53
+ - **Claude Code 2.0 以上**
54
+ - **Claude Max プラン** (`claude -p` で Haiku を起動するため)
55
+
56
+ ## アーキテクチャ
57
+
58
+ ```
59
+ User 発話
60
+
61
+ UserPromptSubmit hook → Spotter がカタログと発話を見て一次判定
62
+
63
+ Bell Thinking (Spotter の推奨を additionalContext で受け取る)
64
+
65
+ Bell 最終応答
66
+
67
+ Stop hook → Spotter が応答と使用済みツールを見て最終チェック
68
+
69
+ 見落としあれば差し戻し (max 1 回、Claude Code の stop_hook_active で自動担保)
70
+ ```
71
+
72
+ 監査対象のツール (name + description) は `<project>/.spotter/tool-db.json` (ローカル) に格納されます。**daemon が監査に使うのはローカル DB のみ** (v1.2.0 以降) で、グローバル DB `~/.spotter/tool-db.json` は他プロジェクトでの description 再利用キャッシュとしてのみ機能します (live fetch コスト削減のため初回 refresh で参照、結果は local に write-through)。各プロジェクトの local DB は **そのプロジェクトの現時点の discovery 結果と一致** (refresh 時に prune される) ため、過去にインストールしていた MCP / スキル / サブエージェントが他プロジェクトに混入することはありません。
73
+
74
+ **v1.1.0 以降、`spotter install` が初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook が bg で `spotter db refresh` を走らせる**ため、通常の運用で手動コマンドを叩く必要はありません。収集経路は (1) MCP サーバー: `claude mcp list` で集合を確定し、env / headers は Claude Code 公式 3 スコープ — User (`~/.claude.json` 直下 `mcpServers`) / Project (`<projectRoot>/.mcp.json`) / Local (`~/.claude.json` `projects[<root>].mcpServers`) — を precedence Local > Project > User で merge して取得 (v1.2.1 以降、互換のため legacy `~/.claude/.mcp.json` も最下位で参照)、各サーバーの `tools/list` を JSON-RPC で取得、HTTP/SSE transport にも対応、(2) スキル: user/project/プラグインの SKILL.md frontmatter から `{name, description}` を抽出、(3) サブエージェント: user/project/プラグインの agent .md frontmatter から抽出、(4) claude.ai baseline: OAuth proxy 経由の Gmail/Calendar/Drive 25 件は手書き baseline で補完 (v1.1.4 以降、`claude mcp list` に該当サーバーが存在する環境でのみ注入)。**手書きでツールリストを管理する必要はありません**。
75
+
76
+ ## Throughline との関係
77
+
78
+ [Throughline](https://github.com/kitepon-rgb/Throughline) と Spotter は同じ作者が作った、**哲学を共有する別プロダクト**です。
79
+
80
+ | | Throughline | Spotter |
81
+ |---|---|---|
82
+ | 思想 | 引き算 (要らないものを退避) | 足し算 (足りない動作に気づかせる) |
83
+ | 対象 | コンテキスト肥大化 | ツール取りこぼし |
84
+ | 仕組み | hook で記憶退避 | hook でサブエージェント並走 |
85
+
86
+ 両者に共通するのは **「主体 (Bell) に頼らない仕組み」**。併用できます。
87
+
88
+ ## よく使うコマンド
89
+
90
+ ```bash
91
+ spotter db list # 現在のローカル tool-db (daemon が実際に audit に使う) を表示
92
+ spotter db refresh # MCP / スキル / サブエージェントから description を収集して DB 更新
93
+ # (v1.1.0 以降、install 時と SessionStart 時に自動実行されるので通常は不要)
94
+ spotter db rebuild # local + global DB を両方消してから refresh (カタログ設計変更時のクリーン用)
95
+ spotter status # 稼働中の daemon 一覧
96
+ spotter doctor # 環境診断 (Node / claude CLI / tool-db 整合性)
97
+ spotter uninstall # hook 登録を解除 (~/.spotter は残す)
98
+ ```
99
+
100
+ ## 設計ドキュメント
101
+
102
+ - **現行設計 (カタログ / 収集経路 / 分類軸)**: [docs/catalog-design.md](docs/catalog-design.md) — v1.0.0 以降の真実源
103
+ - **現時点で塞がっていない穴 + 実測未検証の懸念**: [docs/open-issues.md](docs/open-issues.md) — 新規作業に入る前に必読
104
+ - **実装規範と不変条件 (§0)**: [CLAUDE.md](CLAUDE.md) — フォールバック禁止 / silent fallback 禁止 / 暫定コード禁止
105
+ - **歴史記録 (v0.1 時点の設計議事録)**: [docs/spotter-plan.md](docs/spotter-plan.md) — 作成時点で固定された議論過程のスナップショット、現行設計は上記 3 点を参照
106
+
107
+ ## 既知の制約
108
+
109
+ - Stop hook は Bell の最初の応答が**出力された後**に発火するため、Spotter が Stop で差し戻した場合、ユーザーは「最初の応答 + 補正応答」の 2 連続を見ます (Claude Code の hook 仕様による制約)。UserPromptSubmit 段階での先回り検出を精度の軸にしています
110
+ - **JSON スキーマ違反は v0.5.0 以降「想定済み異常」として silent pass + session renew で回復**します (role collapse 検知パス、daemon ログに `role_collapse_reset` を残す)。一方 **Haiku timeout は引き続き throw** され、UserPromptSubmit がブロックされてユーザー入力が Bell に届かない症状として顕在化します (timeout は v0.5.0 で 30s、v0.13.1 で 45s に拡張)。timeout の fail-open 化 (pass 扱い) は §0 改訂とセットで今後検討
111
+
112
+ <details>
113
+ <summary><strong>📋 v1.2.0 リリースノート (2026-04-26)</strong></summary>
114
+
115
+ **当該プロジェクトで使えないツールが提案される回帰を構造的に修正**。daemon が監査に使うカタログを**ローカル DB のみ**に変更し、グローバル DB は他プロジェクトでの description 再利用キャッシュに役割を限定。`resolveAll` 末尾に prune ループ追加で、現プロジェクトの discovery 結果に含まれない既存ローカルエントリを削除 (= 過去の別プロジェクトで discover された MCP / スキル / サブエージェントが居座る経路を遮断)。既 install プロジェクトは npm global update 後、次の SessionStart で自動 refresh が走り、次の次のセッションから幽霊が消える (即時反映は `spotter db refresh` 手動)。v1.1.0 からの柱 (install 時 tool-db 自動構築 + SessionStart での drift 自動追従) は継続。監査対象は v1.0.0 でユーザー追加分 (MCP / スキル / サブエージェント) に絞り込み済み。
116
+
117
+ 詳細は [CHANGELOG](CHANGELOG.md) を参照。
118
+
119
+ </details>
120
+
121
+ ## ライセンス
122
+
123
+ MIT — see [LICENSE](LICENSE).
package/README.md CHANGED
@@ -1,14 +1,39 @@
1
+ <p align="center">
2
+ <img src=".github/og.svg" alt="Spotter — Audit agent for Claude Code" width="100%">
3
+ </p>
4
+
1
5
  # Spotter
2
6
 
3
- > **v1.2.0 released 2026-04-26**. **当該プロジェクトで使えないツールが提案される回帰を構造的に修正**。daemon が監査に使うカタログを**ローカル DB のみ**に変更し、グローバル DB は他プロジェクトでの description 再利用キャッシュに役割を限定。`resolveAll` 末尾に prune ループ追加で、現プロジェクトの discovery 結果に含まれない既存ローカルエントリを削除 (= 過去の別プロジェクトで discover された MCP / スキル / サブエージェントが居座る経路を遮断)。既 install プロジェクトは npm global update 後、次の SessionStart で自動 refresh が走り、次の次のセッションから幽霊が消える (即時反映は `spotter db refresh` 手動)。v1.1.0 からの柱 (install 時 tool-db 自動構築 + SessionStart での drift 自動追従) は継続。監査対象は v1.0.0 でユーザー追加分 (MCP / スキル / サブエージェント) に絞り込み済み。設計思想は [docs/catalog-design.md](docs/catalog-design.md)、変更詳細は [CHANGELOG](CHANGELOG.md)
7
+ [![npm version](https://img.shields.io/npm/v/claude-spotter.svg?style=flat-square)](https://www.npmjs.com/package/claude-spotter)
8
+ [![CI](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml/badge.svg)](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml)
9
+ [![Node.js](https://img.shields.io/node/v/claude-spotter.svg?style=flat-square)](https://nodejs.org)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
11
+
12
+ **English · [日本語](README.ja.md)**
13
+
14
+ > **Separate the spotter from the doer.** Spotter runs alongside Claude Code and quietly flags the moments when Bell (your primary Claude) **forgets to use a tool it has access to**.
15
+
16
+ Claude has a structural blind spot: **it can't reach for a tool it doesn't realize it needs**. It will guess the current time instead of calling `current_time`, answer with stale knowledge instead of `web_search`, describe a config file from its name instead of `read_file`. The model can't always tell when it doesn't know — so the tool stays unused.
4
17
 
5
- **気づく役と実行する役を分離する。** Spotter Claude Code の横で静かに並走し、Bell (主役の Claude) が**ツールを呼び忘れたとき**に指摘する監査役です。
18
+ Spotter pins a second agent (Claude Haiku 4.5) next to Bell. The second agent has the full tool catalog memorized and audits both the user's prompt and Bell's reply in parallel. When it spots a missed tool, it injects a transparent recommendation into Bell's context and, if needed, asks Bell to amend its answer. **Bell is never asked to self-audit** — that would defeat the entire premise. Detection happens through hooks, independent of Bell's intent.
6
19
 
7
- > Claude には「使えるツールがあるのに、使うべきタイミングで使わない」という構造的な弱点があります。現在時刻を推測で答える、web_search を呼ばずに古い情報で応答する、read_file を使わずにファイルの中身を推測する — 「分からないと自覚できない」から、ツールを取りに行けない。
20
+ ## See it in 30 seconds
21
+
22
+ Examples of what Spotter catches:
23
+
24
+ | Situation | What Bell would do | What Spotter flags |
25
+ |---|---|---|
26
+ | "What's the weather today?" | Guess from training data | Missed call to `web_search` |
27
+ | "What's in this config file?" | Describe based on the filename | Missed call to `read_file` |
28
+ | "What time is it?" | Answer from training-time knowledge | Missed call to `current_time` |
29
+ | Asserting a fact | Stating it without verification | Opportunity to call a verification tool |
8
30
 
9
- Spotter は、ツールカタログを完全に把握した別エージェント (Claude Haiku 4.5) をセッション毎にプロセスとして常駐させ、Bell の発話予定と応答を並走監査します。見落としを検出すると、透明化された指摘として Bell に届け、補正応答を促します。Haiku 呼び出しは session-scoped (`--resume`) で同一セッションに再接続して cold-start を削減し、初回のみ preamble を送って以降は per-turn delta だけ送ることで session 肥大化を防ぎます。role collapse (persona drift で JSON 契約破棄) は構造的に予防せず、検知した瞬間に session を切り直して fresh state から再開する事後回復機構で長時間運用に耐えます。
31
+ Spotter audits in two stages:
10
32
 
11
- ## インストール
33
+ - **`stage=user_input`** — given the user's prompt, list any tools whose `when_to_use` clearly applies. A *prompt-fulfillment* check
34
+ - **`stage=turn_end`** — given Bell's final reply, look for places where a catalog tool (verification / recording / lookup) could plug in. A *missed-opportunity* audit. Zero findings is fine; tools already used in this turn are not re-flagged
35
+
36
+ ## Install
12
37
 
13
38
  ```bash
14
39
  npm install -g claude-spotter
@@ -16,74 +41,87 @@ cd your-project
16
41
  spotter install
17
42
  ```
18
43
 
19
- v0.3.0 以降は**プロジェクト単位の明示的 install** を採用しています (v0.2 までの `postinstall` 自動登録はデーモン増殖の主因だったため撤回)。各プロジェクトの `.claude/settings.json` hook を登録し、そのプロジェクトでの Claude Code セッションのみで有効になります。
44
+ Since `v0.3.0`, Spotter requires **explicit per-project install** (the earlier `postinstall` auto-registration was the leading cause of orphan daemons). `spotter install` writes hooks into the project's `.claude/settings.json`; the audit is then active only in Claude Code sessions for that project.
20
45
 
21
46
  ```bash
22
- spotter uninstall # このプロジェクトの hook 登録を解除
47
+ spotter uninstall # remove hooks from this project
23
48
  ```
24
49
 
25
- ## 動作要件
50
+ ## Requirements
26
51
 
27
- - Node.js **22.5 以上**
28
- - Claude Code **2.0 以上**
29
- - Claude **Max プラン** (`claude -p` で Haiku を起動するため)
52
+ - **Node.js 22.5+**
53
+ - **Claude Code 2.0+**
54
+ - **Claude Max plan** (Spotter spawns Haiku via `claude -p`)
30
55
 
31
- ## コンセプト
56
+ ## Architecture
32
57
 
33
58
  ```
34
- User 発話
59
+ User prompt
35
60
 
36
- UserPromptSubmit hook → Spotter がカタログと発話を見て一次判定
61
+ UserPromptSubmit hook → Spotter does a first pass against the catalog
37
62
 
38
- Bell Thinking (Spotter の推奨を additionalContext で受け取る)
63
+ Bell thinking (receives Spotter's recommendations as additionalContext)
39
64
 
40
- Bell 最終応答
65
+ Bell's final answer
41
66
 
42
- Stop hook → Spotter が応答と使用済みツールを見て最終チェック
67
+ Stop hook → Spotter re-audits the answer + tools actually used
43
68
 
44
- 見落としあれば差し戻し (max 1 回、Claude Code stop_hook_active で自動担保)
69
+ If something was missed: send-back (max 1 round, guaranteed by Claude Code's stop_hook_active)
45
70
  ```
46
71
 
47
- 監査対象のツール (name + description) `<project>/.spotter/tool-db.json` (ローカル) に格納されます。**daemon が監査に使うのはローカル DB のみ** (v1.2.0 以降) で、グローバル DB `~/.spotter/tool-db.json` は他プロジェクトでの description 再利用キャッシュとしてのみ機能します (live fetch コスト削減のため初回 refresh で参照、結果は local write-through)。各プロジェクトの local DB **そのプロジェクトの現時点の discovery 結果と一致** (refresh 時に prune される) ため、過去にインストールしていた MCP / スキル / サブエージェントが他プロジェクトに混入することはありません。
72
+ The audited tool catalog (name + description) lives in `<project>/.spotter/tool-db.json`. **Since v1.2.0 the daemon audits against the local DB only**; the global DB at `~/.spotter/tool-db.json` is a description-reuse cache shared across projects, not an audit source. Each project's local DB always matches the **current** discovery snapshot for that project (entries are pruned on refresh), so MCP servers, skills, or sub-agents installed in *other* projects can never bleed into this project's audit.
48
73
 
49
- **v1.1.0 以降、`spotter install` が初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook bg `spotter db refresh` を走らせる**ため、通常の運用で手動コマンドを叩く必要はありません。収集経路は (1) MCP サーバー: user/project scope `.mcp.json` + `claude mcp list` で列挙、各サーバーの `tools/list` JSON-RPC で取得、HTTP/SSE transport にも対応、(2) スキル: user/project/プラグインの SKILL.md frontmatter から `{name, description}` を抽出、(3) サブエージェント: user/project/プラグインの agent .md frontmatter から抽出、(4) claude.ai baseline: OAuth proxy 経由の Gmail/Calendar/Drive 25 件は手書き baseline で補完 (v1.1.4 以降、`claude mcp list` に該当サーバーが存在する環境でのみ注入)。**手書きでツールリストを管理する必要はありません**。
74
+ **Since v1.1.0, `spotter install` runs the initial seed automatically, and the SessionStart hook triggers a background `spotter db refresh` on every Claude Code session start** — so you don't need to invoke catalog commands by hand. The discovery sources are: (1) **MCP servers** — enumerated from `claude mcp list` (membership) and merged with all three official Claude Code scopes for env / headers: User (`~/.claude.json` direct `mcpServers`) / Project (`<projectRoot>/.mcp.json`) / Local (`~/.claude.json` `projects[<root>].mcpServers`), with precedence Local > Project > User (v1.2.1+); a legacy `~/.claude/.mcp.json` source is also read at the lowest priority for backward compatibility. Each server's `tools/list` is fetched via JSON-RPC (HTTP/SSE transport supported); (2) **skills** `{name, description}` extracted from SKILL.md frontmatter at user / project / plugin scope; (3) **sub-agents** — same pattern from agent `.md` frontmatter; (4) **claude.ai baseline** Gmail / Calendar / Drive (25 entries via OAuth proxy) injected from a hand-maintained baseline only when `claude mcp list` confirms the server is present (v1.1.4+). **You never have to maintain the tool list by hand.**
50
75
 
51
- ## Throughline との関係
76
+ ## Spotter and Throughline
52
77
 
53
- [Throughline](https://github.com/kitepon-rgb/Throughline) Spotter は同じ作者が作った、**哲学を共有する別プロダクト**です。
78
+ [Throughline](https://github.com/kitepon-rgb/Throughline) is a sibling project from the same author. Different mechanism, **shared philosophy**.
54
79
 
55
80
  | | Throughline | Spotter |
56
81
  |---|---|---|
57
- | 思想 | 引き算 (要らないものを退避) | 足し算 (足りない動作に気づかせる) |
58
- | 対象 | コンテキスト肥大化 | ツール取りこぼし |
59
- | 仕組み | hook で記憶退避 | hook でサブエージェント並走 |
82
+ | Direction | Subtraction evict what isn't needed | Addition surface what's missing |
83
+ | Target | Context bloat | Missed tool calls |
84
+ | Mechanism | Hook-driven memory eviction | Hook-driven sub-agent in parallel |
60
85
 
61
- 両者に共通するのは **「主体 (Bell) に頼らない仕組み」**。併用できます。
86
+ Both share the principle of **"don't rely on the primary agent (Bell) to do it itself."** They compose well — you can run them together.
62
87
 
63
- ## よく使うコマンド
88
+ ## Common commands
64
89
 
65
90
  ```bash
66
- spotter db list # 現在のローカル tool-db (daemon が実際に audit に使う) を表示
67
- spotter db refresh # MCP / スキル / サブエージェントから description を収集して DB 更新
68
- # (v1.1.0 以降、install 時と SessionStart 時に自動実行されるので通常は不要)
69
- spotter db rebuild # local + global DB を両方消してから refresh (カタログ設計変更時のクリーン用)
70
- spotter status # 稼働中の daemon 一覧
71
- spotter doctor # 環境診断 (Node / claude CLI / tool-db 整合性)
72
- spotter uninstall # hook 登録を解除 (~/.spotter は残す)
91
+ spotter db list # show the current local tool-db (what the daemon actually audits against)
92
+ spotter db refresh # rediscover MCP / skills / sub-agents and update the DB
93
+ # (run automatically on install and on SessionStart since v1.1.0,
94
+ # so this is rarely needed by hand)
95
+ spotter db rebuild # wipe both local + global DBs and refresh from scratch
96
+ # (use after catalog-shape changes)
97
+ spotter status # list running daemons
98
+ spotter doctor # environment check (Node / claude CLI / tool-db integrity)
99
+ spotter uninstall # remove hooks from this project (leaves ~/.spotter intact)
73
100
  ```
74
101
 
75
- ## 設計ドキュメント
102
+ ## Design docs
103
+
104
+ - **Current design** (catalog, discovery, classification axes): [docs/catalog-design.md](docs/catalog-design.md) — source of truth from v1.0.0
105
+ - **Open issues + unverified concerns**: [docs/open-issues.md](docs/open-issues.md) — read this before starting new work
106
+ - **Implementation invariants (§0)**: [CLAUDE.md](CLAUDE.md) — no fallbacks, no silent failures, no provisional code
107
+ - **Historical record (v0.1 design discussion)**: [docs/spotter-plan.md](docs/spotter-plan.md) — frozen design-discussion snapshot
108
+
109
+ ## Known limitations
110
+
111
+ - The `Stop` hook fires **after** Bell's first answer has already been streamed to the user. When Spotter sends Bell back, the user sees both the original answer and the corrected one. Detection accuracy in `UserPromptSubmit` (the *pre-response* stage) is therefore Spotter's primary axis of quality
112
+ - **Since v0.5.0, JSON schema violations from Haiku are treated as expected-anomalies** (silent pass + session renew, logged as `role_collapse_reset`) — this is the role-collapse recovery path. **Haiku timeouts still throw**, which surfaces as `UserPromptSubmit` blocking the user's prompt from reaching Bell. Timeouts have been raised twice (30s in v0.5.0, 45s in v0.13.1); making timeouts fail-open is deferred until §0 is revisited
113
+
114
+ <details>
115
+ <summary><strong>📋 v1.2.0 release notes (2026-04-26)</strong></summary>
116
+
117
+ **Structural fix for a regression that suggested tools the current project can't actually use.** The catalog the daemon audits against is now **local-only**. The global DB has been demoted to a cross-project description-reuse cache. A prune step was added to the end of `resolveAll`: any local entry not present in the current project's discovery snapshot is removed. This closes the path through which MCP servers / skills / sub-agents discovered in other projects used to linger in this project's audit set.
76
118
 
77
- - **現行設計 (カタログ / 収集経路 / 分類軸)**: [docs/catalog-design.md](docs/catalog-design.md) v1.0.0 以降の真実源
78
- - **現時点で塞がっていない穴 + 実測未検証の懸念**: [docs/open-issues.md](docs/open-issues.md) — 新規作業に入る前に必読
79
- - **実装規範と不変条件 (§0)**: [CLAUDE.md](CLAUDE.md) — フォールバック禁止 / silent fallback 禁止 / 暫定コード禁止
80
- - **歴史記録 (v0.1 時点の設計議事録)**: [docs/spotter-plan.md](docs/spotter-plan.md) — 作成時点で固定された議論過程のスナップショット、現行設計は上記 3 点を参照
119
+ For projects that already have Spotter installed, the next SessionStart after the npm global upgrade triggers an auto-refresh; the ghosts disappear from the session-after-next (immediate cleanup: run `spotter db refresh` by hand). The pillars introduced in v1.1.0 (auto-build of the tool DB at install, automatic drift tracking on SessionStart) are unchanged. The audit scope set in v1.0.0 (user-added MCP / skills / sub-agents) is unchanged.
81
120
 
82
- ## 既知の制約
121
+ Full notes: [CHANGELOG](CHANGELOG.md).
83
122
 
84
- - Stop hook は Bell の最初の応答が**出力された後**に発火するため、Spotter が Stop で差し戻した場合、ユーザーは「最初の応答 + 補正応答」の 2 連続を見ます (Claude Code の hook 仕様による制約)。UserPromptSubmit 段階での先回り検出を精度の軸にしています
85
- - **JSON スキーマ違反は v0.5.0 以降「想定済み異常」として silent pass + session renew で回復**します (role collapse 検知パス、daemon ログに `role_collapse_reset` を残す)。一方 **Haiku timeout は引き続き throw** され、UserPromptSubmit がブロックされてユーザー入力が Bell に届かない症状として顕在化します (timeout は v0.5.0 で 30s、v0.13.1 で 45s に拡張)。timeout の fail-open 化 (pass 扱い) は §0 改訂とセットで今後検討
123
+ </details>
86
124
 
87
- ## ライセンス
125
+ ## License
88
126
 
89
127
  MIT — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -170,13 +170,36 @@ function splitArgs(s) {
170
170
  return s.split(/\s+/).filter((t) => t.length > 0);
171
171
  }
172
172
 
173
- // On Windows, `.cmd` / `.bat` shims cannot be spawned directly without cmd.exe.
174
- // Unix-like paths or `.exe` binaries go through spawn as-is.
175
- function buildStdioSpawn(command, args) {
176
- if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)) {
177
- return { cmd: 'cmd.exe', cmdArgs: ['/c', command, ...args] };
173
+ // On Windows, npm-global CLI tools (e.g. `claude-mermaid`) ship as `<name>.cmd`
174
+ // batch wrappers. Node's `child_process.spawn` without `shell: true` calls Windows
175
+ // `CreateProcess`, which only directly executes `.exe` files — it does NOT search
176
+ // PATHEXT for `.cmd`/`.bat` shims when given a bare command name. So
177
+ // `spawn('claude-mermaid', ...)` fails with ENOENT even though `claude-mermaid.cmd`
178
+ // is on PATH.
179
+ //
180
+ // Until v1.2.1 this function only wrapped commands whose name literally ended in
181
+ // `.cmd`/`.bat`, which missed the common case where the registered command is a
182
+ // bare name (the CLI as installed). v1.2.2 routes any non-`.exe` command through
183
+ // `cmd.exe /c` on Windows, which makes PATHEXT lookup apply and runs both `.cmd`
184
+ // shims and bare names transparently.
185
+ //
186
+ // We keep absolute `.exe` paths un-wrapped because (a) they spawn correctly as-is
187
+ // and (b) wrapping them through `cmd.exe /c "<path with spaces>" args` runs into
188
+ // cmd.exe's quoting rules for paths containing spaces, which add risk for zero
189
+ // benefit.
190
+ //
191
+ // We use `cmd.exe /c` explicitly rather than `spawn({ shell: true })` because the
192
+ // latter triggers DEP0190 on Node 24+ and re-introduces argument-quoting risks
193
+ // (matches the rationale in haiku-caller's buildSpawnArgs and the caveat
194
+ // `windows-node-spawn-claude-fails-with-enoent-because-claude-is-a-cmd-wrapper`).
195
+ export function buildStdioSpawn(command, args) {
196
+ if (process.platform !== 'win32') {
197
+ return { cmd: command, cmdArgs: args };
198
+ }
199
+ if (/\.exe$/i.test(command)) {
200
+ return { cmd: command, cmdArgs: args };
178
201
  }
179
- return { cmd: command, cmdArgs: args };
202
+ return { cmd: 'cmd.exe', cmdArgs: ['/c', command, ...args] };
180
203
  }
181
204
 
182
205
  async function spawnAndQuery({ command, args, env = {} }, serverName) {
@@ -1,25 +1,45 @@
1
- // Read MCP server definitions directly from `.mcp.json` rather than parsing
2
- // `claude mcp list` text output.
1
+ // Read MCP server definitions from Claude Code's official scope storage.
3
2
  //
4
- // Why: `.mcp.json` is the authoritative source for stdio env vars (e.g. bearer tokens
5
- // passed to the MCP subprocess) and HTTP headers (e.g. Authorization). The CLI output
6
- // of `claude mcp list` / `claude mcp get` hides those secrets. Without them, an HTTP
7
- // MCP server returns 401 and a stdio MCP server spawns without its API key.
3
+ // Why: The CLI output of `claude mcp list` / `claude mcp get` hides secrets such as
4
+ // stdio env vars (bearer tokens passed to the MCP subprocess) and HTTP headers
5
+ // (Authorization). Without them, an HTTP MCP server returns 401 and a stdio MCP
6
+ // server spawns without its API key. Reading the underlying config files directly is
7
+ // the only way to recover full transport details.
8
8
  //
9
- // Scope: merges user-level `~/.claude/.mcp.json` with optional project-level
10
- // `<projectRoot>/.mcp.json`. Project scope overrides user scope on name collision
11
- // (matches Claude Code's own precedence more-specific scope wins).
12
- // `settings.local.json` (local scope) is not yet consulted.
9
+ // Scope (per Claude Code's official spec — https://code.claude.com/docs/en/mcp):
10
+ //
11
+ // - User — `~/.claude.json` direct `mcpServers`. Loaded in all projects.
12
+ // - Project — `<projectRoot>/.mcp.json`. Loaded in current project only.
13
+ // - Local — `~/.claude.json` `projects["<projectRoot>"].mcpServers`. Loaded in
14
+ // current project only.
15
+ //
16
+ // Precedence on name collision (more specific wins): Local > Project > User.
17
+ //
18
+ // Additionally, `~/.claude/.mcp.json` is read as a "legacy user-level" source — it is
19
+ // NOT part of the official scope model but some Spotter installations still rely on
20
+ // it, so we keep reading it at the lowest priority for backward compatibility.
21
+ //
22
+ // Until v1.2.0 we only read project + legacy. That left two of the three official
23
+ // sources unread, so a server registered with `claude mcp add -s user` (env stored in
24
+ // `~/.claude.json` direct `mcpServers`) was discovered by `claude mcp list` but
25
+ // spawned without its env, returning empty `tools/list` and getting pruned from the
26
+ // catalog. This module now covers all four sources.
13
27
  //
14
28
  // This file does NOT read ~/.claude/.credentials.json (Anthropic OAuth token). That
15
- // remains off-limits per the v0.8.0 design decision. `.mcp.json` is user-authored
16
- // configuration where the user has already chosen to persist their own MCP credentials.
29
+ // remains off-limits per the v0.8.0 design decision. The `.mcp.json` files and
30
+ // `~/.claude.json`'s `mcpServers` are user-authored / Claude-Code-managed
31
+ // configuration where the user has already chosen to persist their own MCP
32
+ // credentials.
17
33
 
18
34
  import { readFile } from 'node:fs/promises';
19
35
  import { homedir } from 'node:os';
20
36
  import { join } from 'node:path';
21
37
 
22
- export function userMcpConfigPath() {
38
+ export function userClaudeJsonPath() {
39
+ return join(homedir(), '.claude.json');
40
+ }
41
+
42
+ export function legacyUserMcpConfigPath() {
23
43
  return join(homedir(), '.claude', '.mcp.json');
24
44
  }
25
45
 
@@ -27,25 +47,122 @@ export function projectMcpConfigPath(projectRoot) {
27
47
  return join(projectRoot, '.mcp.json');
28
48
  }
29
49
 
30
- async function readOne(path) {
50
+ // Back-compat alias. Older callers / external integrators may still import this.
51
+ export function userMcpConfigPath() {
52
+ return legacyUserMcpConfigPath();
53
+ }
54
+
55
+ // Read `~/.claude.json`. Missing file → null. Malformed JSON → null. Other I/O errors
56
+ // re-throw. Lenient parsing because this file is owned by Claude Code (not Spotter
57
+ // nor the user directly): a transient corruption mid-write must not crash Spotter.
58
+ async function readClaudeJsonFile(path) {
59
+ let text;
60
+ try {
61
+ text = await readFile(path, 'utf8');
62
+ } catch (err) {
63
+ if (err.code === 'ENOENT') return null;
64
+ throw err;
65
+ }
66
+ try {
67
+ return JSON.parse(text);
68
+ } catch (err) {
69
+ if (err instanceof SyntaxError) return null;
70
+ throw err;
71
+ }
72
+ }
73
+
74
+ // Read an `.mcp.json`-shaped file (user-authored). Missing → empty map. Malformed
75
+ // JSON or other I/O errors re-throw — these files are written by hand or by the
76
+ // `claude mcp add` CLI; surfacing a parse error helps the user notice the corruption.
77
+ async function readMcpServersFile(path) {
78
+ let text;
31
79
  try {
32
- const text = await readFile(path, 'utf8');
33
- const data = JSON.parse(text);
34
- return data.mcpServers ?? {};
80
+ text = await readFile(path, 'utf8');
35
81
  } catch (err) {
36
82
  if (err.code === 'ENOENT') return {};
37
83
  throw err;
38
84
  }
85
+ const data = JSON.parse(text);
86
+ if (!data || typeof data !== 'object') return {};
87
+ return (data.mcpServers && typeof data.mcpServers === 'object') ? data.mcpServers : {};
88
+ }
89
+
90
+ // Normalize a project path for matching against `~/.claude.json` `projects[]` keys.
91
+ // Claude stores absolute paths verbatim, but representation can drift across:
92
+ // - separator: `\` on Windows vs `/`
93
+ // - drive-letter case: `C:\` vs `c:\` on Windows
94
+ // - trailing slash: `/foo/bar` vs `/foo/bar/`
95
+ // We canonicalize to forward slashes, strip trailing slashes, and lower-case on
96
+ // Windows (case-insensitive filesystem). POSIX stays case-sensitive.
97
+ export function normalizeProjectPath(p) {
98
+ if (typeof p !== 'string' || p.length === 0) return '';
99
+ let s = p.replace(/\\/g, '/').replace(/\/+$/, '');
100
+ if (process.platform === 'win32') s = s.toLowerCase();
101
+ return s;
102
+ }
103
+
104
+ // Extract user-scope `mcpServers` from a parsed `~/.claude.json` object.
105
+ export function extractUserScopeServers(claudeJson) {
106
+ if (!claudeJson || typeof claudeJson !== 'object') return {};
107
+ const servers = claudeJson.mcpServers;
108
+ return (servers && typeof servers === 'object') ? servers : {};
109
+ }
110
+
111
+ // Extract local-scope `mcpServers` for a given projectRoot from a parsed
112
+ // `~/.claude.json` object. Tries the exact key first, then a normalized match
113
+ // (separator / case / trailing-slash insensitive on Windows). If no key matches,
114
+ // returns empty — we deliberately do NOT fuzzy-match (e.g. partial-prefix), since
115
+ // that could pull in another project's secrets.
116
+ export function findLocalScopeServers(claudeJson, projectRoot) {
117
+ if (!claudeJson || typeof claudeJson !== 'object') return {};
118
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0) return {};
119
+ const projects = claudeJson.projects;
120
+ if (!projects || typeof projects !== 'object') return {};
121
+
122
+ const direct = projects[projectRoot];
123
+ if (direct && typeof direct === 'object'
124
+ && direct.mcpServers && typeof direct.mcpServers === 'object') {
125
+ return direct.mcpServers;
126
+ }
127
+
128
+ const target = normalizeProjectPath(projectRoot);
129
+ if (target.length === 0) return {};
130
+ for (const key of Object.keys(projects)) {
131
+ if (normalizeProjectPath(key) !== target) continue;
132
+ const entry = projects[key];
133
+ if (entry && typeof entry === 'object'
134
+ && entry.mcpServers && typeof entry.mcpServers === 'object') {
135
+ return entry.mcpServers;
136
+ }
137
+ }
138
+ return {};
39
139
  }
40
140
 
41
- // Returns the merged `mcpServers` object: user scope as base, project scope overrides
42
- // on name collision. Missing files are treated as empty (not an error). If projectRoot
43
- // is not supplied, only user scope is read.
44
- export async function readMcpServers({ projectRoot } = {}) {
45
- const user = await readOne(userMcpConfigPath());
46
- if (!projectRoot) return user;
47
- const project = await readOne(projectMcpConfigPath(projectRoot));
48
- return { ...user, ...project };
141
+ // Returns the merged `mcpServers` map across all known scopes for the current
142
+ // project. Sources, lowest highest priority on name collision:
143
+ // 1. legacy `~/.claude/.mcp.json` (back-compat)
144
+ // 2. user scope `~/.claude.json` `mcpServers` (all projects)
145
+ // 3. project scope `<projectRoot>/.mcp.json` (this project)
146
+ // 4. local scope `~/.claude.json` `projects[<root>].mcpServers` (this project)
147
+ //
148
+ // Missing files / missing keys are treated as empty (no error). If `projectRoot` is
149
+ // not supplied, scopes 3 and 4 are skipped.
150
+ //
151
+ // IMPORTANT: this function does NOT decide which servers are "loaded in this
152
+ // project" — that is `claude mcp list`'s job. Callers in investigate-mcp.mjs
153
+ // intersect this map with the CLI list (CLI authoritative for membership; this map
154
+ // authoritative for transport details + secrets).
155
+ export async function readMcpServers({
156
+ projectRoot,
157
+ claudeJsonPath = userClaudeJsonPath(),
158
+ legacyUserPath = legacyUserMcpConfigPath(),
159
+ } = {}) {
160
+ const legacy = await readMcpServersFile(legacyUserPath);
161
+ const claudeJson = await readClaudeJsonFile(claudeJsonPath);
162
+ const user = extractUserScopeServers(claudeJson);
163
+ const project = projectRoot ? await readMcpServersFile(projectMcpConfigPath(projectRoot)) : {};
164
+ const local = projectRoot ? findLocalScopeServers(claudeJson, projectRoot) : {};
165
+ return { ...legacy, ...user, ...project, ...local };
49
166
  }
50
167
 
51
168
  // Normalise an `.mcp.json` entry into a server descriptor the investigator can use.