Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/cmd/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ func New() *cobra.Command {
}
c.AddCommand(newList())
c.AddCommand(newInstall())
c.AddCommand(newUninstall())
return c
}
48 changes: 48 additions & 0 deletions cli/cmd/plugin/uninstall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package plugin

import (
"github.com/spf13/cobra"
"evercli/internal/cmdctx"
"evercli/internal/plugin"
)

func newUninstall() *cobra.Command {
var uninstallAll bool

c := &cobra.Command{
Use: "uninstall [<platform>...]",
Short: "Remove the EverMe MCP plugin configurations from local Agents",
Long: `Uninstall safely strips EverMe configurations from your local AI agents'
settings files (JSON, TOML, YAML) without touching your other plugins.

Use the --all flag to automatically find and remove EverMe from every supported platform.`,
Example: ` evercli plugin uninstall cursor
evercli plugin uninstall claude-code codex
evercli plugin uninstall --all`,
RunE: func(cmd *cobra.Command, args []string) error {
deps, err := cmdctx.BuildDeps(cmd)
if err != nil {
return deps.Out.Err(err)
}

svc := plugin.NewService(deps.Client, deps.Config.APIBaseURL)

var targetPlatforms []plugin.Platform
if uninstallAll {
targetPlatforms = svc.SupportedPlatforms()
} else {
for _, a := range args {
targetPlatforms = append(targetPlatforms, plugin.Platform(a))
}
}

if err := svc.Uninstall(cmd.Context(), targetPlatforms); err != nil {
return deps.Out.Err(err)
}
return nil
},
}

c.Flags().BoolVar(&uninstallAll, "all", false, "uninstall from all supported platforms")
return c
}
52 changes: 52 additions & 0 deletions cli/internal/plugin/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,58 @@ func (*codexWriter) Commit(_ context.Context, plan *WritePlan, params WriteParam
}, nil
}

// Remove parses ~/.codex/config.toml and strips all EverMe-owned sections:
// the MCP server, the plugin enablement, and the marketplace registration.
// It implements the Remover interface to restore uninstall capabilities.
func (w *codexWriter) Remove(_ context.Context, configPath string) (bool, error) {
// 1. Safely read the TOML file.
cfg, exists, err := readCodexConfig(configPath)
if err != nil {
return false, err
}
if !exists {
return false, nil // File doesn't exist, nothing to remove
}

modified := false

// 2. Strip the active MCP Server (and its nested token)
if mcpServers, ok := cfg["mcp_servers"].(map[string]interface{}); ok {
if _, present := mcpServers[codexMcpEntryName]; present {
delete(mcpServers, codexMcpEntryName)
modified = true
}
}

// 3. Strip the Plugin enablement
if plugins, ok := cfg["plugins"].(map[string]interface{}); ok {
if _, present := plugins[codexPluginSpec]; present {
delete(plugins, codexPluginSpec)
modified = true
}
}

// 4. Strip the Marketplace registration
if marketplaces, ok := cfg["marketplaces"].(map[string]interface{}); ok {
if _, present := marketplaces[codexMarketplaceName]; present {
delete(marketplaces, codexMarketplaceName)
modified = true
}
}

// If we didn't find any EverMe entries, bail out early to avoid a useless disk write
if !modified {
return false, nil
}

// 5. Safely write the cleaned TOML back to disk
if err := writeCodexConfig(configPath, cfg); err != nil {
return false, err
}

return true, nil
}

// Verify re-reads the on-disk config and asserts the three sections
// the install workflow depends on are present:
// - `[marketplaces.everme]` (written by Codex CLI in Prepare)
Expand Down
63 changes: 63 additions & 0 deletions cli/internal/plugin/hermes.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,69 @@ func (*hermesWriter) Commit(_ context.Context, plan *WritePlan, params WritePara
}, nil
}

// Remove uninstalls the EverMe memory provider from Hermes.
// It deletes the embedded Python plugin directory, the credentials env file,
// and strips the memory.provider setting from config.yaml.
// It implements the Remover interface to restore uninstall capabilities.
func (*hermesWriter) Remove(_ context.Context, configPath string) (bool, error) {
home := filepath.Dir(configPath)
modifiedAny := false

// 1. Delete the physical Python plugin directory
pluginDir := filepath.Join(home, "plugins", "everme")
if info, err := os.Stat(pluginDir); err == nil && info.IsDir() {
if err := os.RemoveAll(pluginDir); err != nil {
return false, output.IOErr(pluginDir, "remove-plugin-dir", err)
}
modifiedAny = true
}

// 2. Delete the physical credentials file
envFile := filepath.Join(home, "everme.env")
if _, err := os.Stat(envFile); err == nil {
if err := os.Remove(envFile); err != nil {
return false, output.IOErr(envFile, "remove-env-file", err)
}
modifiedAny = true
}

// 3. Safely read the YAML config
cfg, exists, err := readHermesConfig(configPath)
if err != nil {
return false, err
}

if exists {
yamlModified := false

// 4. Strip the provider setting if it belongs to us
if mem, ok := cfg["memory"].(map[string]interface{}); ok {
if prov, _ := mem["provider"].(string); prov == "everme" {
delete(mem, "provider")
yamlModified = true
}
}

// 5. Clean up the legacy V1.x MCP entry just in case they upgraded
if servers, ok := cfg["mcp_servers"].(map[string]interface{}); ok {
if _, present := servers[hermesMcpEntryName]; present {
delete(servers, hermesMcpEntryName)
yamlModified = true
}
}

// 6. Write back to disk only if we actually changed the YAML
if yamlModified {
if err := writeHermesConfig(configPath, cfg); err != nil {
return false, err
}
modifiedAny = true
}
}

return modifiedAny, nil
}

// writeEvermeEnv writes the three EVERME_* credentials as KEY=VALUE lines
// at mode 0600. The provider's config.py reads this file (priority 3).
func writeEvermeEnv(path string, params WriteParams) error {
Expand Down
34 changes: 34 additions & 0 deletions cli/internal/plugin/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,40 @@ func (m *mcpWriter) Commit(_ context.Context, plan *WritePlan, params WriteParam
}, nil
}

// Remove strips the everme-memory entry from the MCP JSON config if it exists.
// It implements the Remover interface to restore uninstall capabilities.
func (m *mcpWriter) Remove(_ context.Context, configPath string) (bool, error) {
// 1. Safely read the JSON. If it's malformed, readConfig returns an error.
cfg, exists, err := readConfig(configPath)
if err != nil {
return false, err
}
if !exists {
return false, nil // File doesn't exist, nothing to remove
}

// 2. Walk the JSON tree to find the target block (e.g., "mcpServers")
servers := walkServersMap(cfg, m.serversPath)
if servers == nil {
return false, nil // Parent object doesn't exist, nothing to remove
}

// 3. Check if the EverMe entry is actually there
if _, present := servers[mcpEntryName]; !present {
return false, nil // EverMe is not installed here
}

// 4. Safely delete the target key from the Go map
delete(servers, mcpEntryName)

// 5. Use their secure, atomic writer to save the file back to disk
if err := writeConfigAtomic(configPath, cfg); err != nil {
return false, err
}

return true, nil
}

// ---- helpers ---------------------------------------------------------

// readConfig parses the JSON config at path. Returns (cfg, exists, err).
Expand Down
41 changes: 41 additions & 0 deletions cli/internal/plugin/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,44 @@ func (s *Service) buildRegisterReq(p Platform, displayName string) client.Regist
func osTag() string {
return runtimeGOOS()
}

// Uninstall orchestrates the local cleanup of EverMe configurations across the requested
// platforms. It utilizes the Remover interface to safely strip configurations.
func (s *Service) Uninstall(ctx context.Context, platforms []Platform) error {
if len(platforms) == 0 {
return output.Invalid("at least one platform is required (or use --all)", "")
}

for _, p := range platforms {
if !s.reg.Has(p) {
fmt.Printf("✗ %s: unknown platform\n", p)
continue
}

wr := s.reg.writer(p)
remover, ok := wr.(Remover)
if !ok {
fmt.Printf("— %s: uninstall not yet supported for this platform format\n", p)
continue
}

det := s.reg.detector(p)
detection, err := det.Detect(ctx)
if err != nil || detection == nil || detection.ConfigPath == "" {
fmt.Printf("— %s: could not detect config path\n", p)
continue
}

modified, err := remover.Remove(ctx, detection.ConfigPath)
if err != nil {
fmt.Printf("✗ %s failed to uninstall: %v\n", p, err)
} else if modified {
fmt.Printf("✓ %s: successfully removed EverMe configuration from %s\n", p, detection.ConfigPath)
} else {
fmt.Printf("— %s: no EverMe configuration found in %s\n", p, detection.ConfigPath)
}
}

fmt.Println("\nNote: Local configurations removed. Please ensure you disconnect the agents from the EverMe Web UI to revoke server-side tokens.")
return nil
}
12 changes: 12 additions & 0 deletions cli/internal/plugin/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ type Writer interface {
Commit(ctx context.Context, plan *WritePlan, params WriteParams) (*WriteResult, error)
}

// Remover is an extension to Writer that safely strips the everme-memory
// entry from the Agent's MCP config. This restores the uninstall capability
// that was temporarily retired in V1.
type Remover interface {
Writer

// Remove parses the target ConfigPath, deletes the EverMe entry if it
// exists, and atomically rewrites the file. Returns true if the file
// was modified, false if no EverMe entry was found.
Remove(ctx context.Context, configPath string) (bool, error)
}

// Preparer is an optional Writer extension for hosts that need a
// side-effecting setup step BEFORE the backend mints a fresh token.
// Service.installOne calls Prepare immediately after Detect and before
Expand Down