wingman-ai 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. share/wingman/node_listener/package-lock.json +1785 -0
  2. share/wingman/node_listener/package.json +50 -0
  3. share/wingman/node_listener/src/index.ts +108 -0
  4. share/wingman/node_listener/src/ipc.ts +70 -0
  5. share/wingman/node_listener/src/messageHandler.ts +135 -0
  6. share/wingman/node_listener/src/socket.ts +244 -0
  7. share/wingman/node_listener/src/types.d.ts +13 -0
  8. share/wingman/node_listener/tsconfig.json +19 -0
  9. wingman/__init__.py +4 -0
  10. wingman/__main__.py +6 -0
  11. wingman/cli/__init__.py +5 -0
  12. wingman/cli/commands/__init__.py +1 -0
  13. wingman/cli/commands/auth.py +90 -0
  14. wingman/cli/commands/config.py +109 -0
  15. wingman/cli/commands/init.py +71 -0
  16. wingman/cli/commands/logs.py +84 -0
  17. wingman/cli/commands/start.py +111 -0
  18. wingman/cli/commands/status.py +84 -0
  19. wingman/cli/commands/stop.py +33 -0
  20. wingman/cli/commands/uninstall.py +113 -0
  21. wingman/cli/main.py +50 -0
  22. wingman/cli/wizard.py +356 -0
  23. wingman/config/__init__.py +31 -0
  24. wingman/config/paths.py +153 -0
  25. wingman/config/personality.py +155 -0
  26. wingman/config/registry.py +343 -0
  27. wingman/config/settings.py +294 -0
  28. wingman/core/__init__.py +16 -0
  29. wingman/core/agent.py +257 -0
  30. wingman/core/ipc_handler.py +124 -0
  31. wingman/core/llm/__init__.py +5 -0
  32. wingman/core/llm/client.py +77 -0
  33. wingman/core/memory/__init__.py +6 -0
  34. wingman/core/memory/context.py +109 -0
  35. wingman/core/memory/models.py +213 -0
  36. wingman/core/message_processor.py +277 -0
  37. wingman/core/policy/__init__.py +5 -0
  38. wingman/core/policy/evaluator.py +265 -0
  39. wingman/core/process_manager.py +135 -0
  40. wingman/core/safety/__init__.py +8 -0
  41. wingman/core/safety/cooldown.py +63 -0
  42. wingman/core/safety/quiet_hours.py +75 -0
  43. wingman/core/safety/rate_limiter.py +58 -0
  44. wingman/core/safety/triggers.py +117 -0
  45. wingman/core/transports/__init__.py +14 -0
  46. wingman/core/transports/base.py +106 -0
  47. wingman/core/transports/imessage/__init__.py +5 -0
  48. wingman/core/transports/imessage/db_listener.py +280 -0
  49. wingman/core/transports/imessage/sender.py +162 -0
  50. wingman/core/transports/imessage/transport.py +140 -0
  51. wingman/core/transports/whatsapp.py +180 -0
  52. wingman/daemon/__init__.py +5 -0
  53. wingman/daemon/manager.py +303 -0
  54. wingman/installer/__init__.py +5 -0
  55. wingman/installer/node_installer.py +253 -0
  56. wingman_ai-1.0.0.dist-info/METADATA +553 -0
  57. wingman_ai-1.0.0.dist-info/RECORD +60 -0
  58. wingman_ai-1.0.0.dist-info/WHEEL +4 -0
  59. wingman_ai-1.0.0.dist-info/entry_points.txt +2 -0
  60. wingman_ai-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,90 @@
1
+ """wingman auth - WhatsApp authentication."""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from wingman.config.paths import WingmanPaths
10
+ from wingman.config.settings import Settings
11
+
12
+ console = Console()
13
+
14
+
15
+ def auth() -> None:
16
+ """
17
+ Connect to WhatsApp by scanning a QR code.
18
+
19
+ Starts the WhatsApp listener in interactive mode to display
20
+ the QR code for authentication.
21
+ """
22
+ paths = WingmanPaths()
23
+
24
+ # Check if initialized
25
+ if not paths.is_initialized():
26
+ console.print("[red]Wingman is not set up yet.[/red]")
27
+ console.print("Run [bold]wingman init[/bold] first.")
28
+ raise typer.Exit(1)
29
+
30
+ console.print(
31
+ Panel.fit(
32
+ "[bold blue]WhatsApp Authentication[/bold blue]\n\n"
33
+ "A QR code will appear below.\n"
34
+ "Scan it with WhatsApp on your phone:\n\n"
35
+ " 1. Open WhatsApp\n"
36
+ " 2. Go to Settings > Linked Devices\n"
37
+ " 3. Tap 'Link a Device'\n"
38
+ " 4. Scan the QR code",
39
+ border_style="blue",
40
+ )
41
+ )
42
+ console.print()
43
+
44
+ # Load settings
45
+ settings = Settings.load(paths=paths)
46
+
47
+ # Run the auth process
48
+ try:
49
+ asyncio.run(_run_auth(settings))
50
+ except KeyboardInterrupt:
51
+ console.print()
52
+ console.print("[yellow]Authentication cancelled.[/yellow]")
53
+ raise typer.Exit(1)
54
+
55
+
56
+ async def _run_auth(settings: Settings) -> None:
57
+ """Run the WhatsApp authentication process."""
58
+ from wingman.core.transports import WhatsAppTransport
59
+
60
+ connected = asyncio.Event()
61
+
62
+ transport = WhatsAppTransport(settings.node_dir, auth_state_dir=settings.auth_state_dir)
63
+
64
+ async def on_connected(user_id: str) -> None:
65
+ console.print()
66
+ console.print(f"[green]Connected as {user_id}[/green]")
67
+ console.print()
68
+ console.print("[bold green]Authentication successful![/bold green]")
69
+ console.print("You can now run [bold]wingman start[/bold] to start the bot.")
70
+ connected.set()
71
+
72
+ async def on_qr_code() -> None:
73
+ console.print("[dim]QR code displayed above - scan with WhatsApp[/dim]")
74
+
75
+ transport.set_connected_handler(on_connected)
76
+ transport.set_qr_code_handler(on_qr_code)
77
+
78
+ # Start transport and wait for connection
79
+ async def run_until_connected():
80
+ asyncio.create_task(transport.start())
81
+ try:
82
+ # Wait for connection or timeout
83
+ await asyncio.wait_for(connected.wait(), timeout=300) # 5 minute timeout
84
+ except asyncio.TimeoutError:
85
+ console.print("[red]Authentication timed out.[/red]")
86
+ console.print("Please try again with [bold]wingman auth[/bold]")
87
+ finally:
88
+ await transport.stop()
89
+
90
+ await run_until_connected()
@@ -0,0 +1,109 @@
1
+ """wingman config - Edit configuration."""
2
+
3
+ import subprocess
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.syntax import Syntax
8
+
9
+ from wingman.config.paths import WingmanPaths
10
+
11
+ console = Console()
12
+
13
+
14
+ def config(
15
+ edit: bool = typer.Option(
16
+ False,
17
+ "--edit",
18
+ "-e",
19
+ help="Open config file in editor",
20
+ ),
21
+ show: bool = typer.Option(
22
+ False,
23
+ "--show",
24
+ "-s",
25
+ help="Show current config",
26
+ ),
27
+ path: bool = typer.Option(
28
+ False,
29
+ "--path",
30
+ "-p",
31
+ help="Show config file path",
32
+ ),
33
+ ) -> None:
34
+ """
35
+ View or edit Wingman configuration.
36
+
37
+ Without options, shows an overview of config options.
38
+ """
39
+ paths = WingmanPaths()
40
+
41
+ if not paths.config_exists():
42
+ console.print("[red]Wingman is not set up yet.[/red]")
43
+ console.print("Run [bold]wingman init[/bold] first.")
44
+ raise typer.Exit(1)
45
+
46
+ if path:
47
+ console.print(str(paths.config_file))
48
+ return
49
+
50
+ if show:
51
+ _show_config(paths)
52
+ return
53
+
54
+ if edit:
55
+ _edit_config(paths)
56
+ return
57
+
58
+ # Default: show overview
59
+ console.print("[bold]Wingman Configuration[/bold]")
60
+ console.print()
61
+ console.print(f"Config file: {paths.config_file}")
62
+ console.print()
63
+ console.print("Commands:")
64
+ console.print(" [bold]wingman config --show[/bold] Show current config")
65
+ console.print(" [bold]wingman config --edit[/bold] Open in editor")
66
+ console.print(" [bold]wingman config --path[/bold] Print config path")
67
+ console.print()
68
+ console.print("Config files:")
69
+ console.print(f" Main config: {paths.config_file}")
70
+ console.print(f" Contacts: {paths.contacts_config}")
71
+ console.print(f" Groups: {paths.groups_config}")
72
+ console.print(f" Policies: {paths.policies_config}")
73
+
74
+
75
+ def _show_config(paths: WingmanPaths) -> None:
76
+ """Show the current config file contents."""
77
+ config_file = paths.config_file
78
+
79
+ if not config_file.exists():
80
+ console.print("[yellow]Config file not found.[/yellow]")
81
+ return
82
+
83
+ with open(config_file) as f:
84
+ content = f.read()
85
+
86
+ syntax = Syntax(content, "yaml", theme="monokai", line_numbers=True)
87
+ console.print(syntax)
88
+
89
+
90
+ def _edit_config(paths: WingmanPaths) -> None:
91
+ """Open config file in user's editor."""
92
+ import os
93
+
94
+ config_file = paths.config_file
95
+
96
+ # Get editor from environment
97
+ editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "nano"))
98
+
99
+ console.print(f"[dim]Opening {config_file} in {editor}...[/dim]")
100
+
101
+ try:
102
+ subprocess.run([editor, str(config_file)], check=True)
103
+ except FileNotFoundError:
104
+ console.print(f"[red]Editor not found: {editor}[/red]")
105
+ console.print("Set the EDITOR environment variable to your preferred editor.")
106
+ raise typer.Exit(1)
107
+ except subprocess.CalledProcessError as e:
108
+ console.print(f"[red]Editor exited with error: {e}[/red]")
109
+ raise typer.Exit(1)
@@ -0,0 +1,71 @@
1
+ """wingman init - Interactive setup wizard."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+
7
+ from wingman.cli.wizard import SetupWizard
8
+ from wingman.config.paths import WingmanPaths
9
+
10
+ console = Console()
11
+
12
+
13
+ def init(
14
+ force: bool = typer.Option(
15
+ False,
16
+ "--force",
17
+ "-f",
18
+ help="Overwrite existing configuration",
19
+ ),
20
+ ) -> None:
21
+ """
22
+ Interactive setup wizard for Wingman.
23
+
24
+ Sets up OpenAI API key, bot personality, safety settings,
25
+ and installs the WhatsApp listener.
26
+ """
27
+ console.print(
28
+ Panel.fit(
29
+ "[bold blue]Welcome to Wingman![/bold blue]\n\n"
30
+ "This wizard will help you set up your personal AI chat agent.",
31
+ border_style="blue",
32
+ )
33
+ )
34
+ console.print()
35
+
36
+ paths = WingmanPaths()
37
+
38
+ # Check if already initialized
39
+ if paths.is_initialized() and not force:
40
+ console.print("[yellow]Wingman is already set up![/yellow]")
41
+ console.print(f"Config location: {paths.config_dir}")
42
+ console.print()
43
+ console.print("To reconfigure, run: [bold]wingman init --force[/bold]")
44
+ console.print("To connect WhatsApp, run: [bold]wingman auth[/bold]")
45
+ console.print("To start the bot, run: [bold]wingman start[/bold]")
46
+ raise typer.Exit()
47
+
48
+ # Run setup wizard
49
+ wizard = SetupWizard(paths, console)
50
+
51
+ try:
52
+ success = wizard.run()
53
+ if success:
54
+ console.print()
55
+ console.print(
56
+ Panel.fit(
57
+ "[bold green]Setup complete![/bold green]\n\n"
58
+ "Next steps:\n"
59
+ " 1. Run [bold]wingman auth[/bold] to connect WhatsApp\n"
60
+ " 2. Run [bold]wingman start[/bold] to start the bot",
61
+ border_style="green",
62
+ )
63
+ )
64
+ else:
65
+ console.print()
66
+ console.print("[red]Setup incomplete. Please try again.[/red]")
67
+ raise typer.Exit(1)
68
+ except KeyboardInterrupt:
69
+ console.print()
70
+ console.print("[yellow]Setup cancelled.[/yellow]")
71
+ raise typer.Exit(1)
@@ -0,0 +1,84 @@
1
+ """wingman logs - View bot logs."""
2
+
3
+ import subprocess
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from wingman.config.paths import WingmanPaths
9
+
10
+ console = Console()
11
+
12
+
13
+ def logs(
14
+ follow: bool = typer.Option(
15
+ True,
16
+ "--follow/--no-follow",
17
+ "-f/-F",
18
+ help="Follow log output (stream new lines)",
19
+ ),
20
+ lines: int = typer.Option(
21
+ 50,
22
+ "--lines",
23
+ "-n",
24
+ help="Number of lines to show",
25
+ ),
26
+ error: bool = typer.Option(
27
+ False,
28
+ "--error",
29
+ "-e",
30
+ help="Show error log instead of main log",
31
+ ),
32
+ ) -> None:
33
+ """
34
+ View Wingman activity logs.
35
+
36
+ Streams the log file in real-time by default.
37
+ Use --no-follow to just show recent lines.
38
+ """
39
+ paths = WingmanPaths()
40
+
41
+ # Determine log file
42
+ log_file = paths.log_dir / ("agent.log" if not error else "error.log")
43
+
44
+ if not log_file.exists():
45
+ console.print(f"[yellow]Log file not found: {log_file}[/yellow]")
46
+ console.print("Wingman may not have run yet.")
47
+ raise typer.Exit(1)
48
+
49
+ console.print(f"[dim]Log file: {log_file}[/dim]")
50
+ console.print()
51
+
52
+ # Use tail to show/follow logs
53
+ try:
54
+ cmd = ["tail"]
55
+ if follow:
56
+ cmd.append("-f")
57
+ cmd.extend(["-n", str(lines), str(log_file)])
58
+
59
+ # Run tail and stream output
60
+ process = subprocess.Popen(
61
+ cmd,
62
+ stdout=subprocess.PIPE,
63
+ stderr=subprocess.PIPE,
64
+ text=True,
65
+ )
66
+
67
+ try:
68
+ for line in process.stdout:
69
+ # Color-code log levels
70
+ if "ERROR" in line:
71
+ console.print(f"[red]{line.rstrip()}[/red]")
72
+ elif "WARNING" in line:
73
+ console.print(f"[yellow]{line.rstrip()}[/yellow]")
74
+ elif "INFO" in line:
75
+ console.print(line.rstrip())
76
+ else:
77
+ console.print(f"[dim]{line.rstrip()}[/dim]")
78
+ except KeyboardInterrupt:
79
+ process.terminate()
80
+ console.print()
81
+
82
+ except FileNotFoundError:
83
+ console.print("[red]tail command not found[/red]")
84
+ raise typer.Exit(1)
@@ -0,0 +1,111 @@
1
+ """wingman start - Start the bot."""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from wingman.config.paths import WingmanPaths
10
+ from wingman.config.settings import Settings
11
+
12
+ console = Console()
13
+
14
+
15
+ def start(
16
+ foreground: bool = typer.Option(
17
+ False,
18
+ "--foreground",
19
+ "-f",
20
+ help="Run in foreground instead of as daemon",
21
+ ),
22
+ ) -> None:
23
+ """
24
+ Start the Wingman bot.
25
+
26
+ By default, starts as a background daemon (macOS launchd).
27
+ Use --foreground to run in the current terminal.
28
+ """
29
+ paths = WingmanPaths()
30
+
31
+ # Check if initialized
32
+ if not paths.is_initialized():
33
+ console.print("[red]Wingman is not set up yet.[/red]")
34
+ console.print("Run [bold]wingman init[/bold] first.")
35
+ raise typer.Exit(1)
36
+
37
+ # Check if auth state exists
38
+ if not paths.auth_state_dir.exists() or not any(paths.auth_state_dir.iterdir()):
39
+ console.print("[red]WhatsApp is not connected.[/red]")
40
+ console.print("Run [bold]wingman auth[/bold] first.")
41
+ raise typer.Exit(1)
42
+
43
+ # Load settings
44
+ settings = Settings.load(paths=paths)
45
+
46
+ # Validate settings
47
+ errors = settings.validate()
48
+ if errors:
49
+ console.print("[red]Configuration errors:[/red]")
50
+ for error in errors:
51
+ console.print(f" - {error}")
52
+ raise typer.Exit(1)
53
+
54
+ if foreground:
55
+ # Run in foreground
56
+ console.print(
57
+ Panel.fit(
58
+ "[bold blue]Starting Wingman[/bold blue]\n\n"
59
+ f"Bot name: {settings.bot_name}\n"
60
+ f"Model: {settings.openai_model}\n\n"
61
+ "Press Ctrl+C to stop.",
62
+ border_style="blue",
63
+ )
64
+ )
65
+ console.print()
66
+
67
+ try:
68
+ asyncio.run(_run_foreground(settings))
69
+ except KeyboardInterrupt:
70
+ console.print()
71
+ console.print("[yellow]Wingman stopped.[/yellow]")
72
+ else:
73
+ # Start as daemon
74
+ _start_daemon(paths, settings)
75
+
76
+
77
+ async def _run_foreground(settings: Settings) -> None:
78
+ """Run the bot in foreground mode."""
79
+ from wingman.core.agent import run_agent
80
+
81
+ await run_agent(settings)
82
+
83
+
84
+ def _start_daemon(paths: WingmanPaths, settings: Settings) -> None:
85
+ """Start the bot as a background daemon."""
86
+ from wingman.daemon import DaemonManager
87
+
88
+ daemon = DaemonManager(paths)
89
+
90
+ # Check if already running
91
+ if daemon.is_running():
92
+ pid = daemon.get_pid()
93
+ console.print(f"[yellow]Wingman is already running (PID: {pid})[/yellow]")
94
+ console.print("Run [bold]wingman stop[/bold] to stop it first.")
95
+ raise typer.Exit(1)
96
+
97
+ # Start daemon
98
+ console.print("[blue]Starting Wingman as background service...[/blue]")
99
+
100
+ try:
101
+ daemon.start()
102
+ console.print()
103
+ console.print("[green]Wingman started as background service.[/green]")
104
+ console.print()
105
+ console.print("Commands:")
106
+ console.print(" [bold]wingman status[/bold] - Check if running")
107
+ console.print(" [bold]wingman logs[/bold] - View activity logs")
108
+ console.print(" [bold]wingman stop[/bold] - Stop the bot")
109
+ except Exception as e:
110
+ console.print(f"[red]Failed to start daemon: {e}[/red]")
111
+ raise typer.Exit(1)
@@ -0,0 +1,84 @@
1
+ """wingman status - Check bot status."""
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from wingman.config.paths import WingmanPaths
7
+ from wingman.config.settings import Settings
8
+
9
+ console = Console()
10
+
11
+
12
+ def status() -> None:
13
+ """
14
+ Check the status of the Wingman bot.
15
+ """
16
+ paths = WingmanPaths()
17
+
18
+ # Check initialization status
19
+ is_initialized = paths.is_initialized()
20
+ config_exists = paths.config_exists()
21
+
22
+ # Create status table
23
+ table = Table(title="Wingman Status", show_header=False, box=None)
24
+ table.add_column("Item", style="bold")
25
+ table.add_column("Status")
26
+
27
+ # Setup status
28
+ if is_initialized:
29
+ table.add_row("Setup", "[green]Complete[/green]")
30
+ elif config_exists:
31
+ table.add_row("Setup", "[yellow]Partial (run wingman init)[/yellow]")
32
+ else:
33
+ table.add_row("Setup", "[red]Not initialized (run wingman init)[/red]")
34
+
35
+ # Config location
36
+ table.add_row("Config", str(paths.config_dir))
37
+
38
+ # WhatsApp auth status
39
+ has_auth = paths.auth_state_dir.exists() and any(paths.auth_state_dir.iterdir())
40
+ if has_auth:
41
+ table.add_row("WhatsApp", "[green]Connected[/green]")
42
+ else:
43
+ table.add_row("WhatsApp", "[yellow]Not connected (run wingman auth)[/yellow]")
44
+
45
+ # Running status
46
+ from wingman.daemon import DaemonManager
47
+
48
+ daemon = DaemonManager(paths)
49
+
50
+ if daemon.is_running():
51
+ pid = daemon.get_pid()
52
+ uptime = daemon.get_uptime()
53
+ uptime_str = _format_uptime(uptime) if uptime else "unknown"
54
+ table.add_row("Status", f"[green]Running[/green] (PID: {pid})")
55
+ table.add_row("Uptime", uptime_str)
56
+ else:
57
+ table.add_row("Status", "[dim]Not running[/dim]")
58
+
59
+ # Load settings for additional info
60
+ if config_exists:
61
+ try:
62
+ settings = Settings.load(paths=paths)
63
+ table.add_row("Bot Name", settings.bot_name)
64
+ table.add_row("Model", settings.openai_model)
65
+ except Exception:
66
+ pass
67
+
68
+ console.print()
69
+ console.print(table)
70
+ console.print()
71
+
72
+
73
+ def _format_uptime(seconds: float) -> str:
74
+ """Format uptime in human-readable format."""
75
+ if seconds < 60:
76
+ return f"{int(seconds)}s"
77
+ elif seconds < 3600:
78
+ mins = int(seconds / 60)
79
+ secs = int(seconds % 60)
80
+ return f"{mins}m {secs}s"
81
+ else:
82
+ hours = int(seconds / 3600)
83
+ mins = int((seconds % 3600) / 60)
84
+ return f"{hours}h {mins}m"
@@ -0,0 +1,33 @@
1
+ """wingman stop - Stop the bot."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from wingman.config.paths import WingmanPaths
7
+
8
+ console = Console()
9
+
10
+
11
+ def stop() -> None:
12
+ """
13
+ Stop the running Wingman bot.
14
+ """
15
+ paths = WingmanPaths()
16
+
17
+ from wingman.daemon import DaemonManager
18
+
19
+ daemon = DaemonManager(paths)
20
+
21
+ if not daemon.is_running():
22
+ console.print("[yellow]Wingman is not running.[/yellow]")
23
+ raise typer.Exit()
24
+
25
+ pid = daemon.get_pid()
26
+ console.print(f"[blue]Stopping Wingman (PID: {pid})...[/blue]")
27
+
28
+ try:
29
+ daemon.stop()
30
+ console.print("[green]Wingman stopped.[/green]")
31
+ except Exception as e:
32
+ console.print(f"[red]Failed to stop daemon: {e}[/red]")
33
+ raise typer.Exit(1)
@@ -0,0 +1,113 @@
1
+ """wingman uninstall - Remove Wingman completely."""
2
+
3
+ import shutil
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from wingman.config.paths import WingmanPaths
9
+
10
+ console = Console()
11
+
12
+
13
+ def uninstall(
14
+ keep_config: bool = typer.Option(
15
+ False,
16
+ "--keep-config",
17
+ "-k",
18
+ help="Keep configuration files (only remove data and stop daemon)",
19
+ ),
20
+ force: bool = typer.Option(
21
+ False,
22
+ "--force",
23
+ "-f",
24
+ help="Don't ask for confirmation",
25
+ ),
26
+ ) -> None:
27
+ """
28
+ Uninstall Wingman and remove all data.
29
+
30
+ This will:
31
+ - Stop the running daemon
32
+ - Remove the launchd service (macOS)
33
+ - Remove config files (unless --keep-config)
34
+ - Remove data files (database, auth state)
35
+ - Remove log files
36
+
37
+ After running this, you can run `pip uninstall wingman-ai` to
38
+ remove the package itself.
39
+ """
40
+ paths = WingmanPaths()
41
+
42
+ # Show what will be removed
43
+ console.print("[bold]This will remove:[/bold]")
44
+ console.print()
45
+
46
+ if not keep_config:
47
+ console.print(f" Config: {paths.config_dir}")
48
+ console.print(f" Data: {paths.data_dir}")
49
+ console.print(f" Logs: {paths.cache_dir}")
50
+
51
+ if paths.launchd_plist.exists():
52
+ console.print(f" Service: {paths.launchd_plist}")
53
+
54
+ console.print()
55
+
56
+ # Confirm
57
+ if not force:
58
+ confirm = typer.confirm("Are you sure you want to uninstall Wingman?")
59
+ if not confirm:
60
+ console.print("[yellow]Uninstall cancelled.[/yellow]")
61
+ raise typer.Exit()
62
+
63
+ # Stop daemon first
64
+ from wingman.daemon import DaemonManager
65
+
66
+ daemon = DaemonManager(paths)
67
+
68
+ if daemon.is_running():
69
+ console.print("[blue]Stopping daemon...[/blue]")
70
+ try:
71
+ daemon.stop()
72
+ except Exception as e:
73
+ console.print(f"[yellow]Warning: Could not stop daemon: {e}[/yellow]")
74
+
75
+ # Remove launchd service
76
+ daemon.uninstall()
77
+
78
+ # Remove directories
79
+ removed = []
80
+
81
+ if not keep_config and paths.config_dir.exists():
82
+ try:
83
+ shutil.rmtree(paths.config_dir)
84
+ removed.append("config")
85
+ except Exception as e:
86
+ console.print(f"[yellow]Warning: Could not remove config: {e}[/yellow]")
87
+
88
+ if paths.data_dir.exists():
89
+ try:
90
+ shutil.rmtree(paths.data_dir)
91
+ removed.append("data")
92
+ except Exception as e:
93
+ console.print(f"[yellow]Warning: Could not remove data: {e}[/yellow]")
94
+
95
+ if paths.cache_dir.exists():
96
+ try:
97
+ shutil.rmtree(paths.cache_dir)
98
+ removed.append("logs")
99
+ except Exception as e:
100
+ console.print(f"[yellow]Warning: Could not remove logs: {e}[/yellow]")
101
+
102
+ console.print()
103
+ console.print("[green]Wingman uninstalled successfully.[/green]")
104
+
105
+ if removed:
106
+ console.print(f"Removed: {', '.join(removed)}")
107
+
108
+ if keep_config:
109
+ console.print(f"[dim]Config preserved at: {paths.config_dir}[/dim]")
110
+
111
+ console.print()
112
+ console.print("To remove the package, run:")
113
+ console.print(" [bold]pip uninstall wingman-ai[/bold]")