Files
agent-dashboard/cmd/agent-notify/main.go
T
2026-06-02 14:37:40 +08:00

229 lines
5.8 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"github.com/longbin/agent-notify/internal/config"
"github.com/longbin/agent-notify/internal/context"
"github.com/longbin/agent-notify/internal/hook"
"github.com/longbin/agent-notify/internal/install"
"github.com/longbin/agent-notify/internal/logx"
"github.com/longbin/agent-notify/internal/notify"
"github.com/longbin/agent-notify/internal/tmux"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
if err := run(os.Args[1], os.Args[2:]); err != nil {
fmt.Fprintln(os.Stderr, "agent-notify:", err)
os.Exit(1)
}
}
func run(cmd string, args []string) error {
switch cmd {
case "send":
return cmdSend(args)
case "hook":
return cmdHook(args)
case "install":
return cmdInstall(args)
case "test":
return cmdTest(args)
case "doctor":
return cmdDoctor()
case "logs":
return cmdLogs(args)
case "inbox":
return cmdInbox(args, os.Stdout, os.Stderr)
case "server":
return cmdServer(args)
case "version", "-V":
return cmdVersion()
case "help", "-h", "--help":
printUsage()
return nil
default:
printUsage()
return fmt.Errorf("unknown command %q", cmd)
}
}
func cmdSend(args []string) error {
fs := flag.NewFlagSet("send", flag.ExitOnError)
title := fs.String("title", "", "notification title")
body := fs.String("body", "", "notification body")
event := fs.String("event", "stop", "event type")
_ = fs.Parse(args)
cfg, err := config.LoadDefault()
if err != nil {
return err
}
if *title == "" {
meta := context.MetaFromEnv("Agent", *event)
*title = context.Render(cfg.Notify.TitleTemplate, meta)
}
if *body == "" {
*body = cfg.BodyForEvent(*event)
}
return notify.SendAuto(cfg.Notify.Protocol, *title, *body)
}
func cmdHook(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: agent-notify hook <cursor|claude> <event>")
}
agent, event := args[0], args[1]
cfg, err := config.LoadDefault()
if err != nil {
return err
}
switch agent {
case "cursor":
return hook.RunCursor(os.Stdin, cfg, event, os.Stdout)
case "claude":
return hook.RunClaude(os.Stdin, cfg, event, os.Stdout)
default:
return fmt.Errorf("unknown agent %q (use cursor stop|response|tool or claude stop|idle)", agent)
}
}
func cmdInstall(args []string) error {
fs := flag.NewFlagSet("install", flag.ExitOnError)
all := fs.Bool("all", false, "install all")
force := fs.Bool("force", false, "overwrite existing hooks")
_ = fs.Parse(args)
if *all || len(args) == 0 {
return install.InstallAll(*force)
}
return fmt.Errorf("use --all")
}
func cmdTest(args []string) error {
mode := "cursor"
apply := false
tryAll := false
for _, arg := range args {
switch arg {
case "-v", "--verbose":
// accepted for compatibility; status always prints to stderr now
case "--apply":
apply = true
case "--try-all":
tryAll = true
case "cursor", "claude":
mode = arg
case "help", "-h", "--help":
fmt.Fprint(os.Stderr, `Usage: agent-notify test [cursor|claude] [flags]
Flags:
--apply Claude only: emit terminalSequence to terminal
--try-all Cursor only: try every delivery method (debug)
Examples:
agent-notify test cursor
agent-notify test cursor -v
agent-notify test cursor --try-all
agent-notify test claude --apply
`)
return nil
default:
return fmt.Errorf("unknown test argument %q (try: agent-notify test help)", arg)
}
}
switch mode {
case "cursor":
_, err := notify.TestCursor("", "", tryAll)
return err
case "claude":
_, err := notify.TestClaude("", "", apply, os.Stdout)
return err
default:
return fmt.Errorf("usage: agent-notify test [cursor|claude] [--apply] [--try-all]")
}
}
func cmdLogs(args []string) error {
fs := flag.NewFlagSet("logs", flag.ExitOnError)
tail := fs.Int("tail", 30, "number of recent lines")
_ = fs.Parse(args)
lines, err := logx.Tail(*tail)
if err != nil {
return err
}
if len(lines) == 0 {
fmt.Printf("no log entries yet (log file: %s)\n", logx.Path())
return nil
}
fmt.Printf("# %s\n", logx.Path())
for _, line := range lines {
fmt.Println(line)
}
return nil
}
func cmdDoctor() error {
fmt.Println("agent-notify doctor")
if tmux.InTmux() {
fmt.Println("✓ running inside tmux")
if tmux.IsSSHSession() {
fmt.Println("✓ SSH session detected (prefers passthrough delivery)")
}
ok, val, err := tmux.AllowPassthroughEnabled()
if err != nil {
fmt.Printf("✗ allow-passthrough check failed: %v\n", err)
} else if ok {
fmt.Printf("✓ allow-passthrough=%s\n", val)
} else {
fmt.Printf("✗ allow-passthrough=%q — add to ~/.tmux.conf: set -g allow-passthrough on\n", val)
}
tty, _ := tmux.ClientTTY()
fmt.Printf(" client_tty=%s\n", tty)
} else {
fmt.Println(" not in tmux (direct Ghostty mode)")
}
cfgPath := config.DefaultPath()
fmt.Printf(" config=%s\n", cfgPath)
cfg, err := config.LoadDefault()
if err != nil {
fmt.Printf("✗ config load failed: %v\n", err)
} else if cfg.Remote.Enabled {
if cfg.Remote.URL == "" {
fmt.Println("✗ remote.enabled but remote.url is empty — set url to your agent-notify server (e.g. http://host:8080)")
}
if cfg.Remote.Token == "" {
fmt.Println("✗ remote.enabled but remote.token is empty — must match AGENT_NOTIFY_TOKEN on agent-notify server")
}
if cfg.Remote.URL != "" && cfg.Remote.Token != "" {
fmt.Printf("✓ remote dashboard configured (%s)\n", cfg.Remote.URL)
}
}
fmt.Printf(" hook_log=%s\n", logx.Path())
return nil
}
func printUsage() {
fmt.Fprint(os.Stderr, `Usage: agent-notify <command>
Commands:
send [--title T] [--body B] [--event stop|idle|tool]
hook cursor stop|response|tool
hook claude stop|idle
install [--all] [--force]
test cursor [--try-all]
test claude [--apply]
logs [--tail 30]
inbox <list|show|done|rm|clear|serve|ssh-config>
server [--listen ADDR] [--db PATH] [--token TOKEN]
version
doctor
`)
}