package main import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" ) type NtfyConfig struct { Server string `json:"server"` Topic string `json:"topic"` // server events (questions etc.) AllowedTopics []string `json:"allowed_topics"` // POST /api/notify may only use these Enabled bool `json:"enabled"` NotifyOnQuestion bool `json:"notify_on_question"` } type Config struct { Listen string `json:"listen"` Token string `json:"token"` AgentCmd string `json:"agent_cmd"` AgentArgs []string `json:"agent_args"` Workdir string `json:"workdir"` ShareDir string `json:"share_dir"` ShareExts []string `json:"share_exts"` PollMs int `json:"poll_ms"` Cols int `json:"cols"` Rows int `json:"rows"` Ntfy NtfyConfig `json:"ntfy"` path string } func defaultConfig() *Config { home, _ := os.UserHomeDir() return &Config{ Listen: "127.0.0.1:8788", AgentCmd: "agy", AgentArgs: []string{}, Workdir: home, ShareDir: filepath.Join(home, ".local/share/helmd/shares"), ShareExts: []string{".html", ".htm", ".md", ".txt", ".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".pdf"}, PollMs: 500, Cols: 200, Rows: 50, Ntfy: NtfyConfig{ Server: "https://ntfy.brasse-pc.eu", Topic: "agent-helm", AllowedTopics: []string{"agent-helm", "claude"}, Enabled: true, NotifyOnQuestion: true, }, } } func configPath() string { if p := os.Getenv("HELMD_CONFIG"); p != "" { return p } dir, err := os.UserConfigDir() if err != nil { dir = "." } return filepath.Join(dir, "helmd", "config.json") } // loadConfig reads the config file, creating it with defaults (and a // fresh token) on first run. func loadConfig(path string) (*Config, error) { cfg := defaultConfig() cfg.path = path data, err := os.ReadFile(path) if os.IsNotExist(err) { cfg.Token = newToken() if err := cfg.Save(); err != nil { return nil, err } fmt.Fprintf(os.Stderr, "helmd: skapade %s (ny token genererad)\n", path) return cfg, nil } if err != nil { return nil, err } if err := json.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } if cfg.Token == "" { cfg.Token = newToken() if err := cfg.Save(); err != nil { return nil, err } } return cfg, nil } func (c *Config) Save() error { if err := os.MkdirAll(filepath.Dir(c.path), 0o755); err != nil { return err } data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(c.path, append(data, '\n'), 0o600) } func newToken() string { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { panic(err) } return hex.EncodeToString(b) } func (c *Config) extAllowed(name string) bool { ext := strings.ToLower(filepath.Ext(name)) for _, e := range c.ShareExts { if ext == e { return true } } return false } func (c *Config) topicAllowed(topic string) bool { for _, t := range c.Ntfy.AllowedTopics { if topic == t { return true } } return false }