feat: add vmux setup and vmux hook for Claude Code integration
- vmux setup: idempotently adds hooks to ~/.claude/settings.json - vmux hook: receives Claude Code hook events via env vars and forwards to daemon - Daemon handles "hook" action on Unix socket (no HTTP server needed) - Hooks provide instant state detection (permission/question/idle)
This commit is contained in:
@@ -452,6 +452,15 @@ func (d *Daemon) handleConnection(conn net.Conn) {
|
|||||||
d.focus.Set(time.Duration(args.Minutes) * time.Minute)
|
d.focus.Set(time.Duration(args.Minutes) * time.Minute)
|
||||||
writeResponse(conn, Response{OK: true, FocusRemaining: d.focus.Remaining().Minutes()})
|
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":
|
case "stop":
|
||||||
writeResponse(conn, Response{OK: true})
|
writeResponse(conn, Response{OK: true})
|
||||||
d.Stop()
|
d.Stop()
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -148,6 +148,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
runI3Bar(sockPath)
|
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":
|
case "daemon":
|
||||||
runDaemon(sockPath)
|
runDaemon(sockPath)
|
||||||
|
|
||||||
@@ -208,8 +220,8 @@ Commands:
|
|||||||
label <id> <text> Assign a label to a session
|
label <id> <text> Assign a label to a session
|
||||||
focus <minutes> Suppress notifications for N minutes
|
focus <minutes> Suppress notifications for N minutes
|
||||||
i3bar Output i3bar JSON (use as status_command in i3 config)
|
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
|
stop Stop the vmux daemon
|
||||||
daemon Run the daemon in foreground (used internally)
|
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
|
|||||||
106
setup.go
Normal file
106
setup.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user