lql-cli 0.4.0__tar.gz → 0.6.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.
- lql_cli-0.4.0/README.md → lql_cli-0.6.0/PKG-INFO +38 -6
- lql_cli-0.4.0/PKG-INFO → lql_cli-0.6.0/README.md +22 -21
- {lql_cli-0.4.0 → lql_cli-0.6.0}/pyproject.toml +3 -2
- lql_cli-0.6.0/src/lql/__init__.py +1 -0
- lql_cli-0.6.0/src/lql/_group.py +50 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/cli.py +3 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/annotations.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/auth.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/buckets.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/datasets.py +16 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/edits.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/evals.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/highlights.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/instructions.py +24 -3
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/issues.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/preview.py +63 -5
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/reports.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/skills.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/spec.py +3 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/tui.py +248 -20
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/workspaces.py +23 -4
- {lql_cli-0.4.0 → lql_cli-0.6.0}/uv.lock +109 -280
- lql_cli-0.4.0/src/lql/__init__.py +0 -1
- {lql_cli-0.4.0 → lql_cli-0.6.0}/.claude/settings.local.json +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/.gitignore +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/examples/agent-traces.jsonl +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/package-lock.json +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/_opts.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/api.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/__init__.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/commands/update.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/config.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/output.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/sessions.py +0 -0
- {lql_cli-0.4.0 → lql_cli-0.6.0}/src/lql/util.py +0 -0
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lql-cli
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: lql — CLI for the Liquid DataViewer platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/Liquid4All/lql
|
|
6
|
+
Author: Liquid AI
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: huggingface-hub>=0.24
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: textual-image>=0.7
|
|
13
|
+
Requires-Dist: textual>=0.50
|
|
14
|
+
Requires-Dist: typer>=0.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
1
17
|
# lql — Liquid Query Language CLI
|
|
2
18
|
|
|
3
19
|
Scriptable CLI for the [Liquid DataViewer](https://dataviewer.liquid.ai) platform. Designed for both humans and AI agents (Claude Code, Codex, etc.) to automate datasets, eval analysis, spec docs, annotations, and more.
|
|
4
20
|
|
|
5
|
-
A Python package (Python ≥ 3.
|
|
21
|
+
A Python package (Python ≥ 3.12), published on PyPI as **[`lql-cli`](https://pypi.org/project/lql-cli/)** — the installed command is `lql`.
|
|
6
22
|
|
|
7
23
|
## Quick start
|
|
8
24
|
|
|
@@ -14,7 +30,7 @@ lql skills install # teach Claude Code + Codex how to use lql
|
|
|
14
30
|
|
|
15
31
|
## Install
|
|
16
32
|
|
|
17
|
-
`lql` is a Python package (requires Python ≥ 3.
|
|
33
|
+
`lql` is a Python package (requires Python ≥ 3.12), distributed on PyPI as **`lql-cli`** (the
|
|
18
34
|
command it installs is `lql`). Install it as a standalone CLI tool with
|
|
19
35
|
[uv](https://docs.astral.sh/uv/):
|
|
20
36
|
|
|
@@ -84,6 +100,13 @@ lql logout
|
|
|
84
100
|
|
|
85
101
|
## Command reference
|
|
86
102
|
|
|
103
|
+
Running a group with no subcommand (e.g. `lql datasets`) lists its subcommands.
|
|
104
|
+
For faster typing, **aliases** and **unique prefixes** resolve everywhere:
|
|
105
|
+
group aliases (`ds`, `ws`, `ev`, `ann`, `hl`, `rep`, `bkt`, `iss`), verb aliases
|
|
106
|
+
(`ls`=list, `rm`/`del`=delete, `new`/`mk`=create, `info`=show), and prefixes
|
|
107
|
+
(`lql datasets l` → `list`). So `lql ds ls --workspace <id>` ≡ `lql datasets list
|
|
108
|
+
--workspace <id>`. The canonical names below always work (use those in scripts).
|
|
109
|
+
|
|
87
110
|
### Auth
|
|
88
111
|
|
|
89
112
|
```
|
|
@@ -95,7 +118,7 @@ lql whoami Show current user
|
|
|
95
118
|
### Workspaces
|
|
96
119
|
|
|
97
120
|
```
|
|
98
|
-
lql workspaces list
|
|
121
|
+
lql workspaces list [--search <text>] List workspaces (--search filters by name/slug)
|
|
99
122
|
lql workspaces create <name> Create a workspace
|
|
100
123
|
lql workspaces show <id> Show workspace details
|
|
101
124
|
lql workspaces update <id> --name <n> Rename a workspace
|
|
@@ -108,7 +131,7 @@ lql workspaces members remove <id> <uid> Remove member by user ID
|
|
|
108
131
|
### Datasets
|
|
109
132
|
|
|
110
133
|
```
|
|
111
|
-
lql datasets list [--workspace <id>]
|
|
134
|
+
lql datasets list [--workspace <id>] [--search <text>] List datasets (--search filters by name/HF repo)
|
|
112
135
|
lql datasets show <id> Show dataset details
|
|
113
136
|
lql datasets create --workspace <id> --hf-repo <repo> [--name <n>] [--split <s>]
|
|
114
137
|
lql datasets create --workspace <id> --hf-bucket <org/bucket> --key <path-or-glob> [--name <n>]
|
|
@@ -158,11 +181,20 @@ lql preview tatsu-lab/alpaca --hf
|
|
|
158
181
|
lql preview org/name --hf --split validation --workspace <id>
|
|
159
182
|
```
|
|
160
183
|
|
|
161
|
-
**
|
|
184
|
+
**Media.** Images render **inline** in terminals that support an image protocol
|
|
185
|
+
(Kitty/Ghostty, iTerm2, Sixel; falls back to a compact `🖼 …` placeholder
|
|
186
|
+
elsewhere) — both multimodal `image_url`/data-URI segments and image-mode
|
|
187
|
+
columns. Audio can't play inline in a terminal, so each clip shows a `♪` line
|
|
188
|
+
and **`p` plays** the current sample's audio via the system player (`afplay`/
|
|
189
|
+
`open`). Images render inline in pager mode (one sample at a time); scroll mode
|
|
190
|
+
shows placeholders to avoid decoding the whole buffer.
|
|
191
|
+
|
|
192
|
+
**Navigation** — two modes, toggle with `m`:
|
|
162
193
|
|
|
163
194
|
- **pager** (default): one sample at a time · `←/→` or `n`/`b` switch samples · `↑/↓`/`j`/`k`/PgUp-Dn scroll
|
|
164
195
|
- **scroll**: all samples in one buffer · `n`/`b` jump between samples · arrows scroll
|
|
165
|
-
- `
|
|
196
|
+
- **copy**: `Tab`/`Shift+Tab` move a highlight between blocks · `c` copies the focused message/field · `Y` copies the whole sample as JSON (via OSC 52 — reaches your **local** clipboard over SSH where the terminal supports it, e.g. Ghostty/iTerm2; not macOS Terminal)
|
|
197
|
+
- `p` play audio · `q` quits
|
|
166
198
|
|
|
167
199
|
```
|
|
168
200
|
lql preview examples/agent-traces.jsonl # 20-sample file of agent-trace/tool-use formats
|
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lql-cli
|
|
3
|
-
Version: 0.4.0
|
|
4
|
-
Summary: lql — CLI for the Liquid DataViewer platform
|
|
5
|
-
Project-URL: Homepage, https://github.com/Liquid4All/lql
|
|
6
|
-
Author: Liquid AI
|
|
7
|
-
License: MIT
|
|
8
|
-
Requires-Python: >=3.9
|
|
9
|
-
Requires-Dist: httpx>=0.27
|
|
10
|
-
Requires-Dist: huggingface-hub>=0.24
|
|
11
|
-
Requires-Dist: rich>=13.0
|
|
12
|
-
Requires-Dist: textual>=0.50
|
|
13
|
-
Requires-Dist: typer>=0.12
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
1
|
# lql — Liquid Query Language CLI
|
|
17
2
|
|
|
18
3
|
Scriptable CLI for the [Liquid DataViewer](https://dataviewer.liquid.ai) platform. Designed for both humans and AI agents (Claude Code, Codex, etc.) to automate datasets, eval analysis, spec docs, annotations, and more.
|
|
19
4
|
|
|
20
|
-
A Python package (Python ≥ 3.
|
|
5
|
+
A Python package (Python ≥ 3.12), published on PyPI as **[`lql-cli`](https://pypi.org/project/lql-cli/)** — the installed command is `lql`.
|
|
21
6
|
|
|
22
7
|
## Quick start
|
|
23
8
|
|
|
@@ -29,7 +14,7 @@ lql skills install # teach Claude Code + Codex how to use lql
|
|
|
29
14
|
|
|
30
15
|
## Install
|
|
31
16
|
|
|
32
|
-
`lql` is a Python package (requires Python ≥ 3.
|
|
17
|
+
`lql` is a Python package (requires Python ≥ 3.12), distributed on PyPI as **`lql-cli`** (the
|
|
33
18
|
command it installs is `lql`). Install it as a standalone CLI tool with
|
|
34
19
|
[uv](https://docs.astral.sh/uv/):
|
|
35
20
|
|
|
@@ -99,6 +84,13 @@ lql logout
|
|
|
99
84
|
|
|
100
85
|
## Command reference
|
|
101
86
|
|
|
87
|
+
Running a group with no subcommand (e.g. `lql datasets`) lists its subcommands.
|
|
88
|
+
For faster typing, **aliases** and **unique prefixes** resolve everywhere:
|
|
89
|
+
group aliases (`ds`, `ws`, `ev`, `ann`, `hl`, `rep`, `bkt`, `iss`), verb aliases
|
|
90
|
+
(`ls`=list, `rm`/`del`=delete, `new`/`mk`=create, `info`=show), and prefixes
|
|
91
|
+
(`lql datasets l` → `list`). So `lql ds ls --workspace <id>` ≡ `lql datasets list
|
|
92
|
+
--workspace <id>`. The canonical names below always work (use those in scripts).
|
|
93
|
+
|
|
102
94
|
### Auth
|
|
103
95
|
|
|
104
96
|
```
|
|
@@ -110,7 +102,7 @@ lql whoami Show current user
|
|
|
110
102
|
### Workspaces
|
|
111
103
|
|
|
112
104
|
```
|
|
113
|
-
lql workspaces list
|
|
105
|
+
lql workspaces list [--search <text>] List workspaces (--search filters by name/slug)
|
|
114
106
|
lql workspaces create <name> Create a workspace
|
|
115
107
|
lql workspaces show <id> Show workspace details
|
|
116
108
|
lql workspaces update <id> --name <n> Rename a workspace
|
|
@@ -123,7 +115,7 @@ lql workspaces members remove <id> <uid> Remove member by user ID
|
|
|
123
115
|
### Datasets
|
|
124
116
|
|
|
125
117
|
```
|
|
126
|
-
lql datasets list [--workspace <id>]
|
|
118
|
+
lql datasets list [--workspace <id>] [--search <text>] List datasets (--search filters by name/HF repo)
|
|
127
119
|
lql datasets show <id> Show dataset details
|
|
128
120
|
lql datasets create --workspace <id> --hf-repo <repo> [--name <n>] [--split <s>]
|
|
129
121
|
lql datasets create --workspace <id> --hf-bucket <org/bucket> --key <path-or-glob> [--name <n>]
|
|
@@ -173,11 +165,20 @@ lql preview tatsu-lab/alpaca --hf
|
|
|
173
165
|
lql preview org/name --hf --split validation --workspace <id>
|
|
174
166
|
```
|
|
175
167
|
|
|
176
|
-
**
|
|
168
|
+
**Media.** Images render **inline** in terminals that support an image protocol
|
|
169
|
+
(Kitty/Ghostty, iTerm2, Sixel; falls back to a compact `🖼 …` placeholder
|
|
170
|
+
elsewhere) — both multimodal `image_url`/data-URI segments and image-mode
|
|
171
|
+
columns. Audio can't play inline in a terminal, so each clip shows a `♪` line
|
|
172
|
+
and **`p` plays** the current sample's audio via the system player (`afplay`/
|
|
173
|
+
`open`). Images render inline in pager mode (one sample at a time); scroll mode
|
|
174
|
+
shows placeholders to avoid decoding the whole buffer.
|
|
175
|
+
|
|
176
|
+
**Navigation** — two modes, toggle with `m`:
|
|
177
177
|
|
|
178
178
|
- **pager** (default): one sample at a time · `←/→` or `n`/`b` switch samples · `↑/↓`/`j`/`k`/PgUp-Dn scroll
|
|
179
179
|
- **scroll**: all samples in one buffer · `n`/`b` jump between samples · arrows scroll
|
|
180
|
-
- `
|
|
180
|
+
- **copy**: `Tab`/`Shift+Tab` move a highlight between blocks · `c` copies the focused message/field · `Y` copies the whole sample as JSON (via OSC 52 — reaches your **local** clipboard over SSH where the terminal supports it, e.g. Ghostty/iTerm2; not macOS Terminal)
|
|
181
|
+
- `p` play audio · `q` quits
|
|
181
182
|
|
|
182
183
|
```
|
|
183
184
|
lql preview examples/agent-traces.jsonl # 20-sample file of agent-trace/tool-use formats
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lql-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "lql — CLI for the Liquid DataViewer platform"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [{ name = "Liquid AI" }]
|
|
13
13
|
dependencies = [
|
|
@@ -16,6 +16,7 @@ dependencies = [
|
|
|
16
16
|
"rich>=13.0",
|
|
17
17
|
"huggingface-hub>=0.24",
|
|
18
18
|
"textual>=0.50",
|
|
19
|
+
"textual-image>=0.7",
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
[project.scripts]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""A Typer/Click group that adds two conveniences to every command group:
|
|
2
|
+
|
|
3
|
+
* short aliases — `ds`→`datasets`, `ws`→`workspaces`, `ls`→`list`, …
|
|
4
|
+
* unique-prefix matching — `lql datasets l` resolves to `list`
|
|
5
|
+
|
|
6
|
+
Both are purely additive: the canonical command names always work, and
|
|
7
|
+
`lql instructions` documents the canonical forms, so agents are unaffected —
|
|
8
|
+
these are conveniences for humans typing at a prompt.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typer.core import TyperGroup
|
|
12
|
+
|
|
13
|
+
# alias -> canonical. Kept intentionally small and unambiguous.
|
|
14
|
+
ALIASES = {
|
|
15
|
+
# command groups
|
|
16
|
+
"ds": "datasets",
|
|
17
|
+
"ws": "workspaces",
|
|
18
|
+
"ev": "eval",
|
|
19
|
+
"evals": "eval",
|
|
20
|
+
"ann": "annotations",
|
|
21
|
+
"hl": "highlights",
|
|
22
|
+
"rep": "reports",
|
|
23
|
+
"bkt": "buckets",
|
|
24
|
+
"iss": "issues",
|
|
25
|
+
# verbs
|
|
26
|
+
"ls": "list",
|
|
27
|
+
"rm": "delete",
|
|
28
|
+
"del": "delete",
|
|
29
|
+
"new": "create",
|
|
30
|
+
"mk": "create",
|
|
31
|
+
"info": "show",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AliasGroup(TyperGroup):
|
|
36
|
+
def get_command(self, ctx, name):
|
|
37
|
+
# Exact name first so a real command is never shadowed by an alias;
|
|
38
|
+
# then explicit alias; then an unambiguous prefix.
|
|
39
|
+
cmd = super().get_command(ctx, name)
|
|
40
|
+
if cmd is not None:
|
|
41
|
+
return cmd
|
|
42
|
+
alias = ALIASES.get(name)
|
|
43
|
+
if alias is not None:
|
|
44
|
+
cmd = super().get_command(ctx, alias)
|
|
45
|
+
if cmd is not None:
|
|
46
|
+
return cmd
|
|
47
|
+
matches = [c for c in self.list_commands(ctx) if c.startswith(name)]
|
|
48
|
+
if len(matches) == 1:
|
|
49
|
+
return super().get_command(ctx, matches[0])
|
|
50
|
+
return None
|
|
@@ -3,6 +3,8 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
|
+
from ._group import AliasGroup
|
|
7
|
+
|
|
6
8
|
from . import __version__
|
|
7
9
|
from .output import print_error
|
|
8
10
|
from .commands import (
|
|
@@ -32,6 +34,7 @@ app = typer.Typer(
|
|
|
32
34
|
),
|
|
33
35
|
no_args_is_help=True,
|
|
34
36
|
add_completion=False,
|
|
37
|
+
cls=AliasGroup,
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
|
|
@@ -3,13 +3,15 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
|
+
from .._group import AliasGroup
|
|
7
|
+
|
|
6
8
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
7
9
|
from ..api import ApiClient
|
|
8
10
|
from ..output import print_json, print_table
|
|
9
11
|
from ..sessions import resolve_session_id
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage annotations")
|
|
14
|
+
app = typer.Typer(help="Manage annotations", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
SessionOpt = Annotated[Optional[str], typer.Option("--session", help="Target a specific review session (advanced)")]
|
|
15
17
|
|
|
@@ -7,6 +7,8 @@ from typing import Annotated, Optional
|
|
|
7
7
|
import httpx
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
+
from .._group import AliasGroup
|
|
11
|
+
|
|
10
12
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
11
13
|
from ..api import ApiClient
|
|
12
14
|
from ..config import get_api_url, read_config, validate_api_url, write_config
|
|
@@ -148,7 +150,7 @@ def whoami(
|
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
|
|
151
|
-
auth_app = typer.Typer(help="Authentication commands")
|
|
153
|
+
auth_app = typer.Typer(help="Authentication commands", cls=AliasGroup, no_args_is_help=True)
|
|
152
154
|
auth_app.command("login")(login)
|
|
153
155
|
auth_app.command("logout")(logout)
|
|
154
156
|
auth_app.command("whoami")(whoami)
|
|
@@ -4,12 +4,14 @@ from typing import Annotated, Optional
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
+
from .._group import AliasGroup
|
|
8
|
+
|
|
7
9
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
8
10
|
from ..api import ApiClient
|
|
9
11
|
from ..output import print_json, print_table
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage S3 and Hugging Face buckets")
|
|
14
|
+
app = typer.Typer(help="Manage S3 and Hugging Face buckets", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@app.command("list")
|
|
@@ -6,12 +6,14 @@ from typing import Annotated, Optional
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from .._group import AliasGroup
|
|
10
|
+
|
|
9
11
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
10
12
|
from ..api import ApiClient
|
|
11
13
|
from ..output import print_error, print_json, print_table
|
|
12
14
|
from ..util import q
|
|
13
15
|
|
|
14
|
-
app = typer.Typer(help="Manage datasets")
|
|
16
|
+
app = typer.Typer(help="Manage datasets", cls=AliasGroup, no_args_is_help=True)
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def _truncate(v: object, n: int = 80) -> str:
|
|
@@ -22,6 +24,10 @@ def _truncate(v: object, n: int = 80) -> str:
|
|
|
22
24
|
@app.command("list")
|
|
23
25
|
def list_datasets(
|
|
24
26
|
workspace: Annotated[Optional[str], typer.Option("--workspace", help="Filter by workspace ID")] = None,
|
|
27
|
+
search: Annotated[
|
|
28
|
+
Optional[str],
|
|
29
|
+
typer.Option("--search", "-s", help="Filter by case-insensitive substring of name / HF repo"),
|
|
30
|
+
] = None,
|
|
25
31
|
json_out: JsonOpt = False,
|
|
26
32
|
profile: ProfileOpt = None,
|
|
27
33
|
api_url: ApiUrlOpt = None,
|
|
@@ -32,6 +38,15 @@ def list_datasets(
|
|
|
32
38
|
if workspace:
|
|
33
39
|
params["workspace_id"] = workspace
|
|
34
40
|
items = client.get("/v1/datasets", params=params).json()
|
|
41
|
+
if search:
|
|
42
|
+
s = search.lower()
|
|
43
|
+
items = [
|
|
44
|
+
d
|
|
45
|
+
for d in items
|
|
46
|
+
if s in (d.get("display_name") or "").lower()
|
|
47
|
+
or s in (d.get("name") or "").lower()
|
|
48
|
+
or s in (d.get("hf_repo_id") or "").lower()
|
|
49
|
+
]
|
|
35
50
|
print_table(
|
|
36
51
|
["ID", "Name", "Workspace", "Rows"],
|
|
37
52
|
[
|
|
@@ -4,12 +4,14 @@ from typing import Annotated, Optional
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
+
from .._group import AliasGroup
|
|
8
|
+
|
|
7
9
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
8
10
|
from ..api import ApiClient
|
|
9
11
|
from ..output import print_error, print_json, print_table
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage row edits")
|
|
14
|
+
app = typer.Typer(help="Manage row edits", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@app.command("list")
|
|
@@ -6,12 +6,14 @@ from typing import Annotated, List, Optional
|
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
|
+
from .._group import AliasGroup
|
|
10
|
+
|
|
9
11
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
10
12
|
from ..api import ApiClient
|
|
11
13
|
from ..output import print_error, print_json, print_table
|
|
12
14
|
from ..util import q
|
|
13
15
|
|
|
14
|
-
app = typer.Typer(help="Inspect and analyze eval datasets (accuracy, failure modes, samples)")
|
|
16
|
+
app = typer.Typer(help="Inspect and analyze eval datasets (accuracy, failure modes, samples)", cls=AliasGroup, no_args_is_help=True)
|
|
15
17
|
|
|
16
18
|
# Mirrors front/src/lib/eval-dataset.ts so `eval samples --search` searches the
|
|
17
19
|
# same prompt/response columns the eval views do.
|
|
@@ -3,13 +3,15 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
|
+
from .._group import AliasGroup
|
|
7
|
+
|
|
6
8
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
7
9
|
from ..api import ApiClient
|
|
8
10
|
from ..output import print_error, print_json, print_table
|
|
9
11
|
from ..sessions import resolve_session_id
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage highlights")
|
|
14
|
+
app = typer.Typer(help="Manage highlights", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
SessionOpt = Annotated[Optional[str], typer.Option("--session", help="Target a specific review session (advanced)")]
|
|
15
17
|
|
|
@@ -8,6 +8,15 @@ INSTRUCTIONS = r"""
|
|
|
8
8
|
CLI for the DataViewer platform. Gives agents and humans complete scriptable
|
|
9
9
|
control over workspaces, datasets, spec docs, annotations, and S3.
|
|
10
10
|
|
|
11
|
+
Running a group with no subcommand (e.g. `lql datasets`) lists its subcommands.
|
|
12
|
+
|
|
13
|
+
Shortcuts (for humans typing): group aliases (`ds`=datasets, `ws`=workspaces,
|
|
14
|
+
`ev`=eval, `ann`=annotations, `hl`=highlights, `rep`=reports, `bkt`=buckets,
|
|
15
|
+
`iss`=issues), verb aliases (`ls`=list, `rm`/`del`=delete, `new`/`mk`=create,
|
|
16
|
+
`info`=show), and unique prefixes (`lql datasets l` → list) all resolve.
|
|
17
|
+
AGENTS: use the full canonical names shown throughout this reference — they are
|
|
18
|
+
the stable contract; the aliases are conveniences layered on top.
|
|
19
|
+
|
|
11
20
|
## Authentication
|
|
12
21
|
|
|
13
22
|
lql login # Open browser → click Authorize → token stored automatically
|
|
@@ -43,7 +52,7 @@ Pagination: --limit N --offset N on list commands.
|
|
|
43
52
|
|
|
44
53
|
A workspace is the top-level container for datasets, spec docs, and members.
|
|
45
54
|
|
|
46
|
-
lql workspaces list
|
|
55
|
+
lql workspaces list [--search <text>] # --search filters by name / slug substring
|
|
47
56
|
lql workspaces create <name>
|
|
48
57
|
lql workspaces show <id>
|
|
49
58
|
lql workspaces update <id> --name <new-name>
|
|
@@ -54,7 +63,7 @@ A workspace is the top-level container for datasets, spec docs, and members.
|
|
|
54
63
|
|
|
55
64
|
## Datasets
|
|
56
65
|
|
|
57
|
-
lql datasets list [--workspace <id>]
|
|
66
|
+
lql datasets list [--workspace <id>] [--search <text>] # --search filters by name / HF repo substring
|
|
58
67
|
lql datasets show <id>
|
|
59
68
|
lql datasets create --workspace <id> --hf-repo <org/repo> [--name <display>] [--split <split>]
|
|
60
69
|
lql datasets create --workspace <id> --hf-bucket <org/bucket> --key <path-or-glob> [--name <display>]
|
|
@@ -94,10 +103,20 @@ Options: -c/--column (field(s) to treat as conversations; default auto-detect,
|
|
|
94
103
|
repeatable), -n/--limit (page size when paging a platform dataset), --offset
|
|
95
104
|
(start row index), --title, --hf, --split, --workspace, --profile, --api-url.
|
|
96
105
|
|
|
97
|
-
Navigation: two modes toggled with m
|
|
106
|
+
Navigation: two modes toggled with m — pager (one sample at a time; ←/→ or
|
|
98
107
|
n/b switch samples, ↑/↓/j/k scroll) and scroll (all samples; n/b jump between
|
|
99
108
|
them). q quits. Works over plain SSH with no browser or port-forward.
|
|
100
109
|
|
|
110
|
+
Media: images render inline in image-capable terminals (Kitty/Ghostty, iTerm2,
|
|
111
|
+
Sixel; placeholder otherwise) for both multimodal image segments and image-mode
|
|
112
|
+
columns; audio shows a ♪ line and `p` plays the current sample's clip via the
|
|
113
|
+
system player (afplay/open).
|
|
114
|
+
|
|
115
|
+
Copy: Tab/Shift+Tab move a highlight between blocks, `c` copies the focused
|
|
116
|
+
message/field, `Y` copies the whole sample as JSON — via OSC 52, reaching the
|
|
117
|
+
local clipboard over SSH where the terminal supports it (e.g. Ghostty/iTerm2;
|
|
118
|
+
not macOS Terminal).
|
|
119
|
+
|
|
101
120
|
## Evals
|
|
102
121
|
|
|
103
122
|
Eval datasets (evaluation-run output: each row a sample with a model 'response'
|
|
@@ -220,7 +239,9 @@ never goes stale.
|
|
|
220
239
|
|
|
221
240
|
### Discover workspaces and datasets
|
|
222
241
|
lql workspaces list --json
|
|
242
|
+
lql workspaces list --search <name> --json # find a workspace by name
|
|
223
243
|
lql datasets list --workspace <id> --json
|
|
244
|
+
lql datasets list --search <name> --json # find a dataset by name / HF repo
|
|
224
245
|
|
|
225
246
|
### Read dataset contents
|
|
226
247
|
lql datasets schema <id> --json
|
|
@@ -3,12 +3,14 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
|
+
from .._group import AliasGroup
|
|
7
|
+
|
|
6
8
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
7
9
|
from ..api import ApiClient
|
|
8
10
|
from ..output import print_json, print_table
|
|
9
11
|
from ..util import q
|
|
10
12
|
|
|
11
|
-
app = typer.Typer(help="Manage issues")
|
|
13
|
+
app = typer.Typer(help="Manage issues", cls=AliasGroup, no_args_is_help=True)
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@app.command("list")
|
|
@@ -35,6 +35,9 @@ _KNOWN_ROLES = {"user", "assistant", "system", "tool"}
|
|
|
35
35
|
_AUDIO_EXT_RE = re.compile(r"\.(wav|mp3|ogg|flac|m4a|aac|opus|webm)(\?|$)", re.I)
|
|
36
36
|
_AUDIO_KEYS = ["audio", "audio_url", "audio_data", "input_audio", "src", "url"]
|
|
37
37
|
|
|
38
|
+
_IMAGE_EXT_RE = re.compile(r"\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)", re.I)
|
|
39
|
+
_IMAGE_KEYS = ["image", "image_url", "image_data", "img", "src", "url"]
|
|
40
|
+
|
|
38
41
|
|
|
39
42
|
def _as_audio_url(v: object, key_hint: Optional[str] = None) -> Optional[str]:
|
|
40
43
|
if isinstance(v, str) and v:
|
|
@@ -74,6 +77,58 @@ def _segment_audio_url(seg: dict) -> Optional[str]:
|
|
|
74
77
|
return None
|
|
75
78
|
|
|
76
79
|
|
|
80
|
+
def _as_image_url(v: object, key_hint: Optional[str] = None) -> Optional[str]:
|
|
81
|
+
"""Mirror of _as_audio_url for images. Returns a data: URI or http(s) image
|
|
82
|
+
URL. Also handles HF Image structs ({bytes,path}) and {url}/{src} wrappers."""
|
|
83
|
+
if isinstance(v, str) and v:
|
|
84
|
+
if v.startswith("data:image/"):
|
|
85
|
+
return v
|
|
86
|
+
is_http = bool(re.match(r"^https?://", v, re.I))
|
|
87
|
+
if is_http and _IMAGE_EXT_RE.search(v):
|
|
88
|
+
return v
|
|
89
|
+
# Under an explicitly image-named key, accept an extensionless URL too
|
|
90
|
+
# (signed S3/HF/CDN links often have no .png/.jpg suffix).
|
|
91
|
+
if key_hint and "image" in key_hint.lower():
|
|
92
|
+
if is_http:
|
|
93
|
+
return v
|
|
94
|
+
if re.fullmatch(r"[A-Za-z0-9+/=\n]{100,}", v):
|
|
95
|
+
return f"data:image/png;base64,{v}"
|
|
96
|
+
return None
|
|
97
|
+
if isinstance(v, dict):
|
|
98
|
+
url = (v.get("url") if isinstance(v.get("url"), str) else None) or (
|
|
99
|
+
v.get("src") if isinstance(v.get("src"), str) else None
|
|
100
|
+
)
|
|
101
|
+
if url:
|
|
102
|
+
resolved = _as_image_url(url, key_hint) # keep the hint so {image_url:{url}} resolves
|
|
103
|
+
if resolved:
|
|
104
|
+
return resolved
|
|
105
|
+
# HF datasets Image struct: {"bytes": <base64-or-bytes>, "path": ...}
|
|
106
|
+
b = v.get("bytes")
|
|
107
|
+
if isinstance(b, str) and b:
|
|
108
|
+
return b if b.startswith("data:") else f"data:image/png;base64,{b}"
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _segment_image_url(seg: dict) -> Optional[str]:
|
|
113
|
+
for key in _IMAGE_KEYS:
|
|
114
|
+
got = _as_image_url(seg.get(key), key)
|
|
115
|
+
if got:
|
|
116
|
+
return got
|
|
117
|
+
t = seg.get("type") if isinstance(seg.get("type"), str) else ""
|
|
118
|
+
if "image" in t.lower():
|
|
119
|
+
for v in seg.values():
|
|
120
|
+
got = _as_image_url(v, "image")
|
|
121
|
+
if got:
|
|
122
|
+
return got
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_image_string(s: object) -> bool:
|
|
127
|
+
"""True for a plain value that is itself an image (data: URI or image URL) —
|
|
128
|
+
used to render top-level image-mode columns instead of a giant text blob."""
|
|
129
|
+
return isinstance(s, str) and (s.startswith("data:image/") or bool(re.match(r"^https?://", s, re.I) and _IMAGE_EXT_RE.search(s)))
|
|
130
|
+
|
|
131
|
+
|
|
77
132
|
def _is_structured_content(val: object) -> bool:
|
|
78
133
|
# Anchor recognizes text + audio segments; we also accept any explicitly
|
|
79
134
|
# typed segment (e.g. {type:"image_url", ...}) so vision traces render too.
|
|
@@ -128,8 +183,8 @@ def _is_conversation(val: object) -> bool:
|
|
|
128
183
|
def _flatten_structured(segs: List[dict]) -> str:
|
|
129
184
|
parts = []
|
|
130
185
|
for seg in segs:
|
|
131
|
-
if _segment_audio_url(seg):
|
|
132
|
-
continue # surfaced separately
|
|
186
|
+
if _segment_audio_url(seg) or _segment_image_url(seg):
|
|
187
|
+
continue # surfaced separately (audio player / inline image)
|
|
133
188
|
text = seg.get("text") if isinstance(seg.get("text"), str) else ""
|
|
134
189
|
t = seg.get("type")
|
|
135
190
|
if t and t != "text":
|
|
@@ -147,6 +202,7 @@ def _parse_messages(raw: List[dict]) -> List[dict]:
|
|
|
147
202
|
for msg in raw:
|
|
148
203
|
shape = _message_shape(msg)
|
|
149
204
|
audio = None
|
|
205
|
+
images = None
|
|
150
206
|
tool_calls = None
|
|
151
207
|
if shape == "from-value":
|
|
152
208
|
role, content = str(msg.get("from") or ""), str(msg.get("value") or "")
|
|
@@ -154,14 +210,16 @@ def _parse_messages(raw: List[dict]) -> List[dict]:
|
|
|
154
210
|
role = str(msg.get("role") or "")
|
|
155
211
|
segs = msg.get("content") or []
|
|
156
212
|
content = _flatten_structured(segs)
|
|
157
|
-
|
|
158
|
-
|
|
213
|
+
audio = [u for u in (_segment_audio_url(s) for s in segs) if u] or None
|
|
214
|
+
images = [u for u in (_segment_image_url(s) for s in segs) if u] or None
|
|
159
215
|
else:
|
|
160
216
|
role, content = str(msg.get("role") or ""), str(msg.get("content") or "")
|
|
161
217
|
if isinstance(msg.get("tool_calls"), list) and msg["tool_calls"]:
|
|
162
218
|
tool_calls = _normalize_tool_calls(msg["tool_calls"])
|
|
163
219
|
extras = {k: v for k, v in msg.items() if k not in _CONV_BASE_KEYS and v not in (None, "")}
|
|
164
|
-
out.append(
|
|
220
|
+
out.append(
|
|
221
|
+
{"role": role, "content": content, "audio": audio, "images": images, "tool_calls": tool_calls, "extras": extras or None}
|
|
222
|
+
)
|
|
165
223
|
return out
|
|
166
224
|
|
|
167
225
|
|
|
@@ -3,13 +3,15 @@ from typing import Annotated, Optional
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
|
+
from .._group import AliasGroup
|
|
7
|
+
|
|
6
8
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
7
9
|
from ..api import ApiClient
|
|
8
10
|
from ..output import print_json, print_table
|
|
9
11
|
from ..sessions import resolve_session_id
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage reports")
|
|
14
|
+
app = typer.Typer(help="Manage reports", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
SessionOpt = Annotated[Optional[str], typer.Option("--session", help="Target a specific review session (advanced)")]
|
|
15
17
|
|
|
@@ -5,10 +5,12 @@ from typing import Annotated, List, Optional
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
|
+
from .._group import AliasGroup
|
|
9
|
+
|
|
8
10
|
from .._opts import JsonOpt
|
|
9
11
|
from ..output import print_error, print_json
|
|
10
12
|
|
|
11
|
-
app = typer.Typer(help="Install the lql agent skill into Claude Code and Codex")
|
|
13
|
+
app = typer.Typer(help="Install the lql agent skill into Claude Code and Codex", cls=AliasGroup, no_args_is_help=True)
|
|
12
14
|
|
|
13
15
|
# Thin pointer skill. The body deliberately does NOT embed the full reference —
|
|
14
16
|
# it tells the agent to run `lql instructions`, which is always in sync with the
|
|
@@ -4,12 +4,14 @@ from typing import Annotated, Optional
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
+
from .._group import AliasGroup
|
|
8
|
+
|
|
7
9
|
from .._opts import ApiUrlOpt, JsonOpt, ProfileOpt
|
|
8
10
|
from ..api import ApiClient
|
|
9
11
|
from ..output import print_error, print_json
|
|
10
12
|
from ..util import q
|
|
11
13
|
|
|
12
|
-
app = typer.Typer(help="Manage workspace spec docs")
|
|
14
|
+
app = typer.Typer(help="Manage workspace spec docs", cls=AliasGroup, no_args_is_help=True)
|
|
13
15
|
|
|
14
16
|
WorkspaceOpt = Annotated[str, typer.Option("--workspace", help="Workspace ID")]
|
|
15
17
|
|