qualia-cli 0.1.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.
qualia_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Qualia CLI - Terminal interface for the Qualia VLA fine-tuning platform."""
2
+
3
+ __version__ = "0.1.0"
qualia_cli/client.py ADDED
@@ -0,0 +1,29 @@
1
+ """Shared client factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from qualia import AuthenticationError, Qualia
6
+
7
+ from qualia_cli.config import load_config
8
+ from qualia_cli.output import print_error
9
+
10
+
11
+ def get_client() -> Qualia:
12
+ from qualia_cli.main import _base_url_override, _token_override
13
+
14
+ cfg = load_config()
15
+ kwargs: dict = {}
16
+ if _token_override:
17
+ kwargs["api_key"] = _token_override
18
+ elif cfg.api_key:
19
+ kwargs["api_key"] = cfg.api_key
20
+ if _base_url_override:
21
+ kwargs["base_url"] = _base_url_override
22
+ elif cfg.base_url:
23
+ kwargs["base_url"] = cfg.base_url
24
+ try:
25
+ return Qualia(**kwargs)
26
+ except AuthenticationError as e:
27
+ print_error(str(e))
28
+ print_error("Set QUALIA_API_KEY or run: qualia auth login --token <KEY>")
29
+ raise SystemExit(1) from e
File without changes
@@ -0,0 +1,43 @@
1
+ """Auth commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from qualia_cli.config import load_config, save_config
8
+ from qualia_cli.output import print_error
9
+
10
+ app = typer.Typer(help="Authentication.")
11
+
12
+
13
+ @app.command("login")
14
+ def login(
15
+ api_key: str = typer.Option(
16
+ ..., prompt="API key", hide_input=True, help="Your Qualia API key"
17
+ ),
18
+ base_url: str | None = typer.Option(
19
+ None, help="API base URL (e.g. http://localhost:8000)"
20
+ ),
21
+ ) -> None:
22
+ """Save your API key (and optional base URL) for future use."""
23
+ try:
24
+ path = save_config(api_key, base_url)
25
+ typer.echo(f"Config saved to {path}")
26
+ except OSError as e:
27
+ print_error(f"Could not write config: {e}")
28
+ raise SystemExit(1) from e
29
+
30
+
31
+ @app.command("status")
32
+ def status() -> None:
33
+ """Check current authentication config."""
34
+ cfg = load_config()
35
+ if cfg.api_key:
36
+ typer.echo(f"API key: {cfg.api_key[:8]}...")
37
+ else:
38
+ typer.echo("Not authenticated. Run: qualia auth login")
39
+ raise SystemExit(1)
40
+ if cfg.base_url:
41
+ typer.echo(f"Base URL: {cfg.base_url}")
42
+ else:
43
+ typer.echo("Base URL: https://api.qualiastudios.dev (default)")
@@ -0,0 +1,21 @@
1
+ """Credits commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from qualia_cli.client import get_client
8
+ from qualia_cli.output import print_json
9
+
10
+ app = typer.Typer(help="View credit balance.")
11
+
12
+
13
+ @app.callback(invoke_without_command=True)
14
+ def credits(json: bool = typer.Option(False, "--json", help="JSON output")) -> None:
15
+ """Show your current credit balance."""
16
+ client = get_client()
17
+ balance = client.credits.get()
18
+ if json:
19
+ print_json(balance)
20
+ else:
21
+ typer.echo(f"Credits: {balance.balance}")
@@ -0,0 +1,85 @@
1
+ """Datasets commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ import typer
8
+
9
+ from qualia_cli.client import get_client
10
+ from qualia_cli.output import print_json, print_table
11
+
12
+ app = typer.Typer(help="Dataset utilities.")
13
+
14
+
15
+ @app.command("list")
16
+ def list_datasets(
17
+ limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
18
+ cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
19
+ sort: str = typer.Option(
20
+ "newest", "--sort", "-s", help="newest|oldest|updated|name"
21
+ ),
22
+ search: str = typer.Option(None, "--search", "-q", help="Search dataset path"),
23
+ subtype: str = typer.Option(
24
+ None, "--dataset-type", help="lerobot_v3 or sarm_progress"
25
+ ),
26
+ flow: str = typer.Option(None, "--flow", help="input or output"),
27
+ project_id: str = typer.Option(None, "--project", help="Filter by project ID"),
28
+ job_id: str = typer.Option(None, "--job", help="Filter by job ID"),
29
+ json: bool = typer.Option(False, "--json", help="JSON output"),
30
+ ) -> None:
31
+ """List datasets (paginated)."""
32
+ client = get_client()
33
+ page = client.datasets.list(
34
+ limit=limit,
35
+ cursor=cursor,
36
+ sort=sort,
37
+ search=search,
38
+ subtype=subtype,
39
+ flow=flow,
40
+ project_id=UUID(project_id) if project_id else None,
41
+ job_id=UUID(job_id) if job_id else None,
42
+ )
43
+ if json:
44
+ print_json(page)
45
+ else:
46
+ rows = [
47
+ {
48
+ "id": str(d.artifact_id)[:8],
49
+ "subtype": d.subtype,
50
+ "path": d.path,
51
+ "jobs": len(d.jobs),
52
+ "created": d.created_at.strftime("%Y-%m-%d"),
53
+ }
54
+ for d in page.items
55
+ ]
56
+ print_table(
57
+ rows,
58
+ [
59
+ ("id", "Dataset ID"),
60
+ ("subtype", "Subtype"),
61
+ ("path", "Path"),
62
+ ("jobs", "Jobs"),
63
+ ("created", "Created"),
64
+ ],
65
+ )
66
+ if page.page_info.has_next:
67
+ typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
68
+
69
+
70
+ @app.command("get-image-keys")
71
+ def get_image_keys(
72
+ dataset_id: str = typer.Argument(help="HuggingFace dataset ID"),
73
+ json: bool = typer.Option(False, "--json", help="JSON output"),
74
+ ) -> None:
75
+ """Get available image keys for camera mapping."""
76
+ client = get_client()
77
+ result = client.datasets.get_image_keys(dataset_id)
78
+ if json:
79
+ print_json(result)
80
+ else:
81
+ print_table(
82
+ [{"key": k} for k in result.image_keys],
83
+ [("key", "Image Key")],
84
+ title=f"Image keys for {result.dataset_id}",
85
+ )
@@ -0,0 +1,200 @@
1
+ """Job commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json as json_mod
6
+
7
+ import typer
8
+
9
+ from qualia_cli.client import get_client
10
+ from qualia_cli.output import console, print_error, print_json, print_table
11
+
12
+ app = typer.Typer(help="Manage fine-tuning jobs.")
13
+
14
+
15
+ @app.command("list")
16
+ def list_jobs(
17
+ limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
18
+ cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
19
+ sort: str = typer.Option("newest", "--sort", "-s", help="newest|oldest|updated"),
20
+ search: str = typer.Option(None, "--search", "-q", help="Search job description"),
21
+ project_id: str = typer.Option(None, "--project", help="Filter by project ID"),
22
+ vla_type: str = typer.Option(None, "--vla-type", help="Filter by VLA type"),
23
+ job_type: str = typer.Option(None, "--job-type", help="vla|reward|vla_w_reward"),
24
+ use_json: bool = typer.Option(False, "--json", help="JSON output"),
25
+ ) -> None:
26
+ """List fine-tuning jobs (paginated)."""
27
+ client = get_client()
28
+ page = client.jobs.list(
29
+ limit=limit,
30
+ cursor=cursor,
31
+ sort=sort,
32
+ project_id=project_id,
33
+ vla_type=vla_type,
34
+ job_type=job_type,
35
+ search=search,
36
+ )
37
+ if use_json:
38
+ print_json(page)
39
+ else:
40
+ rows = [
41
+ {
42
+ "job_id": str(j.job_id)[:8],
43
+ "desc": j.job_desc or "",
44
+ "phase": j.current_phase or "",
45
+ "vla": j.vla_type or "",
46
+ "model": j.model or "",
47
+ "dataset": j.dataset or "",
48
+ "created": j.created_at.strftime("%Y-%m-%d") if j.created_at else "",
49
+ }
50
+ for j in page.items
51
+ ]
52
+ print_table(
53
+ rows,
54
+ [
55
+ ("job_id", "Job ID"),
56
+ ("desc", "Description"),
57
+ ("phase", "Phase"),
58
+ ("vla", "VLA Type"),
59
+ ("model", "Model"),
60
+ ("dataset", "Dataset"),
61
+ ("created", "Created"),
62
+ ],
63
+ )
64
+ if page.page_info.has_next:
65
+ typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
66
+
67
+
68
+ @app.command("launch")
69
+ def launch(
70
+ project_id: str = typer.Option(..., help="Project ID"),
71
+ vla_type: str = typer.Option(
72
+ ..., help="VLA type (act, smolvla, pi0, pi05, gr00t_n1_5)"
73
+ ),
74
+ dataset_id: str = typer.Option(..., help="HuggingFace dataset ID"),
75
+ hours: float = typer.Option(..., help="Training hours"),
76
+ camera_mappings: str = typer.Option(
77
+ ...,
78
+ help='Camera mappings as JSON, e.g. \'{"cam_1": "observation.images.top"}\'',
79
+ ),
80
+ model_id: str | None = typer.Option(
81
+ None, help="HuggingFace model ID (required for smolvla, pi0, pi05)"
82
+ ),
83
+ instance_type: str | None = typer.Option(None, help="GPU instance type"),
84
+ region: str | None = typer.Option(None, help="Cloud region"),
85
+ batch_size: int = typer.Option(32, help="Training batch size"),
86
+ name: str | None = typer.Option(None, help="Job name"),
87
+ hyper_spec: str | None = typer.Option(None, help="Custom hyperparams as JSON"),
88
+ use_json: bool = typer.Option(False, "--json", help="JSON output"),
89
+ ) -> None:
90
+ """Launch a new fine-tuning job."""
91
+ try:
92
+ cam_map = json_mod.loads(camera_mappings)
93
+ except json_mod.JSONDecodeError as e:
94
+ print_error("Invalid JSON for --camera-mappings")
95
+ raise SystemExit(1) from e
96
+
97
+ kwargs: dict = {
98
+ "project_id": project_id,
99
+ "vla_type": vla_type,
100
+ "dataset_id": dataset_id,
101
+ "hours": hours,
102
+ "camera_mappings": cam_map,
103
+ "batch_size": batch_size,
104
+ }
105
+ if model_id:
106
+ kwargs["model_id"] = model_id
107
+ if instance_type:
108
+ kwargs["instance_type"] = instance_type
109
+ if region:
110
+ kwargs["region"] = region
111
+ if name:
112
+ kwargs["name"] = name
113
+ if hyper_spec:
114
+ try:
115
+ kwargs["vla_hyper_spec"] = json_mod.loads(hyper_spec)
116
+ except json_mod.JSONDecodeError as e:
117
+ print_error("Invalid JSON for --hyper-spec")
118
+ raise SystemExit(1) from e
119
+
120
+ client = get_client()
121
+ job = client.finetune.create(**kwargs)
122
+ if use_json:
123
+ print_json(job)
124
+ else:
125
+ typer.echo(f"Job launched: {job.job_id} (status: {job.status})")
126
+
127
+
128
+ @app.command("get")
129
+ def get_status(
130
+ job_id: str = typer.Argument(help="Job ID"),
131
+ use_json: bool = typer.Option(False, "--json", help="JSON output"),
132
+ ) -> None:
133
+ """Get job status."""
134
+ client = get_client()
135
+ status = client.finetune.get(job_id)
136
+ if use_json:
137
+ print_json(status)
138
+ else:
139
+ console.print(f"[bold]Job:[/bold] {status.job_id}")
140
+ console.print(f"[bold]Status:[/bold] {status.status}")
141
+ console.print(f"[bold]Phase:[/bold] {status.current_phase}")
142
+ if status.instance_type:
143
+ console.print(f"[bold]Instance:[/bold] {status.instance_type}")
144
+ if status.region:
145
+ console.print(f"[bold]Region:[/bold] {status.region}")
146
+ if status.phases:
147
+ console.print()
148
+ rows = [
149
+ {
150
+ "name": p.name,
151
+ "status": p.status,
152
+ "started": str(p.started_at or "-"),
153
+ "completed": str(p.completed_at or "-"),
154
+ "error": p.error or "",
155
+ }
156
+ for p in status.phases
157
+ ]
158
+ print_table(
159
+ rows,
160
+ [
161
+ ("name", "Phase"),
162
+ ("status", "Status"),
163
+ ("started", "Started"),
164
+ ("completed", "Completed"),
165
+ ("error", "Error"),
166
+ ],
167
+ )
168
+
169
+
170
+ @app.command("cancel")
171
+ def cancel(
172
+ job_id: str = typer.Argument(help="Job ID to cancel"),
173
+ use_json: bool = typer.Option(False, "--json", help="JSON output"),
174
+ ) -> None:
175
+ """Cancel a job."""
176
+ client = get_client()
177
+ result = client.finetune.cancel(job_id)
178
+ if use_json:
179
+ print_json(result)
180
+ else:
181
+ if result.cancelled:
182
+ typer.echo(f"Cancelled job {result.job_id}")
183
+ else:
184
+ typer.echo(f"Could not cancel job {result.job_id}: {result.message}")
185
+
186
+
187
+ @app.command("hyperparams")
188
+ def hyperparams(
189
+ vla_type: str = typer.Argument(help="VLA type"),
190
+ model_id: str | None = typer.Option(
191
+ None, help="Model ID for type-specific defaults"
192
+ ),
193
+ ) -> None:
194
+ """Show default hyperparameters for a VLA type."""
195
+ client = get_client()
196
+ defaults = client.finetune.get_hyperparams_defaults(
197
+ vla_type=vla_type,
198
+ model_id=model_id,
199
+ )
200
+ print_json(defaults)
@@ -0,0 +1,42 @@
1
+ """Instances commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from qualia_cli.client import get_client
8
+ from qualia_cli.output import print_json, print_table
9
+
10
+ app = typer.Typer(help="GPU instance information.")
11
+
12
+
13
+ @app.command("list")
14
+ def list_instances(
15
+ json: bool = typer.Option(False, "--json", help="JSON output"),
16
+ ) -> None:
17
+ """List available GPU instance types."""
18
+ client = get_client()
19
+ instances = client.instances.list()
20
+ if json:
21
+ print_json(instances)
22
+ else:
23
+ rows = [
24
+ {
25
+ "id": i.id,
26
+ "name": i.name,
27
+ "gpu": i.gpu_description,
28
+ "cost": str(i.credits_per_hour),
29
+ "regions": str(i.region_count),
30
+ }
31
+ for i in instances
32
+ ]
33
+ print_table(
34
+ rows,
35
+ [
36
+ ("id", "ID"),
37
+ ("name", "Name"),
38
+ ("gpu", "GPU"),
39
+ ("cost", "Credits/hr"),
40
+ ("regions", "Regions"),
41
+ ],
42
+ )
@@ -0,0 +1,95 @@
1
+ """Models commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import UUID
6
+
7
+ import typer
8
+
9
+ from qualia_cli.client import get_client
10
+ from qualia_cli.output import print_json, print_table
11
+
12
+ app = typer.Typer(help="VLA model information.")
13
+
14
+
15
+ @app.command("types")
16
+ def list_types(
17
+ json: bool = typer.Option(False, "--json", help="JSON output"),
18
+ ) -> None:
19
+ """List available VLA model types."""
20
+ client = get_client()
21
+ models = client.models.types()
22
+ if json:
23
+ print_json(models)
24
+ else:
25
+ rows = [
26
+ {
27
+ "id": m.id,
28
+ "name": m.name,
29
+ "base": m.base_model_id or "-",
30
+ "cameras": ", ".join(m.camera_slots),
31
+ "custom": "yes" if m.supports_custom_model else "no",
32
+ }
33
+ for m in models
34
+ ]
35
+ print_table(
36
+ rows,
37
+ [
38
+ ("id", "ID"),
39
+ ("name", "Name"),
40
+ ("base", "Base Model"),
41
+ ("cameras", "Camera Slots"),
42
+ ("custom", "Custom Model"),
43
+ ],
44
+ )
45
+
46
+
47
+ @app.command("list")
48
+ def list_models(
49
+ limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
50
+ cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
51
+ sort: str = typer.Option(
52
+ "newest", "--sort", "-s", help="newest|oldest|updated|name"
53
+ ),
54
+ search: str = typer.Option(None, "--search", "-q", help="Search model path"),
55
+ subtype: str = typer.Option(None, "--model-type", help="vla_model or reward_model"),
56
+ project_id: str = typer.Option(None, "--project", help="Filter by project ID"),
57
+ job_id: str = typer.Option(None, "--job", help="Filter by job ID"),
58
+ json: bool = typer.Option(False, "--json", help="JSON output"),
59
+ ) -> None:
60
+ """List trained models (paginated)."""
61
+ client = get_client()
62
+ page = client.models.list_trained(
63
+ limit=limit,
64
+ cursor=cursor,
65
+ sort=sort,
66
+ search=search,
67
+ subtype=subtype,
68
+ project_id=UUID(project_id) if project_id else None,
69
+ job_id=UUID(job_id) if job_id else None,
70
+ )
71
+ if json:
72
+ print_json(page)
73
+ else:
74
+ rows = [
75
+ {
76
+ "id": str(m.artifact_id)[:8],
77
+ "subtype": m.subtype,
78
+ "path": m.path,
79
+ "jobs": len(m.jobs),
80
+ "created": m.created_at.strftime("%Y-%m-%d"),
81
+ }
82
+ for m in page.items
83
+ ]
84
+ print_table(
85
+ rows,
86
+ [
87
+ ("id", "Model ID"),
88
+ ("subtype", "Subtype"),
89
+ ("path", "Path"),
90
+ ("jobs", "Jobs"),
91
+ ("created", "Created"),
92
+ ],
93
+ )
94
+ if page.page_info.has_next:
95
+ typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
@@ -0,0 +1,82 @@
1
+ """Projects commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from qualia_cli.client import get_client
8
+ from qualia_cli.output import print_json, print_table
9
+
10
+ app = typer.Typer(help="Manage projects.")
11
+
12
+
13
+ @app.command("list")
14
+ def list_projects(
15
+ limit: int = typer.Option(20, "--limit", "-n", help="Page size (1-100)"),
16
+ cursor: str = typer.Option(None, "--cursor", help="Pagination cursor"),
17
+ sort: str = typer.Option(
18
+ "newest", "--sort", "-s", help="newest|oldest|updated|name"
19
+ ),
20
+ search: str = typer.Option(None, "--search", "-q", help="Search project name"),
21
+ json: bool = typer.Option(False, "--json", help="JSON output"),
22
+ ) -> None:
23
+ """List projects (paginated)."""
24
+ client = get_client()
25
+ page = client.projects.list(
26
+ limit=limit,
27
+ cursor=cursor,
28
+ sort=sort,
29
+ search=search,
30
+ )
31
+ if json:
32
+ print_json(page)
33
+ else:
34
+ rows = [
35
+ {
36
+ "id": str(p.project_id)[:8],
37
+ "name": p.name,
38
+ "desc": p.description or "",
39
+ "created": p.created_at.strftime("%Y-%m-%d"),
40
+ }
41
+ for p in page.items
42
+ ]
43
+ print_table(
44
+ rows,
45
+ [
46
+ ("id", "Project ID"),
47
+ ("name", "Name"),
48
+ ("desc", "Description"),
49
+ ("created", "Created"),
50
+ ],
51
+ )
52
+ if page.page_info.has_next:
53
+ typer.echo(f"\nNext page: --cursor {page.page_info.end_cursor}")
54
+
55
+
56
+ @app.command("create")
57
+ def create_project(
58
+ name: str = typer.Argument(help="Project name"),
59
+ description: str = typer.Option(None, "--description", "-d", help="Description"),
60
+ json: bool = typer.Option(False, "--json", help="JSON output"),
61
+ ) -> None:
62
+ """Create a new project."""
63
+ client = get_client()
64
+ result = client.projects.create(name=name, description=description)
65
+ if json:
66
+ print_json(result)
67
+ else:
68
+ typer.echo(f"Created project {result.project_id}")
69
+
70
+
71
+ @app.command("delete")
72
+ def delete_project(
73
+ project_id: str = typer.Argument(help="Project ID to delete"),
74
+ json: bool = typer.Option(False, "--json", help="JSON output"),
75
+ ) -> None:
76
+ """Delete a project."""
77
+ client = get_client()
78
+ result = client.projects.delete(project_id)
79
+ if json:
80
+ print_json(result)
81
+ else:
82
+ typer.echo(f"Deleted project {result.project_id}")
qualia_cli/config.py ADDED
@@ -0,0 +1,63 @@
1
+ """Config file loading and writing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ CONFIG_DIR = Path(click.get_app_dir("qualia"))
12
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
13
+
14
+
15
+ @dataclass
16
+ class Config:
17
+ api_key: str | None = None
18
+ base_url: str | None = None
19
+
20
+
21
+ def _parse_toml_value(line: str, key: str) -> str | None:
22
+ """Extract value for a key from a simple TOML line."""
23
+ stripped = line.strip()
24
+ if stripped.startswith(key):
25
+ _, _, val = stripped.partition("=")
26
+ return val.strip().strip('"').strip("'")
27
+ return None
28
+
29
+
30
+ def load_config() -> Config:
31
+ """Load config from env vars, falling back to config file."""
32
+ cfg = Config()
33
+
34
+ # env vars take precedence
35
+ cfg.api_key = os.environ.get("QUALIA_API_KEY")
36
+ cfg.base_url = os.environ.get("QUALIA_BASE_URL")
37
+
38
+ if (cfg.api_key and cfg.base_url) or not CONFIG_FILE.exists():
39
+ return cfg
40
+
41
+ for line in CONFIG_FILE.read_text().splitlines():
42
+ if not cfg.api_key:
43
+ val = _parse_toml_value(line, "api_key")
44
+ if val:
45
+ cfg.api_key = val
46
+ if not cfg.base_url:
47
+ val = _parse_toml_value(line, "base_url")
48
+ if val:
49
+ cfg.base_url = val
50
+
51
+ return cfg
52
+
53
+
54
+ def save_config(api_key: str, base_url: str | None = None) -> Path:
55
+ """Write config to file. Returns the path written."""
56
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
57
+ lines = [f'api_key = "{api_key}"']
58
+ if base_url:
59
+ lines.append(f'base_url = "{base_url}"')
60
+ CONFIG_FILE.write_text("\n".join(lines) + "\n")
61
+ if os.name != "nt":
62
+ CONFIG_FILE.chmod(0o600)
63
+ return CONFIG_FILE
qualia_cli/main.py ADDED
@@ -0,0 +1,76 @@
1
+ """Qualia CLI - terminal interface for the Qualia VLA fine-tuning platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from qualia import QualiaAPIError, QualiaError
7
+
8
+ from qualia_cli.commands import (
9
+ auth,
10
+ credits,
11
+ datasets,
12
+ finetune,
13
+ instances,
14
+ models,
15
+ projects,
16
+ )
17
+ from qualia_cli.output import print_error
18
+
19
+ app = typer.Typer(
20
+ name="qualia",
21
+ help="Qualia CLI - fine-tune Vision-Language-Action models.",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ app.add_typer(auth.app, name="auth")
26
+ app.add_typer(credits.app, name="credit")
27
+ app.add_typer(datasets.app, name="dataset")
28
+ app.add_typer(finetune.app, name="job")
29
+ app.add_typer(instances.app, name="instance")
30
+ app.add_typer(models.app, name="model")
31
+ app.add_typer(projects.app, name="project")
32
+
33
+ # Global overrides set by the callback, consumed by get_client()
34
+ _token_override: str | None = None
35
+ _base_url_override: str | None = None
36
+
37
+
38
+ @app.callback()
39
+ def main(
40
+ token: str | None = typer.Option(
41
+ None, "--token", envvar="QUALIA_API_KEY", help="API key (overrides config)"
42
+ ),
43
+ base_url: str | None = typer.Option(
44
+ None, "--base-url", envvar="QUALIA_BASE_URL", help="API base URL"
45
+ ),
46
+ ) -> None:
47
+ """Qualia CLI - fine-tune Vision-Language-Action models."""
48
+ global _token_override, _base_url_override
49
+ _token_override = token
50
+ _base_url_override = base_url
51
+
52
+
53
+ @app.command()
54
+ def version() -> None:
55
+ """Show CLI and SDK versions."""
56
+ from qualia import __version__ as sdk_version
57
+
58
+ from qualia_cli import __version__ as cli_version
59
+
60
+ typer.echo(f"qualia-cli {cli_version}")
61
+ typer.echo(f"qualia-sdk {sdk_version}")
62
+
63
+
64
+ def cli() -> None:
65
+ try:
66
+ app()
67
+ except QualiaAPIError as e:
68
+ print_error(str(e))
69
+ raise SystemExit(1) from e
70
+ except QualiaError as e:
71
+ print_error(e.message)
72
+ raise SystemExit(1) from e
73
+
74
+
75
+ if __name__ == "__main__":
76
+ cli()
qualia_cli/output.py ADDED
@@ -0,0 +1,42 @@
1
+ """Output formatting helpers for table and JSON display."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+ err_console = Console(stderr=True)
13
+
14
+
15
+ def print_json(data: Any) -> None:
16
+ """Print data as formatted JSON."""
17
+ if hasattr(data, "model_dump"):
18
+ data = data.model_dump(mode="json")
19
+ elif isinstance(data, list) and data and hasattr(data[0], "model_dump"):
20
+ data = [d.model_dump(mode="json") for d in data]
21
+ console.print_json(json.dumps(data, default=str))
22
+
23
+
24
+ def print_table(
25
+ rows: list[dict[str, Any]],
26
+ columns: list[tuple[str, str]],
27
+ title: str | None = None,
28
+ ) -> None:
29
+ """Print rows as a rich table.
30
+
31
+ columns: list of (key, header_label) tuples.
32
+ """
33
+ table = Table(title=title)
34
+ for _, header in columns:
35
+ table.add_column(header)
36
+ for row in rows:
37
+ table.add_row(*(str(row.get(k, "")) for k, _ in columns))
38
+ console.print(table)
39
+
40
+
41
+ def print_error(msg: str) -> None:
42
+ err_console.print(f"[red]Error:[/red] {msg}")
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: qualia-cli
3
+ Version: 0.1.0
4
+ Summary: CLI for the Qualia Studios VLA fine-tuning platform
5
+ Project-URL: Homepage, https://qualiastudios.dev
6
+ Project-URL: Documentation, https://docs.qualiastudios.dev
7
+ Author: Qualia Studios
8
+ License-Expression: MIT
9
+ Keywords: cli,fine-tuning,qualia,robotics,vla
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: qualia-sdk>=0.2.0
20
+ Requires-Dist: rich>=13.0.0
21
+ Requires-Dist: typer>=0.15.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Qualia CLI
25
+
26
+ Terminal interface for the [Qualia](https://qualiastudios.dev) VLA fine-tuning platform.
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ pip install qualia-cli
32
+ ```
33
+
34
+ Or with [uv](https://docs.astral.sh/uv/):
35
+
36
+ ```sh
37
+ uv tool install qualia-cli
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```sh
43
+ # Authenticate (pick one)
44
+ qualia auth login # interactive, saves to config
45
+ export QUALIA_API_KEY="your-api-key" # env var
46
+ qualia --token <KEY> <command> # inline, for CI/scripts
47
+
48
+ # List available models
49
+ qualia models list
50
+
51
+ # List GPU instances
52
+ qualia instances list
53
+
54
+ # Create a project
55
+ qualia projects create "My Robot"
56
+
57
+ # Start a fine-tuning job
58
+ qualia finetune create \
59
+ --project-id <id> \
60
+ --vla-type smolvla \
61
+ --model-id lerobot/smolvla_base \
62
+ --dataset-id lerobot/pusht \
63
+ --dataset-last-modified "2025-01-15T10:00:00Z" \
64
+ --model-last-modified "2025-01-15T10:00:00Z" \
65
+ --hours 2.0 \
66
+ --camera-mappings '{"cam_1": "observation.images.top"}'
67
+
68
+ # Check job status
69
+ qualia finetune get <job-id>
70
+ ```
71
+
72
+ ## JSON output
73
+
74
+ All commands support `--json` for machine-readable output:
75
+
76
+ ```sh
77
+ qualia projects list --json
78
+ qualia credits --json
79
+ ```
@@ -0,0 +1,17 @@
1
+ qualia_cli/__init__.py,sha256=xpooCLfcAl2BTsYxuAJzb8lBfskcCi-r7b-HLuGEKKc,102
2
+ qualia_cli/client.py,sha256=yrb2xrfn2OabbBaJYVmTknKPDcvHHPckHko78KcglhI,836
3
+ qualia_cli/config.py,sha256=QD4IZ1tsYbfrRkRUc9BNrfnXHrXGVkNdVNo0JtEEtIw,1725
4
+ qualia_cli/main.py,sha256=mwEncy2S1UomSSyz5SwY0RY4mVTxnEkxu8hpHGrViv0,1920
5
+ qualia_cli/output.py,sha256=bazOtQRA6p0124GfAglha0d-wOFwJsLAHBl2aqr9wiY,1110
6
+ qualia_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ qualia_cli/commands/auth.py,sha256=551vEGWPAexEE1WFdx6VIa1SeUmh32X6os395KiCBF4,1209
8
+ qualia_cli/commands/credits.py,sha256=g7SuguSWwai3kTiFh3CMbkfBWesHOl0F9uYtu3VCyQg,541
9
+ qualia_cli/commands/datasets.py,sha256=mlWOxNe8mvBfnrKvyXKD-70-8UlrxEzuEXqqBJGjQhE,2671
10
+ qualia_cli/commands/finetune.py,sha256=xlwkS_ZkAtzclRolyQjxW8aSejBPh8bUt5CQJXIktXo,6710
11
+ qualia_cli/commands/instances.py,sha256=4ozIzvR-MQUBjceZQMUUUtjBjY83nqRJ7vUSkVluRk0,1050
12
+ qualia_cli/commands/models.py,sha256=fl_eKsXSqEhecr1Nh0LC-lGt0JRsuvzhzpcSzx72nPc,2865
13
+ qualia_cli/commands/projects.py,sha256=CmI1tHOZvt84rB4w_SyygoDkse7U3hTzP1ZlBl2Rtw0,2434
14
+ qualia_cli-0.1.0.dist-info/METADATA,sha256=BTpdMBqXTbCtMMJF9hi90HxThgMXLE2q3GwJoRHL3jE,1977
15
+ qualia_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ qualia_cli-0.1.0.dist-info/entry_points.txt,sha256=hjLKGQ8xf2rlxWfPuZFyCvVk8PSDtDNxpXHyz6woUM0,47
17
+ qualia_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qualia = qualia_cli.main:cli