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.
- share/wingman/node_listener/package-lock.json +1785 -0
- share/wingman/node_listener/package.json +50 -0
- share/wingman/node_listener/src/index.ts +108 -0
- share/wingman/node_listener/src/ipc.ts +70 -0
- share/wingman/node_listener/src/messageHandler.ts +135 -0
- share/wingman/node_listener/src/socket.ts +244 -0
- share/wingman/node_listener/src/types.d.ts +13 -0
- share/wingman/node_listener/tsconfig.json +19 -0
- wingman/__init__.py +4 -0
- wingman/__main__.py +6 -0
- wingman/cli/__init__.py +5 -0
- wingman/cli/commands/__init__.py +1 -0
- wingman/cli/commands/auth.py +90 -0
- wingman/cli/commands/config.py +109 -0
- wingman/cli/commands/init.py +71 -0
- wingman/cli/commands/logs.py +84 -0
- wingman/cli/commands/start.py +111 -0
- wingman/cli/commands/status.py +84 -0
- wingman/cli/commands/stop.py +33 -0
- wingman/cli/commands/uninstall.py +113 -0
- wingman/cli/main.py +50 -0
- wingman/cli/wizard.py +356 -0
- wingman/config/__init__.py +31 -0
- wingman/config/paths.py +153 -0
- wingman/config/personality.py +155 -0
- wingman/config/registry.py +343 -0
- wingman/config/settings.py +294 -0
- wingman/core/__init__.py +16 -0
- wingman/core/agent.py +257 -0
- wingman/core/ipc_handler.py +124 -0
- wingman/core/llm/__init__.py +5 -0
- wingman/core/llm/client.py +77 -0
- wingman/core/memory/__init__.py +6 -0
- wingman/core/memory/context.py +109 -0
- wingman/core/memory/models.py +213 -0
- wingman/core/message_processor.py +277 -0
- wingman/core/policy/__init__.py +5 -0
- wingman/core/policy/evaluator.py +265 -0
- wingman/core/process_manager.py +135 -0
- wingman/core/safety/__init__.py +8 -0
- wingman/core/safety/cooldown.py +63 -0
- wingman/core/safety/quiet_hours.py +75 -0
- wingman/core/safety/rate_limiter.py +58 -0
- wingman/core/safety/triggers.py +117 -0
- wingman/core/transports/__init__.py +14 -0
- wingman/core/transports/base.py +106 -0
- wingman/core/transports/imessage/__init__.py +5 -0
- wingman/core/transports/imessage/db_listener.py +280 -0
- wingman/core/transports/imessage/sender.py +162 -0
- wingman/core/transports/imessage/transport.py +140 -0
- wingman/core/transports/whatsapp.py +180 -0
- wingman/daemon/__init__.py +5 -0
- wingman/daemon/manager.py +303 -0
- wingman/installer/__init__.py +5 -0
- wingman/installer/node_installer.py +253 -0
- wingman_ai-1.0.0.dist-info/METADATA +553 -0
- wingman_ai-1.0.0.dist-info/RECORD +60 -0
- wingman_ai-1.0.0.dist-info/WHEEL +4 -0
- wingman_ai-1.0.0.dist-info/entry_points.txt +2 -0
- 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]")
|