helmd v2: Go-server som styr agent i tmux — REST+SSE, frågedetektor, fildelning, ntfy, CI-release
- tmux som sanningskälla (agentoberoende, överlever omstart via adoption) - API: sessions, prompt, question/answer, keys, mode, config, shares, notify, events - frågedetektor testad mot riktiga agy 1.1.9-dumpar + mock - integrationstest: 26 tester i isolerad debian-container (scripts/run-integration.sh) - e2e-verifierad mot riktig agy (trust-fråga -> svar -> prompt) - .gitea/workflows/helmd-release.yaml -> rullande helmd-latest (x64+arm64) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
135
helmd/config.go
Normal file
135
helmd/config.go
Normal file
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user