claude-spotter 0.6.2 → 0.7.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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ **カタログを tool-db に置き換え**。手書きの `tools.yaml` (5 つの抽象ツール) を捨て、**実際にセッションで使えるツール (MCP + Claude Code 組込み 遅延ツール) の name + description を自動収集してキャッシュする** 仕組みに置き換え。
6
+
7
+ ### 事の発端
8
+
9
+ Haiku が「Bell が呼び忘れているツール」を判定するには、Bell が今のセッションで実際に呼べるツールを知っている必要がある。v0.6.x までのカタログは `current_time` / `web_search` / `read_file` のような **抽象的な汎用ツール 5 件** を手書きしていただけで、Caveat や Gmail のような MCP ツール、TodoWrite や WebSearch のような Claude Code 組込みの遅延ツールは Haiku の視野に入っていなかった。結果、ユーザーが「過去に解決したナレッジを残したい」と言っても Spotter は Caveat を推奨できないという的外れな状態だった。
10
+
11
+ 設計思想は [docs/catalog-design-deferred-mcp.md](docs/catalog-design-deferred-mcp.md) に集約。要点:
12
+
13
+ - **Haiku に渡すのは name + description のペアだけ**。schema は不要 — どう呼ぶかは Bell が ToolSearch で解決する責任 (役割分業)
14
+ - **MCP ツールの description は MCP サーバーから直接取得**。Spotter は中継者に徹し、手書きで言い換えない (single source of truth = MCP server)
15
+ - **3 段階キャッシュ DB**: ローカル (プロジェクト) → グローバル (`~`) → 「調べる」(調査結果は両方に書き込む)
16
+ - **drift 補正**: ローカル ≠ グローバルなら再調査して MCP server の現在値で両方上書き
17
+ - **明示的な無効化機構なし**: drift 補正が間接無効化として機能、TTL なし
18
+
19
+ ### 変更点
20
+
21
+ - **新規 [src/tool-db/loader.mjs](src/tool-db/loader.mjs)**: JSON DB の atomic 読み書き、`{version, tools: {name → description}}` スキーマ検証
22
+ - **新規 [src/tool-db/lookup.mjs](src/tool-db/lookup.mjs)**: 3 段階 lookup + write-through + drift 補正
23
+ - **新規 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `claude mcp list` / `claude mcp get` で MCP サーバー列挙、stdio サーバーに JSON-RPC で `initialize` + `tools/list` を実行して description 取得
24
+ - **新規 [src/tool-db/deferred-baseline.mjs](src/tool-db/deferred-baseline.mjs)**: Claude Code 組込み 遅延ツール (WebSearch / TodoWrite / 等 17 件) の手書き description ベースライン (Claude Code 自体は MCP 経由で query できないため)
25
+ - **新規 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**: 投資 = 利用可能ツール一覧取得 + 各ツールを 3 段階解決 + DB 書き戻し
26
+ - **新規 [src/cli/db-cmd.mjs](src/cli/db-cmd.mjs)**: `spotter db list` / `refresh` / `rebuild`
27
+ - **編集 [src/daemon/daemon.mjs](src/daemon/daemon.mjs)**: `loadCatalog` 廃止、`startDaemon({ projectRoot })` で tool-db を読み込み (テスト用に `tools` 直接指定も可)
28
+ - **編集 [src/daemon/haiku-caller.mjs](src/daemon/haiku-caller.mjs)**: `buildPreamble({ catalog })` → `buildPreamble({ tools })`、tools は `[{name, description}]`
29
+ - **編集 [src/cli/daemon-cmd.mjs](src/cli/daemon-cmd.mjs)**, **[src/hooks/session-start.mjs](src/hooks/session-start.mjs)**: `--project-root` を hook → daemon に伝達
30
+ - **編集 [src/cli/install.mjs](src/cli/install.mjs)**: `tool-catalog/` 作成と template コピー削除、install 完了時に `spotter db refresh` 実行を案内
31
+ - **編集 [src/cli/doctor.mjs](src/cli/doctor.mjs)**: catalog チェック → tool-db (global + local) のチェック
32
+ - **編集 [bin/spotter.mjs](bin/spotter.mjs)**: `spotter catalog edit/lint` を `spotter db list/refresh/rebuild` に置換
33
+ - **削除**: [src/catalog/](src/catalog/), [src/cli/catalog.mjs](src/cli/catalog.mjs), `templates/tools.yaml`, `test/catalog.test.mjs`, `test/loader.test.mjs`
34
+ - **新規 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: 21 件 (loader/lookup/investigate-mcp/deferred-baseline)
35
+
36
+ ### Breaking
37
+
38
+ - `~/.spotter/tool-catalog/tools.yaml` は読まれなくなる。install 後 `spotter db refresh` を実行して `~/.spotter/tool-db.json` (グローバル) と `<project>/.spotter/tool-db.json` (ローカル) を populate する必要がある
39
+ - `spotter catalog edit/lint` コマンド廃止 → `spotter db list/refresh/rebuild`
40
+ - `startDaemon` シグネチャ変更: `catalogPath` 廃止、`tools` または `projectRoot` のいずれか必須
41
+ - `buildPreamble({ catalog })` → `buildPreamble({ tools })`
42
+ - `src/index.mjs` から `loadCatalog`, `validateCatalog`, `runLint` 等を削除、tool-db API を export
43
+ - `claude mcp list` のエラー / SSE/HTTP transport 未対応のため、これらサーバーの description は取れない (今後の課題)
44
+
45
+ ### 既知の制約
46
+
47
+ - HTTP/SSE transport の MCP サーバーは investigate でスキップ (将来 HTTP MCP クライアント実装で対応)
48
+ - Claude Code の遅延ツール一覧は hardcoded baseline のみ。Claude Code が新しい built-in を追加したら baseline 更新が必要
49
+ - `claude mcp list` の出力フォーマット変更には脆い (parse 依存)。JSON 出力モードが将来追加されたらそちらに切り替えたい
50
+
3
51
  ## 0.6.2
4
52
 
5
53
  **親プロセス watch による孤児 daemon 自動回収**。SessionEnd が発火しない経路 (Claude Code crash, kill -9, IDE reload) で daemon が永久に残る問題への対処。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Spotter
2
2
 
3
- > **v0.6.2 released 2026-04-19**. v0.4 で stateless に回帰したが cold-start レイテンシ (毎ターン 30 秒前後の待ち) が運用で問題化したため、v0.5.0 **session-scoped Haiku を「JSON パース失敗検知 → session renew + silent pass」の事後回復機構付き**で復活。v0.6.0**preamble (role + schema + catalog + few-shot) を初回のみ送信** する形 (preamble-once) に変更して `--resume` 経由の resumed 呼び出しが first より遅くなる v0.5.x の逆転現象を解消。v0.6.2 **親プロセス (Claude Code) の死を 5 秒間隔で検知して daemon を自動 shutdown** する watch を追加し、SessionEnd が発火しない経路 (crash / kill / IDE reload) での孤児 daemon 残存を解消。詳細は [CHANGELOG](CHANGELOG.md)。
3
+ > **v0.7.0 released 2026-04-19**. **カタログを tool-db に置き換え**: 手書きの抽象ツール 5 件カタログを廃止し、`claude mcp list` + JSON-RPC `tools/list`MCP ツールの description を自動取得、Claude Code 組込み 遅延ツール (WebSearch/TodoWrite ) hardcoded baseline からカバー、3 段階キャッシュ DB (ローカル グローバル 調査して両方に追記) 2 回目以降は通信ゼロ。これで Caveat Gmail のような MCP ツールが初めて Haiku の視野に入る。設計思想は [docs/catalog-design-deferred-mcp.md](docs/catalog-design-deferred-mcp.md)、変更詳細は [CHANGELOG](CHANGELOG.md)。
4
4
 
5
5
  **気づく役と実行する役を分離する。** Spotter は Claude Code の横で静かに並走し、Bell (主役の Claude) が**ツールを呼び忘れたとき**に指摘する監査役です。
6
6
 
@@ -44,7 +44,7 @@ Stop hook → Spotter が応答と使用済みツールを見て最終チェッ
44
44
  見落としあれば差し戻し (max 1 回、Claude Code の stop_hook_active で自動担保)
45
45
  ```
46
46
 
47
- `~/.spotter/tool-catalog/tools.yaml` に監査対象のツール用途を記述します。`current_time` / `web_search` / `read_file` / `list_directory` / `run_command` の雛形付き。
47
+ 監査対象のツール (name + description) は `~/.spotter/tool-db.json` (グローバル) と `<project>/.spotter/tool-db.json` (ローカル) に格納されます。install 後に `spotter db refresh` を実行すると、`claude mcp list` MCP サーバーを列挙し、各サーバーの `tools/list` を JSON-RPC で叩いて description を収集、Claude Code 組込み 遅延ツール (WebSearch / TodoWrite 等) は同梱の baseline からカバーします。**手書きでツールリストを管理する必要はありません**。
48
48
 
49
49
  ## Throughline との関係
50
50
 
@@ -61,10 +61,11 @@ Stop hook → Spotter が応答と使用済みツールを見て最終チェッ
61
61
  ## よく使うコマンド
62
62
 
63
63
  ```bash
64
- spotter catalog edit # ツールカタログを $EDITOR で開く
65
- spotter catalog lint # YAML 検証 + test_cases Haiku 実呼びで検証
64
+ spotter db list # 現在の tool-db (local + global merged) を表示
65
+ spotter db refresh # MCP サーバーと組込み 遅延ツールから description を収集して DB 更新
66
+ spotter db rebuild # ローカル DB を消してから refresh (強制再投資)
66
67
  spotter status # 稼働中の daemon 一覧
67
- spotter doctor # 環境診断 (Node / claude CLI / カタログ整合性)
68
+ spotter doctor # 環境診断 (Node / claude CLI / tool-db 整合性)
68
69
  spotter uninstall # hook 登録を解除 (~/.spotter は残す)
69
70
  ```
70
71
 
package/bin/spotter.mjs CHANGED
@@ -6,7 +6,7 @@ import { runInstall } from '../src/cli/install.mjs';
6
6
  import { runUninstall } from '../src/cli/uninstall.mjs';
7
7
  import { runDoctor } from '../src/cli/doctor.mjs';
8
8
  import { runStatus } from '../src/cli/status.mjs';
9
- import { runCatalogEdit, runCatalogLint } from '../src/cli/catalog.mjs';
9
+ import { runDbList, runDbRefresh, runDbRebuild } from '../src/cli/db-cmd.mjs';
10
10
  import { runDaemonStart } from '../src/cli/daemon-cmd.mjs';
11
11
  import { runSessionStart } from '../src/hooks/session-start.mjs';
12
12
  import { runUserPrompt } from '../src/hooks/user-prompt.mjs';
@@ -26,8 +26,9 @@ Usage:
26
26
  spotter uninstall [-y] remove spotter hooks from <cwd>/.claude/settings.json
27
27
  and remove <cwd>/.spotter/marker.json
28
28
  spotter uninstall --user [-y] remove from ~/.claude/settings.json
29
- spotter catalog edit open tool catalog in $EDITOR
30
- spotter catalog lint validate catalog + run test_cases (Haiku live call)
29
+ spotter db list show merged tool-db (local + global)
30
+ spotter db refresh discover MCP / deferred tools and update DB
31
+ spotter db rebuild wipe local DB then refresh
31
32
  spotter status show running daemons
32
33
  spotter doctor environment diagnostic
33
34
  spotter daemon start --session-id ID (internal) run session daemon
@@ -64,11 +65,12 @@ async function main() {
64
65
  await runUninstall({ target, autoYes });
65
66
  return;
66
67
  }
67
- case 'catalog': {
68
+ case 'db': {
68
69
  const sub = rest[0];
69
- if (sub === 'edit') { await runCatalogEdit(); return; }
70
- if (sub === 'lint') { await runCatalogLint(); return; }
71
- process.stderr.write(`unknown catalog subcommand: ${sub}\n${USAGE}`);
70
+ if (sub === 'list') { await runDbList(); return; }
71
+ if (sub === 'refresh') { await runDbRefresh(); return; }
72
+ if (sub === 'rebuild') { await runDbRebuild(); return; }
73
+ process.stderr.write(`unknown db subcommand: ${sub}\n${USAGE}`);
72
74
  process.exit(2);
73
75
  return;
74
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,6 @@
43
43
  "bin",
44
44
  "src",
45
45
  "scripts",
46
- "templates",
47
46
  "README.md",
48
47
  "LICENSE",
49
48
  "CHANGELOG.md"
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  import { open } from 'node:fs/promises';
7
7
 
8
8
  function parseArgs(argv) {
9
- const out = { sessionId: null, parentPid: null };
9
+ const out = { sessionId: null, parentPid: null, projectRoot: null };
10
10
  for (let i = 0; i < argv.length; i += 1) {
11
11
  if (argv[i] === '--session-id') {
12
12
  out.sessionId = argv[i + 1];
@@ -15,17 +15,24 @@ function parseArgs(argv) {
15
15
  const n = parseInt(argv[i + 1], 10);
16
16
  if (Number.isInteger(n) && n > 0) out.parentPid = n;
17
17
  i += 1;
18
+ } else if (argv[i] === '--project-root') {
19
+ out.projectRoot = argv[i + 1];
20
+ i += 1;
18
21
  }
19
22
  }
20
23
  return out;
21
24
  }
22
25
 
23
26
  export async function runDaemonStart({ argv }) {
24
- const { sessionId, parentPid } = parseArgs(argv);
27
+ const { sessionId, parentPid, projectRoot } = parseArgs(argv);
25
28
  if (!sessionId) {
26
29
  process.stderr.write('spotter daemon start: --session-id is required\n');
27
30
  process.exit(2);
28
31
  }
32
+ if (!projectRoot) {
33
+ process.stderr.write('spotter daemon start: --project-root is required (the path containing .spotter/marker.json)\n');
34
+ process.exit(2);
35
+ }
29
36
 
30
37
  const logFile = await open(
31
38
  join(homedir(), '.spotter', 'logs', `daemon-${sessionId}.log`),
@@ -44,7 +51,8 @@ export async function runDaemonStart({ argv }) {
44
51
  // v0.6.2: parentPid (Claude Code PID, captured by SessionStart hook as process.ppid)
45
52
  // is threaded in so the daemon self-terminates when the parent dies without
46
53
  // SessionEnd (crash / kill / IDE reload).
47
- running = await startDaemon({ sessionId, parentPid, logFn: log });
54
+ // v0.7.0: projectRoot drives tool-db loading (replaces the old tools.yaml catalog).
55
+ running = await startDaemon({ sessionId, projectRoot, parentPid, logFn: log });
48
56
  } catch (err) {
49
57
  if (err instanceof DaemonAlreadyRunningError) {
50
58
  // v0.2 PID-preexist layer: a sibling daemon already serves this session.
@@ -0,0 +1,55 @@
1
+ // `spotter db` — manage the tool-db.
2
+ //
3
+ // spotter db list — print merged tool-db (local overrides global)
4
+ // spotter db refresh — discover available tools and update DB (3-tier resolve)
5
+ // spotter db rebuild — wipe local DB then refresh (forces re-investigation)
6
+ //
7
+ // Run inside a project that has been `spotter install`-ed.
8
+
9
+ import { findSpotterMarker } from '../hooks/lib.mjs';
10
+ import { refresh, readMerged } from '../tool-db/refresh.mjs';
11
+ import { localDbPath, globalDbPath, loadDb, saveDb, emptyDb } from '../tool-db/loader.mjs';
12
+ import { writeFile } from 'node:fs/promises';
13
+
14
+ function requireProjectRoot() {
15
+ const root = findSpotterMarker(process.cwd());
16
+ if (!root) {
17
+ process.stderr.write('spotter db: no .spotter/marker.json found in or above cwd. Run `spotter install` first.\n');
18
+ process.exit(2);
19
+ }
20
+ return root;
21
+ }
22
+
23
+ export async function runDbList() {
24
+ const projectRoot = requireProjectRoot();
25
+ const tools = await readMerged({ projectRoot });
26
+ if (tools.length === 0) {
27
+ process.stdout.write('(empty — run `spotter db refresh` to populate)\n');
28
+ return;
29
+ }
30
+ for (const { name, description } of tools) {
31
+ process.stdout.write(`${name}\n ${description}\n\n`);
32
+ }
33
+ }
34
+
35
+ export async function runDbRefresh() {
36
+ const projectRoot = requireProjectRoot();
37
+ const log = (msg) => process.stderr.write(`spotter db refresh: ${msg}\n`);
38
+ log('discovering MCP servers and built-in deferred tools...');
39
+ const resolved = await refresh({ projectRoot, logFn: log });
40
+ const counts = { local: 0, global: 0, investigated: 0 };
41
+ for (const { source } of resolved.values()) counts[source] = (counts[source] ?? 0) + 1;
42
+ process.stdout.write(
43
+ `${resolved.size} tool(s) resolved (local=${counts.local}, global=${counts.global}, investigated=${counts.investigated})\n`
44
+ + `local DB: ${localDbPath(projectRoot)}\n`
45
+ + `global DB: ${globalDbPath()}\n`
46
+ );
47
+ }
48
+
49
+ export async function runDbRebuild() {
50
+ const projectRoot = requireProjectRoot();
51
+ // Wipe local DB so every tool's resolution falls through to global → investigate.
52
+ await saveDb(localDbPath(projectRoot), emptyDb());
53
+ process.stderr.write(`spotter db rebuild: cleared local DB, refreshing...\n`);
54
+ await runDbRefresh();
55
+ }
@@ -5,7 +5,8 @@ import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
- import { loadCatalog } from '../catalog/loader.mjs';
8
+ import { loadDb, globalDbPath, localDbPath } from '../tool-db/loader.mjs';
9
+ import { findSpotterMarker } from '../hooks/lib.mjs';
9
10
 
10
11
  const execFileP = promisify(execFile);
11
12
 
@@ -37,23 +38,41 @@ export async function runDoctor() {
37
38
 
38
39
  // ~/.spotter directories
39
40
  const home = join(homedir(), '.spotter');
40
- for (const sub of ['tool-catalog', 'runtime', 'workdir', 'logs']) {
41
+ for (const sub of ['runtime', 'workdir', 'logs']) {
41
42
  const path = join(home, sub);
42
43
  const ok = await exists(path);
43
44
  mark(ok, `dir ${path}`);
44
45
  if (!ok) warnings += 1;
45
46
  }
46
47
 
47
- // catalog
48
- const catalogPath = join(home, 'tool-catalog', 'tools.yaml');
48
+ // tool-db (global)
49
49
  try {
50
- const cat = await loadCatalog(catalogPath);
51
- mark(true, `catalog: ${cat.tools.length} tools at ${catalogPath}`);
50
+ const global = await loadDb(globalDbPath());
51
+ const count = Object.keys(global.tools).length;
52
+ if (count === 0) {
53
+ mark(false, `global tool-db: empty (run \`spotter db refresh\`)`);
54
+ warnings += 1;
55
+ } else {
56
+ mark(true, `global tool-db: ${count} tools at ${globalDbPath()}`);
57
+ }
52
58
  } catch (err) {
53
- mark(false, 'catalog', err.message);
59
+ mark(false, 'global tool-db', err.message);
54
60
  failures += 1;
55
61
  }
56
62
 
63
+ // tool-db (local) if cwd is inside a Spotter project
64
+ const projectRoot = findSpotterMarker(process.cwd());
65
+ if (projectRoot) {
66
+ try {
67
+ const local = await loadDb(localDbPath(projectRoot));
68
+ const count = Object.keys(local.tools).length;
69
+ mark(true, `local tool-db: ${count} tools at ${localDbPath(projectRoot)}`);
70
+ } catch (err) {
71
+ mark(false, 'local tool-db', err.message);
72
+ failures += 1;
73
+ }
74
+ }
75
+
57
76
  console.log('');
58
77
  if (failures > 0) {
59
78
  console.log(`result: ${failures} failure(s), ${warnings} warning(s)`);
@@ -1,12 +1,16 @@
1
- // `spotter install` — create ~/.spotter/, place template catalog, register hooks in .claude/settings.json.
1
+ // `spotter install` — create ~/.spotter/, register hooks in .claude/settings.json.
2
2
  //
3
3
  // Per plan §15.4, this shows a diff and asks for confirmation before touching settings.json.
4
4
  //
5
5
  // v0.3: also writes <cwd>/.spotter/marker.json (project mode) so hooks can detect
6
6
  // "this Claude Code session is rooted in a project where Spotter is installed" and
7
7
  // silently exit otherwise (prevents Throughline-style proliferation).
8
+ //
9
+ // v0.7.0: tool catalog (the old YAML) is replaced by tool-db.json (auto-discovered MCP
10
+ // + hardcoded built-in deferred). Install no longer seeds a template — user runs
11
+ // `spotter db refresh` after install to populate.
8
12
 
9
- import { mkdir, writeFile, readFile, access, copyFile } from 'node:fs/promises';
13
+ import { mkdir, writeFile, readFile, access } from 'node:fs/promises';
10
14
  import { homedir } from 'node:os';
11
15
  import { join, resolve, dirname } from 'node:path';
12
16
  import { fileURLToPath } from 'node:url';
@@ -15,11 +19,9 @@ import { version as SPOTTER_VERSION } from '../version.mjs';
15
19
 
16
20
  const HERE = dirname(fileURLToPath(import.meta.url));
17
21
  const PACKAGE_ROOT = resolve(HERE, '..', '..');
18
- const TEMPLATE_CATALOG = join(PACKAGE_ROOT, 'templates', 'tools.yaml');
19
22
  const SPOTTER_BIN = join(PACKAGE_ROOT, 'bin', 'spotter.mjs');
20
23
 
21
24
  const SPOTTER_HOME = join(homedir(), '.spotter');
22
- const CATALOG_DEST = join(SPOTTER_HOME, 'tool-catalog', 'tools.yaml');
23
25
 
24
26
  const MARKER_VERSION = '1';
25
27
 
@@ -42,20 +44,11 @@ export async function runInstall({ target = 'project', autoYes = false, cwd = pr
42
44
 
43
45
  // 1. create directories
44
46
  await mkdir(SPOTTER_HOME, { recursive: true });
45
- await mkdir(join(SPOTTER_HOME, 'tool-catalog'), { recursive: true });
46
47
  await mkdir(join(SPOTTER_HOME, 'runtime'), { recursive: true });
47
48
  await mkdir(join(SPOTTER_HOME, 'workdir'), { recursive: true });
48
49
  await mkdir(join(SPOTTER_HOME, 'logs'), { recursive: true });
49
50
 
50
- // 2. place catalog template if missing
51
- if (!(await exists(CATALOG_DEST))) {
52
- await copyFile(TEMPLATE_CATALOG, CATALOG_DEST);
53
- console.log(` wrote ${CATALOG_DEST}`);
54
- } else {
55
- console.log(` catalog already present at ${CATALOG_DEST} (not overwritten)`);
56
- }
57
-
58
- // 3. project marker (v0.3): hooks use this to detect installed projects.
51
+ // 2. project marker (v0.3): hooks use this to detect installed projects.
59
52
  // Skipped in user-mode install — user-mode is a deprecated escape hatch and
60
53
  // intentionally has no marker, so all hooks would exit. (Existing user-mode
61
54
  // installs from <0.3 won't surprise-stop working only because of this — they
@@ -103,7 +96,9 @@ export async function runInstall({ target = 'project', autoYes = false, cwd = pr
103
96
  await mkdir(dirname(settingsPath), { recursive: true });
104
97
  await writeFile(settingsPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
105
98
  console.log(`wrote ${settingsPath}`);
106
- console.log('\nnext: reload Claude Code (or open a new session) to activate Spotter.');
99
+ console.log('\nnext steps:');
100
+ console.log(' 1. run `spotter db refresh` to discover available MCP/deferred tools');
101
+ console.log(' 2. reload Claude Code (or open a new session) to activate Spotter');
107
102
  }
108
103
 
109
104
  async function exists(path) {
@@ -79,7 +79,7 @@ export async function runUninstall({ target = 'project', autoYes = false, cwd =
79
79
 
80
80
  await writeFile(settingsPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
81
81
  console.log(`wrote ${settingsPath}`);
82
- console.log('note: ~/.spotter/ (catalog, logs) was not removed. delete manually if no longer needed.');
82
+ console.log('note: ~/.spotter/ (tool-db, logs) was not removed. delete manually if no longer needed.');
83
83
  }
84
84
 
85
85
  async function removeMarker(cwd) {
@@ -35,12 +35,10 @@ import {
35
35
  createHaikuCaller,
36
36
  HaikuError,
37
37
  } from './haiku-caller.mjs';
38
- import { loadCatalog } from '../catalog/loader.mjs';
38
+ import { readMerged } from '../tool-db/refresh.mjs';
39
39
  import { homedir } from 'node:os';
40
40
  import { join } from 'node:path';
41
41
  import { writeFile, unlink } from 'node:fs/promises';
42
-
43
- const DEFAULT_CATALOG_PATH = join(homedir(), '.spotter', 'tool-catalog', 'tools.yaml');
44
42
  const DEFAULT_HAIKU_CALL_WINDOW_MS = 10_000;
45
43
  // v0.5.0: lowered 60s → 30s. Session-scoped (--resume) means the first call still pays
46
44
  // cold-start but subsequent calls skip it. 30s covers the first-call cold path without
@@ -64,7 +62,8 @@ export class DaemonAlreadyRunningError extends Error {
64
62
 
65
63
  export async function startDaemon({
66
64
  sessionId,
67
- catalogPath = DEFAULT_CATALOG_PATH,
65
+ projectRoot,
66
+ tools,
68
67
  haikuCaller,
69
68
  logFn = () => {},
70
69
  haikuCallWindowMs = DEFAULT_HAIKU_CALL_WINDOW_MS,
@@ -84,14 +83,24 @@ export async function startDaemon({
84
83
  // a sibling daemon is already serving this session_id — throw so the caller can exit.
85
84
  await assertNoLiveDaemon(sessionId);
86
85
 
87
- // Load catalog up front daemon cannot run without it (§14.1).
88
- const catalog = await loadCatalog(catalogPath);
89
- logFn(`catalog loaded: ${catalog.tools.length} tools from ${catalogPath}`);
86
+ // v0.7.0: tool list comes from tool-db (local + global merged with local-wins).
87
+ // For tests, the caller can pass `tools` directly. For production, projectRoot drives
88
+ // the load from <projectRoot>/.spotter/tool-db.json + ~/.spotter/tool-db.json.
89
+ let toolList;
90
+ if (Array.isArray(tools)) {
91
+ toolList = tools;
92
+ } else {
93
+ if (!projectRoot) {
94
+ throw new TypeError('startDaemon: either `tools` or `projectRoot` must be provided');
95
+ }
96
+ toolList = await readMerged({ projectRoot });
97
+ }
98
+ logFn(`tool-db loaded: ${toolList.length} tools` + (projectRoot ? ` (project=${projectRoot})` : ''));
90
99
 
91
100
  // v0.6.0: preamble (role + schema + catalog) is built once and threaded into the Haiku
92
101
  // caller. The caller prepends it on the first call only; --resume keeps it in session
93
102
  // history for all subsequent calls.
94
- const preamble = buildPreamble({ catalog });
103
+ const preamble = buildPreamble({ tools: toolList });
95
104
  const callHaiku = haikuCaller ?? createHaikuCaller({ preamble, timeoutMs: DEFAULT_HAIKU_TIMEOUT_MS });
96
105
 
97
106
  // Per-turn state, reset on turn_end.
@@ -62,12 +62,21 @@ const SHARED_HEADER = [
62
62
  // output contract, few-shot examples, and the tool catalog. The Anthropic session
63
63
  // retains this in history so subsequent --resume calls can judge with only a small
64
64
  // per-turn payload.
65
- export function buildPreamble({ catalog }) {
65
+ //
66
+ // v0.7.0: `tools` is an array of {name, description} pairs (the new tool-db format).
67
+ // `description` is the natural-language explanation supplied by the MCP server (or the
68
+ // hardcoded baseline for Claude Code built-in deferred tools). No schema, no usage —
69
+ // Haiku only needs to decide whether the tool should be called; "how to call" is Bell's
70
+ // responsibility (via ToolSearch).
71
+ export function buildPreamble({ tools }) {
72
+ if (!Array.isArray(tools)) {
73
+ throw new TypeError('buildPreamble: tools must be an array of {name, description}');
74
+ }
66
75
  return [
67
76
  SHARED_HEADER,
68
77
  '',
69
78
  '## カタログ',
70
- JSON.stringify(projectCatalog(catalog), null, 2),
79
+ JSON.stringify(tools, null, 2),
71
80
  ].join('\n');
72
81
  }
73
82
 
@@ -99,14 +108,6 @@ export function buildFinalStagePrompt({ userInput, usedTools, finalResponse }) {
99
108
  ].join('\n');
100
109
  }
101
110
 
102
- function projectCatalog(catalog) {
103
- return catalog.tools.map((t) => ({
104
- name: t.name,
105
- purpose: t.purpose,
106
- when_to_use: t.when_to_use,
107
- }));
108
- }
109
-
110
111
  // Parse Haiku's response. Throws HaikuError on schema violation.
111
112
  export function parseHaikuResponse(raw) {
112
113
  const trimmed = raw.trim();
@@ -16,7 +16,7 @@ import { spawn } from 'node:child_process';
16
16
  import { setTimeout as delay } from 'node:timers/promises';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { dirname, resolve } from 'node:path';
19
- import { readStdinJson, requireString, die, isChildCall, isSubagentCall, isOutsideSpotterProject } from './lib.mjs';
19
+ import { readStdinJson, requireString, die, isChildCall, isSubagentCall, isOutsideSpotterProject, findSpotterMarker } from './lib.mjs';
20
20
  import { sendRequest, TransportError } from '../daemon/transport.mjs';
21
21
 
22
22
  const READINESS_TIMEOUT_MS = 3_000;
@@ -39,8 +39,12 @@ export async function runSessionStart({ argv = process.argv, now = Date.now } =
39
39
  if (isOutsideSpotterProject(input)) return;
40
40
 
41
41
  const sessionId = requireString(input, 'session_id');
42
+ const projectRoot = findSpotterMarker(input.cwd);
43
+ if (!projectRoot) {
44
+ die(`SessionStart: failed to locate project root from cwd=${input.cwd}`, 2);
45
+ }
42
46
 
43
- spawnDaemon(sessionId, argv);
47
+ spawnDaemon(sessionId, projectRoot, argv);
44
48
 
45
49
  const deadline = now() + READINESS_TIMEOUT_MS;
46
50
  while (now() < deadline) {
@@ -65,14 +69,20 @@ export async function runSessionStart({ argv = process.argv, now = Date.now } =
65
69
  die(`daemon did not reach readiness within ${READINESS_TIMEOUT_MS}ms for session ${sessionId}`, 2);
66
70
  }
67
71
 
68
- function spawnDaemon(sessionId, argv) {
72
+ function spawnDaemon(sessionId, projectRoot, argv) {
69
73
  // Invoke `node <spotter-bin> daemon start --session-id ...` detached.
70
74
  // v0.6.2: pass --parent-pid so the daemon can self-terminate when Claude Code dies
71
75
  // without firing SessionEnd. process.ppid here is Claude Code (this hook's parent).
76
+ // v0.7.0: pass --project-root for tool-db loading.
72
77
  const spotterBin = resolveSpotterBin(argv);
73
78
  const child = spawn(
74
79
  process.execPath,
75
- [spotterBin, 'daemon', 'start', '--session-id', sessionId, '--parent-pid', String(process.ppid)],
80
+ [
81
+ spotterBin, 'daemon', 'start',
82
+ '--session-id', sessionId,
83
+ '--parent-pid', String(process.ppid),
84
+ '--project-root', projectRoot,
85
+ ],
76
86
  {
77
87
  detached: true,
78
88
  stdio: 'ignore',
package/src/index.mjs CHANGED
@@ -13,7 +13,9 @@ export {
13
13
  createHaikuCaller,
14
14
  HaikuError,
15
15
  } from './daemon/haiku-caller.mjs';
16
- export { loadCatalog, CatalogLoadError, CatalogSchemaError } from './catalog/loader.mjs';
17
- export { validateCatalog } from './catalog/schema.mjs';
18
- export { runLint } from './catalog/lint.mjs';
16
+ export { loadDb, saveDb, emptyDb, ToolDbSchemaError, globalDbPath, localDbPath } from './tool-db/loader.mjs';
17
+ export { resolveAll } from './tool-db/lookup.mjs';
18
+ export { refresh, readMerged, buildInvestigationSnapshot } from './tool-db/refresh.mjs';
19
+ export { listMcpServers, listMcpToolsAll, bellVisibleName, McpInvestigationError } from './tool-db/investigate-mcp.mjs';
20
+ export { DEFERRED_TOOL_BASELINE, getDeferredDescription, listDeferredNames } from './tool-db/deferred-baseline.mjs';
19
21
  export { version } from './version.mjs';
@@ -0,0 +1,49 @@
1
+ // Hand-curated descriptions for Claude Code built-in deferred tools.
2
+ //
3
+ // Why hardcoded: Claude Code's built-in tools cannot be queried introspectively from a
4
+ // daemon process (they're internal to the Claude binary, not exposed via MCP). The list
5
+ // is small and stable; we maintain it here and update on Claude Code releases.
6
+ //
7
+ // `spotter db refresh` merges this baseline into the global DB. Users can override
8
+ // per-project by editing their local DB (per docs/catalog-design-deferred-mcp.md).
9
+
10
+ export const DEFERRED_TOOL_BASELINE = {
11
+ // Reasoning / dialogue
12
+ 'AskUserQuestion': 'Ask the user a clarifying question with multi-choice or free-text answer when requirements are ambiguous; preferable to guessing.',
13
+ 'TodoWrite': 'Create or update a structured task list to plan and track multi-step work; surfaces progress to the user and to future turns.',
14
+ 'EnterPlanMode': 'Enter Plan Mode: Claude proposes a plan first and waits for user approval before doing the work.',
15
+ 'ExitPlanMode': 'Exit Plan Mode after the user has approved the proposed plan.',
16
+
17
+ // Web access
18
+ 'WebSearch': 'Search the web for up-to-date information (news, prices, releases, events that postdate training).',
19
+ 'WebFetch': 'Fetch the contents of a specific URL to read its body (HTML/Markdown/JSON), e.g. when the user pastes a link.',
20
+
21
+ // Notebook
22
+ 'NotebookEdit': 'Edit cells in a Jupyter notebook (.ipynb) — insert, replace, or delete cells.',
23
+
24
+ // Worktree / isolated execution
25
+ 'EnterWorktree': 'Create a temporary git worktree to isolate experimental edits from the main checkout.',
26
+ 'ExitWorktree': 'Tear down a previously-created worktree.',
27
+
28
+ // Background processes / monitoring
29
+ 'Monitor': 'Stream events from a long-running background process; receives a notification per stdout line.',
30
+ 'PushNotification': 'Send a push notification to the user (e.g. on long task completion).',
31
+
32
+ // Scheduling / cron
33
+ 'CronCreate': 'Schedule a recurring agent (cron-style trigger) that runs Claude Code on an interval.',
34
+ 'CronDelete': 'Delete a scheduled cron trigger by id.',
35
+ 'CronList': 'List all currently configured cron triggers.',
36
+ 'RemoteTrigger': 'Create or run a remote trigger (one-shot or recurring scheduled agent).',
37
+
38
+ // Sub-agent / task control
39
+ 'TaskOutput': 'Read the latest output from a running background sub-agent.',
40
+ 'TaskStop': 'Stop a running background sub-agent.',
41
+ };
42
+
43
+ export function getDeferredDescription(name) {
44
+ return DEFERRED_TOOL_BASELINE[name] ?? null;
45
+ }
46
+
47
+ export function listDeferredNames() {
48
+ return Object.keys(DEFERRED_TOOL_BASELINE);
49
+ }