feat: security hardening, tests, doctor command, CI update, CHANGELOG
All checks were successful
CI / build (push) Successful in 2m37s
All checks were successful
CI / build (push) Successful in 2m37s
- Add AES-256-GCM encryption for API keys (internal/secret) - Add dangerous command detection in terminal - Add muyue doctor command for system health checks - Add scanner TTL cache, orchestrator history mutex, shared HTTP client - Deduplicate MCP config generation, refactor skills YAML parser - Add XDG-compliant config dir with legacy migration - Add cleanup on all TUI quit paths - Add 8 test files (config, workflow, skills, orchestrator, version, platform, scanner, secret) - Update CI to actions/setup-go@v5 - Add CHANGELOG.md, update README and Makefile 🤖 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -19,165 +19,108 @@ type MCPServer struct {
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type mcpEntry struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
var knownMCPServers = []MCPServer{
|
||||
{
|
||||
Name: "filesystem",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-filesystem"},
|
||||
Category: "core",
|
||||
},
|
||||
{
|
||||
Name: "github",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-github"},
|
||||
Env: map[string]string{"GITHUB_PERSONAL_ACCESS_TOKEN": ""},
|
||||
Category: "vcs",
|
||||
},
|
||||
{
|
||||
Name: "git",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-git"},
|
||||
Category: "vcs",
|
||||
},
|
||||
{
|
||||
Name: "fetch",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-fetch"},
|
||||
Category: "web",
|
||||
},
|
||||
{
|
||||
Name: "memory",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-memory"},
|
||||
Category: "core",
|
||||
},
|
||||
{
|
||||
Name: "sequential-thinking",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-sequential-thinking"},
|
||||
Category: "ai",
|
||||
},
|
||||
{
|
||||
Name: "brave-search",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-brave-search"},
|
||||
Env: map[string]string{"BRAVE_API_KEY": ""},
|
||||
Category: "web",
|
||||
},
|
||||
{
|
||||
Name: "sqlite",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-sqlite"},
|
||||
Category: "database",
|
||||
},
|
||||
{
|
||||
Name: "postgres",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-postgres"},
|
||||
Category: "database",
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@modelcontextprotocol/server-docker"},
|
||||
Category: "devops",
|
||||
},
|
||||
{
|
||||
Name: "minimax-web-search",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@minimax/mcp-web-search"},
|
||||
Env: map[string]string{"MINIMAX_API_KEY": ""},
|
||||
Category: "ai",
|
||||
},
|
||||
{
|
||||
Name: "minimax-image",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@minimax/mcp-image-understanding"},
|
||||
Env: map[string]string{"MINIMAX_API_KEY": ""},
|
||||
Category: "ai",
|
||||
},
|
||||
{Name: "filesystem", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-filesystem"}, Category: "core"},
|
||||
{Name: "github", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-github"}, Env: map[string]string{"GITHUB_PERSONAL_ACCESS_TOKEN": ""}, Category: "vcs"},
|
||||
{Name: "git", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-git"}, Category: "vcs"},
|
||||
{Name: "fetch", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-fetch"}, Category: "web"},
|
||||
{Name: "memory", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-memory"}, Category: "core"},
|
||||
{Name: "sequential-thinking", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-sequential-thinking"}, Category: "ai"},
|
||||
{Name: "brave-search", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-brave-search"}, Env: map[string]string{"BRAVE_API_KEY": ""}, Category: "web"},
|
||||
{Name: "sqlite", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-sqlite"}, Category: "database"},
|
||||
{Name: "postgres", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-postgres"}, Category: "database"},
|
||||
{Name: "docker", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-docker"}, Category: "devops"},
|
||||
{Name: "minimax-web-search", Command: "npx", Args: []string{"-y", "@minimax/mcp-web-search"}, Env: map[string]string{"MINIMAX_API_KEY": ""}, Category: "ai"},
|
||||
{Name: "minimax-image", Command: "npx", Args: []string{"-y", "@minimax/mcp-image-understanding"}, Env: map[string]string{"MINIMAX_API_KEY": ""}, Category: "ai"},
|
||||
}
|
||||
|
||||
func ScanServers() []MCPServer {
|
||||
servers := make([]MCPServer, len(knownMCPServers))
|
||||
for i, s := range knownMCPServers {
|
||||
servers[i] = s
|
||||
if s.Command == "npx" {
|
||||
_, err := exec.LookPath("npx")
|
||||
servers[i].Installed = err == nil
|
||||
} else {
|
||||
_, err := exec.LookPath(s.Command)
|
||||
servers[i].Installed = err == nil
|
||||
}
|
||||
_, err := exec.LookPath(s.Command)
|
||||
servers[i].Installed = err == nil
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
func getCoreEntries(homeDir string) []mcpEntry {
|
||||
return []mcpEntry{
|
||||
{"filesystem", "npx", []string{"-y", "@modelcontextprotocol/server-filesystem", homeDir + "/projects"}, nil},
|
||||
{"fetch", "npx", []string{"-y", "@modelcontextprotocol/server-fetch"}, nil},
|
||||
{"memory", "npx", []string{"-y", "@modelcontextprotocol/server-memory"}, nil},
|
||||
}
|
||||
}
|
||||
|
||||
func withProviderEntries(base []mcpEntry, cfg *config.MuyueConfig, extraEntries []mcpEntry) []mcpEntry {
|
||||
entries := make([]mcpEntry, len(base))
|
||||
copy(entries, base)
|
||||
entries = append(entries, extraEntries...)
|
||||
|
||||
if cfg != nil {
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Name == "minimax" && p.APIKey != "" {
|
||||
entries = append(entries,
|
||||
mcpEntry{"minimax-web-search", "npx", []string{"-y", "@minimax/mcp-web-search"}, map[string]string{"MINIMAX_API_KEY": p.APIKey}},
|
||||
mcpEntry{"minimax-image", "npx", []string{"-y", "@minimax/mcp-image-understanding"}, map[string]string{"MINIMAX_API_KEY": p.APIKey}},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func writeMCPConfig(configPath string, mcpKey string, entries []mcpEntry) error {
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
return fmt.Errorf("create config dir: %w", err)
|
||||
}
|
||||
|
||||
existing := map[string]interface{}{}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err == nil {
|
||||
json.Unmarshal(data, &existing)
|
||||
}
|
||||
|
||||
mcpMap := map[string]interface{}{}
|
||||
for _, e := range entries {
|
||||
entry := map[string]interface{}{
|
||||
"command": e.cmd,
|
||||
"args": e.args,
|
||||
}
|
||||
if len(e.env) > 0 {
|
||||
entry["env"] = e.env
|
||||
}
|
||||
mcpMap[e.name] = entry
|
||||
}
|
||||
|
||||
existing[mcpKey] = mcpMap
|
||||
|
||||
out, err := json.MarshalIndent(existing, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, out, 0600)
|
||||
}
|
||||
|
||||
func GenerateCrushMCPConfig(cfg *config.MuyueConfig, homeDir string) error {
|
||||
if homeDir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
homeDir = home
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "crush")
|
||||
crusherPath := filepath.Join(configDir, "crush.json")
|
||||
|
||||
os.MkdirAll(configDir, 0755)
|
||||
|
||||
existing := map[string]interface{}{}
|
||||
data, err := os.ReadFile(crusherPath)
|
||||
if err == nil {
|
||||
if jsonErr := json.Unmarshal(data, &existing); jsonErr != nil {
|
||||
existing = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
core := []MCPServer{
|
||||
{Name: "filesystem", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-filesystem", homeDir + "/projects"}},
|
||||
{Name: "fetch", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-fetch"}},
|
||||
{Name: "memory", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-memory"}},
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Name == "minimax" && p.APIKey != "" {
|
||||
core = append(core, MCPServer{
|
||||
Name: "minimax-web-search",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@minimax/mcp-web-search"},
|
||||
Env: map[string]string{"MINIMAX_API_KEY": p.APIKey},
|
||||
})
|
||||
core = append(core, MCPServer{
|
||||
Name: "minimax-image",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@minimax/mcp-image-understanding"},
|
||||
Env: map[string]string{"MINIMAX_API_KEY": p.APIKey},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mcps := map[string]interface{}{}
|
||||
|
||||
for _, s := range core {
|
||||
entry := map[string]interface{}{
|
||||
"command": s.Command,
|
||||
"args": s.Args,
|
||||
}
|
||||
if len(s.Env) > 0 {
|
||||
entry["env"] = s.Env
|
||||
}
|
||||
mcps[s.Name] = entry
|
||||
}
|
||||
|
||||
existing["mcps"] = mcps
|
||||
|
||||
out, err := json.MarshalIndent(existing, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(crusherPath, out, 0644)
|
||||
core := getCoreEntries(homeDir)
|
||||
entries := withProviderEntries(core, cfg, nil)
|
||||
configPath := filepath.Join(homeDir, ".config", "crush", "crush.json")
|
||||
return writeMCPConfig(configPath, "mcps", entries)
|
||||
}
|
||||
|
||||
func GenerateClaudeMCPConfig(cfg *config.MuyueConfig, homeDir string) error {
|
||||
@@ -186,62 +129,13 @@ func GenerateClaudeMCPConfig(cfg *config.MuyueConfig, homeDir string) error {
|
||||
homeDir = home
|
||||
}
|
||||
|
||||
configPath := filepath.Join(homeDir, ".claude.json")
|
||||
|
||||
existing := map[string]interface{}{}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err == nil {
|
||||
if jsonErr := json.Unmarshal(data, &existing); jsonErr != nil {
|
||||
existing = map[string]interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
mcpservers := map[string]interface{}{}
|
||||
|
||||
core := []struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
env map[string]string
|
||||
}{
|
||||
{"filesystem", "npx", []string{"-y", "@modelcontextprotocol/server-filesystem", homeDir + "/projects"}, nil},
|
||||
{"fetch", "npx", []string{"-y", "@modelcontextprotocol/server-fetch"}, nil},
|
||||
{"memory", "npx", []string{"-y", "@modelcontextprotocol/server-memory"}, nil},
|
||||
core := getCoreEntries(homeDir)
|
||||
extra := []mcpEntry{
|
||||
{"sequential-thinking", "npx", []string{"-y", "@modelcontextprotocol/server-sequential-thinking"}, nil},
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
for _, p := range cfg.AI.Providers {
|
||||
if p.Name == "minimax" && p.APIKey != "" {
|
||||
core = append(core, struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
env map[string]string
|
||||
}{"minimax-web-search", "npx", []string{"-y", "@minimax/mcp-web-search"}, map[string]string{"MINIMAX_API_KEY": p.APIKey}})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range core {
|
||||
entry := map[string]interface{}{
|
||||
"command": s.cmd,
|
||||
"args": s.args,
|
||||
}
|
||||
if len(s.env) > 0 {
|
||||
entry["env"] = s.env
|
||||
}
|
||||
mcpservers[s.name] = entry
|
||||
}
|
||||
|
||||
existing["mcpServers"] = mcpservers
|
||||
|
||||
out, err := json.MarshalIndent(existing, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, out, 0644)
|
||||
entries := withProviderEntries(core, cfg, extra)
|
||||
configPath := filepath.Join(homeDir, ".claude.json")
|
||||
return writeMCPConfig(configPath, "mcpServers", entries)
|
||||
}
|
||||
|
||||
func ConfigureAll(cfg *config.MuyueConfig) error {
|
||||
|
||||
Reference in New Issue
Block a user