muapi-cli 0.2.5__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.
muapi/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
muapi/client.py ADDED
@@ -0,0 +1,121 @@
1
+ """Async HTTP client wrapping the muapi submit → poll pattern."""
2
+ import time
3
+ from typing import Any, Optional
4
+
5
+ import httpx
6
+
7
+ from .config import BASE_URL, get_api_key
8
+
9
+ _DEFAULT_TIMEOUT = 30.0
10
+ _POLL_INTERVAL = 3 # seconds between polls
11
+ _MAX_POLL_SECONDS = 600 # 10 minutes
12
+
13
+
14
+ class MuapiError(Exception):
15
+ def __init__(self, message: str, status_code: int = 0):
16
+ super().__init__(message)
17
+ self.status_code = status_code
18
+
19
+ @property
20
+ def exit_code(self) -> int:
21
+ """Map HTTP status codes to semantic CLI exit codes."""
22
+ from .exitcodes import AUTH_ERROR, RATE_LIMITED, BILLING_ERROR, NOT_FOUND, ERROR
23
+ return {
24
+ 401: AUTH_ERROR,
25
+ 403: AUTH_ERROR,
26
+ 404: NOT_FOUND,
27
+ 429: RATE_LIMITED,
28
+ 402: BILLING_ERROR,
29
+ }.get(self.status_code, ERROR)
30
+
31
+
32
+ def _headers(api_key: str) -> dict:
33
+ return {"x-api-key": api_key, "Content-Type": "application/json"}
34
+
35
+
36
+ def _get_key() -> str:
37
+ key = get_api_key()
38
+ if not key:
39
+ raise MuapiError(
40
+ "No API key configured. Run: muapi auth configure"
41
+ )
42
+ return key
43
+
44
+
45
+ # ── Low-level calls ────────────────────────────────────────────────────────────
46
+
47
+ def post(endpoint: str, payload: dict) -> dict:
48
+ """Submit a generation request; returns raw response dict."""
49
+ key = _get_key()
50
+ url = f"{BASE_URL}/{endpoint.lstrip('/')}"
51
+ with httpx.Client(timeout=_DEFAULT_TIMEOUT) as client:
52
+ resp = client.post(url, json=payload, headers=_headers(key))
53
+ if resp.status_code >= 400:
54
+ raise MuapiError(resp.text, resp.status_code)
55
+ return resp.json()
56
+
57
+
58
+ def get_result(request_id: str) -> dict:
59
+ """Fetch a single prediction result (no polling)."""
60
+ key = _get_key()
61
+ url = f"{BASE_URL}/predictions/{request_id}/result"
62
+ with httpx.Client(timeout=_DEFAULT_TIMEOUT) as client:
63
+ resp = client.get(url, headers=_headers(key))
64
+ if resp.status_code >= 400:
65
+ raise MuapiError(resp.text, resp.status_code)
66
+ return resp.json()
67
+
68
+
69
+ def upload_file(file_path: str) -> dict:
70
+ """Upload a local file, returns the upload response (url, etc.)."""
71
+ key = _get_key()
72
+ url = f"{BASE_URL}/upload_file"
73
+ with open(file_path, "rb") as f:
74
+ with httpx.Client(timeout=120.0) as client:
75
+ resp = client.post(
76
+ url,
77
+ files={"file": (file_path.split("/")[-1], f)},
78
+ headers={"x-api-key": key},
79
+ )
80
+ if resp.status_code >= 400:
81
+ raise MuapiError(resp.text, resp.status_code)
82
+ return resp.json()
83
+
84
+
85
+ # ── Polling ────────────────────────────────────────────────────────────────────
86
+
87
+ def wait_for_result(
88
+ request_id: str,
89
+ poll_interval: int = _POLL_INTERVAL,
90
+ max_seconds: int = _MAX_POLL_SECONDS,
91
+ progress_callback=None,
92
+ ) -> dict:
93
+ """Poll until status is 'completed' or 'failed'."""
94
+ deadline = time.time() + max_seconds
95
+ while time.time() < deadline:
96
+ result = get_result(request_id)
97
+ status = result.get("status", "")
98
+ if progress_callback:
99
+ progress_callback(status)
100
+ if status == "completed":
101
+ return result
102
+ if status == "failed":
103
+ raise MuapiError(f"Generation failed: {result.get('error', 'unknown error')}")
104
+ time.sleep(poll_interval)
105
+ raise MuapiError(f"Timed out waiting for result after {max_seconds}s")
106
+
107
+
108
+ # ── Convenience: submit + optional wait ───────────────────────────────────────
109
+
110
+ def generate(
111
+ endpoint: str,
112
+ payload: dict,
113
+ wait: bool = True,
114
+ poll_interval: int = _POLL_INTERVAL,
115
+ progress_callback=None,
116
+ ) -> dict:
117
+ result = post(endpoint, payload)
118
+ request_id = result.get("request_id") or result.get("id")
119
+ if not wait or not request_id:
120
+ return result
121
+ return wait_for_result(request_id, poll_interval, progress_callback=progress_callback)
File without changes
@@ -0,0 +1,89 @@
1
+ """muapi account — check balance and top up credits."""
2
+ import json
3
+ import webbrowser
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from .. import exitcodes
9
+ from ..config import BASE_URL, get_api_key
10
+ from ..utils import console, error_exit, out
11
+
12
+ app = typer.Typer(help="Manage your muapi.ai account balance and credits.")
13
+
14
+
15
+ def _headers() -> dict:
16
+ key = get_api_key()
17
+ if not key:
18
+ error_exit("No API key configured. Run: muapi auth configure", exitcodes.AUTH_ERROR)
19
+ return {"x-api-key": key}
20
+
21
+
22
+ @app.command("balance")
23
+ def balance(
24
+ output_json: bool = typer.Option(False, "--output-json", "-j", help="Print raw JSON"),
25
+ ):
26
+ """Show your current account balance."""
27
+ try:
28
+ resp = httpx.get(f"{BASE_URL}/account/balance", headers=_headers(), timeout=30.0)
29
+ except httpx.RequestError as exc:
30
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
31
+
32
+ if resp.status_code == 401 or resp.status_code == 403:
33
+ error_exit("Authentication failed. Run: muapi auth configure", exitcodes.AUTH_ERROR)
34
+ if resp.status_code >= 400:
35
+ error_exit(f"Request failed: {resp.text}", exitcodes.ERROR)
36
+
37
+ data = resp.json()
38
+ if output_json:
39
+ out.print_json(json.dumps(data))
40
+ else:
41
+ bal = data.get("balance", 0.0)
42
+ currency = data.get("currency", "USD").upper()
43
+ email = data.get("email", "")
44
+ console.print(f"[bold]{email}[/bold]")
45
+ console.print(f"Balance: [green bold]${bal:.4f} {currency}[/green bold]")
46
+
47
+
48
+ @app.command("topup")
49
+ def topup(
50
+ amount: int = typer.Option(10, "--amount", "-a", help="Amount in USD to add (minimum $1)"),
51
+ currency: str = typer.Option("usd", "--currency", "-c", help="Currency (default: usd)"),
52
+ output_json: bool = typer.Option(False, "--output-json", "-j", help="Print checkout URL as JSON instead of opening browser"),
53
+ no_open: bool = typer.Option(False, "--no-open", help="Print URL without opening browser"),
54
+ ):
55
+ """Add credits to your account via Stripe checkout."""
56
+ if amount < 1:
57
+ error_exit("Minimum topup amount is $1.", exitcodes.VALIDATION)
58
+
59
+ try:
60
+ resp = httpx.post(
61
+ f"{BASE_URL}/account/topup",
62
+ json={"amount": amount, "currency": currency.lower()},
63
+ headers=_headers(),
64
+ timeout=30.0,
65
+ )
66
+ except httpx.RequestError as exc:
67
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
68
+
69
+ if resp.status_code == 401 or resp.status_code == 403:
70
+ error_exit("Authentication failed. Run: muapi auth configure", exitcodes.AUTH_ERROR)
71
+ if resp.status_code == 402:
72
+ error_exit("Billing error. Please contact support.", exitcodes.BILLING_ERROR)
73
+ if resp.status_code >= 400:
74
+ error_exit(f"Request failed: {resp.text}", exitcodes.ERROR)
75
+
76
+ data = resp.json()
77
+ checkout_url = data.get("checkout_url", "")
78
+
79
+ if output_json:
80
+ out.print_json(json.dumps(data))
81
+ return
82
+
83
+ console.print(f"Checkout URL: [bold blue]{checkout_url}[/bold blue]")
84
+
85
+ if not no_open:
86
+ console.print("Opening browser for payment…")
87
+ webbrowser.open(checkout_url)
88
+ else:
89
+ console.print("Open the URL above to complete payment.")
@@ -0,0 +1,139 @@
1
+ """muapi audio — music creation and audio generation."""
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from .. import client
7
+ from ..utils import console, download_outputs, error_exit, print_result, spinner_status
8
+
9
+ app = typer.Typer(help="Create and remix music and audio.")
10
+
11
+
12
+ @app.command("create")
13
+ def create(
14
+ prompt: str = typer.Argument(..., help="Music description / lyrics prompt"),
15
+ title: str = typer.Option("", "--title", "-t", help="Song title"),
16
+ tags: str = typer.Option("", "--tags", help="Genre/style tags (comma-separated)"),
17
+ instrumental: bool = typer.Option(False, "--instrumental", help="Generate without vocals"),
18
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
19
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
20
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
21
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
22
+ ):
23
+ """Create original music with Suno."""
24
+ payload = {
25
+ "prompt": prompt,
26
+ "title": title,
27
+ "tags": tags,
28
+ "make_instrumental": instrumental,
29
+ }
30
+ if webhook:
31
+ payload["webhook_url"] = webhook
32
+ try:
33
+ with spinner_status("Creating music with Suno..."):
34
+ result = client.generate("suno-create-music", payload, wait=wait)
35
+ except client.MuapiError as e:
36
+ error_exit(str(e))
37
+
38
+ print_result(result, output_json, label="Music (Suno)")
39
+ if download and result.get("status") == "completed":
40
+ download_outputs(result, download)
41
+
42
+
43
+ @app.command("remix")
44
+ def remix(
45
+ song_id: str = typer.Argument(..., help="Suno song ID to remix"),
46
+ prompt: str = typer.Option("", "--prompt", "-p", help="New style/lyric prompt"),
47
+ title: str = typer.Option("", "--title", "-t"),
48
+ tags: str = typer.Option("", "--tags"),
49
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
50
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
51
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
52
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
53
+ ):
54
+ """Remix an existing Suno song."""
55
+ payload = {"song_id": song_id, "prompt": prompt, "title": title, "tags": tags}
56
+ if webhook:
57
+ payload["webhook_url"] = webhook
58
+ try:
59
+ with spinner_status("Remixing with Suno..."):
60
+ result = client.generate("suno-remix-music", payload, wait=wait)
61
+ except client.MuapiError as e:
62
+ error_exit(str(e))
63
+
64
+ print_result(result, output_json, label="Remix (Suno)")
65
+ if download and result.get("status") == "completed":
66
+ download_outputs(result, download)
67
+
68
+
69
+ @app.command("extend")
70
+ def extend(
71
+ song_id: str = typer.Argument(..., help="Suno song ID to extend"),
72
+ prompt: str = typer.Option("", "--prompt", "-p"),
73
+ continue_at: float = typer.Option(0, "--continue-at", help="Time (seconds) to extend from"),
74
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
75
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
76
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
77
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
78
+ ):
79
+ """Extend a Suno song."""
80
+ payload = {"song_id": song_id, "prompt": prompt, "continue_at": continue_at}
81
+ if webhook:
82
+ payload["webhook_url"] = webhook
83
+ try:
84
+ with spinner_status("Extending with Suno..."):
85
+ result = client.generate("suno-extend-music", payload, wait=wait)
86
+ except client.MuapiError as e:
87
+ error_exit(str(e))
88
+
89
+ print_result(result, output_json, label="Extended (Suno)")
90
+ if download and result.get("status") == "completed":
91
+ download_outputs(result, download)
92
+
93
+
94
+ @app.command("from-text")
95
+ def from_text(
96
+ prompt: str = typer.Argument(..., help="Describe the audio to generate"),
97
+ duration: float = typer.Option(10.0, "--duration", "-D", help="Duration in seconds"),
98
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
99
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
100
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
101
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
102
+ ):
103
+ """Generate audio from text with MMAudio."""
104
+ payload = {"prompt": prompt, "duration": duration}
105
+ if webhook:
106
+ payload["webhook_url"] = webhook
107
+ try:
108
+ with spinner_status("Generating audio with MMAudio..."):
109
+ result = client.generate("mmaudio-v2/text-to-audio", payload, wait=wait)
110
+ except client.MuapiError as e:
111
+ error_exit(str(e))
112
+
113
+ print_result(result, output_json, label="Audio (MMAudio)")
114
+ if download and result.get("status") == "completed":
115
+ download_outputs(result, download)
116
+
117
+
118
+ @app.command("from-video")
119
+ def from_video(
120
+ video_url: str = typer.Argument(..., help="Source video URL"),
121
+ prompt: str = typer.Option("", "--prompt", "-p", help="Audio description prompt"),
122
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
123
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
124
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
125
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
126
+ ):
127
+ """Add AI-generated audio to a video (MMAudio video-to-video)."""
128
+ payload = {"video_url": video_url, "prompt": prompt}
129
+ if webhook:
130
+ payload["webhook_url"] = webhook
131
+ try:
132
+ with spinner_status("Generating audio for video with MMAudio..."):
133
+ result = client.generate("mmaudio-v2/video-to-video", payload, wait=wait)
134
+ except client.MuapiError as e:
135
+ error_exit(str(e))
136
+
137
+ print_result(result, output_json, label="Video+Audio (MMAudio)")
138
+ if download and result.get("status") == "completed":
139
+ download_outputs(result, download)
muapi/commands/auth.py ADDED
@@ -0,0 +1,193 @@
1
+ """muapi auth — configure API key and inspect identity."""
2
+ import typer
3
+ import httpx
4
+ from rich.prompt import Prompt
5
+
6
+ from ..config import delete_api_key, get_api_key, save_api_key, BASE_URL
7
+ from .. import exitcodes
8
+ from ..utils import console, error_exit, out
9
+
10
+ app = typer.Typer(help="Manage authentication and API key.")
11
+
12
+ # Auth endpoints live at the root host, not under /api/v1
13
+ _AUTH_BASE = BASE_URL.replace("/api/v1", "")
14
+
15
+
16
+ @app.command("login")
17
+ def login(
18
+ email: str = typer.Option(..., "--email", "-e", help="Your muapi.ai email address"),
19
+ password: str = typer.Option(None, "--password", "-p", help="Password (will prompt if omitted)"),
20
+ ):
21
+ """Log in with email + password and save an API key automatically."""
22
+ if not password:
23
+ password = Prompt.ask("[bold]Password[/bold]", password=True, console=console)
24
+ if not password:
25
+ error_exit("Password is required.", exitcodes.AUTH_ERROR)
26
+
27
+ try:
28
+ resp = httpx.post(
29
+ f"{_AUTH_BASE}/api/auth/cli/login",
30
+ json={"email": email, "password": password, "username": ""},
31
+ timeout=30.0,
32
+ )
33
+ except httpx.RequestError as exc:
34
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
35
+
36
+ if resp.status_code == 401:
37
+ error_exit("Invalid email or password.", exitcodes.AUTH_ERROR)
38
+ if resp.status_code == 403:
39
+ error_exit("Email not verified. Check your inbox.", exitcodes.AUTH_ERROR)
40
+ if resp.status_code >= 400:
41
+ error_exit(f"Login failed: {resp.text}", exitcodes.AUTH_ERROR)
42
+
43
+ data = resp.json()
44
+ api_key = data.get("api_key", "")
45
+ if not api_key:
46
+ error_exit("No API key in response. Please try again.", exitcodes.AUTH_ERROR)
47
+
48
+ location = save_api_key(api_key)
49
+ console.print(f"[green]Logged in as {data.get('email', email)}. API key saved to {location}.[/green]")
50
+
51
+
52
+ @app.command("register")
53
+ def register(
54
+ email: str = typer.Option(..., "--email", "-e", help="Email address to register"),
55
+ password: str = typer.Option(None, "--password", "-p", help="Password (will prompt if omitted)"),
56
+ username: str = typer.Option("", "--username", "-u", help="Display name (optional)"),
57
+ ):
58
+ """Create a new muapi.ai account and verify via email OTP."""
59
+ if not password:
60
+ password = Prompt.ask("[bold]Choose a password[/bold]", password=True, console=console)
61
+ confirm = Prompt.ask("[bold]Confirm password[/bold]", password=True, console=console)
62
+ if password != confirm:
63
+ error_exit("Passwords do not match.", exitcodes.VALIDATION)
64
+ if not password:
65
+ error_exit("Password is required.", exitcodes.VALIDATION)
66
+
67
+ try:
68
+ resp = httpx.post(
69
+ f"{_AUTH_BASE}/api/auth/register",
70
+ json={"email": email, "password": password, "username": username or email.split("@")[0]},
71
+ timeout=30.0,
72
+ )
73
+ except httpx.RequestError as exc:
74
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
75
+
76
+ if resp.status_code == 400:
77
+ error_exit(f"Registration failed: {resp.json().get('detail', resp.text)}", exitcodes.VALIDATION)
78
+ if resp.status_code >= 400:
79
+ error_exit(f"Registration failed: {resp.text}", exitcodes.ERROR)
80
+
81
+ detail = resp.json().get("detail", "OTP sent to your email.")
82
+ console.print(f"[green]{detail}[/green]")
83
+ console.print("Check your inbox, then run: [bold]muapi auth verify --email {email} --otp <OTP>[/bold]")
84
+
85
+
86
+ @app.command("verify")
87
+ def verify(
88
+ email: str = typer.Option(..., "--email", "-e", help="Email address used during registration"),
89
+ otp: str = typer.Option(..., "--otp", "-o", help="OTP code from your email"),
90
+ ):
91
+ """Verify your email OTP after registration, then save an API key."""
92
+ try:
93
+ resp = httpx.post(
94
+ f"{_AUTH_BASE}/api/auth/verify-otp",
95
+ json={"email": email, "otp": otp},
96
+ timeout=30.0,
97
+ )
98
+ except httpx.RequestError as exc:
99
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
100
+
101
+ if resp.status_code == 401:
102
+ error_exit("Invalid or expired OTP.", exitcodes.AUTH_ERROR)
103
+ if resp.status_code >= 400:
104
+ error_exit(f"Verification failed: {resp.text}", exitcodes.AUTH_ERROR)
105
+
106
+ console.print("[green]Email verified! You can now log in.[/green]")
107
+ console.print(f"Run: [bold]muapi auth login --email {email}[/bold]")
108
+
109
+
110
+ @app.command("forgot-password")
111
+ def forgot_password(
112
+ email: str = typer.Option(..., "--email", "-e", help="Email address on your account"),
113
+ ):
114
+ """Send a password reset OTP to your email."""
115
+ try:
116
+ resp = httpx.post(
117
+ f"{_AUTH_BASE}/api/auth/forgot-password",
118
+ json={"email": email},
119
+ timeout=30.0,
120
+ )
121
+ except httpx.RequestError as exc:
122
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
123
+
124
+ if resp.status_code == 404:
125
+ error_exit("No account found for that email.", exitcodes.NOT_FOUND)
126
+ if resp.status_code >= 400:
127
+ error_exit(f"Request failed: {resp.text}", exitcodes.ERROR)
128
+
129
+ console.print(f"[green]{resp.json().get('detail', 'OTP sent to your email.')}[/green]")
130
+ console.print(f"Then run: [bold]muapi auth reset-password --email {email} --otp <OTP> --password <new>[/bold]")
131
+
132
+
133
+ @app.command("reset-password")
134
+ def reset_password(
135
+ email: str = typer.Option(..., "--email", "-e", help="Your email address"),
136
+ otp: str = typer.Option(..., "--otp", "-o", help="OTP from the reset email"),
137
+ password: str = typer.Option(None, "--password", "-p", help="New password (will prompt if omitted)"),
138
+ ):
139
+ """Reset your password using an OTP sent to your email."""
140
+ if not password:
141
+ password = Prompt.ask("[bold]New password[/bold]", password=True, console=console)
142
+ confirm = Prompt.ask("[bold]Confirm new password[/bold]", password=True, console=console)
143
+ if password != confirm:
144
+ error_exit("Passwords do not match.", exitcodes.VALIDATION)
145
+ if not password:
146
+ error_exit("Password is required.", exitcodes.VALIDATION)
147
+
148
+ try:
149
+ resp = httpx.post(
150
+ f"{_AUTH_BASE}/api/auth/reset-password",
151
+ json={"email": email, "otp": otp, "password": password},
152
+ timeout=30.0,
153
+ )
154
+ except httpx.RequestError as exc:
155
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
156
+
157
+ if resp.status_code == 401:
158
+ error_exit("Invalid or expired OTP.", exitcodes.AUTH_ERROR)
159
+ if resp.status_code >= 400:
160
+ error_exit(f"Request failed: {resp.text}", exitcodes.ERROR)
161
+
162
+ console.print(f"[green]{resp.json().get('detail', 'Password reset successfully.')}[/green]")
163
+ console.print(f"Run: [bold]muapi auth login --email {email}[/bold]")
164
+
165
+
166
+ @app.command("configure")
167
+ def configure(
168
+ api_key: str = typer.Option(None, "--api-key", "-k", help="API key (will prompt if omitted)"),
169
+ ):
170
+ """Save your muapi API key to the OS keychain (or config file)."""
171
+ if not api_key:
172
+ api_key = Prompt.ask("[bold]Enter your muapi API key[/bold]", password=True, console=console)
173
+ if not api_key:
174
+ error_exit("No API key provided.")
175
+ location = save_api_key(api_key.strip())
176
+ console.print(f"[green]API key saved to {location}.[/green]")
177
+
178
+
179
+ @app.command("whoami")
180
+ def whoami():
181
+ """Show the currently configured API key (masked)."""
182
+ key = get_api_key()
183
+ if not key:
184
+ error_exit("No API key configured. Run: muapi auth configure", exitcodes.AUTH_ERROR)
185
+ masked = key[:8] + "..." + key[-4:]
186
+ out.print(f"API key: [bold]{masked}[/bold]")
187
+
188
+
189
+ @app.command("logout")
190
+ def logout():
191
+ """Remove the stored API key."""
192
+ delete_api_key()
193
+ console.print("[green]API key removed.[/green]")
@@ -0,0 +1,80 @@
1
+ """muapi config — get and set persistent CLI settings."""
2
+ import json
3
+
4
+ import typer
5
+
6
+ from ..config import get_all_settings, get_setting, set_setting, _CONFIG_FILE
7
+ from ..utils import console, out
8
+
9
+ app = typer.Typer(help="Manage CLI configuration (default model, output format, etc.).")
10
+
11
+ _KNOWN_KEYS = {
12
+ "output": "Default output format: 'human' or 'json'",
13
+ "model.image": "Default image generation model (e.g. flux-dev)",
14
+ "model.video": "Default video generation model (e.g. kling-master)",
15
+ "model.audio": "Default audio model (e.g. suno-create-music)",
16
+ "no_color": "Disable colored output: 'true' or 'false'",
17
+ "poll_interval": "Seconds between result polls (default 3)",
18
+ "timeout": "Max seconds to wait for results (default 600)",
19
+ }
20
+
21
+
22
+ @app.command("set")
23
+ def config_set(
24
+ key: str = typer.Argument(..., help="Setting key (e.g. output, model.image)"),
25
+ value: str = typer.Argument(..., help="Value to assign"),
26
+ ):
27
+ """Set a persistent CLI configuration value.
28
+
29
+ Examples:
30
+
31
+ \\b
32
+ muapi config set output json
33
+ muapi config set model.image flux-schnell
34
+ muapi config set no_color true
35
+ """
36
+ set_setting(key, value)
37
+ console.print(f"[green]Set [bold]{key}[/bold] = {value}[/green]")
38
+
39
+
40
+ @app.command("get")
41
+ def config_get(
42
+ key: str = typer.Argument(..., help="Setting key to read"),
43
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
44
+ ):
45
+ """Get the current value of a configuration key."""
46
+ value = get_setting(key)
47
+ if value is None:
48
+ console.print(f"[dim]{key} is not set[/dim]")
49
+ raise typer.Exit(0)
50
+ if output_json:
51
+ out.print_json(json.dumps({key: value}))
52
+ else:
53
+ console.print(f"[bold]{key}[/bold] = {value}")
54
+
55
+
56
+ @app.command("list")
57
+ def config_list(
58
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
59
+ ):
60
+ """Show all configured settings."""
61
+ settings = get_all_settings()
62
+ if output_json:
63
+ out.print_json(json.dumps(settings))
64
+ return
65
+
66
+ if not settings:
67
+ console.print(f"[dim]No settings configured. Config file: {_CONFIG_FILE}[/dim]")
68
+ console.print("\nAvailable keys:")
69
+ for k, desc in _KNOWN_KEYS.items():
70
+ console.print(f" [bold]{k}[/bold] — {desc}")
71
+ return
72
+
73
+ from rich.table import Table
74
+ table = Table(show_header=True, header_style="bold")
75
+ table.add_column("Key")
76
+ table.add_column("Value")
77
+ for k, v in settings.items():
78
+ table.add_row(k, str(v))
79
+ console.print(table)
80
+ console.print(f"\n[dim]Config file: {_CONFIG_FILE}[/dim]")