claude-spotter 0.9.0 → 0.11.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 +33 -0
- package/package.json +1 -1
- package/src/hooks/user-prompt.mjs +6 -0
- package/src/tool-db/investigate-mcp-http.mjs +1 -1
- package/src/tool-db/investigate-mcp.mjs +5 -5
- package/src/tool-db/mcp-config.mjs +20 -8
- package/src/tool-db/refresh.mjs +5 -4
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
**短プロンプトの Haiku スキップ**。ユーザーの入力が trim 後 10 文字 (コードポイント) 以下なら、挨拶・相槌・短い確認質問などツール不要な会話が支配的なため、UserPromptSubmit hook で早期 return して Haiku 呼び出しを完全にスキップする。daemon には user_input を送らず、`state.lastUserInput=null` のまま次の turn_end が `reason=no_user_input` で自動 pass する。preamble 57 件の判定コストを、最も的外れになりやすい短文ターンで丸ごと節約する。
|
|
6
|
+
|
|
7
|
+
### 変更点
|
|
8
|
+
|
|
9
|
+
- **編集 [src/hooks/user-prompt.mjs](src/hooks/user-prompt.mjs)**: `SHORT_PROMPT_MAX_CHARS = 10` 定数を追加、`[...prompt.trim()].length <= 10` なら daemon へ送信せず hook を終了
|
|
10
|
+
|
|
11
|
+
### 設計判断
|
|
12
|
+
|
|
13
|
+
- **閾値 10 の根拠**: 「今何時?」「ありがとう」「ok done」等はいずれも 10 文字以下。逆に 10 文字超なら何らかの意図 (質問・依頼・指示) が入る想定
|
|
14
|
+
- **半角/全角の区別なし**: コードポイント数で一律判定。半角 10 文字 ("thanks ok" 等) もツール不要な短文が支配的なので skip で問題ないと判断
|
|
15
|
+
- **daemon 側の変更なし**: 既存の `no_user_input` pass 経路を流用。user_input を送らない = state そのまま、という副作用で turn_end が勝手に pass するので daemon に閾値ロジックを持たせる必要がない
|
|
16
|
+
|
|
17
|
+
### 非互換
|
|
18
|
+
|
|
19
|
+
なし。観測不能な場面 (短文ターン) で Haiku が動かないだけ。
|
|
20
|
+
|
|
21
|
+
## 0.10.0
|
|
22
|
+
|
|
23
|
+
**project scope `.mcp.json` 対応**。v0.9.0 は `~/.claude/.mcp.json` (user scope) だけを読んでいたため、プロジェクト直下の `.mcp.json` に登録された MCP サーバーの認証情報 (env / headers) を拾えなかった。`<projectRoot>/.mcp.json` も読んで user scope に merge (project 勝ち = Claude Code 本体の precedence と整合) するよう変更。
|
|
24
|
+
|
|
25
|
+
### 変更点
|
|
26
|
+
|
|
27
|
+
- **編集 [src/tool-db/mcp-config.mjs](src/tool-db/mcp-config.mjs)**: `readMcpServers({projectRoot})` シグネチャへ変更。`projectRoot` が渡されれば user scope に project scope を merge して返す。missing file は空として扱う
|
|
28
|
+
- **編集 [src/tool-db/investigate-mcp.mjs](src/tool-db/investigate-mcp.mjs)**: `listMcpServers` / `listMcpToolsAll` が `projectRoot` を受け取って伝搬
|
|
29
|
+
- **編集 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**: `buildInvestigationSnapshot` / `refresh` が `projectRoot` を伝搬 (CLI からは既に渡されている、経路が繋がった)
|
|
30
|
+
- **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**: project-scope override + missing-file fallback の 2 ケース追加 (total 99 tests)
|
|
31
|
+
|
|
32
|
+
### 非互換
|
|
33
|
+
|
|
34
|
+
- `readMcpServers()` → `readMcpServers({projectRoot})`: 引数なしも引き続き動く (user scope のみ) ので既存コードは影響なし
|
|
35
|
+
|
|
3
36
|
## 0.9.0
|
|
4
37
|
|
|
5
38
|
**`.mcp.json` を真実源として読み込み、user-registered HTTP/stdio MCP の認証情報を live fetch に活用**。v0.8.0 で HTTP transport を実装したが、`claude mcp list` / `claude mcp get` は bearer token や headers を CLI 出力に含めないため、認証が必要な MCP サーバー (x-api) は依然 401 で落ちていた。`~/.claude/.mcp.json` を直接読んで env / headers を取得、stdio なら spawn 時の env に、HTTP なら fetch request header に渡す。
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { sendRequest } from '../daemon/transport.mjs';
|
|
15
15
|
|
|
16
16
|
const TIMEOUT_MS = 30_000;
|
|
17
|
+
const SHORT_PROMPT_MAX_CHARS = 10;
|
|
17
18
|
|
|
18
19
|
export async function runUserPrompt() {
|
|
19
20
|
if (isChildCall()) return;
|
|
@@ -24,6 +25,11 @@ export async function runUserPrompt() {
|
|
|
24
25
|
const sessionId = requireString(input, 'session_id');
|
|
25
26
|
const prompt = requireString(input, 'prompt');
|
|
26
27
|
|
|
28
|
+
// Short prompts (<=10 codepoints after trim) are almost never tool-required
|
|
29
|
+
// (greetings, acknowledgements, short questions). Skip Haiku entirely —
|
|
30
|
+
// daemon keeps lastUserInput=null, so the next turn_end passes with reason=no_user_input.
|
|
31
|
+
if ([...prompt.trim()].length <= SHORT_PROMPT_MAX_CHARS) return;
|
|
32
|
+
|
|
27
33
|
let response;
|
|
28
34
|
try {
|
|
29
35
|
response = await sendRequest({
|
|
@@ -87,7 +87,7 @@ export async function listToolsHttp({ url, serverName, headers: staticHeaders =
|
|
|
87
87
|
params: {
|
|
88
88
|
protocolVersion: PROTOCOL_VERSION,
|
|
89
89
|
capabilities: {},
|
|
90
|
-
clientInfo: { name: 'spotter', version: '0.
|
|
90
|
+
clientInfo: { name: 'spotter', version: '0.10.0' },
|
|
91
91
|
},
|
|
92
92
|
});
|
|
93
93
|
if (!initResult || initResult.error) {
|
|
@@ -40,8 +40,8 @@ export class McpInvestigationError extends Error {
|
|
|
40
40
|
|
|
41
41
|
// Returns: Map<server-name, Array<{name, description}>>.
|
|
42
42
|
// Skips servers that fail (logs the failure) so one broken server doesn't block the rest.
|
|
43
|
-
export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude' } = {}) {
|
|
44
|
-
const servers = await listMcpServers({ claudeBin });
|
|
43
|
+
export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude', projectRoot } = {}) {
|
|
44
|
+
const servers = await listMcpServers({ claudeBin, projectRoot });
|
|
45
45
|
const out = new Map();
|
|
46
46
|
for (const server of servers) {
|
|
47
47
|
try {
|
|
@@ -64,10 +64,10 @@ export async function listMcpToolsAll({ logFn = () => {}, claudeBin = 'claude' }
|
|
|
64
64
|
// full descriptor (with env/headers). Otherwise we fall back to the parsed CLI line,
|
|
65
65
|
// which at minimum gives us name + transport + url (or triggers `claude mcp get` for
|
|
66
66
|
// stdio command tokenisation).
|
|
67
|
-
export async function listMcpServers({ claudeBin = 'claude' } = {}) {
|
|
67
|
+
export async function listMcpServers({ claudeBin = 'claude', projectRoot } = {}) {
|
|
68
68
|
const [{ stdout }, mcpServers] = await Promise.all([
|
|
69
69
|
execClaude(claudeBin, ['mcp', 'list'], { encoding: 'utf8' }),
|
|
70
|
-
readMcpServers(),
|
|
70
|
+
readMcpServers({ projectRoot }),
|
|
71
71
|
]);
|
|
72
72
|
const cliList = parseMcpListOutput(stdout);
|
|
73
73
|
return cliList.map((cliEntry) => {
|
|
@@ -247,7 +247,7 @@ async function spawnAndQuery({ command, args, env = {} }, serverName) {
|
|
|
247
247
|
await request('initialize', {
|
|
248
248
|
protocolVersion: PROTOCOL_VERSION,
|
|
249
249
|
capabilities: {},
|
|
250
|
-
clientInfo: { name: 'spotter', version: '0.
|
|
250
|
+
clientInfo: { name: 'spotter', version: '0.10.0' },
|
|
251
251
|
});
|
|
252
252
|
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
253
253
|
initializedSent = true;
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// of `claude mcp list` / `claude mcp get` hides those secrets. Without them, an HTTP
|
|
7
7
|
// MCP server returns 401 and a stdio MCP server spawns without its API key.
|
|
8
8
|
//
|
|
9
|
-
// Scope:
|
|
10
|
-
//
|
|
11
|
-
// (
|
|
12
|
-
//
|
|
9
|
+
// Scope: merges user-level `~/.claude/.mcp.json` with optional project-level
|
|
10
|
+
// `<projectRoot>/.mcp.json`. Project scope overrides user scope on name collision
|
|
11
|
+
// (matches Claude Code's own precedence — more-specific scope wins).
|
|
12
|
+
// `settings.local.json` (local scope) is not yet consulted.
|
|
13
13
|
//
|
|
14
14
|
// This file does NOT read ~/.claude/.credentials.json (Anthropic OAuth token). That
|
|
15
15
|
// remains off-limits per the v0.8.0 design decision. `.mcp.json` is user-authored
|
|
@@ -23,11 +23,13 @@ export function userMcpConfigPath() {
|
|
|
23
23
|
return join(homedir(), '.claude', '.mcp.json');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
export function projectMcpConfigPath(projectRoot) {
|
|
27
|
+
return join(projectRoot, '.mcp.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readOne(path) {
|
|
29
31
|
try {
|
|
30
|
-
const text = await readFile(
|
|
32
|
+
const text = await readFile(path, 'utf8');
|
|
31
33
|
const data = JSON.parse(text);
|
|
32
34
|
return data.mcpServers ?? {};
|
|
33
35
|
} catch (err) {
|
|
@@ -36,6 +38,16 @@ export async function readMcpServers() {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
// Returns the merged `mcpServers` object: user scope as base, project scope overrides
|
|
42
|
+
// on name collision. Missing files are treated as empty (not an error). If projectRoot
|
|
43
|
+
// is not supplied, only user scope is read.
|
|
44
|
+
export async function readMcpServers({ projectRoot } = {}) {
|
|
45
|
+
const user = await readOne(userMcpConfigPath());
|
|
46
|
+
if (!projectRoot) return user;
|
|
47
|
+
const project = await readOne(projectMcpConfigPath(projectRoot));
|
|
48
|
+
return { ...user, ...project };
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
// Normalise an `.mcp.json` entry into a server descriptor the investigator can use.
|
|
40
52
|
// Returns { name, transport: 'stdio'|'http'|'sse', ...transport-specific fields } or
|
|
41
53
|
// null if the entry is not recognisable.
|
package/src/tool-db/refresh.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { localDbPath, globalDbPath } from './loader.mjs';
|
|
|
17
17
|
// - Deferred: from the hardcoded baseline
|
|
18
18
|
//
|
|
19
19
|
// Returns an in-memory snapshot the caller can use as the investigate() backend.
|
|
20
|
-
export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin = 'claude' } = {}) {
|
|
20
|
+
export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin = 'claude', projectRoot } = {}) {
|
|
21
21
|
const snapshot = new Map();
|
|
22
22
|
|
|
23
23
|
// Deferred built-ins (Claude Code deferred tools like WebFetch, TodoWrite, etc.).
|
|
@@ -32,8 +32,9 @@ export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin =
|
|
|
32
32
|
snapshot.set(name, getClaudeAiDescription(name));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// MCP servers (stdio + user-registered HTTP/SSE).
|
|
36
|
-
|
|
35
|
+
// MCP servers (stdio + user-registered HTTP/SSE). projectRoot forwards for
|
|
36
|
+
// project-scope `.mcp.json` merge (see mcp-config.mjs).
|
|
37
|
+
const mcp = await listMcpToolsAll({ logFn, claudeBin, projectRoot });
|
|
37
38
|
for (const [serverName, tools] of mcp.entries()) {
|
|
38
39
|
for (const tool of tools) {
|
|
39
40
|
if (!tool.description || tool.description.length === 0) continue;
|
|
@@ -47,7 +48,7 @@ export async function buildInvestigationSnapshot({ logFn = () => {}, claudeBin =
|
|
|
47
48
|
// Refresh the tool-db. Discovers all currently available tools, resolves each via the
|
|
48
49
|
// 3-tier lookup, writes through. Returns the resolved Map.
|
|
49
50
|
export async function refresh({ projectRoot, logFn = () => {}, claudeBin = 'claude' }) {
|
|
50
|
-
const snapshot = await buildInvestigationSnapshot({ logFn, claudeBin });
|
|
51
|
+
const snapshot = await buildInvestigationSnapshot({ logFn, claudeBin, projectRoot });
|
|
51
52
|
const toolNames = Array.from(snapshot.keys());
|
|
52
53
|
const investigate = async (name) => snapshot.get(name) ?? null;
|
|
53
54
|
|
package/src/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.10.0';
|