package cli import ( "context" "errors" "fmt" "io" "email-mcp/internal/mcpserver" "email-mcp/internal/secretstore" "email-mcp/internal/secretstore/kwallet" ) type MCPRunner interface { Run(ctx context.Context) error } type App struct { prompter SetupPrompter store secretstore.Store runner MCPRunner stderr io.Writer } func NewApp() *App { return BuildApp() } func NewAppWithDependencies(prompter SetupPrompter, store secretstore.Store, runner MCPRunner, stderr io.Writer) *App { if stderr == nil { stderr = io.Discard } return &App{ prompter: prompter, store: store, runner: runner, stderr: stderr, } } func (a *App) Run(args []string) error { if len(args) == 0 { return fmt.Errorf("usage: email-mcp ") } switch args[0] { case "setup": return a.runSetup(context.Background()) case "mcp": return a.runMCP(context.Background()) default: return fmt.Errorf("unknown command: %s", args[0]) } } func (a *App) runSetup(ctx context.Context) error { if a.prompter == nil { return fmt.Errorf("setup prompter is not configured") } if a.store == nil { return fmt.Errorf("secret store is not configured") } cred, err := a.prompter.PromptSetup(ctx) if err != nil { return err } if err := cred.Validate(); err != nil { return err } if err := a.store.Save(ctx, secretstore.DefaultAccountKey, cred); err != nil { return mapAppError(err) } return nil } func (a *App) runMCP(ctx context.Context) error { if a.runner == nil { return fmt.Errorf("mcp runner is not configured") } return mapAppError(a.runner.Run(ctx)) } func mapAppError(err error) error { if err == nil { return nil } switch { case errors.Is(err, mcpserver.ErrCredentialsNotConfigured): return newUserFacingError("credentials not configured; run `email-mcp setup`", err) case errors.Is(err, kwallet.ErrKWalletUnavailable): return newUserFacingError("kwallet is not available; make sure KDE Wallet is installed and your session D-Bus is running", err) case errors.Is(err, kwallet.ErrKWalletDisabled): return newUserFacingError("kwallet is disabled in this KDE session", err) case errors.Is(err, kwallet.ErrKWalletOpenFailed): return newUserFacingError("kwallet could not be opened; unlock the wallet and try again", err) case errors.Is(err, kwallet.ErrCredentialNotFound): return newUserFacingError("credentials not configured; run `email-mcp setup`", err) default: return err } } type userFacingError struct { message string err error } func (e *userFacingError) Error() string { return e.message } func (e *userFacingError) Unwrap() error { return e.err } func newUserFacingError(message string, err error) error { return &userFacingError{ message: message, err: err, } }