langfuse-agent-cli 0.3.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.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: langfuse-agent-cli
3
+ Version: 0.3.0
4
+ Summary: Agent-oriented CLI and skill for Langfuse traces, prompts, datasets and scores.
5
+ Author-email: Tarkeshwar Narayan <122815908+tns15june@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tns15june/langfuse-agent-cli
8
+ Project-URL: Repository, https://github.com/tns15june/langfuse-agent-cli
9
+ Project-URL: Issues, https://github.com/tns15june/langfuse-agent-cli/issues
10
+ Keywords: langfuse,llm,observability,agent,cli,skills
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Monitoring
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.28.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # langfuse-agent-cli (`lf`)
30
+
31
+ Command-line access to [Langfuse](https://langfuse.com) traces, prompts, datasets and scores, shaped
32
+ for AI coding agents: JSON by default (`--compact` trims it), failing traces with their error messages
33
+ in one command, a trace as a tree, and an agent skill that teaches the commands.
34
+
35
+ > **Status: prototype.** Langfuse ships an official CLI,
36
+ > [`@langfuse/cli`](https://github.com/langfuse/langfuse-cli), which covers the whole API, and official
37
+ > agent skills, [`langfuse/skills`](https://github.com/langfuse/skills). This project tries out
38
+ > agent-oriented features on the same API.
39
+ > [RFC.md](https://github.com/tns15june/langfuse-agent-cli/blob/main/RFC.md) describes which of them it
40
+ > proposes upstream.
41
+
42
+ ## Requirements
43
+
44
+ - Python 3.10 or newer
45
+ - Langfuse v4, self-hosted or Cloud. The trace commands read `/api/public/v2/observations`;
46
+ `lf doctor` reports if a server does not have it.
47
+ - Node.js 22.20 or newer, only for installing the agent skill
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pipx install langfuse-agent-cli # or: uv tool install langfuse-agent-cli
53
+ lf setup
54
+ ```
55
+
56
+ `lf setup` takes three steps and is safe to run again:
57
+
58
+ 1. **Credentials.** It keeps working credentials that it finds. When no profile is saved, or the
59
+ saved profile's keys are rejected, it asks for the host and the keys (the secret is not echoed)
60
+ and saves them. It never replaces saved keys when the server is merely unreachable, and keys from
61
+ `LANGFUSE_*` variables, `--env-file` or key flags, which take precedence over profiles, are left for
62
+ you to fix.
63
+ 2. **Agent skill.** It installs the `langfuse-agent-cli` skill with the
64
+ [skills CLI](https://github.com/vercel-labs/skills), which asks which coding agents to install it
65
+ for when it finds more than one. The skill comes from the release tag that matches your `lf`
66
+ version.
67
+ 3. **Check.** It runs `lf doctor`.
68
+
69
+ To have a coding agent install it, give the agent this prompt:
70
+
71
+ ```text
72
+ Install langfuse-agent-cli by following https://raw.githubusercontent.com/tns15june/langfuse-agent-cli/main/INSTALL.md
73
+ ```
74
+
75
+ The agent installs the CLI and runs `lf setup --yes`, which never prompts and installs the skill for
76
+ that agent. If no working credentials are configured, the agent asks you to run `lf setup` yourself,
77
+ which asks for the host and the keys.
78
+
79
+ To install the latest code from GitHub instead of PyPI:
80
+ `pipx install git+https://github.com/tns15june/langfuse-agent-cli`.
81
+
82
+ ### Upgrade and uninstall
83
+
84
+ ```bash
85
+ pipx upgrade langfuse-agent-cli && lf setup # lf setup also installs the matching skill
86
+ npx skills remove --global langfuse-agent-cli
87
+ pipx uninstall langfuse-agent-cli
88
+ ```
89
+
90
+ With uv, use `uv tool upgrade langfuse-agent-cli` and `uv tool uninstall langfuse-agent-cli`. Saved
91
+ profiles stay in `~/.config/langfuse/profiles.json` until you delete that file.
92
+
93
+ ## Credentials
94
+
95
+ `lf` takes the host and both keys from one source; the first match wins:
96
+
97
+ 1. `--env-file <path>`
98
+ 2. `--profile <name>` or `LANGFUSE_PROFILE`
99
+ 3. `LANGFUSE_PUBLIC_KEY` or `LANGFUSE_SECRET_KEY` (either one selects this source), with the host from
100
+ `LANGFUSE_BASE_URL`, then `LANGFUSE_HOST` (default `https://cloud.langfuse.com`)
101
+ 4. The saved current profile
102
+
103
+ `--host`, `--public-key` and `--secret-key` override single values; prefer environment variables or a
104
+ profile over passing keys on the command line, where they end up in shell history. `lf` reads a
105
+ `.env` file only when it is passed with `--env-file`.
106
+
107
+ To save another profile (the keys are prompted for, and the secret is not echoed):
108
+
109
+ ```bash
110
+ lf auth login --profile-name local --host http://localhost:3000 --default
111
+ lf doctor
112
+ ```
113
+
114
+ Profiles live in `~/.config/langfuse/profiles.json`, readable only by you (mode 0600).
115
+
116
+ ## Commands
117
+
118
+ Data commands print JSON by default and short lines with `-o text`; `doctor`, `setup`, `auth` and
119
+ `help-map` print text. Three options keep JSON small:
120
+
121
+ - `--compact` drops null and empty fields.
122
+ - `--agent-fields name,level` keeps only the listed fields of each row.
123
+ - `--output-summary` writes JSON output over 16 KB to a file and prints the file's path, size and keys
124
+ instead.
125
+
126
+ ```bash
127
+ # Failing traces, one entry per trace, with each error message
128
+ lf trace list --status error --limit 10 --compact
129
+
130
+ # One trace as a tree; add --io for short inputs and outputs
131
+ lf trace get <trace-id> --timeline
132
+
133
+ # A trace with its inputs and outputs; over 16 KB it goes to a file
134
+ lf trace get <trace-id> --io --output-summary
135
+
136
+ # The traces of a session, oldest first (the newest 1000)
137
+ lf trace session <session-id> --compact
138
+
139
+ # Recent traces, filtered
140
+ lf trace list --user-id user-42 --from-ts 2026-10-24T00:00:00Z --limit 20
141
+
142
+ # Prompts
143
+ lf prompt list -o text
144
+ lf prompt get support/greeter
145
+ lf prompt get welcome-bot --vars '{"user_name": "Alice", "plan": "pro"}' -o text
146
+ lf prompt push welcome-bot --text 'Hello {{user_name}}!' --labels staging
147
+
148
+ # Datasets and scores
149
+ lf dataset list
150
+ lf dataset get intent-classification-evals
151
+ lf score create --trace-id <trace-id> --name accuracy --value 1
152
+
153
+ # Discover commands and options
154
+ lf help-map
155
+ lf help "trace get"
156
+ ```
157
+
158
+ A timeline, from synthetic data:
159
+
160
+ ```text
161
+ Trace 33579e8e8ec602e878f0744ad53ac1fe: 3 observations, 1 error
162
+ ✓ AGENT support-agent-turn
163
+ ✓ GENERATION plan-step (model: gpt-4o-mini)
164
+ ✗ [ERROR] TOOL search_tool_call
165
+ Message: 503 Service Unavailable
166
+ ```
167
+
168
+ Observations with a measured duration show it in brackets, such as `[1.24s]`. Langfuse 4.70 reports
169
+ no duration for observations of zero length, like these synthetic ones.
170
+
171
+ Exit codes: `0` success, `1` API or configuration error, `2` invalid command. The reason for a failure is
172
+ on stderr, except that `doctor`, `setup --yes` and `auth status` print their whole report, failures
173
+ included, on stdout. `setup` also exits `1` when the skill was not installed.
174
+
175
+ ## API endpoints used
176
+
177
+ | Command | Endpoint |
178
+ | --- | --- |
179
+ | `trace list` | `GET /api/public/v2/observations?isRootObservation=true` |
180
+ | `trace list --status error` | `GET /api/public/v2/observations?level=ERROR`, grouped by trace |
181
+ | `trace get` | `GET /api/public/v2/observations?traceId=…`, all pages |
182
+ | `trace session` | `GET /api/public/v2/observations?sessionId=…&isRootObservation=true` |
183
+ | `prompt list`, `prompt get`, `prompt push` | `/api/public/v2/prompts` |
184
+ | `dataset list`, `dataset get` | `/api/public/v2/datasets`, `/api/public/dataset-items` |
185
+ | `score create` | `POST /api/public/scores` |
186
+ | `doctor` | `/api/public/health`, `/api/public/projects`, `/api/public/v2/observations?limit=1` |
187
+ | `auth login`, `auth status` | `/api/public/projects` |
188
+ | `setup` | `/api/public/projects`, then the `doctor` requests |
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ pip install -e ".[dev]"
194
+ ruff check .
195
+ pytest -q
196
+ ```
197
+
198
+ The unit tests replay responses recorded from Langfuse 4.70.0 (synthetic data). The live tests run
199
+ every command against a real instance, for example one started from Langfuse's official
200
+ `docker-compose.yml`:
201
+
202
+ ```bash
203
+ export LANGFUSE_BASE_URL=http://localhost:3000 LANGFUSE_PUBLIC_KEY=pk-lf-... LANGFUSE_SECRET_KEY=sk-lf-...
204
+ pip install langfuse
205
+ python scripts/seed_langfuse.py # synthetic prompts, a dataset and traces: use a throwaway project
206
+ LF_LIVE=1 pytest tests/test_live.py -v
207
+ ```
208
+
209
+ CI runs the unit tests on Python 3.10 to 3.13 and the live tests against the latest Langfuse release,
210
+ on every push and weekly.
211
+
212
+ ### Releasing
213
+
214
+ The Publish workflow uploads to PyPI with trusted publishing, so no token is stored; the PyPI project
215
+ lists `publish.yml` and the GitHub environment `pypi` as its trusted publisher. Bump `__version__` in
216
+ `langfuse_cli/__init__.py`, commit, then push that commit and its tag together:
217
+
218
+ ```bash
219
+ git tag v0.3.0 && git push --atomic origin main v0.3.0
220
+ ```
221
+
222
+ The workflow checks that the tag matches the version, runs the tests, builds and checks the
223
+ distributions, installs the wheel, and uploads the wheel and the source archive. `main` should never
224
+ carry a version without its tag, because `lf setup` and `lf doctor` point at the skill under that tag.
225
+
226
+ ## Prior art
227
+
228
+ The command map (`lf help-map`), the `--output-summary` option, `lf setup` and the agent install prompt
229
+ follow Atlassian's
230
+ [Teamwork Graph CLI](https://developer.atlassian.com/platform/teamwork-graph/twg-cli/getting-started/installation/),
231
+ whose `--select` option does what `lf`'s `--agent-fields` does.
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,23 @@
1
+ langfuse_agent_cli-0.3.0.dist-info/licenses/LICENSE,sha256=9nIkgumqkB1u89JeP4O9VdtrVyVLvXO8cvxDix2GEKY,1075
2
+ langfuse_cli/__init__.py,sha256=4tkLLiPD61IoWp3dkCTcboDjPGC_FUVinOGMs2Hopik,56
3
+ langfuse_cli/__main__.py,sha256=TdqB9UA_fmdjrh94qBuh5_OhVjYL9Y_R7-OIW8ebMa8,141
4
+ langfuse_cli/cli.py,sha256=ttXX0Hmzy8KDHpPM2_kzGQJkhPGD6AVJHyDw0QEN5pc,10169
5
+ langfuse_cli/client.py,sha256=e8PwSdSZgQ4vHp6iBrD3Tkr6aRkkhBZ_1WtWsIocK5Y,7881
6
+ langfuse_cli/config.py,sha256=6K0VCRUM3S7ys_JLJ3_Hv7vwAueVN0SwF2FjJUVGD3c,7024
7
+ langfuse_cli/commands/__init__.py,sha256=KmuWdd_HmO2m_9qdcN4tCNAkK6tMti7Lu3wHTTpUz5c,41
8
+ langfuse_cli/commands/auth.py,sha256=nM18xNiSTLMS13dpDN_H7Hl-C9fKFoXsTEj5GlHu34c,3617
9
+ langfuse_cli/commands/dataset.py,sha256=l3ZDVPyDtpSvuHiyirytdFAKCmeYtfBNl8DpKNTvJXQ,1419
10
+ langfuse_cli/commands/doctor.py,sha256=E8JNwJQQvFTdeUBtXfTNPNsM8rFzwEQeSIIIG1etPkY,3737
11
+ langfuse_cli/commands/help_map.py,sha256=LkRWx_pvRItttezTICSCVIbGMoOM0eRdwl7HN-UHQeI,6785
12
+ langfuse_cli/commands/prompt.py,sha256=8bIQL3aedHSDl36VjGFV_zO6dITbJIetfgkNaFdhpcM,4885
13
+ langfuse_cli/commands/score.py,sha256=iJJomKkxyTTVpMWcWQ8VHxedS5tazgtSzS3WuTKPFw8,722
14
+ langfuse_cli/commands/setup.py,sha256=k9IgLydHGEsI4f89vFvol7eYVxMHcHLsG2FVip-hLGg,5151
15
+ langfuse_cli/commands/trace.py,sha256=r8_Mfr_XnpLqudDh_Gu48ZJ8D7b_lAWqIryAQ6H1V9E,4244
16
+ langfuse_cli/formatters/__init__.py,sha256=hRVMMENxKY6y0hWKT6tpmDqY3y23qM3SY68DZiymIvs,701
17
+ langfuse_cli/formatters/agent.py,sha256=dj-jDNHxY06lEwX81v-7P8XAaLMm3YCrX3q5PBhvN3A,3900
18
+ langfuse_cli/formatters/timeline.py,sha256=kntmxvpwMV027927EVY4Eq5mrPJrtUXMoivWB2sCO_I,2106
19
+ langfuse_agent_cli-0.3.0.dist-info/METADATA,sha256=eOnsEdwtjdG_54jnGB9e2BqgHjzUWpTxLR_DZHOLc6Y,9546
20
+ langfuse_agent_cli-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ langfuse_agent_cli-0.3.0.dist-info/entry_points.txt,sha256=izFtfLvAQLMy90cHykh16bjzZ9xW7Rey09tCcOMX6Kk,45
22
+ langfuse_agent_cli-0.3.0.dist-info/top_level.txt,sha256=-VrFPWF0TnKyWSILzdVTq6U8oz6ydPVE4PM9Ev8Ozyc,13
23
+ langfuse_agent_cli-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lf = langfuse_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tarkeshwar Narayan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ langfuse_cli
@@ -0,0 +1,2 @@
1
+ """Langfuse Agent CLI package."""
2
+ __version__ = "0.3.0"
@@ -0,0 +1,7 @@
1
+ """Entry point for python -m langfuse_cli."""
2
+ import sys
3
+
4
+ from langfuse_cli.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
langfuse_cli/cli.py ADDED
@@ -0,0 +1,197 @@
1
+ """Main CLI entrypoint for Langfuse Agent CLI."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from typing import List, Optional
7
+
8
+ from langfuse_cli import __version__
9
+ from langfuse_cli.client import LangfuseAPIError
10
+ from langfuse_cli.commands.auth import handle_auth
11
+ from langfuse_cli.commands.dataset import handle_dataset
12
+ from langfuse_cli.commands.doctor import handle_doctor
13
+ from langfuse_cli.commands.help_map import handle_help_describe, handle_help_map
14
+ from langfuse_cli.commands.prompt import handle_prompt
15
+ from langfuse_cli.commands.score import handle_score
16
+ from langfuse_cli.commands.setup import handle_setup
17
+ from langfuse_cli.commands.trace import handle_trace
18
+ from langfuse_cli.config import ConfigError
19
+ from langfuse_cli.formatters import clean
20
+
21
+ # Common options use argparse.SUPPRESS so a subcommand's default never overwrites a value given
22
+ # before the subcommand; these defaults are applied once after parsing instead.
23
+ COMMON_DEFAULTS = {
24
+ "profile": None,
25
+ "host": None,
26
+ "public_key": None,
27
+ "secret_key": None,
28
+ "env_file": None,
29
+ "output": "json",
30
+ "compact": False,
31
+ "agent_fields": None,
32
+ "output_summary": False,
33
+ }
34
+
35
+
36
+ class _SafeStream:
37
+ """Wraps stdout/stderr so nothing a command prints, server data included, can drive the terminal."""
38
+
39
+ def __init__(self, stream):
40
+ self._stream = stream
41
+
42
+ def write(self, text: str) -> int:
43
+ return self._stream.write(clean(text))
44
+
45
+ def __getattr__(self, name):
46
+ return getattr(self._stream, name)
47
+
48
+
49
+ def create_parser() -> argparse.ArgumentParser:
50
+ common_parser = argparse.ArgumentParser(add_help=False)
51
+ common = common_parser.add_argument
52
+ unset = argparse.SUPPRESS # defaults come from COMMON_DEFAULTS
53
+ common("-p", "--profile", default=unset, help="Saved profile to use (see 'lf auth list')")
54
+ common("--host", default=unset, help="Langfuse host, e.g. http://localhost:3000")
55
+ common("--public-key", default=unset, help="Langfuse public key (pk-lf-...)")
56
+ common("--secret-key", default=unset, help="Langfuse secret key (sk-lf-...); prefer env vars or a profile")
57
+ common("--env-file", default=unset, help="Read LANGFUSE_* credentials from this .env file")
58
+ common("-o", "--output", choices=["json", "text"], default=unset, help="Output format (default: json)")
59
+ common("--compact", action="store_true", default=unset, help="Drop null and empty fields from JSON output")
60
+ common("--agent-fields", default=unset, help="Comma-separated field paths to keep in JSON output")
61
+ common("--output-summary", action="store_true", default=unset,
62
+ help="With JSON output, write output over 16 KB to a file and print its path, size and keys instead")
63
+
64
+ parser = argparse.ArgumentParser(
65
+ prog="lf",
66
+ description="Agent-oriented CLI for Langfuse traces, prompts, datasets and scores.",
67
+ parents=[common_parser],
68
+ )
69
+ parser.add_argument("-v", "--version", action="version", version=f"lf {__version__}")
70
+
71
+ subparsers = parser.add_subparsers(dest="command", help="Available command families")
72
+
73
+ # 1. Auth command
74
+ p_auth = subparsers.add_parser("auth", parents=[common_parser], help="Manage authentication profiles")
75
+ auth_sub = p_auth.add_subparsers(dest="auth_action", required=True)
76
+ p_auth_login = auth_sub.add_parser("login", parents=[common_parser], help="Save a profile")
77
+ p_auth_login.add_argument("--profile-name", help="Profile name (default: default)")
78
+ p_auth_login.add_argument("--default", action="store_true", help="Set as default active profile")
79
+
80
+ p_auth_switch = auth_sub.add_parser("switch", parents=[common_parser], help="Switch active profile")
81
+ p_auth_switch.add_argument("target_profile", help="Profile name to activate")
82
+
83
+ auth_sub.add_parser("list", parents=[common_parser], help="List configured profiles")
84
+ auth_sub.add_parser("status", parents=[common_parser], help="Check current profile status and connectivity")
85
+
86
+ # 2. Doctor and setup
87
+ subparsers.add_parser("doctor", parents=[common_parser], help="Check credentials, server and the agent skill")
88
+ p_setup = subparsers.add_parser("setup", parents=[common_parser],
89
+ help="Log in, install the agent skill and run doctor")
90
+ p_setup.add_argument("-y", "--yes", action="store_true",
91
+ help="Never prompt: skip login when no working credentials are found and install the skill "
92
+ "without asking")
93
+
94
+ # 3. Prompt command
95
+ p_prompt = subparsers.add_parser("prompt", parents=[common_parser], help="Inspect and manage prompt versions")
96
+ prompt_sub = p_prompt.add_subparsers(dest="prompt_action", required=True)
97
+
98
+ p_prompt_list = prompt_sub.add_parser("list", parents=[common_parser], help="List registered prompts")
99
+ p_prompt_list.add_argument("--tag", help="Filter prompts by tag")
100
+ p_prompt_list.add_argument("--limit", type=int, default=20, help="Maximum prompts to return")
101
+
102
+ p_prompt_get = prompt_sub.add_parser("get", parents=[common_parser], help="Fetch a prompt by name")
103
+ p_prompt_get.add_argument("name", help="Prompt name in Langfuse")
104
+ p_prompt_get.add_argument("--version", type=int, help="Prompt version number")
105
+ p_prompt_get.add_argument("--label", help="Prompt label (e.g. production, staging)")
106
+ p_prompt_get.add_argument("--vars", help="JSON dictionary of variables to compile into {{mustache}} template")
107
+
108
+ p_prompt_push = prompt_sub.add_parser("push", parents=[common_parser], help="Publish a new prompt version")
109
+ p_prompt_push.add_argument("name", help="Prompt name")
110
+ p_prompt_push.add_argument("--file", help="Path to file containing prompt text or chat JSON")
111
+ p_prompt_push.add_argument("--text", help="Raw prompt string")
112
+ p_prompt_push.add_argument("--type", choices=["text", "chat"], default="text", help="Prompt format type")
113
+ p_prompt_push.add_argument("--labels", help="Comma-separated labels (e.g. staging,v2)")
114
+ p_prompt_push.add_argument("--tags", help="Comma-separated tags")
115
+
116
+ # 4. Trace command
117
+ p_trace = subparsers.add_parser("trace", parents=[common_parser], help="Observability and trace inspection")
118
+ trace_sub = p_trace.add_subparsers(dest="trace_action", required=True)
119
+
120
+ p_trace_list = trace_sub.add_parser("list", parents=[common_parser], help="List recent traces")
121
+ p_trace_list.add_argument("--session-id", help="Filter by session ID")
122
+ p_trace_list.add_argument("--user-id", help="Filter by user ID")
123
+ p_trace_list.add_argument("--status", choices=["all", "error"], default="all", help="Filter traces (e.g. error)")
124
+ p_trace_list.add_argument("--limit", type=int, default=20, help="Maximum traces to return")
125
+ p_trace_list.add_argument("--from-ts", help="Start timestamp ISO8601")
126
+ p_trace_list.add_argument("--to-ts", help="End timestamp ISO8601")
127
+
128
+ p_trace_get = trace_sub.add_parser("get", parents=[common_parser], help="Get trace details")
129
+ p_trace_get.add_argument("trace_id", help="Trace ID")
130
+ p_trace_get.add_argument("--timeline", action="store_true", help="Display visual timeline tree")
131
+ p_trace_get.add_argument("--io", action="store_true", help="Include observation inputs and outputs (large)")
132
+
133
+ p_trace_session = trace_sub.add_parser("session", parents=[common_parser], help="Get traces for an entire session")
134
+ p_trace_session.add_argument("session_id", help="Session ID")
135
+
136
+ # 5. Dataset command
137
+ p_dataset = subparsers.add_parser("dataset", parents=[common_parser], help="Manage datasets and test suites")
138
+ dataset_sub = p_dataset.add_subparsers(dest="dataset_action", required=True)
139
+
140
+ dataset_sub.add_parser("list", parents=[common_parser], help="List datasets")
141
+ p_dataset_get = dataset_sub.add_parser("get", parents=[common_parser], help="Get dataset items")
142
+ p_dataset_get.add_argument("name", help="Dataset name")
143
+
144
+
145
+ # 6. Score command
146
+ p_score = subparsers.add_parser("score", parents=[common_parser], help="Record evaluation scores")
147
+ score_sub = p_score.add_subparsers(dest="score_action", required=True)
148
+ p_score_create = score_sub.add_parser("create", parents=[common_parser], help="Create a score on a trace")
149
+ p_score_create.add_argument("--trace-id", required=True, help="Target trace ID")
150
+ p_score_create.add_argument("--name", required=True, help="Metric name (e.g. accuracy, relevance)")
151
+ p_score_create.add_argument("--value", required=True, type=float, help="Numeric value (e.g. 1.0, 0.8)")
152
+ p_score_create.add_argument("--comment", help="Optional comment explaining score")
153
+ p_score_create.add_argument("--observation-id", help="Optional observation ID within the trace")
154
+
155
+ # 7. Help map and progressive disclosure
156
+ subparsers.add_parser("help-map", parents=[common_parser],
157
+ help="Compact command map for agents (* marks commands that write)")
158
+ p_help = subparsers.add_parser("help", parents=[common_parser], help="Describe command details")
159
+ p_help.add_argument("target_command", nargs="+", help="Command to describe (e.g. 'prompt get')")
160
+
161
+ return parser
162
+
163
+
164
+ def main(argv: Optional[List[str]] = None) -> int:
165
+ parser = create_parser()
166
+ args = parser.parse_args(argv)
167
+
168
+ if not args.command:
169
+ parser.print_help()
170
+ return 0
171
+ for key, value in COMMON_DEFAULTS.items():
172
+ vars(args).setdefault(key, value)
173
+
174
+ handlers = {
175
+ "auth": handle_auth,
176
+ "doctor": handle_doctor,
177
+ "setup": handle_setup,
178
+ "prompt": handle_prompt,
179
+ "trace": handle_trace,
180
+ "dataset": handle_dataset,
181
+ "score": handle_score,
182
+ "help-map": handle_help_map,
183
+ "help": handle_help_describe,
184
+ }
185
+ streams = sys.stdout, sys.stderr
186
+ sys.stdout, sys.stderr = _SafeStream(sys.stdout), _SafeStream(sys.stderr)
187
+ try:
188
+ return handlers[args.command](args)
189
+ except (LangfuseAPIError, ConfigError, OSError) as e: # OSError: --file, the profiles file
190
+ print(f"Error: {e}", file=sys.stderr)
191
+ return 1
192
+ finally:
193
+ sys.stdout, sys.stderr = streams
194
+
195
+
196
+ if __name__ == "__main__":
197
+ sys.exit(main())