feat: implement full CLI command tree

Implement the core CLI for molecule-cli:

- cmd/molecule/main.go: entry point calling cmd.Execute()
- internal/cmd/root.go: cobra root with global flags (--api-url,
  --verbose, --output, --config), registers all 4 command groups
- internal/cmd/workspace.go: 7 subcommands (list, create, inspect,
  delete, restart, audit, delegate)
- internal/cmd/agent.go: 4 subcommands (list, inspect, send, peers)
- internal/cmd/platform.go: 2 subcommands (audit, health)
- internal/cmd/config.go: 5 subcommands (list, get, set, init, view)
- internal/cmd/http.go: runHTTP helper shared by agent send and
  workspace delegate
- internal/client/platform.go: control plane HTTP client with
  workspace/agent/health/audit operations

All 18 subcommands wire to platform API via MOLECULE_API_URL.
Binary builds to ./bin/mol. Resolves KI-001, KI-002 (partial),
KI-003.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Molecule AI · sdk-dev 2026-04-21 00:57:49 +00:00 committed by Molecule AI Plugin-Dev
parent 25601ba758
commit 3eabe3c780
11 changed files with 1293 additions and 818 deletions

View File

@ -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 <agent-id>
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)
}
}

95
go.mod
View File

@ -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
)

263
go.sum
View File

@ -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=

276
internal/client/platform.go Normal file
View File

@ -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
}

175
internal/cmd/agent.go Normal file
View File

@ -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 <agent-id>",
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 <agent-id> <message>",
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 <workspace-id>",
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()
}

175
internal/cmd/config.go Normal file
View File

@ -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 <key> print a single config value
mol config set <key> <value> 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 <key> <value>` 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 <key>",
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 <key> <value>",
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
}

34
internal/cmd/http.go Normal file
View File

@ -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}
}

145
internal/cmd/platform.go Normal file
View File

@ -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)
}

126
internal/cmd/root.go Normal file
View File

@ -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())
}

289
internal/cmd/workspace.go Normal file
View File

@ -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 <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 <workspace-id>",
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 <workspace-id>",
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 <workspace-id>",
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 <workspace-id> <target-workspace-id> <task>",
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
}

View File

@ -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