astromesh-cli 0.1.1__tar.gz → 0.2.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.
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/.gitignore +7 -1
- astromesh_cli-0.2.0/CHANGELOG.md +18 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/PKG-INFO +2 -2
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/__init__.py +1 -1
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/ask.py +17 -4
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/run.py +39 -6
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/main.py +7 -1
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/pyproject.toml +2 -2
- astromesh_cli-0.2.0/tests/test_ask.py +84 -0
- astromesh_cli-0.2.0/tests/test_run_usage.py +40 -0
- astromesh_cli-0.2.0/tests/test_version.py +11 -0
- astromesh_cli-0.2.0/uv.lock +983 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/client.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/__init__.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/agents.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/dev.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/doctor.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/mesh.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/metrics.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/new.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/peers.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/providers.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/services.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/status.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/tools.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/commands/traces.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/output.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/templates/agent.yaml.j2 +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/templates/tool.py.j2 +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/astromesh_cli/templates/workflow.yaml.j2 +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/tests/conftest.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/tests/test_client.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/tests/test_main.py +0 -0
- {astromesh_cli-0.1.1 → astromesh_cli-0.2.0}/tests/test_output.py +0 -0
|
@@ -22,7 +22,9 @@ htmlcov/
|
|
|
22
22
|
.coverage
|
|
23
23
|
*.log
|
|
24
24
|
.uv/
|
|
25
|
-
uv.lock
|
|
25
|
+
# uv.lock se versiona a propósito: sin él CI resolvía dependencias frescas en
|
|
26
|
+
# cada corrida y el gate podía romperse por calendario en vez de por commits
|
|
27
|
+
# (pasó con ruff 0.16). Para actualizar una dependencia: `uv lock --upgrade-package <nombre>`.
|
|
26
28
|
target/
|
|
27
29
|
native/target/
|
|
28
30
|
*.so
|
|
@@ -37,3 +39,7 @@ docs-site/.astro/
|
|
|
37
39
|
|
|
38
40
|
# Astromesh Orbit working directory
|
|
39
41
|
.orbit/
|
|
42
|
+
|
|
43
|
+
# SDD scratch
|
|
44
|
+
.superpowers/
|
|
45
|
+
config/connections.yaml
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `run` now surfaces the per-model consumption breakdown (`usage.by_model`, astromesh core v0.36.0): a "Por modelo" table with provider / model / role / calls / tokens in-out / cost.
|
|
14
|
+
- `version` now also reports the installed astromesh **core** version (via `importlib.metadata`), not just the CLI's own.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Bumped the astromesh core dependency pin to `>=0.36.0`.
|
|
18
|
+
- `run` and `ask` now read the real run-response contract (`answer` + `usage`) instead of the stale `response`/`tokens_used` field names, via a shared `_format_run_output` helper; `trace_id` is read from the nested `trace`. `ask` previously rendered an empty body against core ≥ the version that introduced `answer`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astromesh-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI tool for managing Astromesh nodes and clusters
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
|
-
Requires-Dist: astromesh>=0.
|
|
7
|
+
Requires-Dist: astromesh>=0.36.0
|
|
8
8
|
Requires-Dist: jinja2>=3.1.0
|
|
9
9
|
Requires-Dist: rich>=13.0.0
|
|
10
10
|
Requires-Dist: typer>=0.12.0
|
|
@@ -7,8 +7,10 @@ from typing import Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from rich.markdown import Markdown
|
|
9
9
|
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
10
11
|
|
|
11
12
|
from astromesh_cli.client import api_post_with_timeout
|
|
13
|
+
from astromesh_cli.commands.run import _format_run_output
|
|
12
14
|
from astromesh_cli.output import console, print_error, print_json
|
|
13
15
|
|
|
14
16
|
COPILOT_AGENT = "astromesh-copilot"
|
|
@@ -98,14 +100,25 @@ def ask_command(
|
|
|
98
100
|
print_json(data)
|
|
99
101
|
return
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
trace_id = data.get("trace_id", "N/A")
|
|
103
|
+
text, subtitle, by_model_rows = _format_run_output(data)
|
|
103
104
|
|
|
104
105
|
console.print(
|
|
105
106
|
Panel(
|
|
106
|
-
Markdown(
|
|
107
|
+
Markdown(text),
|
|
107
108
|
title="[cyan]Astromesh Copilot[/cyan]",
|
|
108
|
-
subtitle=
|
|
109
|
+
subtitle=subtitle,
|
|
109
110
|
border_style="cyan",
|
|
110
111
|
)
|
|
111
112
|
)
|
|
113
|
+
if by_model_rows:
|
|
114
|
+
table = Table(title="Por modelo", show_header=True)
|
|
115
|
+
for col in ("Provider", "Model", "Role", "Calls", "In", "Out", "Cost"):
|
|
116
|
+
table.add_column(col, style="cyan" if col == "Provider" else None)
|
|
117
|
+
for r in by_model_rows:
|
|
118
|
+
cost = r.get("cost", 0.0) or 0.0
|
|
119
|
+
table.add_row(
|
|
120
|
+
r.get("provider", ""), r.get("model", ""), r.get("role", ""),
|
|
121
|
+
str(r.get("calls", 0)), str(r.get("tokens_in", 0)), str(r.get("tokens_out", 0)),
|
|
122
|
+
f"${cost:.4f}" if cost else "—",
|
|
123
|
+
)
|
|
124
|
+
console.print(table)
|
|
@@ -12,6 +12,30 @@ from astromesh_cli.client import api_post_with_timeout
|
|
|
12
12
|
from astromesh_cli.output import console, print_error, print_json
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _format_run_output(data: dict) -> tuple[str, str, list[dict]]:
|
|
16
|
+
"""Extrae (texto, subtítulo, filas by_model) de la respuesta de /run.
|
|
17
|
+
|
|
18
|
+
Prefiere el contrato del core v0.36.0 (answer + usage{tokens_in, tokens_out,
|
|
19
|
+
by_model}); cae a los campos legacy (response + tokens_used) si no está.
|
|
20
|
+
El trace_id se extrae de `trace.trace_id` (el trace anidado), no de un
|
|
21
|
+
campo top-level, y también se usa para armar el subtítulo.
|
|
22
|
+
"""
|
|
23
|
+
text = data.get("answer") or data.get("response", "")
|
|
24
|
+
trace_id = (data.get("trace") or {}).get("trace_id", "N/A")
|
|
25
|
+
usage = data.get("usage")
|
|
26
|
+
if isinstance(usage, dict):
|
|
27
|
+
tin = usage.get("tokens_in", 0)
|
|
28
|
+
tout = usage.get("tokens_out", 0)
|
|
29
|
+
tokens = tin + tout
|
|
30
|
+
by_model = usage.get("by_model") or []
|
|
31
|
+
else:
|
|
32
|
+
tokens = data.get("tokens_used", "N/A")
|
|
33
|
+
by_model = []
|
|
34
|
+
rows = [m for m in by_model if isinstance(m, dict)]
|
|
35
|
+
subtitle = f"trace: {trace_id} | tokens: {tokens}"
|
|
36
|
+
return text, subtitle, rows
|
|
37
|
+
|
|
38
|
+
|
|
15
39
|
def run_command(
|
|
16
40
|
name: str = typer.Argument(..., help="Agent or workflow name to run"),
|
|
17
41
|
query: str = typer.Argument("", help="Query to send to the agent"),
|
|
@@ -48,18 +72,27 @@ def run_command(
|
|
|
48
72
|
print_json(data)
|
|
49
73
|
return
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
trace_id = data.get("trace_id", "N/A")
|
|
53
|
-
tokens = data.get("tokens_used", "N/A")
|
|
54
|
-
|
|
75
|
+
text, subtitle, by_model_rows = _format_run_output(data)
|
|
55
76
|
console.print(
|
|
56
77
|
Panel(
|
|
57
|
-
|
|
78
|
+
text,
|
|
58
79
|
title=f"[cyan]{name}[/cyan]",
|
|
59
|
-
subtitle=
|
|
80
|
+
subtitle=subtitle,
|
|
60
81
|
border_style="blue",
|
|
61
82
|
)
|
|
62
83
|
)
|
|
84
|
+
if by_model_rows:
|
|
85
|
+
table = Table(title="Por modelo", show_header=True)
|
|
86
|
+
for col in ("Provider", "Model", "Role", "Calls", "In", "Out", "Cost"):
|
|
87
|
+
table.add_column(col, style="cyan" if col == "Provider" else None)
|
|
88
|
+
for r in by_model_rows:
|
|
89
|
+
cost = r.get("cost", 0.0) or 0.0
|
|
90
|
+
table.add_row(
|
|
91
|
+
r.get("provider", ""), r.get("model", ""), r.get("role", ""),
|
|
92
|
+
str(r.get("calls", 0)), str(r.get("tokens_in", 0)), str(r.get("tokens_out", 0)),
|
|
93
|
+
f"${cost:.4f}" if cost else "—",
|
|
94
|
+
)
|
|
95
|
+
console.print(table)
|
|
63
96
|
|
|
64
97
|
|
|
65
98
|
def _run_workflow(
|
|
@@ -49,7 +49,13 @@ app.command("ask")(ask.ask_command)
|
|
|
49
49
|
@app.command()
|
|
50
50
|
def version():
|
|
51
51
|
"""Show astromesh version."""
|
|
52
|
-
|
|
52
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
53
|
+
|
|
54
|
+
typer.echo(f"astromesh-cli {__version__}")
|
|
55
|
+
try:
|
|
56
|
+
typer.echo(f"astromesh core {_pkg_version('astromesh')}")
|
|
57
|
+
except PackageNotFoundError:
|
|
58
|
+
typer.echo("astromesh core (not installed)")
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
# Plugin discovery — after all static registrations
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "astromesh-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "CLI tool for managing Astromesh nodes and clusters"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
11
11
|
dependencies = [
|
|
12
|
-
"astromesh>=0.
|
|
12
|
+
"astromesh>=0.36.0",
|
|
13
13
|
"typer>=0.12.0",
|
|
14
14
|
"rich>=13.0.0",
|
|
15
15
|
"jinja2>=3.1.0",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for the `ask` command against the core v0.36.0 response shape."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from astromesh_cli.main import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _mock_httpx_post(payload: dict):
|
|
13
|
+
mock_response = MagicMock()
|
|
14
|
+
mock_response.json.return_value = payload
|
|
15
|
+
mock_response.raise_for_status = MagicMock()
|
|
16
|
+
return mock_response
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_ask_renders_answer_for_v036_shape():
|
|
20
|
+
"""The ask command must render `answer` (not the stale `response` field)."""
|
|
21
|
+
payload = {
|
|
22
|
+
"answer": "Esta es la respuesta del copiloto.",
|
|
23
|
+
"steps": [],
|
|
24
|
+
"usage": {"tokens_in": 12, "tokens_out": 8, "model": "llama3", "by_model": []},
|
|
25
|
+
"trace": {"trace_id": "trace-xyz"},
|
|
26
|
+
}
|
|
27
|
+
with patch("astromesh_cli.client.httpx") as mock_httpx:
|
|
28
|
+
mock_httpx.post.return_value = _mock_httpx_post(payload)
|
|
29
|
+
result = runner.invoke(app, ["ask", "hola"])
|
|
30
|
+
|
|
31
|
+
assert result.exit_code == 0
|
|
32
|
+
assert "Esta es la respuesta del copiloto." in result.stdout
|
|
33
|
+
# trace_id must come from the nested `trace` dict, not a stale top-level field.
|
|
34
|
+
assert "trace-xyz" in result.stdout
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_ask_falls_back_to_legacy_response_field():
|
|
38
|
+
payload = {"response": "respuesta legacy", "tokens_used": 5}
|
|
39
|
+
with patch("astromesh_cli.client.httpx") as mock_httpx:
|
|
40
|
+
mock_httpx.post.return_value = _mock_httpx_post(payload)
|
|
41
|
+
result = runner.invoke(app, ["ask", "hola"])
|
|
42
|
+
|
|
43
|
+
assert result.exit_code == 0
|
|
44
|
+
assert "respuesta legacy" in result.stdout
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_ask_renders_by_model_table():
|
|
48
|
+
payload = {
|
|
49
|
+
"answer": "hola",
|
|
50
|
+
"usage": {
|
|
51
|
+
"tokens_in": 10,
|
|
52
|
+
"tokens_out": 5,
|
|
53
|
+
"model": "llama3",
|
|
54
|
+
"by_model": [
|
|
55
|
+
{
|
|
56
|
+
"provider": "ollama",
|
|
57
|
+
"model": "llama3",
|
|
58
|
+
"role": "default",
|
|
59
|
+
"calls": 2,
|
|
60
|
+
"tokens_in": 10,
|
|
61
|
+
"tokens_out": 5,
|
|
62
|
+
"cost": 0.0,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
"trace": {"trace_id": "trace-abc"},
|
|
67
|
+
}
|
|
68
|
+
with patch("astromesh_cli.client.httpx") as mock_httpx:
|
|
69
|
+
mock_httpx.post.return_value = _mock_httpx_post(payload)
|
|
70
|
+
result = runner.invoke(app, ["ask", "hola"])
|
|
71
|
+
|
|
72
|
+
assert result.exit_code == 0
|
|
73
|
+
assert "Por modelo" in result.stdout
|
|
74
|
+
assert "ollama" in result.stdout
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_ask_json_output_unchanged():
|
|
78
|
+
payload = {"answer": "hola", "usage": {"tokens_in": 1, "tokens_out": 1}}
|
|
79
|
+
with patch("astromesh_cli.client.httpx") as mock_httpx:
|
|
80
|
+
mock_httpx.post.return_value = _mock_httpx_post(payload)
|
|
81
|
+
result = runner.invoke(app, ["ask", "hola", "--json"])
|
|
82
|
+
|
|
83
|
+
assert result.exit_code == 0
|
|
84
|
+
assert '"answer"' in result.stdout
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from astromesh_cli.commands.run import _format_run_output
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_reads_answer_and_usage_totals():
|
|
5
|
+
data = {
|
|
6
|
+
"answer": "hola",
|
|
7
|
+
"steps": [],
|
|
8
|
+
"usage": {"tokens_in": 10, "tokens_out": 5, "model": "llama3", "by_model": []},
|
|
9
|
+
}
|
|
10
|
+
text, subtitle, rows = _format_run_output(data)
|
|
11
|
+
assert text == "hola"
|
|
12
|
+
assert "15" in subtitle or ("10" in subtitle and "5" in subtitle)
|
|
13
|
+
assert rows == []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_builds_by_model_rows():
|
|
17
|
+
data = {
|
|
18
|
+
"answer": "hola",
|
|
19
|
+
"usage": {
|
|
20
|
+
"tokens_in": 10,
|
|
21
|
+
"tokens_out": 5,
|
|
22
|
+
"model": "llama3",
|
|
23
|
+
"by_model": [
|
|
24
|
+
{"provider": "ollama", "model": "llama3", "role": "default",
|
|
25
|
+
"calls": 2, "tokens_in": 10, "tokens_out": 5, "cost": 0.0},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
_text, _subtitle, rows = _format_run_output(data)
|
|
30
|
+
assert rows[0]["provider"] == "ollama"
|
|
31
|
+
assert rows[0]["model"] == "llama3"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_falls_back_to_legacy_fields():
|
|
35
|
+
# Pre-v0.36 / daemon that returns response+tokens_used and no usage object.
|
|
36
|
+
data = {"response": "hola", "tokens_used": 42}
|
|
37
|
+
text, subtitle, rows = _format_run_output(data)
|
|
38
|
+
assert text == "hola"
|
|
39
|
+
assert "42" in subtitle
|
|
40
|
+
assert rows == []
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typer.testing import CliRunner
|
|
2
|
+
from astromesh_cli.main import app
|
|
3
|
+
|
|
4
|
+
runner = CliRunner()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_version_reports_cli_and_core():
|
|
8
|
+
result = runner.invoke(app, ["version"])
|
|
9
|
+
assert result.exit_code == 0
|
|
10
|
+
assert "cli" in result.stdout.lower()
|
|
11
|
+
assert "core" in result.stdout.lower()
|