From 4a8cc75793df4f71b23756e7d32e693cf1b5f85e Mon Sep 17 00:00:00 2001 From: db123-ai Date: Wed, 10 Jun 2026 00:49:39 +0330 Subject: [PATCH] feat(cmd): add main entrypoint wiring TUI, API, scheduler, and llama.cpp --- cmd/divad/main.go | 151 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 cmd/divad/main.go diff --git a/cmd/divad/main.go b/cmd/divad/main.go new file mode 100644 index 0000000..fcffc62 --- /dev/null +++ b/cmd/divad/main.go @@ -0,0 +1,151 @@ +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()) + 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) + } +}