- Root command: --output, --verbose, --config flags - Workspace subcommands: create/list/inspect/delete/restart/delegate/audit - Agent subcommands: list/inspect/send/peers - Platform subcommands: audit/health - Config subcommands: list/get/set/init/view - Exit codes 0/1/2, errors to stderr, text/json/yaml output - Viper config file support, Gin mode (verbose = debug, silent = release) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
507 lines
14 KiB
Go
507 lines
14 KiB
Go
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"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
} |