Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions .claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,36 @@ nmake clean # クリーンアップ
```
claudecode-notifier/
├── src/
│ ├── main.cpp - メインアプリケーションロジック
│ ├── tts_speaker.cpp - SAPI 5.0 音声合成実装
│ ├── tts_speaker.h - TTS スピーカーヘッダー
│ ├── json_parser.cpp - JSON 解析ロジック
│ ├── json_parser.h - JSON パーサーヘッダー
│ └── picojson.h - PicoJSON ライブラリ
│ ├── main.cpp - エントリポイント/引数分岐(--daemon, --ensure-daemon, ワンショット)
│ ├── daemon.cpp/.h - デーモンモードの HTTP サーバー(/hook, /health)
│ ├── event_dispatch.cpp/.h - フックイベント→読み上げのマッピング、GenerateNotificationMessage
│ ├── speech_queue.cpp/.h - 非同期 SAPI 音声+バージイン対応キュー(専用ワーカースレッド)
│ ├── text_shaper.cpp/.h - 読み上げ用テキスト整形(markdown/コードブロック)
│ ├── config.cpp/.h - デーモン設定(%APPDATA%\claudecode-notifier\config.json)
│ ├── tts_speaker.cpp/.h - SAPI 5.0 音声合成(レガシーなワンショット経路)
│ ├── json_parser.cpp/.h - JSON 解析ロジック
│ ├── picojson.h - PicoJSON ライブラリ
│ └── httplib.h - cpp-httplib(HTTP サーバー/クライアント)
├── build/ - ビルド成果物
└── Makefile - ビルド設定
```

## デーモンモード(ストリーミング読み上げ)

- `--daemon`: 常駐起動。SAPI 音声1つ+読み上げキューを保持し、`http://host:port` で待ち受け。`type:"http"` フックが各イベントを `/hook` に POST する。
- `--ensure-daemon`: `/health` を見て未起動なら detached でデーモンを起動。`SessionStart` フックから呼ぶ。
- 引数なし: 従来の stdin ワンショット(後方互換)。
- `MessageDisplay` の `delta`(応答+中間テキスト)、`PreToolUse` の `tool_name`/`tool_input`(ツール/コマンド/ファイル)、`Notification`/`Stop` を読み上げ。`turn_id` 変化や `UserPromptSubmit` でキューをフラッシュ(バージイン)。
- 日本語文字列リテラルのため `/utf-8` でビルドし、`MultiByteToWideChar(CP_UTF8)` で UTF-16 に変換して SAPI へ渡す。

## 技術詳細

- **言語**: Visual C++ による C++
- **音声**: Windows Speech API (SAPI) 5.0
- **JSON 解析**: PicoJSON ライブラリ使用
- **ビルドシステム**: Visual Studio コンパイラによる nmake
- **依存関係**: Windows システムライブラリ(ole32.lib、oleaut32.lib、sapi.lib)
- **HTTP(デーモンモード)**: cpp-httplib(単一ヘッダ、MIT)
- **ビルドシステム**: Visual Studio コンパイラによる nmake(`/utf-8`)
- **依存関係**: Windows システムライブラリ(ole32.lib、oleaut32.lib、sapi.lib、ws2_32.lib)

## 入力形式

Expand Down
41 changes: 31 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Makefile for Claude Code Notifier

# Compiler and flags
# /utf-8 makes narrow string literals UTF-8 so Japanese text round-trips
# correctly through CP_UTF8 -> UTF-16 before being handed to SAPI.
CXX = cl
CXXFLAGS = /EHsc /std:c++17 /W3
LDFLAGS = ole32.lib oleaut32.lib sapi.lib
CXXFLAGS = /EHsc /std:c++17 /W3 /utf-8
LDFLAGS = ole32.lib oleaut32.lib sapi.lib ws2_32.lib

# Directories
SRC_DIR = src
BUILD_DIR = build
TARGET = claudecode-notifier.exe

# Source files
SOURCES = $(SRC_DIR)\main.cpp $(SRC_DIR)\tts_speaker.cpp $(SRC_DIR)\json_parser.cpp

# Object files
OBJECTS = $(BUILD_DIR)\main.obj $(BUILD_DIR)\tts_speaker.obj $(BUILD_DIR)\json_parser.obj
OBJECTS = $(BUILD_DIR)\main.obj \
$(BUILD_DIR)\tts_speaker.obj \
$(BUILD_DIR)\json_parser.obj \
$(BUILD_DIR)\speech_queue.obj \
$(BUILD_DIR)\text_shaper.obj \
$(BUILD_DIR)\config.obj \
$(BUILD_DIR)\event_dispatch.obj \
$(BUILD_DIR)\daemon.obj

# Default target
all: $(BUILD_DIR) $(TARGET)
Expand All @@ -28,22 +34,37 @@ $(TARGET): $(OBJECTS)
$(CXX) $(OBJECTS) /Fe:$(TARGET) $(LDFLAGS)

# Compile source files
$(BUILD_DIR)\main.obj: $(SRC_DIR)\main.cpp $(SRC_DIR)\tts_speaker.h $(SRC_DIR)\json_parser.h
$(BUILD_DIR)\main.obj: $(SRC_DIR)\main.cpp $(SRC_DIR)\tts_speaker.h $(SRC_DIR)\json_parser.h $(SRC_DIR)\event_dispatch.h $(SRC_DIR)\daemon.h $(SRC_DIR)\config.h $(SRC_DIR)\httplib.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\main.cpp /Fo:$(BUILD_DIR)\main.obj

$(BUILD_DIR)\tts_speaker.obj: $(SRC_DIR)\tts_speaker.cpp $(SRC_DIR)\tts_speaker.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\tts_speaker.cpp /Fo:$(BUILD_DIR)\tts_speaker.obj

$(BUILD_DIR)\json_parser.obj: $(SRC_DIR)\json_parser.cpp $(SRC_DIR)\json_parser.h
$(BUILD_DIR)\json_parser.obj: $(SRC_DIR)\json_parser.cpp $(SRC_DIR)\json_parser.h $(SRC_DIR)\picojson.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\json_parser.cpp /Fo:$(BUILD_DIR)\json_parser.obj

$(BUILD_DIR)\speech_queue.obj: $(SRC_DIR)\speech_queue.cpp $(SRC_DIR)\speech_queue.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\speech_queue.cpp /Fo:$(BUILD_DIR)\speech_queue.obj

$(BUILD_DIR)\text_shaper.obj: $(SRC_DIR)\text_shaper.cpp $(SRC_DIR)\text_shaper.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\text_shaper.cpp /Fo:$(BUILD_DIR)\text_shaper.obj

$(BUILD_DIR)\config.obj: $(SRC_DIR)\config.cpp $(SRC_DIR)\config.h $(SRC_DIR)\picojson.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\config.cpp /Fo:$(BUILD_DIR)\config.obj

$(BUILD_DIR)\event_dispatch.obj: $(SRC_DIR)\event_dispatch.cpp $(SRC_DIR)\event_dispatch.h $(SRC_DIR)\speech_queue.h $(SRC_DIR)\text_shaper.h $(SRC_DIR)\json_parser.h $(SRC_DIR)\config.h $(SRC_DIR)\picojson.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\event_dispatch.cpp /Fo:$(BUILD_DIR)\event_dispatch.obj

$(BUILD_DIR)\daemon.obj: $(SRC_DIR)\daemon.cpp $(SRC_DIR)\daemon.h $(SRC_DIR)\httplib.h $(SRC_DIR)\speech_queue.h $(SRC_DIR)\event_dispatch.h $(SRC_DIR)\json_parser.h $(SRC_DIR)\config.h
$(CXX) $(CXXFLAGS) /c $(SRC_DIR)\daemon.cpp /Fo:$(BUILD_DIR)\daemon.obj

# Clean build artifacts
clean:
del /q $(BUILD_DIR)\*.obj 2>nul || echo No object files to clean
del /q $(TARGET) 2>nul || echo No executable to clean
rmdir $(BUILD_DIR) 2>nul || echo Build directory not empty or doesn't exist

# Test target
# Test target (legacy stdin one-shot path)
test: $(TARGET)
@echo Testing claudecode-notifier...
@echo {"session_id":"test123","message":"Claude is waiting for your input","hook_event_name":"Notification","cwd":"C:\\test"} | $(TARGET)
Expand All @@ -56,4 +77,4 @@ help:
@echo test - Build and run a test
@echo help - Show this help message

.PHONY: all clean test help
.PHONY: all clean test help
100 changes: 93 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A Windows command-line application that provides voice notifications for Claude
## Features

- **Voice Notifications**: Uses Windows SAPI 5.0 for text-to-speech announcements
- **Streaming Speech (Daemon Mode)**: Optionally speaks Claude's response text, intermediate messages between tool calls, and tool/command/file activity in real time, so screen-reader users can rely on clean spoken output instead of the terminal's redrawn spinner text (see [Streaming Speech](#streaming-speech-daemon-mode))
- **Hook Integration**: Designed to work with Claude Code's notification hooks
- **Session Awareness**: Identifies sessions by their ID for clear notifications
- **Multiple Message Types**: Handles various notification scenarios:
Expand Down Expand Up @@ -151,6 +152,81 @@ You can test the application manually by piping JSON data to it:
echo {"session_id":"test123","message":"Claude is waiting for your input","hook_event_name":"Notification","cwd":"C:\\test"} | claudecode-notifier.exe
```

## Streaming Speech (Daemon Mode)

The classic mode above speaks short status cues ("waiting", "stopped"), spawning one process per hook. **Daemon mode** additionally speaks Claude's actual output — response text, intermediate messages between tool calls, and tool/command/file activity — by intercepting hook events and routing them to SAPI. This is aimed at screen-reader users who want clean spoken output instead of the terminal's redrawn/spinner text.

### How it works

A resident process (`claudecode-notifier.exe --daemon`) holds a single SAPI voice plus an async speech queue and listens on `http://127.0.0.1:8765`. Claude Code hooks of `type: "http"` POST each event to it. Because no process is spawned per event, the high-frequency `MessageDisplay` stream is handled smoothly. When a new turn or a new prompt starts, the queue is flushed so speech follows the latest output (barge-in).

| Hook | Spoken |
|------|--------|
| `MessageDisplay` | Assistant response + intermediate text (streamed via `delta`) |
| `PreToolUse` | "Reading file X", "Running command …", etc. from `tool_name` / `tool_input` |
| `PostToolUse` | Tool completion (off by default) |
| `Notification` | Waiting / permission cues |
| `Stop` | Turn finished |
| `UserPromptSubmit` | No speech — triggers a barge-in flush |

Tool announcements and the code-block notice are spoken in Japanese.

### Start the daemon

The daemon auto-starts from a `SessionStart` hook (`--ensure-daemon` launches it only if it isn't already running). You can also start it manually or add it to Windows startup:

```cmd
claudecode-notifier.exe --daemon
```

Verify it is up: `curl http://127.0.0.1:8765/health` returns `ok`.

### settings.json (HTTP hooks)

Each hook block is independent — remove one to silence that source.

```json
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "/home/you/.local/bin/claudecode-notifier", "args": ["--ensure-daemon"] }] }],
"MessageDisplay": [{ "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }],
"PreToolUse": [{ "matcher": "", "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }],
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }],
"Notification": [{ "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }],
"Stop": [{ "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }],
"UserPromptSubmit":[{ "hooks": [{ "type": "http", "url": "http://127.0.0.1:8765/hook" }] }]
}
}
```

### Configuration

Finer control lives in `%APPDATA%\claudecode-notifier\config.json` (all keys optional; defaults shown):

```json
{
"host": "127.0.0.1",
"port": 8765,
"speak_messages": true,
"speak_tool_use": true,
"speak_tool_result": false,
"speak_notifications": true,
"speak_stop": true,
"strip_markdown": true,
"speak_code_blocks": false,
"voice_rate": 0
}
```

### WSL networking

The HTTP request originates from Claude Code (inside WSL) and targets the Windows daemon, so WSL must be able to reach the Windows host. (The `--ensure-daemon` health check and the daemon itself are Windows processes, so they are unaffected.)

- **WSL2 mirrored networking** (Windows 11; `.wslconfig` → `[wsl2]` / `networkingMode=mirrored`): `http://127.0.0.1:8765` works as-is. **Recommended.**
- **NAT mode (default)**: set `"host": "0.0.0.0"` in `config.json` and point the hook `url` at the Windows host IP reachable from WSL (e.g. the default gateway from `ip route show default`).

The daemon must run as a Windows process because SAPI is Windows-only.

## How It Works

1. **Input**: Receives JSON data from Claude Code via stdin containing session information
Expand Down Expand Up @@ -178,11 +254,16 @@ The Stop hook runs immediately when the Claude Code agent finishes its response,
```
claudecode-notifier/
├── src/
│ ├── main.cpp - Main application logic
│ ├── tts_speaker.cpp - SAPI 5.0 text-to-speech implementation
│ ├── tts_speaker.h - TTS speaker header
│ ├── main.cpp - Entry point / arg dispatch (--daemon, --ensure-daemon, one-shot)
│ ├── daemon.cpp - HTTP server (hook endpoint) for daemon mode
│ ├── event_dispatch.cpp- Maps hook events to speech actions
│ ├── speech_queue.cpp - Async SAPI voice + speech queue with barge-in
│ ├── text_shaper.cpp - Cleans assistant text for speech (markdown/code)
│ ├── config.cpp - Daemon configuration (config.json)
│ ├── tts_speaker.cpp - SAPI 5.0 text-to-speech (legacy one-shot path)
│ ├── json_parser.cpp - JSON parsing logic
│ └── json_parser.h - JSON parser header
│ ├── picojson.h - PicoJSON library
│ └── httplib.h - cpp-httplib (HTTP server/client)
├── build/ - Build artifacts (created during build)
├── Makefile - Build configuration
├── README.md - This file
Expand All @@ -195,9 +276,10 @@ claudecode-notifier/

- **Language**: C++ with Visual C++
- **Audio**: Windows Speech API (SAPI) 5.0
- **JSON Parsing**: Custom lightweight parser
- **Build System**: nmake with Visual Studio compiler
- **Dependencies**: Windows system libraries (ole32.lib, oleaut32.lib, sapi.lib)
- **JSON Parsing**: PicoJSON
- **HTTP (daemon mode)**: cpp-httplib
- **Build System**: nmake with Visual Studio compiler (`/utf-8` for correct Japanese text)
- **Dependencies**: Windows system libraries (ole32.lib, oleaut32.lib, sapi.lib, ws2_32.lib)

## Troubleshooting

Expand Down Expand Up @@ -258,6 +340,10 @@ This project uses the following third-party library:
Copyright (c) 2011-2014 Kazuho Oku
Licensed under the BSD-2-Clause License

- **cpp-httplib** (https://github.com/yhirose/cpp-httplib)
Copyright (c) 2024 Yuji Hirose
Licensed under the MIT License

## Related Documentation

- [Claude Code Hooks Documentation](https://docs.anthropic.com/en/docs/claude-code/hooks)
Expand Down
Loading