claude-spotter 1.1.6 → 1.2.0
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 +36 -0
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/cli/db-cmd.mjs +3 -3
- package/src/daemon/daemon.mjs +7 -5
- package/src/index.mjs +1 -1
- package/src/tool-db/lookup.mjs +18 -0
- package/src/tool-db/refresh.mjs +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
**当該プロジェクトで使えないツールが提案される回帰を構造的に修正**。daemon が監査に使うカタログを **ローカル DB のみ** に変更し、グローバル DB は他プロジェクトでの description 再利用のためのキャッシュ層に役割を限定。同時に `resolveAll` で「現プロジェクトの discovery 結果に含まれないローカルエントリ」を prune するよう変更し、過去にインストールされていた MCP / スキル / サブエージェントが local に居座って Haiku 視野に残る経路を塞いだ。
|
|
6
|
+
|
|
7
|
+
### 変更点
|
|
8
|
+
|
|
9
|
+
- **編集 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**: `readMerged` を `readLocal` にリネーム + 実装を local DB 限定に変更。global DB を一切混ぜない。コメントで「他プロジェクトの幻ツール混入」が起きていた経路を明示
|
|
10
|
+
- **編集 [src/tool-db/lookup.mjs](src/tool-db/lookup.mjs)**: `resolveAll` 末尾に prune ループ追加。`toolNames` (現プロジェクトの discovery 結果) に含まれない既存ローカルエントリを削除。investigate が null を返した (transient failure) ツールは toolNames に含まれている限り既存値を保持して prune しない (auth / network / quota の一過性失敗で audit 範囲を縮めない防御)
|
|
11
|
+
- **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: tool-db 読み込みを `readLocal` に切り替え、コメントで設計意図を明記
|
|
12
|
+
- **編集 [src/cli/db-cmd.mjs](src/cli/db-cmd.mjs)**: `spotter db list` も local DB 限定に変更 (daemon と表示の整合)
|
|
13
|
+
- **編集 [src/index.mjs](src/index.mjs)**: 公開 export を `readMerged` → `readLocal` にリネーム (programmatic API の破壊変更 = minor bump)
|
|
14
|
+
- **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: 回帰テスト 3 件追加 — (1) prune 挙動 (snapshot に無いツールは local から消える、global は append-only で残る)、(2) investigate 失敗時の保持 (toolNames に含まれている限り既存値を残す)、(3) `readLocal` 単独動作 (global DB の中身が leak しない)
|
|
15
|
+
|
|
16
|
+
### 背景
|
|
17
|
+
|
|
18
|
+
#### 何が起きていたか
|
|
19
|
+
|
|
20
|
+
`readMerged` は `{ ...global.tools, ...local.tools }` で local-wins マージしたツール一覧を daemon の preamble (Haiku のカタログ) に渡していた。local に**ない**が global に**ある**ツール (= 過去の別プロジェクトで discover したが現プロジェクトでは未インストール) が Haiku 視野に入り、Haiku が「Gmail の検索を提案」「別プロジェクトの skill を推奨」等を出す症状を生んでいた。
|
|
21
|
+
|
|
22
|
+
加えて `resolveAll` は snapshot ベースで write-through するだけで、過去 discover されて local に残った後に削除された MCP / skill / agent を local DB から取り除く機構が無かった。両者が組み合わさって「一度 local に書き込まれた幽霊が居座り続ける」状態が定着していた。
|
|
23
|
+
|
|
24
|
+
#### なぜ global DB は残すか
|
|
25
|
+
|
|
26
|
+
global DB を廃止せず「他プロジェクト用キャッシュ」に格下げした理由: 同じ MCP サーバー / 同じスキル / 同じサブエージェントを別プロジェクトでも使うとき、global にヒットすれば live fetch (MCP の `tools/list` JSON-RPC、frontmatter パース) を skip できる。`resolveAll` の 3-tier (local → global → investigate) フローはそのために残し、daemon の audit 入力からだけ global を切り離した。
|
|
27
|
+
|
|
28
|
+
#### 自動追従の経路
|
|
29
|
+
|
|
30
|
+
既に Spotter を導入済みのプロジェクトは npm の global update 後、次の Claude Code SessionStart で v1.1.0 機構の `spawnRefreshDetached` が走り、その refresh が prune ループ入りの `resolveAll` を実行する。**次の次のセッション**から幽霊が消えた状態で daemon が起動する (detached の仕様)。即時反映したい場合は `spotter db refresh` を手動実行する逃げ道は既存。
|
|
31
|
+
|
|
32
|
+
### 設計判断
|
|
33
|
+
|
|
34
|
+
- **global を完全廃止しなかった**: live fetch コストが軽くないため (MCP stdio spawn 数百ミリ秒〜秒、HTTP MCP も auth 込みでそれ以上)、初回 refresh の体感を損ねない
|
|
35
|
+
- **prune を `resolveAll` 末尾に置いた理由**: snapshot 構築 (buildInvestigationSnapshot) と prune を別関数に分けると「snapshot に基づいて local を upsert する」契約が複数箇所に散る。Single source of truth として `resolveAll` 内で完結させた
|
|
36
|
+
- **investigate failure 時の prune skip**: live fetch が失敗したからといって audit 範囲を縮めると、auth が一時的に通らない / MCP サーバーが一時的に応答しない場合に Haiku 視野が穴だらけになる。toolNames (= 現プロジェクトに**実在する**サーバー / skill / agent の name 一覧) に含まれているなら local の既存 description を保持し、次回成功時に上書きする方が頑健
|
|
37
|
+
- **public API の minor bump**: `readMerged` → `readLocal` は import 名の変更を含むので破壊変更扱い。programmatic に Spotter を埋め込んでいる第三者が居る前提 (内部利用想定でも安全側に倒す)
|
|
38
|
+
|
|
3
39
|
## 1.1.6
|
|
4
40
|
|
|
5
41
|
**Bell の isolated `CLAUDE_CONFIG_DIR` が Spotter haiku の auth を破壊する bug を修正**。bellbot 等で CLAUDE_CONFIG_DIR を分離する運用 (user scope MCP を流入させない隔離設計、credentials 非共有) のとき、hook → daemon → haiku の spawn 連鎖で Bell の isolated config が継承され、Spotter haiku が credentials 不在の config を読みに行き exit 1。その後同じ session-id が claude CLI 側で "already in use" と判定されて失敗が固定化し、user_input hook が非 0 exit し続けてベル本体のプロンプト処理が破綻していた。
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Spotter
|
|
2
2
|
|
|
3
|
-
> **v1.
|
|
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)。
|
|
4
4
|
|
|
5
5
|
**気づく役と実行する役を分離する。** Spotter は Claude Code の横で静かに並走し、Bell (主役の Claude) が**ツールを呼び忘れたとき**に指摘する監査役です。
|
|
6
6
|
|
|
@@ -44,7 +44,9 @@ Stop hook → Spotter が応答と使用済みツールを見て最終チェッ
|
|
|
44
44
|
見落としあれば差し戻し (max 1 回、Claude Code の stop_hook_active で自動担保)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
監査対象のツール (name + description) は
|
|
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 / スキル / サブエージェントが他プロジェクトに混入することはありません。
|
|
48
|
+
|
|
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` に該当サーバーが存在する環境でのみ注入)。**手書きでツールリストを管理する必要はありません**。
|
|
48
50
|
|
|
49
51
|
## Throughline との関係
|
|
50
52
|
|
|
@@ -61,7 +63,7 @@ Stop hook → Spotter が応答と使用済みツールを見て最終チェッ
|
|
|
61
63
|
## よく使うコマンド
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
|
-
spotter db list #
|
|
66
|
+
spotter db list # 現在のローカル tool-db (daemon が実際に audit に使う) を表示
|
|
65
67
|
spotter db refresh # MCP / スキル / サブエージェントから description を収集して DB 更新
|
|
66
68
|
# (v1.1.0 以降、install 時と SessionStart 時に自動実行されるので通常は不要)
|
|
67
69
|
spotter db rebuild # local + global DB を両方消してから refresh (カタログ設計変更時のクリーン用)
|
package/package.json
CHANGED
package/src/cli/db-cmd.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// `spotter db` — manage the tool-db.
|
|
2
2
|
//
|
|
3
|
-
// spotter db list — print
|
|
3
|
+
// spotter db list — print the LOCAL tool-db (what the daemon actually audits)
|
|
4
4
|
// spotter db refresh — discover available tools and update DB (3-tier resolve)
|
|
5
5
|
// spotter db rebuild — wipe local DB then refresh (forces re-investigation)
|
|
6
6
|
//
|
|
7
7
|
// Run inside a project that has been `spotter install`-ed.
|
|
8
8
|
|
|
9
9
|
import { findSpotterMarker } from '../hooks/lib.mjs';
|
|
10
|
-
import { refresh,
|
|
10
|
+
import { refresh, readLocal } from '../tool-db/refresh.mjs';
|
|
11
11
|
import { localDbPath, globalDbPath, loadDb, saveDb, emptyDb } from '../tool-db/loader.mjs';
|
|
12
12
|
import { writeFile } from 'node:fs/promises';
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ function requireProjectRoot() {
|
|
|
22
22
|
|
|
23
23
|
export async function runDbList() {
|
|
24
24
|
const projectRoot = requireProjectRoot();
|
|
25
|
-
const tools = await
|
|
25
|
+
const tools = await readLocal({ projectRoot });
|
|
26
26
|
if (tools.length === 0) {
|
|
27
27
|
process.stdout.write('(empty — run `spotter db refresh` to populate)\n');
|
|
28
28
|
return;
|
package/src/daemon/daemon.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
createHaikuCaller,
|
|
43
43
|
HaikuError,
|
|
44
44
|
} from './haiku-caller.mjs';
|
|
45
|
-
import {
|
|
45
|
+
import { readLocal } from '../tool-db/refresh.mjs';
|
|
46
46
|
import { homedir } from 'node:os';
|
|
47
47
|
import { join } from 'node:path';
|
|
48
48
|
import { writeFile, unlink } from 'node:fs/promises';
|
|
@@ -90,9 +90,11 @@ export async function startDaemon({
|
|
|
90
90
|
// a sibling daemon is already serving this session_id — throw so the caller can exit.
|
|
91
91
|
await assertNoLiveDaemon(sessionId);
|
|
92
92
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
93
|
+
// v1.2.0: tool list comes from the LOCAL tool-db only. The audit must reflect what
|
|
94
|
+
// this specific project can use — mixing in the global DB caused phantom suggestions
|
|
95
|
+
// from previously-visited projects (Gmail tools popping up in projects with no Gmail
|
|
96
|
+
// MCP, etc.). For tests, the caller can pass `tools` directly. For production,
|
|
97
|
+
// projectRoot drives the load from <projectRoot>/.spotter/tool-db.json.
|
|
96
98
|
let toolList;
|
|
97
99
|
if (Array.isArray(tools)) {
|
|
98
100
|
toolList = tools;
|
|
@@ -100,7 +102,7 @@ export async function startDaemon({
|
|
|
100
102
|
if (!projectRoot) {
|
|
101
103
|
throw new TypeError('startDaemon: either `tools` or `projectRoot` must be provided');
|
|
102
104
|
}
|
|
103
|
-
toolList = await
|
|
105
|
+
toolList = await readLocal({ projectRoot });
|
|
104
106
|
}
|
|
105
107
|
logFn(`tool-db loaded: ${toolList.length} tools` + (projectRoot ? ` (project=${projectRoot})` : ''));
|
|
106
108
|
|
package/src/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export {
|
|
|
15
15
|
} from './daemon/haiku-caller.mjs';
|
|
16
16
|
export { loadDb, saveDb, emptyDb, ToolDbSchemaError, globalDbPath, localDbPath } from './tool-db/loader.mjs';
|
|
17
17
|
export { resolveAll } from './tool-db/lookup.mjs';
|
|
18
|
-
export { refresh,
|
|
18
|
+
export { refresh, readLocal, buildInvestigationSnapshot } from './tool-db/refresh.mjs';
|
|
19
19
|
export { listMcpServers, listMcpToolsAll, bellVisibleName, McpInvestigationError } from './tool-db/investigate-mcp.mjs';
|
|
20
20
|
export { listSkillsAll, listActivePlugins } from './tool-db/investigate-skills.mjs';
|
|
21
21
|
export { listAgentsAll } from './tool-db/investigate-agents.mjs';
|
package/src/tool-db/lookup.mjs
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
// 3. both miss → investigate, write to BOTH
|
|
7
7
|
// * local≠global hit → re-investigate (MCP server is source of truth), overwrite BOTH
|
|
8
8
|
//
|
|
9
|
+
// Local DB pruning: after resolution, any tool currently in the local DB but NOT in
|
|
10
|
+
// `toolNames` (i.e. no longer discoverable in this project — server uninstalled, skill
|
|
11
|
+
// deleted, agent moved away) is removed. The local DB MUST mirror "what this project
|
|
12
|
+
// can use right now" since it is the sole input to the audit. Global DB is the
|
|
13
|
+
// long-term knowledge store and is never pruned.
|
|
14
|
+
//
|
|
9
15
|
// `investigate(name)` is supplied by the caller. Returns description string, or null
|
|
10
16
|
// if the tool cannot be resolved (keep going, don't throw).
|
|
11
17
|
|
|
@@ -72,6 +78,18 @@ export async function resolveAll({ toolNames, localPath, globalPath, investigate
|
|
|
72
78
|
resolved.set(name, { description: fresh, source: 'investigated' });
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
// Prune local entries no longer present in this project's discovery.
|
|
82
|
+
// Skip names that were requested but failed to resolve (investigation may be a
|
|
83
|
+
// transient failure — keep the existing description rather than blanking the audit).
|
|
84
|
+
const requested = new Set(toolNames);
|
|
85
|
+
for (const name of Object.keys(local.tools)) {
|
|
86
|
+
if (!requested.has(name)) {
|
|
87
|
+
delete local.tools[name];
|
|
88
|
+
localDirty = true;
|
|
89
|
+
logFn(`pruned local entry "${name}" (no longer in this project's discovery)`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
if (localDirty) await saveDb(localPath, local);
|
|
76
94
|
if (globalDirty) await saveDb(globalPath, global);
|
|
77
95
|
|
package/src/tool-db/refresh.mjs
CHANGED
|
@@ -97,13 +97,13 @@ export async function refresh({ projectRoot, logFn = () => {}, claudeBin = 'clau
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// Read-only: load the
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
100
|
+
// Read-only: load the LOCAL tool-db only — the daemon's audit must reflect what this
|
|
101
|
+
// specific project can actually use. The global DB is a knowledge store written by
|
|
102
|
+
// `refresh` (so other projects can pick up descriptions cheaply) but is NEVER mixed
|
|
103
|
+
// into the daemon's audit catalog. Mixing global in caused phantom-tool suggestions
|
|
104
|
+
// from previously-visited projects bleeding into unrelated ones.
|
|
105
|
+
export async function readLocal({ projectRoot }) {
|
|
104
106
|
const { loadDb } = await import('./loader.mjs');
|
|
105
107
|
const local = await loadDb(localDbPath(projectRoot));
|
|
106
|
-
|
|
107
|
-
const merged = { ...global.tools, ...local.tools }; // local overrides
|
|
108
|
-
return Object.entries(merged).map(([name, description]) => ({ name, description }));
|
|
108
|
+
return Object.entries(local.tools).map(([name, description]) => ({ name, description }));
|
|
109
109
|
}
|