diff --git a/cmd/molecule/main.go b/cmd/molecule/main.go index 8f90d96..feb3e37 100644 --- a/cmd/molecule/main.go +++ b/cmd/molecule/main.go @@ -1,507 +1,16 @@ +// molecule-cli — Molecule AI platform CLI +// +// Entry point. Wires cobra root command and runs it. package main import ( - "encoding/json" - "fmt" - "io" "os" - "strings" - "time" - "github.com/charmbracelet/bubbletea" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" + "github.com/Molecule-AI/molecule-cli/internal/cmd" ) -// Global flags -var ( - flagOutput string - flagVerbose bool - flagConfig string -) - -// Output formats -type OutputFormat int - -const ( - FormatText OutputFormat = iota - FormatJSON - FormatYAML -) - -var formatMap = map[string]OutputFormat{ - "text": FormatText, - "json": FormatJSON, - "yaml": FormatYAML, -} - -// writeOutput writes the result in the requested format -func writeOutput(w io.Writer, data any, format string) { - switch formatMap[format] { - case FormatJSON: - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - enc.Encode(data) - case FormatYAML: - // Simple YAML-like output for maps - if m, ok := data.(map[string]any); ok { - for k, v := range m { - fmt.Fprintf(w, "%s: %v\n", k, v) - } - } else { - fmt.Fprintf(w, "%v\n", data) - } - default: - dumpText(w, data) - } -} - -func dumpText(w io.Writer, data any) { - switch v := data.(type) { - case string: - fmt.Fprintln(w, v) - case []string: - for _, s := range v { - fmt.Fprintln(w, s) - } - case map[string]any: - for k, val := range v { - fmt.Fprintf(w, "%-20s %v\n", k+":", val) - } - default: - fmt.Fprintf(w, "%v\n", v) - } -} - -func errorExit(format string, args ...any) { - fmt.Fprintf(os.Stderr, format, args...) - os.Exit(1) -} - -// PlatformClient is a minimal client for the Molecule AI platform API -type PlatformClient struct { - BaseURL string - Token string -} - -func newPlatformClient() *PlatformClient { - baseURL := os.Getenv("MOLECULE_API_URL") - if baseURL == "" { - baseURL = "https://api.moleculesai.app" - } - return &PlatformClient{BaseURL: baseURL, Token: os.Getenv("MOLECULE_API_TOKEN")} -} - -// Workspace represents a Molecule AI workspace -type Workspace struct { - ID string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Tier int `json:"tier"` - ParentID string `json:"parent_id,omitempty"` - Created string `json:"created_at,omitempty"` -} - -// Agent represents a Molecule AI agent -type Agent struct { - ID string `json:"id"` - WorkspaceID string `json:"workspace_id"` - Name string `json:"name"` - Status string `json:"status"` - Role string `json:"role"` -} - -// ─── Workspace Commands ─────────────────────────────────────────────────────── - -var workspaceCmd = &cobra.Command{ - Use: "workspace", - Short: "Manage workspaces", -} - -var workspaceCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a new workspace", - Run: func(cmd *cobra.Command, args []string) { - name, _ := cmd.Flags().GetString("name") - tier, _ := cmd.Flags().GetInt("tier") - template, _ := cmd.Flags().GetString("template") - - if name == "" { - errorExit("workspace create: --name is required") - } - - // Placeholder — actual POST to platform API - ws := Workspace{ - ID: uuid.New().String(), - Name: name, - Status: "provisioning", - Tier: tier, - Created: time.Now().Format(time.RFC3339), - } - writeOutput(os.Stdout, ws, flagOutput) - }, -} - -var workspaceListCmd = &cobra.Command{ - Use: "list", - Short: "List all workspaces", - Run: func(cmd *cobra.Command, args []string) { - // Placeholder — actual GET /workspaces - workspaces := []Workspace{ - {ID: "placeholder-workspace-id", Name: "example-workspace", Status: "online", Tier: 2}, - } - writeOutput(os.Stdout, workspaces, flagOutput) - }, -} - -var workspaceInspectCmd = &cobra.Command{ - Use: "inspect", - Args: cobra.ExactArgs(1), - Short: "Inspect a workspace by ID", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - ws := Workspace{ID: id, Name: "placeholder", Status: "online", Tier: 2} - writeOutput(os.Stdout, ws, flagOutput) - }, -} - -var workspaceDeleteCmd = &cobra.Command{ - Use: "delete", - Args: cobra.ExactArgs(1), - Short: "Delete a workspace", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - fmt.Fprintf(os.Stderr, "workspace delete: deleted %s\n", id) - }, -} - -var workspaceRestartCmd = &cobra.Command{ - Use: "restart", - Args: cobra.ExactArgs(1), - Short: "Restart a workspace", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - fmt.Fprintf(os.Stderr, "workspace restart: restarting %s\n", id) - }, -} - -var workspaceDelegateCmd = &cobra.Command{ - Use: "delegate", - Args: cobra.ExactArgs(1), - Short: "Delegate a task to a workspace", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - task, _ := cmd.Flags().GetString("task") - async, _ := cmd.Flags().GetBool("async") - - if task == "" { - errorExit("workspace delegate: --task is required") - } - - if async { - taskID := uuid.New().String() - fmt.Fprintf(os.Stderr, "workspace delegate: async task %s dispatched to %s\n", taskID, id) - } else { - fmt.Fprintf(os.Stderr, "workspace delegate: sync task dispatched to %s\n", id) - } - }, -} - -var workspaceAuditCmd = &cobra.Command{ - Use: "audit", - Args: cobra.ExactArgs(1), - Short: "Audit a workspace's configuration", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - report := map[string]any{ - "workspace_id": id, - "audit_passed": true, - "issues_found": 0, - "recommendations": []string{}, - } - writeOutput(os.Stdout, report, flagOutput) - }, -} - -// ─── Agent Commands ───────────────────────────────────────────────────────── - -var agentCmd = &cobra.Command{ - Use: "agent", - Short: "Manage agents", -} - -var agentListCmd = &cobra.Command{ - Use: "list", - Args: cobra.ExactArgs(1), - Short: "List agents in a workspace", - Run: func(cmd *cobra.Command, args []string) { - workspaceID := args[0] - agents := []Agent{ - {ID: "placeholder-agent-id", WorkspaceID: workspaceID, Name: "example-agent", Status: "online", Role: "assistant"}, - } - writeOutput(os.Stdout, agents, flagOutput) - }, -} - -var agentInspectCmd = &cobra.Command{ - Use: "inspect", - Args: cobra.ExactArgs(1), - Short: "Inspect an agent by ID", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - agent := Agent{ID: id, Status: "online", Role: "assistant"} - writeOutput(os.Stdout, agent, flagOutput) - }, -} - -var agentSendCmd = &cobra.Command{ - Use: "send", - Args: cobra.ExactArgs(1), - Short: "Send a message to an agent", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - msg, _ := cmd.Flags().GetString("message") - if msg == "" { - errorExit("agent send: --message is required") - } - fmt.Fprintf(os.Stderr, "agent send: message sent to %s\n", id) - }, -} - -var agentPeersCmd = &cobra.Command{ - Use: "peers", - Args: cobra.ExactArgs(1), - Short: "List peers for an agent", - Run: func(cmd *cobra.Command, args []string) { - id := args[0] - peers := []string{} - writeOutput(os.Stdout, peers, flagOutput) - }, -} - -// ─── Platform Commands ─────────────────────────────────────────────────────── - -var platformCmd = &cobra.Command{ - Use: "platform", - Short: "Interact with the platform", -} - -var platformAuditCmd = &cobra.Command{ - Use: "audit", - Short: "Audit platform configuration", - Run: func(cmd *cobra.Command, args []string) { - url, _ := cmd.Flags().GetString("url") - token, _ := cmd.Flags().GetString("token") - if url == "" { - url = os.Getenv("MOLECULE_API_URL") - } - - report := map[string]any{ - "platform_url": url, - "api_key_configured": token != "" || os.Getenv("MOLECULE_API_TOKEN") != "", - "audit_passed": true, - } - writeOutput(os.Stdout, report, flagOutput) - }, -} - -var platformHealthCmd = &cobra.Command{ - Use: "health", - Short: "Check platform health", - Run: func(cmd *cobra.Command, args []string) { - health := map[string]any{ - "status": "ok", - "api_url": os.Getenv("MOLECULE_API_URL"), - "checked_at": time.Now().Format(time.RFC3339), - } - writeOutput(os.Stdout, health, flagOutput) - }, -} - -// ─── Config Commands ───────────────────────────────────────────────────────── - -var configCmd = &cobra.Command{ - Use: "config", - Short: "Manage CLI configuration", -} - -var configListCmd = &cobra.Command{ - Use: "list", - Short: "Show current configuration", - Run: func(cmd *cobra.Command, args []string) { - cfg := map[string]string{ - "MOLECULE_API_URL": os.Getenv("MOLECULE_API_URL"), - "MOLECULE_RUNTIME_URL": os.Getenv("MOLECULE_RUNTIME_URL"), - "MOLECULE_API_TOKEN": maskToken(os.Getenv("MOLECULE_API_TOKEN")), - "config_file": viper.ConfigFileUsed(), - } - writeOutput(os.Stdout, cfg, flagOutput) - }, -} - -var configGetCmd = &cobra.Command{ - Use: "get", - Args: cobra.ExactArgs(1), - Short: "Get a specific config value", - Run: func(cmd *cobra.Command, args []string) { - key := args[0] - val := viper.GetString(key) - fmt.Fprintln(os.Stdout, val) - }, -} - -var configSetCmd = &cobra.Command{ - Use: "set", - Args: cobra.ExactArgs(2), - Short: "Set a config value", - Run: func(cmd *cobra.Command, args []string) { - key, val := args[0], args[1] - viper.Set(key, val) - fmt.Fprintf(os.Stderr, "config set: %s = %s\n", key, val) - }, -} - -var configInitCmd = &cobra.Command{ - Use: "init", - Short: "Bootstrap ~/.config/molecule/cli.yaml", - Run: func(cmd *cobra.Command, args []string) { - home, err := os.UserHomeDir() - if err != nil { - errorExit("config init: cannot determine home directory") - } - dir := home + "/.config/molecule" - os.MkdirAll(dir, 0755) - path := dir + "/cli.yaml" - content := `# Molecule CLI configuration -# Copy this file to ~/.config/molecule/cli.yaml - -api_url: "https://api.moleculesai.app" -runtime_url: "" - -# Token is read from MOLECULE_API_TOKEN env var — do not store here. -` - os.WriteFile(path, []byte(content), 0644) - fmt.Fprintf(os.Stderr, "config init: created %s\n", path) - }, -} - -var configViewCmd = &cobra.Command{ - Use: "view", - Short: "Print config file path and current values", - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(os.Stderr, "config file: %s\n", viper.ConfigFileUsed()) - fmt.Fprintf(os.Stderr, "effective config:\n") - for _, key := range viper.AllKeys() { - fmt.Fprintf(os.Stderr, " %s = %v\n", key, viper.Get(key)) - } - }, -} - -// ─── Root Command ──────────────────────────────────────────────────────────── - -var rootCmd = &cobra.Command{ - Use: "molecule", - Short: "Molecule AI CLI — agent platform management tool", - Long: `Molecule AI CLI for managing agents, workspaces, and deployments. - -Environment variables: - MOLECULE_API_URL Control plane API base URL (default: https://api.moleculesai.app) - MOLECULE_RUNTIME_URL Workspace runtime URL - MOLECULE_API_TOKEN API authentication token - -Examples: - molecule workspace list - molecule agent inspect - molecule config init -`, - SilenceUsage: true, - SilenceErrors: true, -} - -func init() { - rootCmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "text", "Output format: text, json, yaml") - rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Enable verbose output") - rootCmd.PersistentFlags().StringVar(&flagConfig, "config", "", "Path to config file") - - // Workspace subcommands - workspaceCreateCmd.Flags().String("name", "", "Workspace name") - workspaceCreateCmd.Flags().Int("tier", 2, "Workspace tier (1-4)") - workspaceCreateCmd.Flags().String("template", "default", "Template ID") - workspaceCreateCmd.MarkFlagRequired("name") - - workspaceDelegateCmd.Flags().String("task", "", "Task prompt") - workspaceDelegateCmd.Flags().Bool("async", false, "Fire and forget (async)") - - // Agent subcommands - agentSendCmd.Flags().String("message", "", "Message to send") - agentSendCmd.MarkFlagRequired("message") - - // Platform subcommands - platformAuditCmd.Flags().String("url", "", "Platform URL override") - platformAuditCmd.Flags().String("token", "", "API token override") - - // Wire up workspace tree - workspaceCmd.AddCommand(workspaceCreateCmd, workspaceListCmd, workspaceInspectCmd, - workspaceDeleteCmd, workspaceRestartCmd, workspaceDelegateCmd, workspaceAuditCmd) - rootCmd.AddCommand(workspaceCmd) - - // Wire up agent tree - agentCmd.AddCommand(agentListCmd, agentInspectCmd, agentSendCmd, agentPeersCmd) - rootCmd.AddCommand(agentCmd) - - // Wire up platform tree - platformCmd.AddCommand(platformAuditCmd, platformHealthCmd) - rootCmd.AddCommand(platformCmd) - - // Wire up config tree - configCmd.AddCommand(configListCmd, configGetCmd, configSetCmd, configInitCmd, configViewCmd) - rootCmd.AddCommand(configCmd) -} - -func maskToken(token string) string { - if len(token) <= 8 { - return "***" - } - return token[:4] + strings.Repeat("*", len(token)-8) + token[len(token)-4:] -} - func main() { - // Configure viper for config file support - viper.SetConfigName("cli") - viper.SetConfigType("yaml") - viper.AddConfigPath("$HOME/.config/molecule") - viper.AutomaticEnv() - - // Bind CLI flags to viper - viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output")) - viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) - - // Override flag values from config file - if viper.IsSet("output") { - flagOutput = viper.GetString("output") - } - if viper.IsSet("verbose") { - flagVerbose = viper.GetBool("verbose") - } - - // Set Gin mode based on verbosity - if flagVerbose { - gin.SetMode(gin.DebugMode) - } else { - gin.SetMode(gin.ReleaseMode) - } - - // Execute - if err := rootCmd.Execute(); err != nil { - if strings.Contains(err.Error(), "unknown subcommand") || - strings.Contains(err.Error(), "missing required") || - strings.Contains(err.Error(), "flag") { - os.Exit(2) - } + if err := cmd.Execute(); err != nil { os.Exit(1) } } \ No newline at end of file diff --git a/go.mod b/go.mod index c35aec7..6c03571 100644 --- a/go.mod +++ b/go.mod @@ -3,88 +3,25 @@ module github.com/Molecule-AI/molecule-cli go 1.25.0 require ( - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/gin-contrib/cors v1.7.2 - github.com/gin-gonic/gin v1.10.0 - github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 - github.com/lib/pq v1.10.9 - github.com/redis/go-redis/v9 v9.7.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 ) require ( - github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect - github.com/alicebob/miniredis/v2 v2.37.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.9.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.2.2+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.9 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/net v0.25.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/text v0.28.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index d509601..e64888e 100644 --- a/go.sum +++ b/go.sum @@ -1,252 +1,57 @@ -github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= -github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68= -github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= -github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= -github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= -github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= -github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/client/platform.go b/internal/client/platform.go new file mode 100644 index 0000000..5283e1d --- /dev/null +++ b/internal/client/platform.go @@ -0,0 +1,276 @@ +// Package client provides a thin HTTP client for the Molecule AI platform API. +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Platform is the root API client. +type Platform struct { + BaseURL string + client *http.Client +} + +// New returns a Platform client configured with baseURL. +func New(baseURL string) *Platform { + return &Platform{ + BaseURL: baseURL, + client: &http.Client{Timeout: 30 * time.Second}, + } +} + +// Workspace represents a Molecule AI workspace. +type Workspace struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Role string `json:"role,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Runtime string `json:"runtime,omitempty"` + WorkspaceDir string `json:"workspace_dir,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + Tier int `json:"tier,omitempty"` + Canvas *Canvas `json:"canvas,omitempty"` +} + +// Canvas holds the workspace's position on the canvas. +type Canvas struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + +// Agent represents an agent running in a workspace. +type Agent struct { + ID string `json:"id"` + Name string `json:"name"` + WorkspaceID string `json:"workspace_id,omitempty"` + Status string `json:"status"` + Model string `json:"model,omitempty"` + Runtime string `json:"runtime,omitempty"` + CreatedAt string `json:"created_at,omitempty"` +} + +// CreateWorkspaceRequest mirrors the platform's POST /workspaces body. +type CreateWorkspaceRequest struct { + Name string `json:"name"` + Role string `json:"role,omitempty"` + Template string `json:"template,omitempty"` + Tier int `json:"tier,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Runtime string `json:"runtime,omitempty"` + WorkspaceDir string `json:"workspace_dir,omitempty"` +} + +// PlatformHealth holds the /health endpoint response. +type PlatformHealth struct { + Status string `json:"status"` + Version string `json:"version,omitempty"` + Uptime string `json:"uptime,omitempty"` + Database string `json:"database,omitempty"` +} + +// ConfigEntry represents a config key-value pair. +type ConfigEntry struct { + Key string `json:"key"` + Value string `json:"value,omitempty"` +} + +// ListWorkspaces returns all workspaces in the org. +func (p *Platform) ListWorkspaces() ([]Workspace, error) { + var out []Workspace + if err := p.getInto("/workspaces", &out); err != nil { + return nil, err + } + return out, nil +} + +// GetWorkspace returns a single workspace by ID. +func (p *Platform) GetWorkspace(id string) (*Workspace, error) { + var out Workspace + if err := p.getInto(fmt.Sprintf("/workspaces/%s", id), &out); err != nil { + return nil, err + } + return &out, nil +} + +// CreateWorkspace creates a new workspace. +func (p *Platform) CreateWorkspace(req CreateWorkspaceRequest) (*Workspace, error) { + var out Workspace + if err := p.postInto("/workspaces", req, &out); err != nil { + return nil, err + } + return &out, nil +} + +// DeleteWorkspace deletes a workspace by ID. +func (p *Platform) DeleteWorkspace(id string) error { + _, err := p.delete(fmt.Sprintf("/workspaces/%s?confirm=true", id)) + return err +} + +// RestartWorkspace triggers a restart for a workspace. +func (p *Platform) RestartWorkspace(id string) error { + _, err := p.postEmpty(fmt.Sprintf("/workspaces/%s/restart", id)) + return err +} + +// ListAgents returns all agents across the org. +func (p *Platform) ListAgents() ([]Agent, error) { + var out []Agent + if err := p.getInto("/agents", &out); err != nil { + return nil, err + } + return out, nil +} + +// ListWorkspaceAgents returns agents for a given workspace. +func (p *Platform) ListWorkspaceAgents(workspaceID string) ([]Agent, error) { + var out []Agent + if err := p.getInto(fmt.Sprintf("/workspaces/%s/agents", workspaceID), &out); err != nil { + return nil, err + } + return out, nil +} + +// GetAgent returns a single agent by ID. +func (p *Platform) GetAgent(id string) (*Agent, error) { + var out Agent + if err := p.getInto(fmt.Sprintf("/agents/%s", id), &out); err != nil { + return nil, err + } + return &out, nil +} + +// Health returns the platform's /health status. +func (p *Platform) Health() (*PlatformHealth, error) { + var out PlatformHealth + if err := p.getInto("/health", &out); err != nil { + return nil, err + } + return &out, nil +} + +// AuditWorkspaces returns all workspaces and agents. +func (p *Platform) AuditWorkspaces() ([]Workspace, []Agent, error) { + ws, err := p.ListWorkspaces() + if err != nil { + return nil, nil, fmt.Errorf("audit workspaces: %w", err) + } + agents, err := p.ListAgents() + if err != nil { + return ws, nil, fmt.Errorf("audit agents: %w", err) + } + return ws, agents, nil +} + +// GetPeers returns peer workspaces reachable from a workspace. +func (p *Platform) GetPeers(workspaceID string) ([]Agent, error) { + var out []Agent + if err := p.getInto(fmt.Sprintf("/registry/%s/peers", workspaceID), &out); err != nil { + return nil, err + } + return out, nil +} + +// GetDelegations returns delegation status for a workspace. +func (p *Platform) GetDelegations(workspaceID string) ([]map[string]interface{}, error) { + var out []map[string]interface{} + if err := p.getInto(fmt.Sprintf("/workspaces/%s/delegations", workspaceID), &out); err != nil { + return nil, err + } + return out, nil +} + +// --------------------------------------------------------------------------- +// Private HTTP helpers +// --------------------------------------------------------------------------- + +func (p *Platform) getInto(path string, out interface{}) error { + url := p.BaseURL + path + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("new GET request: %w", err) + } + resp, err := p.client.Do(req) + if err != nil { + return fmt.Errorf("GET %s: %w", url, err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return fmt.Errorf("GET %s: HTTP %d — %s", url, resp.StatusCode, string(body)) + } + if err := json.Unmarshal(body, out); err != nil { + return fmt.Errorf("decode GET %s: %w", path, err) + } + return nil +} + +func (p *Platform) postInto(path string, body interface{}, out interface{}) error { + encoded, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal POST body: %w", err) + } + url := p.BaseURL + path + req, err := http.NewRequest("POST", url, bytes.NewReader(encoded)) + if err != nil { + return fmt.Errorf("new POST request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := p.client.Do(req) + if err != nil { + return fmt.Errorf("POST %s: %w", url, err) + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return fmt.Errorf("POST %s: HTTP %d — %s", url, resp.StatusCode, string(respBody)) + } + if out != nil { + if err := json.Unmarshal(respBody, out); err != nil { + return fmt.Errorf("decode POST %s response: %w", path, err) + } + } + return nil +} + +func (p *Platform) delete(path string) ([]byte, error) { + url := p.BaseURL + path + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return nil, fmt.Errorf("new DELETE request: %w", err) + } + resp, err := p.client.Do(req) + if err != nil { + return nil, fmt.Errorf("DELETE %s: %w", url, err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("DELETE %s: HTTP %d — %s", url, resp.StatusCode, string(body)) + } + return body, nil +} + +func (p *Platform) postEmpty(path string) ([]byte, error) { + url := p.BaseURL + path + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return nil, fmt.Errorf("new POST request: %w", err) + } + resp, err := p.client.Do(req) + if err != nil { + return nil, fmt.Errorf("POST %s: %w", url, err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("POST %s: HTTP %d — %s", url, resp.StatusCode, string(body)) + } + return body, nil +} \ No newline at end of file diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go new file mode 100644 index 0000000..9569d75 --- /dev/null +++ b/internal/cmd/agent.go @@ -0,0 +1,175 @@ +// Package cmd implements the CLI command tree. +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + "github.com/Molecule-AI/molecule-cli/internal/client" + "github.com/spf13/cobra" +) + +// --------------------------------------------------------------------------- +// Agent command group +// --------------------------------------------------------------------------- + +var agentCmd = &cobra.Command{ + Use: "agent", + Short: "Inspect and interact with agents", + Long: `List agents, inspect individual agents, send messages, and discover peers.`, +} + +func init() { + agentCmd.AddCommand( + agentListCmd, agentInspectCmd, agentSendCmd, agentPeersCmd, + ) +} + +// =========================================================================== +// mol agent list +// =========================================================================== +var agentListCmd = &cobra.Command{ + Use: "list [workspace-id]", + Short: "List all agents (optionally filtered to one workspace)", + Args: cobra.RangeArgs(0, 1), + RunE: runAgentList, +} + +func runAgentList(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + var agents []client.Agent + var err error + if len(args) == 0 { + agents, err = cl.ListAgents() + } else { + agents, err = cl.ListWorkspaceAgents(args[0]) + } + if err != nil { + return fmt.Errorf("agent list: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(agents) + } + if len(agents) == 0 { + fmt.Println("No agents found.") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL\tRUNTIME") + for _, a := range agents { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + a.ID, a.Name, a.WorkspaceID, a.Status, a.Model, a.Runtime) + } + return w.Flush() +} + +// =========================================================================== +// mol agent inspect +// =========================================================================== +var agentInspectCmd = &cobra.Command{ + Use: "inspect ", + Short: "Show full details for an agent", + Args: cobra.ExactArgs(1), + RunE: runAgentInspect, +} + +func runAgentInspect(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + a, err := cl.GetAgent(args[0]) + if err != nil { + return fmt.Errorf("agent inspect: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(a) + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + kv(w, "ID", a.ID) + kv(w, "Name", a.Name) + kv(w, "WorkspaceID", a.WorkspaceID) + kv(w, "Status", a.Status) + kv(w, "Model", a.Model) + kv(w, "Runtime", a.Runtime) + kv(w, "CreatedAt", a.CreatedAt) + return w.Flush() +} + +// =========================================================================== +// mol agent send +// =========================================================================== +var agentSendCmd = &cobra.Command{ + Use: "send ", + Short: "Send a one-shot message to an agent via A2A", + Args: cobra.ExactArgs(2), + RunE: runAgentSend, +} + +func runAgentSend(cmd *cobra.Command, args []string) error { + agentID, message := args[0], args[1] + cl := client.New(apiURL) + + a, err := cl.GetAgent(agentID) + if err != nil { + return fmt.Errorf("agent send: %w", err) + } + wsID := a.WorkspaceID + if wsID == "" { + return fmt.Errorf("agent send: workspace_id unknown for agent %q", agentID) + } + + type a2aReq struct { + AgentID string `json:"agent_id"` + Message string `json:"message"` + } + type a2aResp struct { + Result string `json:"result,omitempty"` + Error string `json:"error,omitempty"` + } + encoded, _ := json.Marshal(a2aReq{AgentID: agentID, Message: message}) + body, err := runHTTP("POST", cl.BaseURL+"/workspaces/"+wsID+"/a2a", encoded) + if err != nil { + return fmt.Errorf("agent send: %w", err) + } + var resp a2aResp + if err := json.Unmarshal(body, &resp); err != nil { + return fmt.Errorf("agent send: parse response: %w", err) + } + if resp.Error != "" { + return fmt.Errorf("agent send: platform error: %s", resp.Error) + } + fmt.Println(resp.Result) + return nil +} + +// =========================================================================== +// mol agent peers +// =========================================================================== +var agentPeersCmd = &cobra.Command{ + Use: "peers ", + Short: "List peer workspaces reachable from a workspace", + Args: cobra.ExactArgs(1), + RunE: runAgentPeers, +} + +func runAgentPeers(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + peers, err := cl.GetPeers(args[0]) + if err != nil { + return fmt.Errorf("agent peers: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(peers) + } + if len(peers) == 0 { + fmt.Println("No peers found.") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL") + for _, p := range peers { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + p.ID, p.Name, p.WorkspaceID, p.Status, p.Model) + } + return w.Flush() +} \ No newline at end of file diff --git a/internal/cmd/config.go b/internal/cmd/config.go new file mode 100644 index 0000000..23edc4b --- /dev/null +++ b/internal/cmd/config.go @@ -0,0 +1,175 @@ +// Package cmd implements the CLI command tree. +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// --------------------------------------------------------------------------- +// Config command group +// --------------------------------------------------------------------------- + +var configCmd = &cobra.Command{ + Use: "config", + Short: "View and manage CLI and workspace configuration", + Long: `mol config list — list all config keys (from file + env) +mol config get — print a single config value +mol config set — write a key to the config file +mol config init — scaffold a default mol.yaml in the current directory +mol config view — print the current config file with sources annotated`, +} + +func init() { + configCmd.AddCommand( + configListCmd, configGetCmd, configSetCmd, configInitCmd, configViewCmd, + ) +} + +// =========================================================================== +// mol config list +// =========================================================================== +var configListCmd = &cobra.Command{ + Use: "list", + Short: "List all known config keys and their effective values", + RunE: runConfigList, +} + +func runConfigList(cmd *cobra.Command, _ []string) error { + settings := viper.AllSettings() + if len(settings) == 0 { + fmt.Println("No config keys set. Use `mol config set ` or set env vars.") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "KEY\tVALUE\tSOURCE") + for k, v := range settings { + source := "default" + if viper.InConfig(k) { + source = "file" + } + if strings.HasPrefix(k, "MOLECULE_") || strings.HasPrefix(k, "MOL_") { + source = "env" + } + fmt.Fprintf(w, "%s\t%v\t%s\n", k, v, source) + } + return w.Flush() +} + +// =========================================================================== +// mol config get +// =========================================================================== +var configGetCmd = &cobra.Command{ + Use: "get ", + Short: "Print the effective value of a config key", + Args: cobra.ExactArgs(1), + RunE: runConfigGet, +} + +func runConfigGet(cmd *cobra.Command, args []string) error { + if !viper.IsSet(args[0]) { + return fmt.Errorf("config get: key %q not set (check env var MOLECULE_%s)", args[0], args[0]) + } + fmt.Println(viper.GetString(args[0])) + return nil +} + +// =========================================================================== +// mol config set +// =========================================================================== +var configSetCmd = &cobra.Command{ + Use: "set ", + Short: "Write a config key to the config file (~/.config/mol.yaml)", + Args: cobra.ExactArgs(2), + RunE: runConfigSet, +} + +func runConfigSet(cmd *cobra.Command, args []string) error { + key, value := args[0], args[1] + configDir, err := os.UserConfigDir() + if err != nil { + configDir = "." + } + configFile := filepath.Join(configDir, "mol.yaml") + + v := viper.New() + v.SetConfigFile(configFile) + _ = v.ReadInConfig() // ignore not-found + v.Set(key, value) + if err := v.WriteConfig(); err != nil { + if err2 := v.SafeWriteConfig(); err2 != nil { + return fmt.Errorf("config set: write %s: %w (tried WriteConfig then SafeWriteConfig)", configFile, err) + } + } + fmt.Printf("Set %s=%q in %s\n", key, value, v.ConfigFileUsed()) + return nil +} + +// =========================================================================== +// mol config init +// =========================================================================== +var configInitCmd = &cobra.Command{ + Use: "init", + Short: "Scaffold a default mol.yaml in the current directory", + RunE: runConfigInit, +} + +func runConfigInit(cmd *cobra.Command, _ []string) error { + const defaultConfig = `# mol CLI config — https://github.com/Molecule-AI/molecule-cli +# +# All values can be overridden by environment variables: +# MOLECULE_API_URL, MOLECULE_RUNTIME_URL, MOL_OUTPUT, MOL_VERBOSE, etc. + +# Platform API base URL (env: MOLECULE_API_URL) +# api_url: http://localhost:8080 + +# Output format: table | json | yaml (env: MOL_OUTPUT) +# output: table + +# Verbose logging: true | false (env: MOL_VERBOSE) +# verbose: false +` + if _, err := os.Stat("mol.yaml"); err == nil { + return fmt.Errorf("config init: mol.yaml already exists (not overwriting)") + } + if err := os.WriteFile("mol.yaml", []byte(defaultConfig), 0o644); err != nil { + return fmt.Errorf("config init: write mol.yaml: %w", err) + } + fmt.Println("Scaffolded mol.yaml — edit it and run mol --config mol.yaml, or move it to ~/.config/mol.yaml") + return nil +} + +// =========================================================================== +// mol config view +// =========================================================================== +var configViewCmd = &cobra.Command{ + Use: "view", + Short: "Print the current config file with sources annotated", + RunE: runConfigView, +} + +func runConfigView(cmd *cobra.Command, _ []string) error { + if viper.ConfigFileUsed() == "" { + fmt.Println("No config file in use. Set one with --config or mol config init.") + fmt.Println("\nActive env vars starting with MOLECULE_ or MOL_:") + for _, env := range os.Environ() { + if strings.HasPrefix(env, "MOLECULE_") || strings.HasPrefix(env, "MOL_") { + fmt.Println(" ", env) + } + } + return nil + } + data, err := os.ReadFile(viper.ConfigFileUsed()) + if err != nil { + return fmt.Errorf("config view: read %s: %w", viper.ConfigFileUsed(), err) + } + fmt.Printf("# Config file: %s\n\n", viper.ConfigFileUsed()) + fmt.Print(string(data)) + return nil +} \ No newline at end of file diff --git a/internal/cmd/http.go b/internal/cmd/http.go new file mode 100644 index 0000000..3656b03 --- /dev/null +++ b/internal/cmd/http.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// runHTTP does a raw HTTP call. +func runHTTP(method, url string, body []byte) ([]byte, error) { + req, err := http.NewRequest(method, url, strings.NewReader(string(body))) + if err != nil { + return nil, err + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + resp, err := httpClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP %d — %s", resp.StatusCode, string(b)) + } + return b, nil +} + +func httpClient() *http.Client { + return &http.Client{Timeout: 30 * time.Second} +} \ No newline at end of file diff --git a/internal/cmd/platform.go b/internal/cmd/platform.go new file mode 100644 index 0000000..93173e3 --- /dev/null +++ b/internal/cmd/platform.go @@ -0,0 +1,145 @@ +// Package cmd implements the CLI command tree. +package cmd + +import ( + "fmt" + "io" + "net/http" + "os" + "text/tabwriter" + + "github.com/Molecule-AI/molecule-cli/internal/client" + "github.com/spf13/cobra" +) + +// --------------------------------------------------------------------------- +// Platform command group +// --------------------------------------------------------------------------- + +var platformCmd = &cobra.Command{ + Use: "platform", + Short: "Platform-level operations", + Long: `Audit the platform, check health, and inspect raw API responses.`, +} + +func init() { + platformCmd.AddCommand(platformAuditCmd, platformHealthCmd) +} + +// =========================================================================== +// mol platform audit +// =========================================================================== +var platformAuditCmd = &cobra.Command{ + Use: "audit", + Short: "Full platform audit: workspaces, agents, delegation summary", + RunE: runPlatformAudit, +} + +func runPlatformAudit(cmd *cobra.Command, _ []string) error { + cl := client.New(apiURL) + workspaces, agents, err := cl.AuditWorkspaces() + if err != nil { + return fmt.Errorf("platform audit: %w", err) + } + + delegationsByWS := map[string]int{} + for _, ws := range workspaces { + dels, err := cl.GetDelegations(ws.ID) + if err == nil { + delegationsByWS[ws.ID] = len(dels) + } + } + + type wsRow struct { + ID, Name, Status, Role string + AgentCount, DelegationCount int + } + rows := make([]wsRow, 0, len(workspaces)) + for _, ws := range workspaces { + ac := 0 + for _, a := range agents { + if a.WorkspaceID == ws.ID { + ac++ + } + } + rows = append(rows, wsRow{ + ID: ws.ID, Name: ws.Name, Status: ws.Status, Role: ws.Role, + AgentCount: ac, DelegationCount: delegationsByWS[ws.ID], + }) + } + + if outputFormat == "json" || outputFormat == "yaml" { + type audit struct { + WorkspaceCount int `json:"workspace_count"` + AgentCount int `json:"agent_count"` + ByStatus map[string]int `json:"by_status"` + DelegationMap map[string]int `json:"delegations_by_workspace"` + Rows []wsRow `json:"workspaces"` + Agents []client.Agent `json:"agents"` + } + byStatus := map[string]int{} + for _, ws := range workspaces { + byStatus[ws.Status]++ + } + return printJSON(audit{ + WorkspaceCount: len(workspaces), + AgentCount: len(agents), + ByStatus: byStatus, + DelegationMap: delegationsByWS, + Rows: rows, + Agents: agents, + }) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintf(w, "=== Platform Audit (%d workspaces, %d agents) ===\n\n", + len(workspaces), len(agents)) + fmt.Fprintln(w, "WORKSPACE\tSTATUS\tROLE\tAGENTS\tDELEGATIONS") + for _, r := range rows { + fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%d\n", + r.Name, r.Status, r.Role, r.AgentCount, r.DelegationCount) + } + return w.Flush() +} + +// =========================================================================== +// mol platform health +// =========================================================================== +var platformHealthCmd = &cobra.Command{ + Use: "health", + Short: "Check platform health and version", + RunE: runPlatformHealth, +} + +func runPlatformHealth(cmd *cobra.Command, _ []string) error { + cl := client.New(apiURL) + h, err := cl.Health() + if err != nil { + // Fall back to raw check if /health 404s on older platforms. + body, hErr := platformRawHealth(cl.BaseURL) + if hErr != nil { + return fmt.Errorf("platform health: %w (and /health fallback also failed: %v)", err, hErr) + } + fmt.Printf("Platform reachable at %s — raw status: %s\n", cl.BaseURL, string(body)) + return nil + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(h) + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + kv(w, "Status", h.Status) + kv(w, "Version", h.Version) + kv(w, "Uptime", h.Uptime) + kv(w, "Database", h.Database) + return w.Flush() +} + +func platformRawHealth(baseURL string) ([]byte, error) { + req, _ := http.NewRequest("GET", baseURL+"/health", nil) + resp, err := httpClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} \ No newline at end of file diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..0988d4c --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,126 @@ +// Package cmd implements the CLI command tree. +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "text/tabwriter" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Version is set at build time via -ldflags. +var Version = "dev" + +// Global flags. +var ( + verbose bool + outputFormat string + configPath string + apiURL string +) + +// rootCmd is the top-level molecule command. +var rootCmd = &cobra.Command{ + Use: "mol", + Version: Version, + Short: "mol — Molecule AI platform CLI", + Long: `mol is the CLI for the Molecule AI agent platform. + +Manage workspaces, inspect agents, audit the platform, and configure +agent behaviour from the terminal. + +Quick start: + mol workspace list + mol agent list + mol platform health`, + SilenceUsage: true, + SilenceErrors: true, +} + +func init() { + rootCmd.PersistentFlags().StringVar(&apiURL, "api-url", + envOr("MOLECULE_API_URL", "http://localhost:8080"), + "Platform API base URL (env: MOLECULE_API_URL)") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, + "Enable verbose (DEBUG-level) output to stderr") + rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", + "Output format: table | json | yaml") + rootCmd.PersistentFlags().StringVar(&configPath, "config", "", + "Path to config file (default ~/.config/mol.yaml or ./mol.yaml)") + rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { + return &exitError{code: 2, msg: err.Error()} + }) + rootCmd.SetErr(os.Stderr) +} + +// Execute runs the CLI. +func Execute() error { + // Load config file. + if configPath != "" { + viper.SetConfigFile(configPath) + } else { + viper.SetConfigName("mol") + viper.AddConfigPath("$HOME/.config") + viper.AddConfigPath(".") + } + viper.AutomaticEnv() + _ = viper.ReadInConfig() // ignore not-found; env vars win + + return rootCmd.Execute() +} + +// envOr returns the value of env var key, or fallback if unset/empty. +func envOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// init registers all subcommand trees. +func init() { + rootCmd.AddCommand(workspaceCmd) + rootCmd.AddCommand(agentCmd) + rootCmd.AddCommand(platformCmd) + rootCmd.AddCommand(configCmd) +} + +// exitError wraps a user-facing error with a specific exit code. +type exitError struct{ code int; msg string } + +func (e *exitError) Error() string { return e.msg } + +// handleErr converts an error to the right exit code. +func handleErr(err error) error { + if err == nil { + return nil + } + if ee, ok := err.(*exitError); ok { + fmt.Fprintf(os.Stderr, "%s\n", ee.msg) + os.Exit(ee.code) + } + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + return nil +} + +// printJSON writes v as JSON to stdout. +func printJSON(v interface{}) error { + return json.NewEncoder(os.Stdout).Encode(v) +} + +// kv writes a key-value pair to the tabwriter (only if v is non-empty). +func kv(w *tabwriter.Writer, k, v string) { + if v == "" { + return + } + fmt.Fprintf(w, "%s:\t%s\n", k, v) +} + +func versionInfo() string { + return fmt.Sprintf("mol %s (go %s)", Version, runtime.Version()) +} \ No newline at end of file diff --git a/internal/cmd/workspace.go b/internal/cmd/workspace.go new file mode 100644 index 0000000..c2c083e --- /dev/null +++ b/internal/cmd/workspace.go @@ -0,0 +1,289 @@ +// Package cmd implements the CLI command tree. +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + "github.com/Molecule-AI/molecule-cli/internal/client" + "github.com/spf13/cobra" +) + +// --------------------------------------------------------------------------- +// Workspace command group +// --------------------------------------------------------------------------- + +var workspaceCmd = &cobra.Command{ + Use: "workspace", + Short: "Manage Molecule AI workspaces", + Long: `List, inspect, create, delete, restart, audit, and delegate to workspaces.`, +} + +func init() { + workspaceCmd.AddCommand( + workspaceListCmd, workspaceCreateCmd, workspaceInspectCmd, + workspaceDeleteCmd, workspaceRestartCmd, workspaceAuditCmd, workspaceDelegateCmd, + ) +} + +// =========================================================================== +// mol workspace list +// =========================================================================== +var workspaceListCmd = &cobra.Command{ + Use: "list", + Short: "List all workspaces", + RunE: runWorkspaceList, +} + +func runWorkspaceList(cmd *cobra.Command, _ []string) error { + cl := client.New(apiURL) + ws, err := cl.ListWorkspaces() + if err != nil { + return fmt.Errorf("workspace list: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(ws) + } + if len(ws) == 0 { + fmt.Println("No workspaces found.") + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "ID\tNAME\tSTATUS\tROLE\tRUNTIME\tCREATED AT") + for _, s := range ws { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", + s.ID, s.Name, s.Status, s.Role, s.Runtime, s.CreatedAt) + } + return w.Flush() +} + +// =========================================================================== +// mol workspace create +// =========================================================================== +var createFlags struct { + name string + role string + runtime string + template string + parentID string + workspaceDir string + tier int +} + +var workspaceCreateCmd = &cobra.Command{ + Use: "create --name [flags]", + Short: "Create a new workspace", + RunE: runWorkspaceCreate, +} + +func init() { + f := workspaceCreateCmd.Flags() + f.StringVarP(&createFlags.name, "name", "n", "", "Workspace name (required)") + f.StringVar(&createFlags.role, "role", "", "Role (e.g. pm, report, researcher)") + f.StringVar(&createFlags.runtime, "runtime", "", "Runtime (e.g. claude-code, deepagents)") + f.StringVar(&createFlags.template, "template", "", "Template name or ID") + f.StringVar(&createFlags.parentID, "parent-id", "", "Parent workspace ID") + f.StringVar(&createFlags.workspaceDir, "workspace-dir", "", "Workspace directory path") + f.IntVar(&createFlags.tier, "tier", 0, "Tier value") + workspaceCreateCmd.MarkFlagRequired("name") +} + +func runWorkspaceCreate(cmd *cobra.Command, _ []string) error { + cl := client.New(apiURL) + req := client.CreateWorkspaceRequest{Name: createFlags.name} + if createFlags.role != "" { + req.Role = createFlags.role + } + if createFlags.runtime != "" { + req.Runtime = createFlags.runtime + } + if createFlags.template != "" { + req.Template = createFlags.template + } + if createFlags.parentID != "" { + req.ParentID = createFlags.parentID + } + if createFlags.workspaceDir != "" { + req.WorkspaceDir = createFlags.workspaceDir + } + if createFlags.tier > 0 { + req.Tier = createFlags.tier + } + ws, err := cl.CreateWorkspace(req) + if err != nil { + return fmt.Errorf("workspace create: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(ws) + } + fmt.Printf("Workspace created: %s (%s)\n", ws.Name, ws.ID) + return nil +} + +// =========================================================================== +// mol workspace inspect +// =========================================================================== +var workspaceInspectCmd = &cobra.Command{ + Use: "inspect ", + Short: "Show full details for a workspace", + Args: cobra.ExactArgs(1), + RunE: runWorkspaceInspect, +} + +func runWorkspaceInspect(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + ws, err := cl.GetWorkspace(args[0]) + if err != nil { + return fmt.Errorf("workspace inspect: %w", err) + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(ws) + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + kv(w, "ID", ws.ID) + kv(w, "Name", ws.Name) + kv(w, "Status", ws.Status) + kv(w, "Role", ws.Role) + kv(w, "Runtime", ws.Runtime) + kv(w, "Tier", fmt.Sprintf("%d", ws.Tier)) + kv(w, "ParentID", ws.ParentID) + kv(w, "WorkspaceDir", ws.WorkspaceDir) + kv(w, "CreatedAt", ws.CreatedAt) + if ws.Canvas != nil { + kv(w, "Canvas", fmt.Sprintf("(%.0f, %.0f)", ws.Canvas.X, ws.Canvas.Y)) + } + return w.Flush() +} + +// =========================================================================== +// mol workspace delete +// =========================================================================== +var workspaceDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a workspace (irreversible)", + Args: cobra.ExactArgs(1), + RunE: runWorkspaceDelete, +} + +func runWorkspaceDelete(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + if err := cl.DeleteWorkspace(args[0]); err != nil { + return fmt.Errorf("workspace delete: %w", err) + } + fmt.Printf("Workspace %q deleted.\n", args[0]) + return nil +} + +// =========================================================================== +// mol workspace restart +// =========================================================================== +var workspaceRestartCmd = &cobra.Command{ + Use: "restart ", + Short: "Restart a workspace", + Args: cobra.ExactArgs(1), + RunE: runWorkspaceRestart, +} + +func runWorkspaceRestart(cmd *cobra.Command, args []string) error { + cl := client.New(apiURL) + if err := cl.RestartWorkspace(args[0]); err != nil { + return fmt.Errorf("workspace restart: %w", err) + } + fmt.Printf("Restart triggered for workspace %q.\n", args[0]) + return nil +} + +// =========================================================================== +// mol workspace audit +// =========================================================================== +var workspaceAuditCmd = &cobra.Command{ + Use: "audit", + Short: "Full workspace + agent audit report", + RunE: runWorkspaceAudit, +} + +func runWorkspaceAudit(cmd *cobra.Command, _ []string) error { + cl := client.New(apiURL) + workspaces, agents, err := cl.AuditWorkspaces() + if err != nil { + return fmt.Errorf("workspace audit: %w", err) + } + type auditReport struct { + Workspaces int `json:"workspaces"` + Agents int `json:"agents"` + ByStatus map[string]int `json:"by_status"` + Items []client.Workspace `json:"workspaces_list"` + AgentList []client.Agent `json:"agents_list"` + } + byStatus := map[string]int{} + for _, ws := range workspaces { + byStatus[ws.Status]++ + } + report := auditReport{ + Workspaces: len(workspaces), + Agents: len(agents), + ByStatus: byStatus, + Items: workspaces, + AgentList: agents, + } + if outputFormat == "json" || outputFormat == "yaml" { + return printJSON(report) + } + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "WORKSPACES\t") + fmt.Fprintln(w, "ID\tNAME\tSTATUS\tROLE\tRUNTIME") + for _, ws := range workspaces { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + ws.ID, ws.Name, ws.Status, ws.Role, ws.Runtime) + } + fmt.Fprintln(w) + fmt.Fprintln(w, "AGENTS\t") + fmt.Fprintln(w, "ID\tNAME\tWORKSPACE\tSTATUS\tMODEL") + for _, a := range agents { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + a.ID, a.Name, a.WorkspaceID, a.Status, a.Model) + } + return w.Flush() +} + +// =========================================================================== +// mol workspace delegate +// =========================================================================== +var workspaceDelegateCmd = &cobra.Command{ + Use: "delegate ", + Short: "Delegate a task to another workspace (non-blocking)", + Args: cobra.ExactArgs(3), + RunE: runWorkspaceDelegate, +} + +func runWorkspaceDelegate(cmd *cobra.Command, args []string) error { + workspaceID, targetID, task := args[0], args[1], args[2] + cl := client.New(apiURL) + + type delReq struct { + TargetID string `json:"target_id"` + Task string `json:"task"` + } + type delResp struct { + DelegationID string `json:"delegation_id,omitempty"` + Status string `json:"status,omitempty"` + } + encoded, _ := json.Marshal(delReq{TargetID: targetID, Task: task}) + body, err := runHTTP("POST", cl.BaseURL+"/workspaces/"+workspaceID+"/delegate", encoded) + if err != nil { + return fmt.Errorf("workspace delegate: %w", err) + } + var resp delResp + if err := json.Unmarshal(body, &resp); err != nil { + return fmt.Errorf("workspace delegate: parse response: %w", err) + } + if resp.DelegationID != "" { + fmt.Printf("Delegation queued: %s (status: %s)\n", resp.DelegationID, resp.Status) + } else { + fmt.Printf("Delegation sent to %q.\n", targetID) + } + _ = workspaceID + return nil +} \ No newline at end of file diff --git a/known-issues.md b/known-issues.md index 4e15a04..1467da2 100644 --- a/known-issues.md +++ b/known-issues.md @@ -27,14 +27,15 @@ Format per entry: ## KI-001 — No entry point yet (`cmd/molecule/main.go` does not exist) -**File:** `cmd/molecule/main.go` -**Status:** Not yet implemented -**Severity:** Critical +**File:** `cmd/molecule/main.go` +**Status:** ✅ Resolved +**Resolved in:** `feat/cli-full-command-tree` branch, commit "feat: implement full CLI command tree" ### Symptom -The repo is initialized as a Go module but has no `cmd/molecule/main.go`. Running -`go build ./cmd/molecule` or `go run ./cmd/molecule` fails with -"package cmd/molecule: cannot find module" or "build failed". +`cmd/molecule/main.go` exists and calls `cmd.Execute()`. Root command is wired +with global flags (`--verbose`, `--output`, `--config`, `--api-url`). All +subcommand groups registered: workspace (7 commands), agent (4 commands), +platform (2 commands), config (5 commands). Binary builds to `bin/mol`. ### Impact The CLI is not runnable. No workspace management, agent inspection, or any other @@ -50,9 +51,11 @@ See the stub checklist in `CLAUDE.md` Section 8. ## KI-002 — No API client; all commands will make raw HTTP calls -**File:** `cmd/molecule/` (no API client package yet) -**Status:** Not yet implemented -**Severity:** High +**File:** `cmd/molecule/` (no API client package yet) +**Status:** ✅ Partially resolved +**Resolved in:** `internal/client/platform.go` exists with workspace and agent +operations; `runHTTP` helper in `internal/cmd/http.go` used by `agent send` and +`workspace delegate`. Remaining: workspace runtime client (dev/proxy mode). ### Symptom There is no `internal/client/` or `pkg/api/` package. Any subcommand @@ -76,9 +79,10 @@ are implemented. ## KI-003 — `go.sum` may contain entries from non-release toolchains -**File:** `go.sum` -**Status:** Identified -**Severity:** Low +**File:** `go.sum` +**Status:** ✅ Resolved +**Resolved in:** `go mod tidy` run on `feat/cli-full-command-tree`; `go.sum` regenerated +clean. Dependencies: cobra v1.10.2, viper v1.21.0, their transitive deps. ### Symptom The `go.sum` file was generated during initial module setup. It may contain @@ -102,8 +106,8 @@ resulting `go.sum`. Add `go mod verify` to CI as a lint step. Ensure ## KI-004 — GoReleaser config may not be aligned with go.mod module path -**File:** `.github/workflows/release.yml` -**Status:** Not verified +**File:** `.github/workflows/release.yml` +**Status:** ⚠️ Unverified — needs real tag to confirm **Severity:** Medium ### Symptom