A Claude Code plugin that combines ambient background music (YouTube/local), context-aware audio ducking, TTS/SFX notifications, pomodoro timer, and an AI research subagent into a seamless coding companion experience.
Developers spend hours in the terminal with Claude Code, but the experience is silent and disconnected. Muji transforms Claude Code sessions into an immersive, productivity-enhancing environment by:
- Playing continuous background music (YouTube streams or local files) via
mpv - Delivering context-aware TTS announcements and sound effects on key events
- Auto-ducking background music when notifications play
- Running a pomodoro timer with audio cues
- Providing an AI research subagent that works in the background
Muji (package name: muji)
- macOS (primary)
- Linux (primary)
- Windows via WSL (best-effort)
MIT
┌──────────────────────────────────────────────────────────┐
│ Claude Code Plugin │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Hooks │ │ Slash Cmds │ │ Subagents │ │
│ │ │ │ │ │ │ │
│ │ SessionStart │ │ /focus │ │ research-mate │ │
│ │ PostToolUse │ │ /music │ │ │ │
│ │ Stop │ │ /timer │ │ │ │
│ │ TaskCompleted│ │ /mate │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────┬────────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────────▼────────┐ │
│ │ Core Engine (Node.js) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ BGM │ │ Notifier │ │ Pomodoro Timer │ │ │
│ │ │ Manager │ │ │ │ │ │ │
│ │ │ (mpv IPC)│ │ (TTS+SFX)│ │ (node scheduler) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
| Module | Responsibility | Key Dependencies |
|---|---|---|
bgm.js |
Background music playback, YouTube/local, volume control via mpv IPC socket | mpv, yt-dlp |
notify.js |
Audio ducking + SFX playback + TTS generation and playback | mpv (SFX), TTS engine |
pomodoro.js |
Timer management, session tracking, break scheduling | Node.js built-in |
config.js |
Configuration loading, validation, defaults | yaml (npm) |
tts.js |
Multi-engine TTS abstraction layer | edge-tts, espeak-ng, say (macOS), ElevenLabs API, Coqui |
hook-handlers/*.js |
Individual hook event handlers | Core modules above |
- BGM: A single long-running
mpvprocess with--input-ipc-serverfor runtime control - SFX: Short-lived
mpvinstances per sound effect (fire-and-forget) - TTS: Generate audio file → play via short-lived
mpvinstance - Pomodoro: A background Node.js process managed via PID file (
/tmp/muji-pomodoro.pid)
muji/
├── .claude-plugin/
│ └── plugin.json # Plugin metadata
├── commands/
│ ├── focus.md # /focus [deep|write|chill|off]
│ ├── music.md # /music [lofi|jazz|nature|off|<url>]
│ ├── timer.md # /timer [start|stop|status|config]
│ └── mate.md # /mate research <topic>
├── agents/
│ └── research-mate.md # Background research subagent
├── skills/
│ └── focus-tips.md # Contextual productivity tips
├── hooks/
│ └── hooks.json # All hook definitions
├── scripts/
│ ├── core/
│ │ ├── bgm.js # Background music manager
│ │ ├── notify.js # Ducking + SFX + TTS orchestrator
│ │ ├── tts.js # Multi-engine TTS abstraction
│ │ ├── pomodoro.js # Pomodoro timer daemon
│ │ └── config.js # Configuration loader
│ ├── handlers/
│ │ ├── on-session-start.js
│ │ ├── on-bash-complete.js
│ │ ├── on-file-write.js
│ │ ├── on-task-done.js
│ │ ├── on-stop.js
│ │ └── on-teammate-idle.js
│ └── cli/
│ ├── setup.js # Dependency checker & installer
│ └── reset.js # Kill all managed processes
├── sounds/
│ ├── chime-soft.wav # Session start
│ ├── success.wav # Commit/push success
│ ├── success-big.wav # Major milestone
│ ├── warn-soft.wav # Test failure, lint error
│ ├── error.wav # Build/runtime error
│ ├── knock.wav # Subagent completed
│ ├── bell.wav # Pomodoro end
│ ├── bell-soft.wav # Break end
│ └── tick.wav # Pomodoro 5-min warning
├── config/
│ └── default.yaml # Default configuration
├── package.json
├── DESIGN.md
└── README.md
~/.claude/.muji/config.yaml
Falls back to config/default.yaml if user config doesn't exist.
# ============================================================
# Muji — Configuration
# ============================================================
# ----------------------------------------------------------
# Language & Locale
# ----------------------------------------------------------
language: en # Default language for TTS
# Supported: en, ko, ja, zh, es, fr, de, pt, ru
# This affects TTS voice selection and notification messages
# ----------------------------------------------------------
# TTS (Text-to-Speech) Engine
# ----------------------------------------------------------
tts:
engine: edge-tts # Primary TTS engine
# Options: edge-tts, elevenlabs, coqui, espeak, system
# - edge-tts: Free, natural, requires internet (recommended)
# - elevenlabs: Premium quality, requires API key, paid
# - coqui: Free, offline, moderate quality
# - espeak: Free, offline, robotic quality
# - system: macOS 'say' / Linux 'spd-say'
fallback_engine: system # Fallback if primary fails
cache_enabled: true # Cache generated TTS audio files
cache_dir: ~/.claude/.muji/tts-cache
cache_max_mb: 200 # Max cache size in MB
# Per-engine configuration
engines:
edge-tts:
voices:
en: en-US-AriaNeural # English (female, natural)
ko: ko-KR-SunHiNeural # Korean (female, natural)
ja: ja-JP-NanamiNeural # Japanese (female, natural)
zh: zh-CN-XiaoxiaoNeural # Chinese (female, natural)
es: es-ES-ElviraNeural # Spanish
fr: fr-FR-DeniseNeural # French
de: de-DE-KatjaNeural # German
pt: pt-BR-FranciscaNeural # Portuguese
ru: ru-RU-SvetlanaNeural # Russian
elevenlabs:
api_key: "" # Set via env ELEVENLABS_API_KEY or here
voice_id: 21m00Tcm4TlvDq8ikWAM # Default voice (Rachel)
model_id: eleven_flash_v2_5
# Custom voices per language (optional)
voices:
en: "" # Use default voice_id if empty
ko: ""
coqui:
model: tts_models/en/ljspeech/tacotron2-DDC
# Note: limited language support, mainly English
espeak:
voices:
en: en
ko: ko
ja: ja
zh: zh
es: es
fr: fr
de: de
system:
# macOS 'say' voices
macos_voices:
en: Samantha
ko: Yuna
ja: Kyoko
zh: Ting-Ting
# Linux: uses spd-say with language codes
# ----------------------------------------------------------
# Background Music (BGM)
# ----------------------------------------------------------
bgm:
enabled: true
default_mode: lofi # Default music mode on session start
volume: 30 # Default volume (0-100)
# Auto-start BGM when Claude Code session begins
auto_start: true
# Music modes — each maps to a YouTube URL, playlist, or local path
modes:
lofi:
name: "Lo-Fi Hip Hop"
sources:
- https://www.youtube.com/watch?v=jfKfPfyJRdk # lofi hip hop radio
fallback_local: null # Optional local file fallback
jazz:
name: "Jazz & Rain"
sources:
- https://www.youtube.com/watch?v=rUxyKA_-grg
fallback_local: null
nature:
name: "Nature Sounds"
sources:
- https://www.youtube.com/watch?v=lTRiuFIWV54
fallback_local: null
classical:
name: "Classical Focus"
sources:
- https://www.youtube.com/watch?v=jgpJVI3tDbY
fallback_local: null
silence:
name: "Silence"
sources: []
# mpv configuration
mpv:
socket_path: /tmp/muji-bgm-socket
extra_args:
- "--no-video"
- "--really-quiet"
- "--loop=inf"
# ----------------------------------------------------------
# Sound Effects (SFX)
# ----------------------------------------------------------
sfx:
enabled: true
volume: 80 # SFX volume (0-100)
# Override sound files per event (relative to sounds/ dir or absolute path)
# Set to null to disable specific sounds
events:
session_start: chime-soft.wav
commit_success: success.wav
push_success: success-big.wav
test_fail: warn-soft.wav
build_success: success.wav
build_fail: error.wav
lint_error: warn-soft.wav
subagent_done: knock.wav
pomodoro_end: bell.wav
pomodoro_warning: tick.wav
break_end: bell-soft.wav
error_generic: error.wav
# ----------------------------------------------------------
# Notifications (TTS Messages)
# ----------------------------------------------------------
notifications:
enabled: true
# Ducking: lower BGM volume during TTS/SFX playback
ducking:
enabled: true
duck_volume: 10 # BGM volume during notification (0-100)
fade_duration_ms: 300 # Fade in/out duration in milliseconds
fade_steps: 6 # Number of volume steps during fade
# Message templates per event
# Use {variable} for dynamic content
# Define per language; falls back to 'en' if current language not found
messages:
session_start:
en: "Let's get started."
ko: "같이 시작해볼까?"
ja: "始めよう。"
commit_success:
en: null # null = SFX only, no TTS
ko: null
push_success:
en: "Push complete."
ko: "푸시 완료!"
test_fail:
en: "{count} tests failed."
ko: "테스트 {count}개 실패했어."
build_success:
en: "Build complete, no errors."
ko: "빌드 끝났어, 에러 없어."
build_fail:
en: "Build failed. Check the output."
ko: "빌드 실패했어. 확인해봐."
subagent_done:
en: "Research is ready."
ko: "리서치 정리해뒀어."
pomodoro_end:
en: "25 minutes up. Take a break."
ko: "25분 지났어, 쉬어가자."
pomodoro_warning:
en: "5 minutes left."
ko: "5분 남았어."
break_end:
en: "Break's over. Ready to continue?"
ko: "다시 시작할까?"
task_completed:
en: "Task done."
ko: "작업 끝났어."
session_end:
en: "Good work today."
ko: "오늘 수고했어."
error_generic:
en: "An error occurred."
ko: "에러 발생했어."
# ----------------------------------------------------------
# Pomodoro Timer
# ----------------------------------------------------------
pomodoro:
enabled: true
work_minutes: 25
short_break_minutes: 5
long_break_minutes: 15
sessions_before_long_break: 4
auto_start_break: true # Automatically start break after work
auto_start_work: false # Require manual start for next work session
# Music mode overrides during pomodoro phases
music_override:
work: null # null = keep current mode
short_break: nature
long_break: nature
# ----------------------------------------------------------
# Focus Modes (Presets)
# ----------------------------------------------------------
focus_modes:
deep:
name: "Deep Work"
description: "Minimal distraction, lo-fi music, notifications reduced"
bgm_mode: lofi
bgm_volume: 25
notifications_enabled: true
tts_enabled: false # SFX only in deep mode
pomodoro_auto_start: true
write:
name: "Writing Mode"
description: "Jazz and rain, gentle notifications"
bgm_mode: jazz
bgm_volume: 20
notifications_enabled: true
tts_enabled: true
pomodoro_auto_start: false
chill:
name: "Chill Mode"
description: "Relaxed pace, nature sounds, all notifications"
bgm_mode: nature
bgm_volume: 35
notifications_enabled: true
tts_enabled: true
pomodoro_auto_start: false
# ----------------------------------------------------------
# Advanced
# ----------------------------------------------------------
advanced:
pid_file: /tmp/muji-pomodoro.pid
log_file: ~/.claude/.muji/muji.log
log_level: warn # debug, info, warn, error
# Pattern detection for bash commands
patterns:
git_commit: "git commit"
git_push: "git push"
test_commands:
- "npm test"
- "npx jest"
- "pytest"
- "cargo test"
- "go test"
- "mix test"
- "bundle exec rspec"
build_commands:
- "npm run build"
- "npx next build"
- "cargo build"
- "go build"
- "make"
- "gradle build"
lint_commands:
- "npm run lint"
- "eslint"
- "pylint"
- "cargo clippy"- Start/stop/switch background music via mpv
- Manage mpv process lifecycle
- Expose IPC interface for volume control and track switching
class BGMManager {
constructor(config)
async start(mode?: string) // Start playback (default: config.bgm.default_mode)
async stop() // Stop playback, kill mpv process
async switchMode(mode: string) // Switch to different mode (stop + start)
async playUrl(url: string) // Play arbitrary YouTube URL or local path
async setVolume(level: number) // Set volume 0-100
async getVolume(): number // Get current volume
async fadeVolume(target: number, durationMs: number) // Smooth volume transition
async pause() // Toggle pause
async resume() // Resume playback
isPlaying(): boolean // Check if mpv is running
// Internal
_spawnMpv(source: string)
_sendIPC(command: any[]) // Send JSON IPC command to mpv socket
_cleanup() // Kill mpv process, remove socket
}mpv IPC via Unix domain socket at config.bgm.mpv.socket_path:
// Set volume
_sendIPC(['set_property', 'volume', 30])
// Pause/resume
_sendIPC(['cycle', 'pause'])
// Get property
_sendIPC(['get_property', 'volume'])
// Load new file
_sendIPC(['loadfile', url, 'replace'])- If mpv is not installed → log warning, disable BGM, suggest installation
- If yt-dlp is not installed → log warning, YouTube URLs fail gracefully, local files still work
- If mpv process dies → attempt restart once, then disable BGM for session
- If YouTube URL fails → try next source in list, then try fallback_local
- Provide a unified interface for all TTS engines
- Handle language-based voice selection
- Manage TTS audio caching
- Fall back to secondary engine on failure
class TTSEngine {
constructor(config)
async synthesize(text: string, lang?: string): string // Returns path to audio file
async getAvailableEngines(): string[] // List installed engines
async testEngine(engine: string): boolean // Test if engine works
// Internal
_synthesizeEdgeTTS(text: string, voice: string): string
_synthesizeElevenLabs(text: string, voiceId: string): string
_synthesizeCoqui(text: string, model: string): string
_synthesizeEspeak(text: string, voice: string): string
_synthesizeSystem(text: string, voice: string): string
_getCachedPath(text: string, engine: string, voice: string): string | null
_saveToCache(audioPath: string, text: string, engine: string, voice: string)
_cleanCache() // Enforce cache_max_mb limit (LRU)
}# edge-tts (Python package)
edge-tts --voice "en-US-AriaNeural" --text "Hello" --write-media /tmp/muji-tts.mp3
# ElevenLabs (curl)
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" \
-H "xi-api-key: {api_key}" \
-H "Content-Type: application/json" \
-d '{"text":"Hello","model_id":"eleven_flash_v2_5"}' \
--output /tmp/muji-tts.mp3
# Coqui TTS (Python package)
tts --text "Hello" --model_name "tts_models/en/ljspeech/tacotron2-DDC" --out_path /tmp/muji-tts.wav
# espeak-ng
espeak-ng -v en -w /tmp/muji-tts.wav "Hello"
# macOS system
say -v Samantha -o /tmp/muji-tts.aiff "Hello"
# Then convert: ffmpeg -i /tmp/muji-tts.aiff /tmp/muji-tts.mp3
# Linux system
spd-say -l en -e "Hello" # Direct playback, no file output
# Alternative: pico2wave -l en-US -w /tmp/muji-tts.wav "Hello"Cache key: SHA256(engine + voice + text) → filename
Storage: ~/.claude/.muji/tts-cache/{hash}.mp3
Eviction: LRU when total size exceeds cache_max_mb
- Orchestrate the notification flow: duck BGM → play SFX → play TTS → restore BGM
- Resolve message templates with variables
- Handle concurrent notification queuing
class Notifier {
constructor(config, bgmManager, ttsEngine)
async notify(event: string, vars?: object) // Main notification method
async playSFX(soundFile: string) // Play sound effect only
async speak(text: string, lang?: string) // TTS only
// Internal
_resolveMessage(event: string, vars: object): string | null
_duckAndRestore(callback: () => Promise<void>)
_queueNotification(fn: () => Promise<void>) // Serialize concurrent notifications
}1. Check if event has SFX → resolve sound file path
2. Check if event has TTS message → resolve template with variables
3. If either exists:
a. Duck BGM volume (fade from current → duck_volume)
b. Play SFX (await completion)
c. Play TTS (await completion)
d. Restore BGM volume (fade from duck_volume → original)
4. If neither → no-op
Notifications are queued sequentially. If a notification is already playing, the next one waits. This prevents overlapping audio and ensures ducking works correctly.
// Internal queue implementation
_notificationQueue = Promise.resolve();
_queueNotification(fn) {
this._notificationQueue = this._notificationQueue
.then(() => fn())
.catch(err => logger.error('Notification failed:', err));
}- Manage work/break cycles
- Trigger notifications at appropriate times
- Optionally switch BGM modes during breaks
- Track session statistics
class PomodoroTimer {
constructor(config, notifier, bgmManager)
start() // Start work session
stop() // Stop timer entirely
skip() // Skip current phase (work → break or break → work)
pause() // Pause timer
resume() // Resume timer
status(): PomodoroStatus // Current state
// Internal
_onWorkEnd()
_onBreakEnd()
_onWarning() // 5 minutes before end
_savePID()
_removePID()
}
interface PomodoroStatus {
phase: 'work' | 'short_break' | 'long_break' | 'stopped'
remaining_seconds: number
session_number: number
total_sessions_today: number
is_paused: boolean
}The pomodoro timer runs as a daemon process:
# Start: spawns detached node process
node scripts/core/pomodoro.js start &
echo $! > /tmp/muji-pomodoro.pid
# Stop: reads PID and kills
kill $(cat /tmp/muji-pomodoro.pid)
# Status: communicates via temp file
cat /tmp/muji-pomodoro-status.jsonThe pomodoro daemon communicates with hook handlers via:
- Status file:
/tmp/muji-pomodoro-status.json(polled by/timer status) - Notifications: Directly invokes the Notifier module
class Config {
constructor()
load(): object // Load and merge configs
get(path: string): any // Dot-notation access: config.get('tts.engine')
getLanguage(): string // Current language
getTTSVoice(engine, lang): string
getBGMSources(mode): string[]
getSFXPath(event): string | null
getMessage(event, lang, vars): string | null
// Internal
_loadDefault(): object
_loadUser(): object | null
_merge(defaults, user): object
_validate(config): void
}{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-session-start.js"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-bash-complete.js"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-file-write.js"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-stop.js"
}
]
}
],
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-task-done.js"
}
]
}
],
"TeammateIdle": [
{
"hooks": [
{
"type": "command",
"command": "node $PLUGIN_DIR/scripts/handlers/on-teammate-idle.js"
}
]
}
]
}
}Trigger: SessionStart
Actions:
1. Load config
2. Run dependency check (mpv, yt-dlp, TTS engine)
3. If bgm.auto_start → start BGM with default mode
4. If pomodoro.enabled → do NOT auto-start (user triggers via /timer)
5. Play session_start notification (SFX + TTS)
Trigger: PostToolUse (matcher: Bash)
Input (stdin JSON):
- tool_input.command: string # The bash command that was run
- tool_response.stdout: string # Command stdout
- tool_response.stderr: string # Command stderr
Logic:
Parse command string against config.advanced.patterns:
IF matches git_commit AND no stderr:
→ notify('commit_success')
IF matches git_push AND no stderr:
→ notify('push_success')
IF matches test_commands:
IF stderr contains failure indicators:
Parse failure count from output
→ notify('test_fail', { count })
ELSE:
→ notify('build_success') # tests passed
IF matches build_commands:
IF stderr contains error:
→ notify('build_fail')
ELSE:
→ notify('build_success')
IF matches lint_commands AND stderr:
→ notify('lint_error')
Trigger: PostToolUse (matcher: Write|Edit)
Actions:
Currently a no-op (silent file writes).
Future: track file write frequency for activity monitoring.
Trigger: TaskCompleted
Actions:
1. Notify('task_completed')
Trigger: Stop
Actions:
1. Notify('session_end')
2. Stop pomodoro timer if running
3. Stop BGM
4. Cleanup: remove socket files, PID files
Trigger: TeammateIdle
Actions:
1. Notify('subagent_done')
(This fires when a subagent like research-mate finishes)
# /focus Command
Switch between focus mode presets or turn off all features.
## Usage
- `/focus deep` — Deep work mode: lo-fi, SFX only, pomodoro auto-start
- `/focus write` — Writing mode: jazz + rain, gentle TTS
- `/focus chill` — Chill mode: nature sounds, full notifications
- `/focus off` — Stop BGM, disable notifications
## Implementation
Read focus_modes from config, apply:
1. Switch BGM mode and volume
2. Toggle TTS enabled/disabled
3. Set pomodoro behavior
Confirm mode switch via notification.# /music Command
Control background music playback.
## Usage
- `/music lofi` — Switch to lo-fi mode
- `/music jazz` — Switch to jazz mode
- `/music nature` — Switch to nature sounds
- `/music classical` — Switch to classical
- `/music <url>` — Play any YouTube URL or local file
- `/music off` — Stop music
- `/music volume <n>` — Set volume (0-100)
- `/music status` — Show current playback info
## Implementation
Parse subcommand, call BGMManager methods accordingly.
If URL provided, validate it looks like a YouTube URL or file path.# /timer Command
Manage pomodoro work/break timer.
## Usage
- `/timer start` — Start a 25-min work session
- `/timer stop` — Stop the timer
- `/timer skip` — Skip to next phase
- `/timer pause` — Pause the timer
- `/timer resume` — Resume the timer
- `/timer status` — Show current timer state
- `/timer config <k> <v>`— Adjust settings (e.g., work_minutes 50)
## Implementation
Communicate with pomodoro daemon via PID file and status file.
Start spawns a new daemon if not running.
Status reads /tmp/muji-pomodoro-status.json.# /mate Command
Dispatch background research tasks to a subagent.
## Usage
- `/mate research <topic>` — Start background research on a topic
## Implementation
Launches the research-mate subagent with the given topic.
The subagent runs in a separate context, performs web search and
summarization, and saves results to a temp file.
When done, TeammateIdle hook fires → notification plays.
The user can then ask Claude about the research results.# Research Mate Agent
You are a background research assistant. Your job is to:
1. Research the given topic thoroughly
2. Summarize findings in a clear, structured format
3. Save results where the main session can access them
## Guidelines
- Focus on practical, actionable information
- Prioritize recent sources (last 12 months)
- Keep summaries concise: max 500 words
- Include source URLs for verification
- Save output to /tmp/muji-research-output.md
## Output Format
# Research: {topic}
## Key Findings
- Finding 1
- Finding 2
- ...
## Summary
Brief paragraph summarizing the most important takeaways.
## Sources
- [Title](URL)
- ...| Dependency | Purpose | Install |
|---|---|---|
| Node.js ≥ 18 | Plugin runtime | Pre-installed with Claude Code |
| mpv | Audio playback | brew install mpv / apt install mpv |
| yt-dlp | YouTube stream extraction | brew install yt-dlp / pip install yt-dlp |
| socat | mpv IPC communication | brew install socat / apt install socat |
| Engine | Install |
|---|---|
| edge-tts | pip install edge-tts |
| ElevenLabs | API key required, no local install |
| Coqui TTS | pip install TTS |
| espeak-ng | brew install espeak / apt install espeak-ng |
Actions:
1. Check for required dependencies (mpv, yt-dlp, socat)
2. Check for at least one TTS engine
3. Create config directory (~/.claude/.muji/)
4. Copy default config if user config doesn't exist
5. Create TTS cache directory
6. Test audio playback with a short test sound
7. Report status and any missing optional dependencies
# From marketplace (when published)
/plugin install muji
# From GitHub
/plugin install muji@username/muji
# Local development
claude --plugin-dir ./muji| Event | SFX File | TTS Message (en) | TTS Message (ko) |
|---|---|---|---|
| Session start | chime-soft.wav |
"Let's get started." | "같이 시작해볼까?" |
| Git commit (success) | success.wav |
(none) | (none) |
| Git push (success) | success-big.wav |
"Push complete." | "푸시 완료!" |
| Test failure | warn-soft.wav |
"{count} tests failed." | "테스트 {count}개 실패했어." |
| Build success | success.wav |
"Build complete, no errors." | "빌드 끝났어, 에러 없어." |
| Build failure | error.wav |
"Build failed. Check the output." | "빌드 실패했어. 확인해봐." |
| Lint error | warn-soft.wav |
(none) | (none) |
| Subagent done | knock.wav |
"Research is ready." | "리서치 정리해뒀어." |
| Pomodoro end | bell.wav |
"25 minutes up. Take a break." | "25분 지났어, 쉬어가자." |
| Pomodoro 5-min warning | tick.wav |
"5 minutes left." | "5분 남았어." |
| Break end | bell-soft.wav |
"Break's over. Ready to continue?" | "다시 시작할까?" |
| Task completed | success.wav |
"Task done." | "작업 끝났어." |
| Session end | chime-soft.wav |
"Good work today." | "오늘 수고했어." |
| Generic error | error.wav |
"An error occurred." | "에러 발생했어." |
- Project scaffolding (package.json, plugin.json, directory structure)
config.js— Config loader with defaults and mergingbgm.js— mpv process management and IPC/musiccommand — Basic BGM control
tts.js— Multi-engine TTS with cachingnotify.js— Ducking + SFX + TTS orchestration- Hook handlers — SessionStart, PostToolUse (Bash), Stop
- Sound effect files — Source or generate .wav files
pomodoro.js— Timer daemon with notifications/timercommand/focuscommand — Mode presets- Music override during breaks
research-matesubagent definition/matecommand- TeammateIdle hook handler
setup.js— Dependency checker and onboardingreset.js— Process cleanup utility- Error handling hardening
- README.md with installation and usage guide
- Publish to plugin marketplace
# Send command
echo '{"command":["set_property","volume",30]}' | socat - /tmp/muji-bgm-socket
# Read response
echo '{"command":["get_property","volume"]}' | socat - /tmp/muji-bgm-socket
# Response: {"data":30.0,"error":"success"}In Node.js, use net.connect() for the Unix socket instead of shelling out to socat for better performance:
const net = require('net');
function sendMpvCommand(socketPath, command) {
return new Promise((resolve, reject) => {
const client = net.connect(socketPath);
let data = '';
client.on('data', chunk => data += chunk);
client.on('end', () => resolve(JSON.parse(data)));
client.on('error', reject);
client.write(JSON.stringify({ command }) + '\n');
// mpv sends response then we can end
setTimeout(() => client.end(), 100);
});
}yt-dlpis required for YouTube URL resolution- mpv calls yt-dlp internally when given a YouTube URL
- Live streams (24/7 radio) work with
--loop=infbut mpv handles reconnection - If network drops, mpv will retry; if it fails, BGM stops silently
- Legal note: YouTube ToS technically prohibits stream extraction. For personal use this is widely practiced, but the README should include a disclaimer
| Platform | mpv | TTS (system) | Notifications |
|---|---|---|---|
| macOS | brew install mpv |
say command |
Native via osascript |
| Linux | apt install mpv |
spd-say or pico2wave |
notify-send |
| WSL | Requires PulseAudio bridge | espeak-ng |
Limited |
For the MVP, use royalty-free sounds from:
- Pixabay Sound Effects (free, no attribution required)
- Freesound.org (CC0 or CC-BY)
- Generate with
ffmpeg(simple tones):
# Generate a simple chime (440Hz, 0.3s, fade out)
ffmpeg -f lavfi -i "sine=frequency=440:duration=0.3" -af "afade=t=out:st=0.1:d=0.2" sounds/chime-soft.wav
# Generate a success sound (two ascending tones)
ffmpeg -f lavfi -i "sine=frequency=523:duration=0.15" -f lavfi -i "sine=frequency=659:duration=0.15" \
-filter_complex "[0][1]concat=n=2:v=0:a=1,afade=t=out:st=0.2:d=0.1" sounds/success.wav
# Generate a warning tone (lower pitch)
ffmpeg -f lavfi -i "sine=frequency=330:duration=0.4" -af "afade=t=out:st=0.2:d=0.2" sounds/warn-soft.wav{
"name": "muji",
"version": "0.1.0",
"description": "AI-powered coding companion with ambient music, TTS notifications, and pomodoro timer for Claude Code",
"main": "scripts/core/config.js",
"scripts": {
"setup": "node scripts/cli/setup.js",
"reset": "node scripts/cli/reset.js",
"test": "node --test tests/"
},
"dependencies": {
"yaml": "^2.4.0"
},
"devDependencies": {},
"engines": {
"node": ">=18.0.0"
},
"keywords": [
"claude-code",
"plugin",
"ambient",
"lofi",
"focus",
"pomodoro",
"tts",
"productivity"
],
"license": "MIT"
}{
"name": "muji",
"version": "0.1.0",
"description": "Ambient music, TTS notifications, pomodoro timer, and AI research companion for Claude Code",
"author": "",
"homepage": "",
"commands": ["focus", "music", "timer", "mate"],
"agents": ["research-mate"]
}