package main import ( "context" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "git.db123.ir/db123/divacode/internal/agent" "git.db123.ir/db123/divacode/internal/channel" "git.db123.ir/db123/divacode/internal/companion" "git.db123.ir/db123/divacode/internal/config" "git.db123.ir/db123/divacode/internal/llama" "git.db123.ir/db123/divacode/internal/log" "git.db123.ir/db123/divacode/internal/memory" "git.db123.ir/db123/divacode/internal/storage" "git.db123.ir/db123/divacode/internal/tools" tea "github.com/charmbracelet/bubbletea" ) func main() { cfg := config.Load() log.Init(cfg.LogLevel) if err := cfg.Validate(); err != nil { slog.Error("config validation failed", "error", err) os.Exit(1) } slog.Info("starting divacode", "llama_cpp_url", cfg.LlamaCppURL, "db_path", cfg.DBPath, "log_level", cfg.LogLevel, ) db, err := storage.Open(cfg.DBPath) if err != nil { slog.Error("failed to open database", "path", cfg.DBPath, "error", err) os.Exit(1) } defer db.Close() if err := db.Migrate(); err != nil { slog.Error("database migration failed", "error", err) os.Exit(1) } slog.Info("database migrated", "path", cfg.DBPath) llm := llama.NewClient(cfg.LlamaCppURL) if err := llm.Health(); err != nil { slog.Error("llama.cpp not reachable", "url", cfg.LlamaCppURL, "error", err) os.Exit(1) } slog.Info("llama.cpp connected", "url", cfg.LlamaCppURL) toolReg := tools.NewRegistry() tools.RegisterBuiltins(toolReg) memStore := memory.NewStore(db) compEngine := companion.NewEngine(cfg, db) ag := agent.New(cfg, llm, db, memStore, compEngine, toolReg) if err := ag.NewConversation(); err != nil { slog.Error("failed to start conversation", "error", err) os.Exit(1) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() sched := agent.NewScheduler(ag, cfg.SchedulerInterval, cfg.DiaryHour) go sched.Start(ctx) matrix := channel.NewMatrixClient(cfg, ag) go func() { if err := matrix.Start(ctx); err != nil { slog.Error("matrix client failed", "error", err) } }() if cfg.APIEnabled { go startAPI(cfg, ag) } sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigCh slog.Info("shutting down...") sched.Stop() cancel() }() p := tea.NewProgram(channel.NewTUIModel(ag), tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { slog.Error("tui error", "error", err) os.Exit(1) } } func startAPI(cfg *config.Config, ag *agent.Agent) { mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) mux.HandleFunc("/v1/conversations", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: if err := ag.NewConversation(); err != nil { http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"conversationId":"%s"}`, ag.ConversationID()) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } }) mux.HandleFunc("/v1/chat", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) return } msg := r.FormValue("message") if msg == "" { http.Error(w, `{"error":"message is required"}`, http.StatusBadRequest) return } resp, err := ag.HandleMessage(r.Context(), msg) if err != nil { http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"response":"%s"}`, resp) }) slog.Info("api server starting", "port", cfg.APIPort) if err := http.ListenAndServe(":"+cfg.APIPort, mux); err != nil { slog.Error("api server failed", "error", err) } }