claude-spotter 1.0.0 → 1.1.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,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ **`spotter install` が tool-db を自動構築 + SessionStart hook がバックグラウンド refresh**。install 直後から audit 対象が揃うようになり、以降の session でも MCP / スキル / サブエージェントの追加・削除が自動追従する。
6
+
7
+ ### 背景
8
+
9
+ v1.0.0 以前は `spotter install` が hook 登録だけで tool-db を作らず、別途 `spotter db refresh` を手動実行する必要があった。初回セッションで daemon が空 DB を掴むと Haiku に preamble が届かず audit が機能しない状態で起動してしまう。また、install 後に MCP や スキルを追加しても rebuild/refresh を手動で叩くまで視野に入らず、drift が常に発生していた。
10
+
11
+ ### 変更点
12
+
13
+ - **編集 [src/cli/install.mjs](src/cli/install.mjs)**: settings.json 書き込み後に project-mode で `refresh({projectRoot})` を同期実行し tool-db を seed。失敗時は §0 準拠で throw (hook だけ登録されて DB が無い中途半端な状態を残さない)。`skipRefresh` オプションを新設 (既存テストが user 環境をスキャンしないように)。"next steps" メッセージから `spotter db refresh` の手動実行指示を削除
14
+ - **編集 [src/hooks/session-start.mjs](src/hooks/session-start.mjs)**: daemon readiness 確立後に `spawnRefreshDetached({projectRoot})` を発火。hook 自体は即 return、refresh は detached child として bg 実行。**現セッションの daemon は起動時の tool-db で固定**のため反映は次セッション以降
15
+ - **編集 [src/hooks/spawn-daemon.mjs](src/hooks/spawn-daemon.mjs)**: `spawnRefreshDetached` を追加 export。`node <spotterBin> db refresh` を `detached: true, stdio: 'ignore', unref()` で起動、hook を遅延させない
16
+ - **テスト更新 [test/install.test.mjs](test/install.test.mjs)**: 全 6 件が `skipRefresh: true` を渡すよう更新
17
+
18
+ ### 設計判断
19
+
20
+ - **rebuild ではなく refresh を採用**: 当初 user 指示は rebuild (local+global wipe + 全再スキャン) だったが、(1) 既適用プロジェクトの global キャッシュを毎 SessionStart で破壊するのは副作用が大きい、(2) 並列セッション (Project A rebuild 中に Project B SessionStart) で書き込み競合が発生する、の 2 点から refresh に変更。差分更新でも新規・削除の drift 追従は効く。description drift (同一名の description 更新) のみ取りこぼすが、これは `spotter db rebuild` の手動実行でカバー
21
+ - **SessionStart の refresh は bg detached**: session-start hook の timeout は 5s で、MCP 全サーバー spawn + skills 181 件スキャンは秒単位かかるため同期実行は不可能。detached + unref で hook を遅延させない代わりに、反映は次セッション以降 (現セッションの daemon は既に古い tool-db をロード済みで、実行中の差し替えはしない)
22
+ - **install 時の refresh 失敗は throw**: "hook 登録済みだが DB なし" という中途半端な状態を残すくらいなら install 自体を失敗扱いにするほうがクリーン。再試行は `spotter install` の再実行で、hook 登録は `nothing to change` で skip され refresh だけ走る
23
+
24
+ ### 破壊変更
25
+
26
+ なし (skipRefresh オプションはデフォルト false で既存挙動より機能追加、CLI 利用者には透過)。
27
+
28
+ ### 影響範囲
29
+
30
+ - 新規 `spotter install` 実行時は MCP/skills/agents の discover でセットアップ時間が数秒〜10 秒増える
31
+ - 毎 SessionStart でバックグラウンド `spotter db refresh` プロセスが 1 つ発火 (bg unref なので UX 影響なし)
32
+ - global tool-db への書き込みが session 起動ごとに発生 (atomic write なので corruption リスクなし、last-write-wins の並列 race は idempotent なので次 refresh で収束)
33
+
3
34
  ## 1.0.0
4
35
 
5
36
  **監査対象をユーザー追加分 (MCP / スキル / サブエージェント) に絞り込み**。Claude Code 本体が提供するツール (即時 + 遅延) は監査カタログから全面除外。設計転換の major bump。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Audit agent running alongside Claude Code that catches missed tool calls — 気づく役と実行する役の分離",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,8 +7,9 @@
7
7
  // silently exit otherwise (prevents Throughline-style proliferation).
8
8
  //
9
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.
10
+ // servers, skills, sub-agents see docs/catalog-design.md for v1.0.0 scope).
11
+ // Install seeds the DB automatically via `refresh` (project-mode only user-mode
12
+ // has no projectRoot so DB seeding is skipped there).
12
13
 
13
14
  import { mkdir, writeFile, readFile, access } from 'node:fs/promises';
14
15
  import { homedir } from 'node:os';
@@ -16,6 +17,8 @@ import { join, resolve, dirname } from 'node:path';
16
17
  import { fileURLToPath } from 'node:url';
17
18
  import { createInterface } from 'node:readline/promises';
18
19
  import { version as SPOTTER_VERSION } from '../version.mjs';
20
+ import { refresh } from '../tool-db/refresh.mjs';
21
+ import { localDbPath, globalDbPath } from '../tool-db/loader.mjs';
19
22
 
20
23
  const HERE = dirname(fileURLToPath(import.meta.url));
21
24
  const PACKAGE_ROOT = resolve(HERE, '..', '..');
@@ -33,7 +36,7 @@ const HOOK_EVENTS = [
33
36
  { event: 'SessionEnd', sub: 'session-end', timeout: 3 },
34
37
  ];
35
38
 
36
- export async function runInstall({ target = 'project', autoYes = false, cwd = process.cwd() } = {}) {
39
+ export async function runInstall({ target = 'project', autoYes = false, cwd = process.cwd(), skipRefresh = false } = {}) {
37
40
  const settingsPath = target === 'user'
38
41
  ? join(homedir(), '.claude', 'settings.json')
39
42
  : join(cwd, '.claude', 'settings.json');
@@ -96,9 +99,21 @@ export async function runInstall({ target = 'project', autoYes = false, cwd = pr
96
99
  await mkdir(dirname(settingsPath), { recursive: true });
97
100
  await writeFile(settingsPath, JSON.stringify(updated, null, 2) + '\n', 'utf8');
98
101
  console.log(`wrote ${settingsPath}`);
102
+
103
+ // Seed the tool-db so the first session has something to audit against.
104
+ // Skipped for user-mode (deprecated — no projectRoot) and when caller opts out
105
+ // (tests set skipRefresh=true to avoid scanning the real user environment).
106
+ if (target === 'project' && !skipRefresh) {
107
+ console.log('\ndiscovering MCP servers, skills, and sub-agents...');
108
+ const log = (msg) => process.stderr.write(` ${msg}\n`);
109
+ const resolved = await refresh({ projectRoot: cwd, logFn: log });
110
+ console.log(` ${resolved.size} tool(s) resolved`);
111
+ console.log(` local DB: ${localDbPath(cwd)}`);
112
+ console.log(` global DB: ${globalDbPath()}`);
113
+ }
114
+
99
115
  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');
116
+ console.log(' reload Claude Code (or open a new session) to activate Spotter');
102
117
  }
103
118
 
104
119
  async function exists(path) {
@@ -17,7 +17,7 @@
17
17
  // now heartbeat-based inside the daemon.
18
18
 
19
19
  import { readStdinJson, requireString, die, isChildCall, isSubagentCall, isOutsideSpotterProject, findSpotterMarker } from './lib.mjs';
20
- import { spawnDaemonAndWaitReady } from './spawn-daemon.mjs';
20
+ import { spawnDaemonAndWaitReady, spawnRefreshDetached } from './spawn-daemon.mjs';
21
21
 
22
22
  export async function runSessionStart({ now = Date.now } = {}) {
23
23
  if (isChildCall()) return;
@@ -35,6 +35,7 @@ export async function runSessionStart({ now = Date.now } = {}) {
35
35
  }
36
36
 
37
37
  await spawnDaemonAndWaitReady({ sessionId, projectRoot, now });
38
+ spawnRefreshDetached({ projectRoot });
38
39
  }
39
40
 
40
41
  if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`) {
@@ -66,6 +66,27 @@ function spawnDaemon(sessionId, projectRoot) {
66
66
  child.unref();
67
67
  }
68
68
 
69
+ // Fire-and-forget `spotter db refresh`. The daemon in THIS session already loaded the
70
+ // pre-refresh tool-db, so any updates land in time for the NEXT session. Detached so
71
+ // the hook doesn't wait for MCP/skill/agent discovery to finish.
72
+ export function spawnRefreshDetached({ projectRoot }) {
73
+ const spotterBin = resolveSpotterBin();
74
+ const child = spawn(
75
+ process.execPath,
76
+ [spotterBin, 'db', 'refresh'],
77
+ {
78
+ cwd: projectRoot,
79
+ detached: true,
80
+ stdio: 'ignore',
81
+ windowsHide: true,
82
+ }
83
+ );
84
+ child.on('error', (err) => {
85
+ process.stderr.write(`spotter-hook: refresh spawn error: ${err.message}\n`);
86
+ });
87
+ child.unref();
88
+ }
89
+
69
90
  function resolveSpotterBin() {
70
91
  const here = dirname(fileURLToPath(import.meta.url));
71
92
  return resolve(here, '..', '..', 'bin', 'spotter.mjs');