qualia-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,84 @@
1
+ # qualia
2
+ qcore.config.toml
3
+ .qdev/*
4
+ .env
5
+
6
+ # python-generated
7
+ __pycache__/
8
+ *.py[oc]
9
+ build/
10
+ dist/
11
+ wheels/
12
+ *.egg-info
13
+ .coverage
14
+ .coverage.*
15
+ htmlcov/
16
+
17
+ # virtual envs
18
+ .venv
19
+
20
+ # ruff stuff:
21
+ .ruff_cache/
22
+
23
+ # mac
24
+ .DS_Store
25
+
26
+ # Jupyter Notebook
27
+ .ipynb_checkpoints
28
+ .ipython
29
+
30
+ # Terraform
31
+ */*/.terraform/
32
+ *.tfstate
33
+ *.tfstate.backup
34
+ *.tfplan
35
+
36
+ # supabase
37
+ .temp
38
+ reset.sql
39
+
40
+ # generated docs
41
+ ui/docs/src/assets/openapi.json
42
+
43
+ # nix
44
+ .direnv
45
+ result
46
+
47
+ # playwright-cli
48
+ .playwright-cli/
49
+
50
+ # Windows
51
+ nul
52
+ lerobot
53
+
54
+ # Windows
55
+ nul
56
+
57
+ # qcore runtime files
58
+ qcore.log
59
+ qcore.pid
60
+
61
+ # Dev credential files (auto-generated by setup workflow)
62
+ dev_project_id.txt
63
+ dev_user_id.txt
64
+ dev_access_token.txt
65
+ .env
66
+
67
+ # Claude Code sandbox artifacts (https://github.com/anthropics/claude-code/issues/17727)
68
+ .claude/agents
69
+ .claude/commands
70
+ .bash_profile
71
+ .bashrc
72
+ .gitconfig
73
+ .gitmodules
74
+ .idea
75
+ .mcp.json
76
+ .profile
77
+ .ripgreprc
78
+ .zprofile
79
+ .zshrc
80
+ /HEAD
81
+ /config
82
+ /hooks
83
+ /objects
84
+ /refs
@@ -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,56 @@
1
+ # Qualia CLI
2
+
3
+ Terminal interface for the [Qualia](https://qualiastudios.dev) VLA fine-tuning platform.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pip install qualia-cli
9
+ ```
10
+
11
+ Or with [uv](https://docs.astral.sh/uv/):
12
+
13
+ ```sh
14
+ uv tool install qualia-cli
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```sh
20
+ # Authenticate (pick one)
21
+ qualia auth login # interactive, saves to config
22
+ export QUALIA_API_KEY="your-api-key" # env var
23
+ qualia --token <KEY> <command> # inline, for CI/scripts
24
+
25
+ # List available models
26
+ qualia models list
27
+
28
+ # List GPU instances
29
+ qualia instances list
30
+
31
+ # Create a project
32
+ qualia projects create "My Robot"
33
+
34
+ # Start a fine-tuning job
35
+ qualia finetune create \
36
+ --project-id <id> \
37
+ --vla-type smolvla \
38
+ --model-id lerobot/smolvla_base \
39
+ --dataset-id lerobot/pusht \
40
+ --dataset-last-modified "2025-01-15T10:00:00Z" \
41
+ --model-last-modified "2025-01-15T10:00:00Z" \
42
+ --hours 2.0 \
43
+ --camera-mappings '{"cam_1": "observation.images.top"}'
44
+
45
+ # Check job status
46
+ qualia finetune get <job-id>
47
+ ```
48
+
49
+ ## JSON output
50
+
51
+ All commands support `--json` for machine-readable output:
52
+
53
+ ```sh
54
+ qualia projects list --json
55
+ qualia credits --json
56
+ ```
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "qualia-cli"
3
+ version = "0.1.0"
4
+ description = "CLI for the Qualia Studios VLA fine-tuning platform"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Qualia Studios" },
10
+ ]
11
+ keywords = ["qualia", "vla", "robotics", "fine-tuning", "cli"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "qualia-sdk>=0.2.0",
24
+ "typer>=0.15.0",
25
+ "rich>=13.0.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://qualiastudios.dev"
30
+ Documentation = "https://docs.qualiastudios.dev"
31
+
32
+ [project.scripts]
33
+ qualia = "qualia_cli.main:cli"
34
+
35
+ [dependency-groups]
36
+ test = [
37
+ "pytest>=8.0.0",
38
+ "pytest-cov>=4.0.0",
39
+ ]
40
+ dev = [
41
+ "ruff==0.15.1",
42
+ ]
43
+
44
+ [tool.uv]
45
+ default-groups = [
46
+ "test",
47
+ "dev",
48
+ ]
49
+
50
+ [tool.uv.sources]
51
+ qualia-sdk = { path = "../python", editable = true }
52
+
53
+ [build-system]
54
+ requires = ["hatchling"]
55
+ build-backend = "hatchling.build"
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/qualia_cli"]
59
+
60
+ [tool.ruff]
61
+ line-length = 89
62
+ target-version = "py310"
63
+
64
+ [tool.ruff.lint]
65
+ select = [
66
+ "I", # isort
67
+ "F", # pyflakes
68
+ "E", # pycodestyle errors
69
+ "W", # pycodestyle warnings
70
+ "B", # flake8-bugbear
71
+ "UP", # pyupgrade
72
+ "SIM", # flake8-simplify
73
+ ]
74
+ ignore = [
75
+ "E501", # line too long
76
+ ]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ python_files = ["test_*.py"]
81
+ addopts = "-v --tb=short"
@@ -0,0 +1,3 @@
1
+ """Qualia CLI - Terminal interface for the Qualia VLA fine-tuning platform."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)