claude-spotter 1.2.1 → 1.2.3

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,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.3
4
+
5
+ **v1.2.1 で追加した `normalizeProjectPath` が Linux CI で Windows path key と POSIX path をマッチさせて test を落としていた回帰を修正**。`replace(/\\/g, '/')` をプラットフォーム条件なしで実行していたため、Linux 上で `'C:\Users\u\proj'` (Windows 表記の literal key) と `'C:/Users/u/proj'` (forward-slash 入力) が `C:/Users/u/proj` 同士に正規化されてマッチしてしまい、`findLocalScopeServers` が POSIX で意図しない命中を返していた。CI のみ赤、実運用 (Windows) は元から正しく動いていたので機能影響は無し。
6
+
7
+ ### 変更点
8
+
9
+ - **編集 [src/tool-db/mcp-config.mjs](src/tool-db/mcp-config.mjs)**: `normalizeProjectPath` の separator 変換 + lower-case 化を `process.platform === 'win32'` ブランチに閉じ込め、POSIX では trailing slash 除去のみ。コメントを「POSIX で `\` は legal filename 文字なので separator として畳むと別パスを衝突させる」と書き直し
10
+
11
+ ### 背景
12
+
13
+ `~/.claude.json` の `projects[]` キーには絶対パスが verbatim で書かれる。Windows なら `C:\Users\u\proj` 形式、Linux なら `/home/u/proj` 形式。Spotter の `findLocalScopeServers` は exact 一致が外れたとき正規化フォールバックで再照合する設計だが、その正規化が「全プラットフォームで `\` → `/` + 末尾 `/` 除去 + Windows でだけ lowercase」だったため、Linux で実行された場合でも Windows 表記が forward-slash 表記に化けて当たってしまう。test (`findLocalScopeServers: separator variant matches on Windows only` @ test/tool-db.test.mjs:716) は POSIX で `{}` が返ることを assert していたので、Linux CI で fail。
14
+
15
+ 修正は POSIX ブランチでは何もしないこと。POSIX 上に Windows path key が混入する状況自体ほぼ無いので機能影響は皆無、test 期待値との整合だけが効果。
16
+
17
+ ### Release backfill
18
+
19
+ このリリースに合わせて、これまで tag / GitHub Release が未作成だった v1.2.1 / v1.2.2 を CHANGELOG から流用して backfill。Latest は v1.2.3。
20
+
21
+ ## 1.2.2
22
+
23
+ **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。
24
+
25
+ ### 変更点
26
+
27
+ - **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `buildStdioSpawn` の Windows ブランチ条件を `/\.(cmd|bat)$/i` から「絶対 `.exe` パス以外は `cmd.exe /c` で包む」に拡張。`export` を追加してユニットテスト可能に
28
+ - **編集 [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 リスク回避)
29
+
30
+ ### 背景
31
+
32
+ #### 何が起きていたか
33
+
34
+ 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 サーバ起動経路にこのパターンが横展開されていなかったこと**。
35
+
36
+ #### v1.2.1 までのコード
37
+
38
+ ```js
39
+ function buildStdioSpawn(command, args) {
40
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)) {
41
+ return { cmd: 'cmd.exe', cmdArgs: ['/c', command, ...args] };
42
+ }
43
+ return { cmd: command, cmdArgs: args };
44
+ }
45
+ ```
46
+
47
+ `/\.(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 に脱落していた**。
48
+
49
+ ### 設計判断
50
+
51
+ - **絶対 `.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 リスクは織り込み済み (既存挙動から退化なし)
52
+ - **`shell: true` を使わない理由**: Node 24+ で DEP0190 が出るし、引数 quoting が cmd.exe の rules に丸投げされて `&` `|` `>` 等の metacharacter 処理リスクが復活する。caveat の Resolution と既存 (`execClaude` / `buildSpawnArgs`) の選択を踏襲
53
+ - **裸名を全部 wrap する選択 (= 「`.exe` 以外は wrap」)**: `node` のような既知の `.exe` 配布も Windows では cmd.exe 経由になるが、cmd.exe が PATHEXT で正しく解決するので動作影響なし。コードのシンプルさ (extension の case sensitivity / 配布形態の事前知識を不要にする) を優先
54
+ - **patch bump (fix)**: API 変更なし、`buildStdioSpawn` の export 追加は純粋に additive。Windows での挙動が「ENOENT で死ぬ」→「正しく動く」方向の修正なので破壊変更なし
55
+
56
+ ### 自動追従の経路
57
+
58
+ 既に 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 が上書きされる)。
59
+
3
60
  ## 1.2.1
4
61
 
5
62
  **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 な脱落が発生していた。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-spotter",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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) {
@@ -92,12 +92,18 @@ async function readMcpServersFile(path) {
92
92
  // - separator: `\` on Windows vs `/`
93
93
  // - drive-letter case: `C:\` vs `c:\` on Windows
94
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.
95
+ // On Windows we canonicalize to forward slashes and lower-case (case-insensitive
96
+ // filesystem). On POSIX, `\` is a legal filename character — collapsing it to `/`
97
+ // would conflate genuinely distinct paths (e.g. literal `C:\Users\u\proj` vs the
98
+ // hypothetical POSIX path `C:/Users/u/proj`), and case stays significant. POSIX
99
+ // only normalizes the trailing slash.
97
100
  export function normalizeProjectPath(p) {
98
101
  if (typeof p !== 'string' || p.length === 0) return '';
99
- let s = p.replace(/\\/g, '/').replace(/\/+$/, '');
100
- if (process.platform === 'win32') s = s.toLowerCase();
102
+ let s = p;
103
+ if (process.platform === 'win32') {
104
+ s = s.replace(/\\/g, '/').toLowerCase();
105
+ }
106
+ s = s.replace(/\/+$/, '');
101
107
  return s;
102
108
  }
103
109