feat: implement agent-notify CLI with hooks and tmux passthrough
Add OSC 777 notification sender, Cursor/Claude hook adapters, config/install commands, and README for Ghostty + tmux setup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/longbin/agent-notify/internal/config"
|
||||
)
|
||||
|
||||
const cursorHookCmd = "agent-notify hook cursor stop"
|
||||
const cursorToolCmd = "agent-notify hook cursor tool"
|
||||
const claudeStopCmd = "agent-notify hook claude stop"
|
||||
const claudeIdleCmd = "agent-notify hook claude idle"
|
||||
|
||||
func CursorHooksPath() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".cursor", "hooks.json")
|
||||
}
|
||||
|
||||
func ClaudeSettingsPath() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".claude", "settings.json")
|
||||
}
|
||||
|
||||
func MergeCursorHooks(path string, force bool) error {
|
||||
doc := map[string]any{"version": 1, "hooks": map[string]any{}}
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
_ = json.Unmarshal(data, &doc)
|
||||
}
|
||||
hooks, _ := doc["hooks"].(map[string]any)
|
||||
if hooks == nil {
|
||||
hooks = map[string]any{}
|
||||
doc["hooks"] = hooks
|
||||
}
|
||||
addHook(hooks, "stop", cursorHookCmd, force)
|
||||
cfg, _ := config.LoadDefault()
|
||||
if cfg.Events.Tool {
|
||||
addHook(hooks, "afterShellExecution", cursorToolCmd, force)
|
||||
}
|
||||
return writeJSON(path, doc)
|
||||
}
|
||||
|
||||
func addHook(hooks map[string]any, name, command string, force bool) {
|
||||
if existing, ok := hooks[name]; ok && !force {
|
||||
_ = existing
|
||||
return
|
||||
}
|
||||
hooks[name] = []any{map[string]string{"command": command}}
|
||||
}
|
||||
|
||||
func MergeClaudeSettings(path string, force bool) error {
|
||||
doc := map[string]any{}
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
_ = json.Unmarshal(data, &doc)
|
||||
}
|
||||
hooks, _ := doc["hooks"].(map[string]any)
|
||||
if hooks == nil {
|
||||
hooks = map[string]any{}
|
||||
doc["hooks"] = hooks
|
||||
}
|
||||
setClaudeHook(hooks, "Stop", claudeStopCmd, force)
|
||||
cfg, _ := config.LoadDefault()
|
||||
if cfg.Events.Idle {
|
||||
setClaudeHook(hooks, "Notification", claudeIdleCmd, force)
|
||||
}
|
||||
return writeJSON(path, doc)
|
||||
}
|
||||
|
||||
func setClaudeHook(hooks map[string]any, event, command string, force bool) {
|
||||
if _, ok := hooks[event]; ok && !force {
|
||||
return
|
||||
}
|
||||
hooks[event] = []any{
|
||||
map[string]any{
|
||||
"hooks": []any{
|
||||
map[string]string{
|
||||
"type": "command",
|
||||
"command": command,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(path string, doc any) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func InstallAll(force bool) error {
|
||||
if err := config.Default().WriteDefault(config.DefaultPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := MergeCursorHooks(CursorHooksPath(), force); err != nil {
|
||||
return err
|
||||
}
|
||||
return MergeClaudeSettings(ClaudeSettingsPath(), force)
|
||||
}
|
||||
Reference in New Issue
Block a user