helmd v2: Go-server som styr agent i tmux — REST+SSE, frågedetektor, fildelning, ntfy, CI-release
Some checks failed
build-and-push / build (push) Successful in 42s
release-client / build-release (push) Has been cancelled
helmd-release / build-release (push) Successful in 1m6s

- 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:
2026-08-05 20:54:54 +02:00
parent 3e7152a884
commit f9c79ac84e
17 changed files with 2075 additions and 0 deletions

137
helmd/main.go Normal file
View File

@@ -0,0 +1,137 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
)
var version = "dev" // sätts av CI via -ldflags
var exec_LookPath = exec.LookPath
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "serve":
fs := flag.NewFlagSet("serve", flag.ExitOnError)
listen := fs.String("listen", "", "override listen-adress (t.ex. 0.0.0.0:8788)")
confPath := fs.String("config", configPath(), "sökväg till config.json")
fs.Parse(os.Args[2:])
cfg, err := loadConfig(*confPath)
if err != nil {
fatal(err)
}
if *listen != "" {
cfg.Listen = *listen
}
if err := serve(cfg); err != nil {
fatal(err)
}
case "token":
cfg, err := loadConfig(configPath())
if err != nil {
fatal(err)
}
fmt.Println(cfg.Token)
case "share":
fs := flag.NewFlagSet("share", flag.ExitOnError)
server := fs.String("server", "http://127.0.0.1:8788", "helmd-serverns adress")
note := fs.String("note", "", "beskrivning som visas i klienten")
session := fs.String("session", "", "session som delningen hör till")
fs.Parse(os.Args[2:])
if fs.NArg() != 1 {
fatal(fmt.Errorf("användning: helmd share [flaggor] <fil>"))
}
cfg, err := loadConfig(configPath())
if err != nil {
fatal(err)
}
id, err := shareFile(*server, cfg.Token, fs.Arg(0), *note, *session)
if err != nil {
fatal(err)
}
fmt.Printf("delad: %s (id %s)\n%s/api/shares/%s/raw\n", filepath.Base(fs.Arg(0)), id, *server, id)
case "version", "--version", "-v":
fmt.Println("helmd", version)
default:
usage()
os.Exit(2)
}
}
func usage() {
fmt.Fprintln(os.Stderr, `helmd — agent-helm-server: styr en CLI-agent i tmux via REST-API
användning:
helmd serve [--listen host:port] [--config fil] starta servern
helmd share [--note text] [--session namn] <fil> dela en fil till klienterna
helmd token skriv ut API-token
helmd version`)
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "helmd:", err)
os.Exit(1)
}
// shareFile uploads a file to a running helmd and returns the share id.
func shareFile(server, token, path, note, session string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
fw, err := mw.CreateFormFile("file", filepath.Base(path))
if err != nil {
return "", err
}
if _, err := io.Copy(fw, f); err != nil {
return "", err
}
mw.WriteField("note", note)
mw.WriteField("session", session)
mw.Close()
req, err := http.NewRequest("POST", server+"/api/shares", &buf)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode >= 300 {
return "", fmt.Errorf("servern svarade %s: %s", resp.Status, bytes.TrimSpace(body))
}
var out struct {
ID string `json:"id"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
return out.ID, nil
}