// Command cyrene is the single Cyrene v2 binary. It assembles every subsystem // (config, logging, database, bus, personas, model adapter, tool dispatcher, // memory stack, agent loop, orchestration, execution, IoT, channel sessions and // adapters) into one process and serves the gateway + embedded frontend on a // single port (:8080), shutting down gracefully on a termination signal. // // Build with `go build -ldflags="-s -w" -trimpath -o cyrene ./cmd/cyrene/`. // The frontend must be built first so the SPA can be embedded: // `cd frontend/web && npm run build`. package main import ( "context" "fmt" "io/fs" "log/slog" "os" "path/filepath" "time" // Embed the IANA timezone database so the configured TZ (config.TZ, default // "Asia/Shanghai") resolves everywhere: Windows ships no system tzdata and Go // deliberately does not bundle one, so without this the loop's time injection // silently disables itself there (internal/runtime/loop/time.go logs // "timezone load failed"). Costs ~450 KB and keeps the binary self-contained. _ "time/tzdata" capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/platform" "git.yeij.top/AskaEth/Cyrene_v2/internal/webui" ) // envPath is the .env file merged at boot (development only; production uses // systemd/env_file). A missing .env is not an error. const envPath = ".env" // shutdownTimeout bounds how long the graceful teardown may take. const shutdownTimeout = 15 * time.Second // externalDistCandidates are the on-disk directories checked (in order) for a // frontend override before falling back to the embedded dist. The first that // exists wins. They are relative to the process working directory. func externalDistCandidates() []string { return []string{ filepath.Join("frontend", "web", "dist"), filepath.Join("frontend", "dist"), } } // resolveFrontendFS returns the filesystem serving the SPA: an external dist // directory when present (deploy-time hot override), otherwise the embedded dist. func resolveFrontendFS(log *slog.Logger) fs.FS { for _, dir := range externalDistCandidates() { if info, err := os.Stat(dir); err == nil && info.IsDir() { log.Info("serving external frontend dist", "dir", dir) return os.DirFS(dir) } } log.Info("serving embedded frontend dist") return webui.Assets() } func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "cyrene:", err) os.Exit(1) } } // run boots the application, starts every service and the gateway, then blocks // until a termination signal arrives or the gateway fails. func run() error { cfg := config.MustLoad(envPath) format := logger.FormatText if cfg.Env == "production" { format = logger.FormatJSON } logRing := logger.NewRingBuffer(logger.DefaultRingCapacity) log := logger.New(logger.Options{Level: cfg.LogLevel, Format: string(format), Ring: logRing}) log = logger.WithSource(log, "main") ctx, stop := platform.SignalContext() defer stop() a, err := newApp(ctx, cfg, log) if err != nil { return err } // Observability log tail: the logger's ring buffer is built above (before the // gateway), so hand it to the gateway now that the gateway exists. wireGatewayLogBuffer(a, logRing) if err := a.automation.Start(ctx); err != nil { return fmt.Errorf("start automations: %w", err) } a.backthink.Start(ctx) a.log.Info("cyrene subsystems started", "personas", a.personas.CountPersonas(), "tools", len(a.caps.List(capreg.KindTool)), "channels", len(a.caps.List(capreg.KindChannel))) a.log.Info("cyrene ready", "data_dir", resolveDataDir(cfg)) errCh, err := a.gw.Start(ctx) if err != nil { return fmt.Errorf("start gateway: %w", err) } select { case <-ctx.Done(): a.log.Info("cyrene signal received, shutting down") case gwErr := <-errCh: if gwErr != nil { shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() a.close(shutCtx) return fmt.Errorf("gateway failed: %w", gwErr) } } shutCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() a.close(shutCtx) return nil }