021fc3a828
Route hook notifications through /dev/tty and client_tty so OSC reaches Ghostty when Cursor captures stdout. Add afterAgentResponse hook, hook logging, debounce, split test modes, and use directory name for titles. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
package install
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/longbin/agent-notify/internal/config"
|
|
)
|
|
|
|
const cursorStopCmd = "agent-notify hook cursor stop"
|
|
const cursorResponseCmd = "agent-notify hook cursor response"
|
|
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", cursorStopCmd, force)
|
|
addHook(hooks, "afterAgentResponse", cursorResponseCmd, 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)
|
|
}
|