claude-spotter 1.2.0 → 1.2.1
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 +21 -0
- package/README.ja.md +123 -0
- package/README.md +81 -43
- package/package.json +1 -1
- package/src/tool-db/mcp-config.mjs +142 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.1
|
|
4
|
+
|
|
5
|
+
**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 な脱落が発生していた。
|
|
6
|
+
|
|
7
|
+
### 変更点
|
|
8
|
+
|
|
9
|
+
- **編集 [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 ソースの位置付けを明記
|
|
10
|
+
- **編集 [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 入力)
|
|
11
|
+
|
|
12
|
+
### 設計判断
|
|
13
|
+
|
|
14
|
+
- **`projects[]` キーの照合に正規化を入れた理由**: Claude Code が `~/.claude.json` に書く絶対パスの表記は環境と書き込みタイミングで揺れる (Windows ではドライブレターの大小、separator (`\` vs `/`)、末尾スラッシュ)。正確一致が外れると Local スコープが silent に脱落する = 今回直そうとしている bug の Windows 版が再発する。`normalizeProjectPath` で separator 統一 + 末尾 `/` 除去 + Windows 限定の lower-case を施し、exact 一致が無いときだけ正規化フォールバックする 2 段照合に
|
|
15
|
+
- **fuzzy / prefix マッチを意図的に外した理由**: `/home/u/proj` のキーから `/home/u/other-project` の照合に `mcpServers` を引き渡すと、別プロジェクトの secrets を spotter の audit に混ぜることになる。projectRoot は識別子であって階層ではないので、normalize 後の完全一致のみ採用 (タスク指示の「投機的なファジーマッチはしない」と整合)
|
|
16
|
+
- **`~/.claude.json` の malformed を throw せず空扱い**: このファイルは Claude Code 本体が管理する状態ファイルで、書き込み中に他プロセスが読みに行けば transient corruption に見える可能性がある。Spotter が落ちる方が実害が大きいので寛容に扱う (`.mcp.json` 系 = ユーザー手書きはこれまで通り throw、bug 表面化を優先)
|
|
17
|
+
- **fix 扱いの patch bump**: public API (`readMcpServers` のシグネチャ) は引数が optional 追加のみで後方互換、merge 結果が「漏れてた scope を拾う」方向に増えるだけで既存挙動の縮退なし。programmatic API の破壊変更ではないので minor bump 不要
|
|
18
|
+
- **legacy `~/.claude/.mcp.json` を残した理由**: 仕様外であることは認めるが、現に Spotter で投入されているサンプルや既存ユーザー環境の依存を切ると静かにツールが消える。最下位優先で残しつつ、コメントで「legacy / 公式仕様外 / 互換維持」を明記。撤去は別 PR
|
|
19
|
+
|
|
20
|
+
### 自動追従の経路
|
|
21
|
+
|
|
22
|
+
既に 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` を手動実行。
|
|
23
|
+
|
|
3
24
|
## 1.2.0
|
|
4
25
|
|
|
5
26
|
**当該プロジェクトで使えないツールが提案される回帰を構造的に修正**。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
|
+
[](https://www.npmjs.com/package/claude-spotter)
|
|
8
|
+
[](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](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
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/claude-spotter)
|
|
8
|
+
[](https://github.com/kitepon-rgb/Spotter/actions/workflows/ci.yml)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 #
|
|
47
|
+
spotter uninstall # remove hooks from this project
|
|
23
48
|
```
|
|
24
49
|
|
|
25
|
-
##
|
|
50
|
+
## Requirements
|
|
26
51
|
|
|
27
|
-
- Node.js
|
|
28
|
-
- Claude Code
|
|
29
|
-
- Claude
|
|
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
|
|
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
|
-
|
|
69
|
+
If something was missed: send-back (max 1 round, guaranteed by Claude Code's stop_hook_active)
|
|
45
70
|
```
|
|
46
71
|
|
|
47
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
|
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
|
-
|
|
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 #
|
|
67
|
-
spotter db refresh # MCP /
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
spotter
|
|
71
|
-
|
|
72
|
-
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
|
-
-
|
|
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
|
-
|
|
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,25 +1,45 @@
|
|
|
1
|
-
// Read MCP server definitions
|
|
2
|
-
// `claude mcp list` text output.
|
|
1
|
+
// Read MCP server definitions from Claude Code's official scope storage.
|
|
3
2
|
//
|
|
4
|
-
// Why:
|
|
5
|
-
// passed to the MCP subprocess) and HTTP headers
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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`
|
|
16
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|