diff --git a/daemon.go b/daemon.go index 7b7ac13..76bd0dc 100644 --- a/daemon.go +++ b/daemon.go @@ -452,6 +452,15 @@ func (d *Daemon) handleConnection(conn net.Conn) { d.focus.Set(time.Duration(args.Minutes) * time.Minute) writeResponse(conn, Response{OK: true, FocusRemaining: d.focus.Remaining().Minutes()}) + case "hook": + var event HookEvent + if err := json.Unmarshal(req.Args, &event); err != nil { + writeResponse(conn, Response{Error: "invalid hook args: " + err.Error()}) + return + } + d.processHookEvent(event) + writeResponse(conn, Response{OK: true}) + case "stop": writeResponse(conn, Response{OK: true}) d.Stop() diff --git a/main.go b/main.go index a3c6549..f464fc2 100644 --- a/main.go +++ b/main.go @@ -148,6 +148,18 @@ func main() { } runI3Bar(sockPath) + case "hook": + event := parseHookEnv() + if event.HookEventName == "" { + os.Exit(0) + } + client := NewClient(sockPath) + hookArgs, _ := json.Marshal(event) + client.Send(Request{Action: "hook", Args: hookArgs}) + + case "setup": + runSetup() + case "daemon": runDaemon(sockPath) @@ -208,8 +220,8 @@ Commands: label Assign a label to a session focus Suppress notifications for N minutes i3bar Output i3bar JSON (use as status_command in i3 config) + setup Add vmux hooks to Claude Code settings (idempotent) stop Stop the vmux daemon - daemon Run the daemon in foreground (used internally) Flags: --no-color Disable colored output diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..2362d47 --- /dev/null +++ b/setup.go @@ -0,0 +1,106 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +// parseHookEnv builds a HookEvent from Claude Code hook environment variables. +func parseHookEnv() HookEvent { + return HookEvent{ + SessionID: os.Getenv("SESSION_ID"), + Cwd: os.Getenv("SESSION_CWD"), + HookEventName: os.Getenv("HOOK_EVENT_NAME"), + NotificationType: os.Getenv("NOTIFICATION_TYPE"), + ToolName: os.Getenv("TOOL_NAME"), + } +} + +// runSetup adds vmux hooks to ~/.claude/settings.json (idempotent). +func runSetup() { + home := os.Getenv("HOME") + settingsPath := filepath.Join(home, ".claude", "settings.json") + + vmuxBin, err := os.Executable() + if err != nil { + vmuxBin = filepath.Join(home, "Code", "vibe", "vmux", "vmux") + } + + data, err := os.ReadFile(settingsPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot read %s: %v\n", settingsPath, err) + os.Exit(1) + } + + var settings map[string]interface{} + if err := json.Unmarshal(data, &settings); err != nil { + fmt.Fprintf(os.Stderr, "Cannot parse %s: %v\n", settingsPath, err) + os.Exit(1) + } + + hooks, _ := settings["hooks"].(map[string]interface{}) + if hooks == nil { + hooks = make(map[string]interface{}) + settings["hooks"] = hooks + } + + hookCommand := vmuxBin + " hook" + vmuxHook := map[string]interface{}{ + "type": "command", + "command": hookCommand, + "timeout": 5, + } + + events := []string{"Notification", "Stop", "PostToolUse", "PreToolUse"} + changed := false + + for _, event := range events { + entries, _ := hooks[event].([]interface{}) + + if hasVmuxHook(entries, hookCommand) { + continue + } + + entry := map[string]interface{}{ + "hooks": []interface{}{vmuxHook}, + } + + hooks[event] = append(entries, entry) + changed = true + } + + if !changed { + fmt.Println("vmux hooks already configured.") + return + } + + out, err := json.MarshalIndent(settings, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot marshal settings: %v\n", err) + os.Exit(1) + } + + if err := os.WriteFile(settingsPath, out, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "Cannot write %s: %v\n", settingsPath, err) + os.Exit(1) + } + + fmt.Printf("vmux hooks added to %s for events: %v\n", settingsPath, events) + fmt.Println("Restart Claude Code sessions for hooks to take effect.") +} + +func hasVmuxHook(entries []interface{}, hookCommand string) bool { + for _, entry := range entries { + e, _ := entry.(map[string]interface{}) + hookList, _ := e["hooks"].([]interface{}) + for _, h := range hookList { + hm, _ := h.(map[string]interface{}) + if cmd, _ := hm["command"].(string); cmd == hookCommand { + return true + } + } + } + return false +}