fruxon 0.5.2__tar.gz → 0.5.4__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.
- {fruxon-0.5.2 → fruxon-0.5.4}/PKG-INFO +2 -1
- {fruxon-0.5.2 → fruxon-0.5.4}/pyproject.toml +6 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/_version.py +2 -2
- fruxon-0.5.4/src/fruxon/cli/__init__.py +236 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/agents.py +134 -26
- fruxon-0.5.4/src/fruxon/cli/auth.py +369 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/chat.py +24 -153
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/doctor.py +47 -4
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/run.py +485 -307
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/trace.py +24 -12
- fruxon-0.5.4/src/fruxon/cli_auth.py +183 -0
- fruxon-0.5.4/src/fruxon/credentials.py +322 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/doctor.py +15 -6
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/ui.py +118 -5
- fruxon-0.5.4/tests/conftest.py +51 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_cli.py +558 -51
- fruxon-0.5.4/tests/test_credentials.py +253 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_ui.py +98 -0
- fruxon-0.5.2/src/fruxon/cli/__init__.py +0 -120
- fruxon-0.5.2/src/fruxon/cli/auth.py +0 -239
- fruxon-0.5.2/src/fruxon/credentials.py +0 -160
- fruxon-0.5.2/tests/test_credentials.py +0 -127
- {fruxon-0.5.2 → fruxon-0.5.4}/.gitignore +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/HISTORY.md +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/LICENSE +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/README.md +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/__init__.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/__main__.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/config.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/cli/export.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/exceptions.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/export.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/fruxon.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/models.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/output.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/params.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/src/fruxon/update_check.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/__init__.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_client.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_doctor.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_export.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_fruxon.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_output.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_params.py +0 -0
- {fruxon-0.5.2 → fruxon-0.5.4}/tests/test_update_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
|
|
5
5
|
Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: keyring>=24.0
|
|
21
22
|
Requires-Dist: typer>=0.13
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: coverage; extra == 'test'
|
|
@@ -69,6 +69,12 @@ dependencies = [
|
|
|
69
69
|
# ``--help`` invocation when paired with click 8.2+, which other
|
|
70
70
|
# packages routinely pull in. Floor here protects users from that drift.
|
|
71
71
|
"typer>=0.13",
|
|
72
|
+
# OS-keychain backed credential storage for ``fruxon login`` — macOS
|
|
73
|
+
# Keychain, libsecret/KWallet on Linux, Credential Manager on Windows.
|
|
74
|
+
# We use it via thin shims around the same shape we already write to
|
|
75
|
+
# ``~/.fruxon/credentials`` so callers behind a corp policy that
|
|
76
|
+
# disables the keyring fall back transparently to the file path.
|
|
77
|
+
"keyring>=24.0",
|
|
72
78
|
]
|
|
73
79
|
requires-python = ">= 3.10"
|
|
74
80
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
21
|
+
__version__ = version = '0.5.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 5, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Fruxon CLI · entry point.
|
|
2
|
+
|
|
3
|
+
The Typer ``app`` is the singleton every command registers against. Each
|
|
4
|
+
command lives in its own module under ``fruxon.cli.*`` and imports the
|
|
5
|
+
shared ``app`` from here. Importing those modules at package load is what
|
|
6
|
+
binds their ``@app.command()`` decorators to ``app``.
|
|
7
|
+
|
|
8
|
+
The packaging entry point points at ``fruxon.cli:app`` (see
|
|
9
|
+
``pyproject.toml`` ``[project.scripts]``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import atexit
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Annotated
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from typer.core import TyperGroup
|
|
21
|
+
|
|
22
|
+
from fruxon import credentials
|
|
23
|
+
from fruxon.ui import (
|
|
24
|
+
dashboard_url,
|
|
25
|
+
get_version,
|
|
26
|
+
is_agent_mode,
|
|
27
|
+
maybe_print_update_notice,
|
|
28
|
+
print_banner,
|
|
29
|
+
print_welcome,
|
|
30
|
+
say_err,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _FruxonTyperGroup(TyperGroup):
|
|
35
|
+
"""Branded replacement for Typer's stock ``no such command`` error.
|
|
36
|
+
|
|
37
|
+
Click's default renders the failure as a bordered ASCII box that
|
|
38
|
+
looks nothing like the rest of Fruxon's UI:
|
|
39
|
+
|
|
40
|
+
╭─ Error ──────────────────────────────────╮
|
|
41
|
+
│ No such command 'foobar'. │
|
|
42
|
+
╰──────────────────────────────────────────╯
|
|
43
|
+
|
|
44
|
+
We override ``get_command`` to catch the miss before Click raises,
|
|
45
|
+
print the Fruxon-styled ``✗`` line + a "did you mean?" suggestion
|
|
46
|
+
via :mod:`difflib`, then exit. Same exit code (2 — Click's
|
|
47
|
+
``UsageError.exit_code``) so scripts checking ``$?`` see the same
|
|
48
|
+
signal.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def get_command(self, ctx, cmd_name):
|
|
52
|
+
cmd = super().get_command(ctx, cmd_name)
|
|
53
|
+
if cmd is not None:
|
|
54
|
+
return cmd
|
|
55
|
+
|
|
56
|
+
# Branded error path. Build a "did you mean?" suggestion from
|
|
57
|
+
# the registered subcommand names — same difflib pattern we use
|
|
58
|
+
# for agent IDs so the UX feels consistent.
|
|
59
|
+
import difflib
|
|
60
|
+
|
|
61
|
+
candidates = list(self.commands.keys())
|
|
62
|
+
suggestions = difflib.get_close_matches(cmd_name, candidates, n=1, cutoff=0.6)
|
|
63
|
+
if suggestions:
|
|
64
|
+
hint = (
|
|
65
|
+
f"Did you mean [bold]fruxon {suggestions[0]}[/bold]? "
|
|
66
|
+
"Run [bold]fruxon --help[/bold] for the full command list."
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
hint = "Run [bold]fruxon --help[/bold] for the full command list."
|
|
70
|
+
say_err(f"Unknown command [bold]{cmd_name}[/bold].", hint=hint)
|
|
71
|
+
ctx.exit(2)
|
|
72
|
+
return None # unreachable but keeps the type checker happy
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Print the "newer fruxon is available" notice (if any) at the tail of
|
|
76
|
+
# every invocation. Registered at CLI load so it fires regardless of
|
|
77
|
+
# which subcommand ran, including --help and error exits. The notifier
|
|
78
|
+
# already swallows every exception internally — atexit handlers must
|
|
79
|
+
# never raise, so this is the right place.
|
|
80
|
+
atexit.register(maybe_print_update_notice)
|
|
81
|
+
|
|
82
|
+
app = typer.Typer(
|
|
83
|
+
help="Fruxon CLI · tools for working with the Fruxon platform.",
|
|
84
|
+
pretty_exceptions_enable=False,
|
|
85
|
+
cls=_FruxonTyperGroup,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.callback(invoke_without_command=True)
|
|
90
|
+
def main(
|
|
91
|
+
ctx: typer.Context,
|
|
92
|
+
version: Annotated[
|
|
93
|
+
bool,
|
|
94
|
+
typer.Option("--version", "-V", help="Print the CLI version and exit."),
|
|
95
|
+
] = False,
|
|
96
|
+
):
|
|
97
|
+
"""Fruxon CLI · agent execution, export, and platform tooling.
|
|
98
|
+
|
|
99
|
+
Credentials are resolved in this order (first non-empty wins):
|
|
100
|
+
1. Explicit flags (--api-key / --org / --base-url)
|
|
101
|
+
2. Environment variables (FRUXON_API_KEY / FRUXON_ORG / FRUXON_BASE_URL)
|
|
102
|
+
3. The credentials file managed by `fruxon login` (~/.fruxon/credentials)
|
|
103
|
+
|
|
104
|
+
Environment:
|
|
105
|
+
FRUXON_API_KEY Default API key for `fruxon run`.
|
|
106
|
+
FRUXON_ORG Default organization.
|
|
107
|
+
FRUXON_BASE_URL Override the API base URL (staging / self-hosted).
|
|
108
|
+
FRUXON_CONFIG_DIR Override the credentials directory (default ~/.fruxon).
|
|
109
|
+
FRUXON_DASHBOARD_URL Where `fruxon login` points the browser.
|
|
110
|
+
FRUXON_GLYPH Override the brand glyph (default: ✻).
|
|
111
|
+
FRUXON_BRAND_COLOR Override the brand color (e.g. `#7c5cff`, `cyan`).
|
|
112
|
+
FRUXON_NO_BANNER=1 Suppress all branding chrome.
|
|
113
|
+
FRUXON_AGENT_MODE=1 Optimize for AI-agent / script consumption — no
|
|
114
|
+
banner, JSON manifest on bare invocation. Also
|
|
115
|
+
auto-detected from CLAUDECODE=1 / CI=1.
|
|
116
|
+
NO_COLOR=1 Standard convention — disables all color output.
|
|
117
|
+
"""
|
|
118
|
+
if version:
|
|
119
|
+
print_banner()
|
|
120
|
+
raise typer.Exit()
|
|
121
|
+
|
|
122
|
+
if ctx.invoked_subcommand is not None:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Bare ``fruxon`` — the highest-leverage moment in the whole UX.
|
|
126
|
+
# First-run users decide whether to keep going based on what
|
|
127
|
+
# appears here; AI agents pattern-match on it to decide how to
|
|
128
|
+
# use the tool. We serve two surfaces from the same entrypoint:
|
|
129
|
+
#
|
|
130
|
+
# * Agent mode (Claude Code, CI, ``FRUXON_AGENT_MODE=1``) →
|
|
131
|
+
# single-line JSON manifest on stdout describing state +
|
|
132
|
+
# next-step commands. No banner, no chrome. An LLM reads
|
|
133
|
+
# this once and is fluent in Fruxon.
|
|
134
|
+
# * Human mode → branded welcome with state-aware CTAs.
|
|
135
|
+
# Branching on signed-in vs not surfaces the right next door:
|
|
136
|
+
# first-time users see "sign in" before everything else;
|
|
137
|
+
# signed-in users see the productive paths instead.
|
|
138
|
+
creds = credentials.load()
|
|
139
|
+
signed_in = bool(credentials.resolve_api_key(None))
|
|
140
|
+
|
|
141
|
+
if is_agent_mode():
|
|
142
|
+
_print_agent_manifest(creds=creds, signed_in=signed_in)
|
|
143
|
+
raise typer.Exit()
|
|
144
|
+
|
|
145
|
+
_print_human_welcome(creds=creds, signed_in=signed_in)
|
|
146
|
+
raise typer.Exit()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _print_human_welcome(*, creds: credentials.StoredCredentials, signed_in: bool) -> None:
|
|
150
|
+
"""Branded welcome for a person sitting at the terminal.
|
|
151
|
+
|
|
152
|
+
Tone target: Claude Code's startup card — one tagline, two
|
|
153
|
+
state-aware CTAs, one universal escape hatch (``--help``).
|
|
154
|
+
Deliberately NOT dumping the Typer commands table — that's
|
|
155
|
+
a wall of duplicated info one keystroke away via ``--help``.
|
|
156
|
+
"""
|
|
157
|
+
tagline = "Run, build, and orchestrate AI agents from your terminal."
|
|
158
|
+
|
|
159
|
+
lines: list[str] = [tagline, ""]
|
|
160
|
+
if signed_in:
|
|
161
|
+
org_label = f"org [bold]{creds.org}[/bold]" if creds.org else "no default org"
|
|
162
|
+
lines.append(f"[green]✓[/green] Signed in · {org_label}")
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append("Try: [bold]fruxon agents list[/bold] — browse what's deployed")
|
|
165
|
+
lines.append(" [bold]fruxon run[/bold] <agent> — execute one")
|
|
166
|
+
lines.append(" [bold]fruxon chat[/bold] <agent> — open an interactive REPL")
|
|
167
|
+
else:
|
|
168
|
+
lines.append("[yellow]›[/yellow] Not signed in")
|
|
169
|
+
lines.append("")
|
|
170
|
+
lines.append("Sign in: [bold]fruxon login[/bold]")
|
|
171
|
+
lines.append(f"No account? Visit [link]{dashboard_url()}[/link]")
|
|
172
|
+
|
|
173
|
+
lines.append("")
|
|
174
|
+
lines.append("All commands: [bold]fruxon --help[/bold]")
|
|
175
|
+
lines.append("Diagnostics: [bold]fruxon doctor[/bold]")
|
|
176
|
+
|
|
177
|
+
print_welcome(tips=lines)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _print_agent_manifest(*, creds: credentials.StoredCredentials, signed_in: bool) -> None:
|
|
181
|
+
"""One-line JSON manifest for AI agents and scripts.
|
|
182
|
+
|
|
183
|
+
The shape is intentionally tiny and stable: name, version,
|
|
184
|
+
auth state, and a ``next`` object pointing at the commands an
|
|
185
|
+
LLM should reach for. The goal is "an LLM reads this once and
|
|
186
|
+
knows what to do" — not a full schema dump (that's the future
|
|
187
|
+
``fruxon describe``).
|
|
188
|
+
|
|
189
|
+
Stdout, not stderr — agents expect their parseable signal on
|
|
190
|
+
stdout. JSON one-liner so it composes cleanly with ``| jq``.
|
|
191
|
+
"""
|
|
192
|
+
manifest = {
|
|
193
|
+
"name": "fruxon",
|
|
194
|
+
"version": get_version(),
|
|
195
|
+
"signed_in": signed_in,
|
|
196
|
+
"org": creds.org if signed_in else None,
|
|
197
|
+
"next": (
|
|
198
|
+
{
|
|
199
|
+
"agents": "fruxon agents list --output json",
|
|
200
|
+
"run": "fruxon run <agent> -p key=value --output json",
|
|
201
|
+
"chat": "fruxon chat <agent>",
|
|
202
|
+
"trace": "fruxon trace <agent> <record-id> --output json",
|
|
203
|
+
"help": "fruxon --help",
|
|
204
|
+
}
|
|
205
|
+
if signed_in
|
|
206
|
+
else {
|
|
207
|
+
"login": "fruxon login --api-key <KEY>",
|
|
208
|
+
"dashboard": dashboard_url(),
|
|
209
|
+
"help": "fruxon --help",
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
sys.stdout.write(json.dumps(manifest) + "\n")
|
|
214
|
+
sys.stdout.flush()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
# Command registration
|
|
219
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
#
|
|
221
|
+
# Importing the command modules below executes their ``@app.command()`` and
|
|
222
|
+
# ``app.add_typer(...)`` calls, wiring every subcommand into the singleton
|
|
223
|
+
# above. The order doesn't matter for correctness — only for the order they
|
|
224
|
+
# show up in ``fruxon --help``.
|
|
225
|
+
|
|
226
|
+
from fruxon.cli import agents as _agents # noqa: E402, F401
|
|
227
|
+
from fruxon.cli import auth as _auth # noqa: E402, F401
|
|
228
|
+
from fruxon.cli import chat as _chat # noqa: E402, F401
|
|
229
|
+
from fruxon.cli import config as _config # noqa: E402, F401
|
|
230
|
+
from fruxon.cli import doctor as _doctor # noqa: E402, F401
|
|
231
|
+
from fruxon.cli import export as _export # noqa: E402, F401
|
|
232
|
+
from fruxon.cli import run as _run # noqa: E402, F401
|
|
233
|
+
from fruxon.cli import trace as _trace # noqa: E402, F401
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
app()
|
|
@@ -18,7 +18,15 @@ from fruxon import credentials
|
|
|
18
18
|
from fruxon.cli import app
|
|
19
19
|
from fruxon.exceptions import FruxonError
|
|
20
20
|
from fruxon.fruxon import Agent, FruxonClient
|
|
21
|
-
from fruxon.ui import
|
|
21
|
+
from fruxon.ui import (
|
|
22
|
+
dashboard_url,
|
|
23
|
+
print_banner,
|
|
24
|
+
resolve_output_format,
|
|
25
|
+
say_err,
|
|
26
|
+
say_info,
|
|
27
|
+
say_ok,
|
|
28
|
+
stderr,
|
|
29
|
+
)
|
|
22
30
|
|
|
23
31
|
agents_app = typer.Typer(
|
|
24
32
|
help="Browse and inspect agents in your org.",
|
|
@@ -73,13 +81,17 @@ def agents_list(
|
|
|
73
81
|
] = None,
|
|
74
82
|
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
75
83
|
output: Annotated[
|
|
76
|
-
str,
|
|
84
|
+
str | None,
|
|
77
85
|
typer.Option(
|
|
78
86
|
"--output",
|
|
79
87
|
"-o",
|
|
80
|
-
help=
|
|
88
|
+
help=(
|
|
89
|
+
"Output format: table (default for humans), json (machines, default in "
|
|
90
|
+
"agent mode), id (one ID per line, pipe-safe). Under FRUXON_AGENT_MODE / "
|
|
91
|
+
"CLAUDECODE / CI the implicit default flips to json."
|
|
92
|
+
),
|
|
81
93
|
),
|
|
82
|
-
] =
|
|
94
|
+
] = None,
|
|
83
95
|
disabled: Annotated[
|
|
84
96
|
bool,
|
|
85
97
|
typer.Option(
|
|
@@ -99,6 +111,7 @@ def agents_list(
|
|
|
99
111
|
fruxon agents list --output json
|
|
100
112
|
fruxon agents list --output id | xargs -n1 fruxon run
|
|
101
113
|
"""
|
|
114
|
+
output = resolve_output_format(output, human_default="table", agent_default="json")
|
|
102
115
|
if output not in {"table", "json", "id"}:
|
|
103
116
|
say_err(
|
|
104
117
|
f"Unknown output format: [bold]{output}[/bold]",
|
|
@@ -120,16 +133,44 @@ def agents_list(
|
|
|
120
133
|
say_err(str(e), hint=_hint_for_list_error(e))
|
|
121
134
|
raise typer.Exit(code=1) from e
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
# Distinguish the two "empty" cases so the hint actually fits the
|
|
137
|
+
# situation. A first-time user with zero agents in their org needs
|
|
138
|
+
# to be pointed at the dashboard; a user whose agents are all
|
|
139
|
+
# disabled needs to know ``--all`` exists. Same flat "No agents
|
|
140
|
+
# to show" message used to swallow both — useless in both cases.
|
|
141
|
+
all_agents = agents
|
|
142
|
+
visible_agents = all_agents if disabled else [a for a in all_agents if a.enabled]
|
|
143
|
+
|
|
144
|
+
if not visible_agents:
|
|
145
|
+
if not all_agents:
|
|
146
|
+
# Truly empty org — onboarding moment, not a filter quirk.
|
|
147
|
+
# JSON callers get an empty array (already returned above
|
|
148
|
+
# in the output branches below) so this say_info is purely
|
|
149
|
+
# for the human-facing path.
|
|
150
|
+
if output == "json":
|
|
151
|
+
print("[]")
|
|
152
|
+
return
|
|
153
|
+
if output == "id":
|
|
154
|
+
return
|
|
155
|
+
say_info(
|
|
156
|
+
"No agents deployed in this org yet.",
|
|
157
|
+
hint=f"Create one in the dashboard: [link]{dashboard_url()}[/link]",
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
# Some agents exist but every one is disabled.
|
|
161
|
+
if output == "json":
|
|
162
|
+
print("[]")
|
|
163
|
+
return
|
|
164
|
+
if output == "id":
|
|
165
|
+
return
|
|
127
166
|
say_info(
|
|
128
|
-
"
|
|
129
|
-
hint="
|
|
167
|
+
"All agents in this org are disabled.",
|
|
168
|
+
hint="Use [bold]--all[/bold] to include disabled agents.",
|
|
130
169
|
)
|
|
131
170
|
return
|
|
132
171
|
|
|
172
|
+
agents = visible_agents
|
|
173
|
+
|
|
133
174
|
if output == "id":
|
|
134
175
|
# One ID per line on stdout for trivial shell composition.
|
|
135
176
|
for a in agents:
|
|
@@ -155,21 +196,37 @@ def agents_get(
|
|
|
155
196
|
typer.Option("--api-key", "-k", envvar="FRUXON_API_KEY", help="API key override."),
|
|
156
197
|
] = None,
|
|
157
198
|
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
typer.Option(
|
|
161
|
-
|
|
199
|
+
output: Annotated[
|
|
200
|
+
str | None,
|
|
201
|
+
typer.Option(
|
|
202
|
+
"--output",
|
|
203
|
+
"-o",
|
|
204
|
+
help=(
|
|
205
|
+
"Output format: text (default for humans), json (machines, default in "
|
|
206
|
+
"agent mode). Under FRUXON_AGENT_MODE / CLAUDECODE / CI the implicit "
|
|
207
|
+
"default flips to json."
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
] = None,
|
|
162
211
|
):
|
|
163
212
|
"""Show one agent's details.
|
|
164
213
|
|
|
165
214
|
Examples:
|
|
166
215
|
fruxon agents get support-agent
|
|
167
|
-
fruxon agents get support-agent --json
|
|
216
|
+
fruxon agents get support-agent --output json
|
|
168
217
|
"""
|
|
218
|
+
output = resolve_output_format(output, human_default="text", agent_default="json")
|
|
219
|
+
if output not in {"text", "json"}:
|
|
220
|
+
say_err(
|
|
221
|
+
f"Unknown output format: [bold]{output}[/bold]",
|
|
222
|
+
hint="Valid formats: text, json.",
|
|
223
|
+
)
|
|
224
|
+
raise typer.Exit(code=1)
|
|
225
|
+
|
|
169
226
|
client = _build_client(org, api_key, base_url)
|
|
170
227
|
|
|
171
228
|
try:
|
|
172
|
-
if stderr.is_terminal and
|
|
229
|
+
if stderr.is_terminal and output == "text":
|
|
173
230
|
with stderr.status(f"[bold]Fetching agent [cyan]{agent}[/cyan]…[/bold]"):
|
|
174
231
|
fetched = client.get_agent(agent)
|
|
175
232
|
parameters = _fetch_parameters_safely(client, fetched)
|
|
@@ -177,10 +234,10 @@ def agents_get(
|
|
|
177
234
|
fetched = client.get_agent(agent)
|
|
178
235
|
parameters = _fetch_parameters_safely(client, fetched)
|
|
179
236
|
except FruxonError as e:
|
|
180
|
-
say_err(str(e), hint=_hint_for_get_error(e, agent))
|
|
237
|
+
say_err(str(e), hint=_hint_for_get_error(e, agent, client))
|
|
181
238
|
raise typer.Exit(code=1) from e
|
|
182
239
|
|
|
183
|
-
if
|
|
240
|
+
if output == "json":
|
|
184
241
|
payload = _agent_to_dict(fetched)
|
|
185
242
|
payload["parameters"] = parameters
|
|
186
243
|
print(json.dumps(payload, indent=2))
|
|
@@ -235,6 +292,19 @@ def _fetch_parameters_safely(client: FruxonClient, fetched: Agent) -> list[str]:
|
|
|
235
292
|
def _build_table(agents: list[Agent]):
|
|
236
293
|
"""Render an agent list as a rich Table.
|
|
237
294
|
|
|
295
|
+
Column shape is tuned for readability across terminal widths:
|
|
296
|
+
|
|
297
|
+
* **ID** — the must-have. ``no_wrap=True`` keeps each row on one
|
|
298
|
+
line; long IDs truncate with ``…`` rather than wrapping into
|
|
299
|
+
multi-line entries that destroy scannability.
|
|
300
|
+
* **Name** — wraps with ellipsis overflow if needed.
|
|
301
|
+
* **Status** — fixed-width, near the ID so it stays visible even
|
|
302
|
+
when narrow widths force right-side columns to shrink.
|
|
303
|
+
* **Rev** — small right-aligned column.
|
|
304
|
+
* **Tags** — last because it's the first column to truncate.
|
|
305
|
+
Hard-capped at 24 chars; previously wrapped into 4+ line entries
|
|
306
|
+
on agents with many tags, which made the table unreadable.
|
|
307
|
+
|
|
238
308
|
Imported lazily so callers that don't render a table (``--output id``,
|
|
239
309
|
``--output json``) don't pay for the Table machinery.
|
|
240
310
|
"""
|
|
@@ -246,19 +316,37 @@ def _build_table(agents: list[Agent]):
|
|
|
246
316
|
show_edge=False,
|
|
247
317
|
pad_edge=False,
|
|
248
318
|
box=None,
|
|
249
|
-
padding=(0,
|
|
319
|
+
# ``padding=(0, 1)`` gives one column of gap between cells.
|
|
320
|
+
# With 5 columns the inter-cell gaps consume 4 chars total,
|
|
321
|
+
# leaving more room for content on narrow terminals (80 col
|
|
322
|
+
# ssh sessions, tmux panes, etc.) before Rich starts
|
|
323
|
+
# shrinking columns into ``…``.
|
|
324
|
+
padding=(0, 1),
|
|
250
325
|
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
table
|
|
255
|
-
|
|
326
|
+
# ``no_wrap=True`` on every column forces single-line-per-row.
|
|
327
|
+
# Combined with ``overflow="ellipsis"`` Rich truncates cells that
|
|
328
|
+
# overflow the column width with ``…`` instead of wrapping —
|
|
329
|
+
# critical for table scannability when an agent has many tags or
|
|
330
|
+
# a long display name.
|
|
331
|
+
#
|
|
332
|
+
# Status and Rev sit right after ID (the eye-tracking priority)
|
|
333
|
+
# and use ``no_wrap`` without a max so they always render in
|
|
334
|
+
# full. ID / Name / Tags carry ``max_width`` to bound how much
|
|
335
|
+
# space each grabs on a wide terminal and to anchor the
|
|
336
|
+
# ellipsis-truncation point on narrow ones. Old layout used to
|
|
337
|
+
# let Tags steal the row and wrap across 8 lines for agents with
|
|
338
|
+
# many tags.
|
|
339
|
+
table.add_column("ID", style="bold", no_wrap=True, overflow="ellipsis", max_width=24)
|
|
340
|
+
table.add_column("Status", justify="right", no_wrap=True)
|
|
341
|
+
table.add_column("Rev", justify="right", style="dim", no_wrap=True)
|
|
342
|
+
table.add_column("Name", no_wrap=True, overflow="ellipsis", max_width=20)
|
|
343
|
+
table.add_column("Tags", style="dim", no_wrap=True, overflow="ellipsis", max_width=22)
|
|
256
344
|
|
|
257
345
|
for a in agents:
|
|
258
346
|
status = "[green]enabled[/green]" if a.enabled else "[dim]disabled[/dim]"
|
|
259
347
|
rev = str(a.current_revision) if a.current_revision else "—"
|
|
260
348
|
tags = ", ".join(a.tags) if a.tags else "—"
|
|
261
|
-
table.add_row(a.id, a.display_name or "[dim]—[/dim]",
|
|
349
|
+
table.add_row(a.id, status, rev, a.display_name or "[dim]—[/dim]", tags)
|
|
262
350
|
return table
|
|
263
351
|
|
|
264
352
|
|
|
@@ -292,10 +380,30 @@ def _hint_for_list_error(error: FruxonError) -> str | None:
|
|
|
292
380
|
return None
|
|
293
381
|
|
|
294
382
|
|
|
295
|
-
def _hint_for_get_error(error: FruxonError, agent: str) -> str | None:
|
|
383
|
+
def _hint_for_get_error(error: FruxonError, agent: str, client: FruxonClient) -> str | None:
|
|
384
|
+
"""Hint generator for ``agents get``.
|
|
385
|
+
|
|
386
|
+
NotFound is the high-traffic case — typos in agent IDs are common.
|
|
387
|
+
We fetch the org's agent list and use ``difflib.get_close_matches``
|
|
388
|
+
to surface a "did you mean?" suggestion. Network errors during
|
|
389
|
+
that lookup are swallowed: the user is already in an error path
|
|
390
|
+
and a second failure on top would be hostile.
|
|
391
|
+
"""
|
|
296
392
|
from fruxon.exceptions import NotFoundError
|
|
297
393
|
|
|
298
394
|
if isinstance(error, NotFoundError):
|
|
395
|
+
import difflib
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
candidates = [a.id for a in client.list_agents()]
|
|
399
|
+
suggestions = difflib.get_close_matches(agent, candidates, n=1, cutoff=0.6)
|
|
400
|
+
except Exception:
|
|
401
|
+
suggestions = []
|
|
402
|
+
if suggestions:
|
|
403
|
+
return (
|
|
404
|
+
f"Did you mean [bold]{suggestions[0]}[/bold]? "
|
|
405
|
+
f"Run with: [bold]fruxon agents get {suggestions[0]}[/bold]"
|
|
406
|
+
)
|
|
299
407
|
return (
|
|
300
408
|
f"No agent matches [bold]{agent}[/bold] in this org. "
|
|
301
409
|
"Try [bold]fruxon agents list[/bold] to see what's available."
|