astromesh-cli 0.1.1__tar.gz → 0.3.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.
Files changed (35) hide show
  1. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/.gitignore +7 -1
  2. astromesh_cli-0.3.0/CHANGELOG.md +23 -0
  3. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/PKG-INFO +2 -2
  4. astromesh_cli-0.3.0/README.md +68 -0
  5. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/__init__.py +1 -1
  6. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/ask.py +17 -4
  7. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/run.py +39 -6
  8. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/main.py +7 -1
  9. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/pyproject.toml +2 -2
  10. astromesh_cli-0.3.0/tests/test_ask.py +84 -0
  11. astromesh_cli-0.3.0/tests/test_run_usage.py +40 -0
  12. astromesh_cli-0.3.0/tests/test_version.py +11 -0
  13. astromesh_cli-0.3.0/uv.lock +984 -0
  14. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/client.py +0 -0
  15. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/__init__.py +0 -0
  16. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/agents.py +0 -0
  17. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/dev.py +0 -0
  18. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/doctor.py +0 -0
  19. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/mesh.py +0 -0
  20. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/metrics.py +0 -0
  21. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/new.py +0 -0
  22. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/peers.py +0 -0
  23. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/providers.py +0 -0
  24. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/services.py +0 -0
  25. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/status.py +0 -0
  26. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/tools.py +0 -0
  27. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/commands/traces.py +0 -0
  28. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/output.py +0 -0
  29. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/templates/agent.yaml.j2 +0 -0
  30. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/templates/tool.py.j2 +0 -0
  31. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/astromesh_cli/templates/workflow.yaml.j2 +0 -0
  32. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/tests/conftest.py +0 -0
  33. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/tests/test_client.py +0 -0
  34. {astromesh_cli-0.1.1 → astromesh_cli-0.3.0}/tests/test_main.py +0 -0
  35. {astromesh_cli-0.1.1 → astromesh_cli-0.3.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,23 @@
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.1] - 2026-08-10
11
+
12
+ ### Changed
13
+ - Bumped the `astromesh` core dependency floor to `>=0.40.0` (`pyproject.toml`).
14
+
15
+ ## [0.2.0] - 2026-07-27
16
+
17
+ ### Added
18
+ - `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.
19
+ - `version` now also reports the installed astromesh **core** version (via `importlib.metadata`), not just the CLI's own.
20
+
21
+ ### Changed
22
+ - Bumped the astromesh core dependency pin to `>=0.36.0`.
23
+ - `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.1.1
3
+ Version: 0.3.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.18.0
7
+ Requires-Dist: astromesh>=0.40.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
@@ -0,0 +1,68 @@
1
+ # astromeshctl
2
+
3
+ <p align="center">
4
+ <a href="https://monaccode.github.io/astromesh/#ecosystem"><img src="https://img.shields.io/badge/astromesh-operate-f59e0b?labelColor=161b22" alt="Astromesh · Operate"></a>
5
+ <a href="https://github.com/monaccode/astromesh/actions/workflows/ci.yml"><img src="https://github.com/monaccode/astromesh/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
6
+ <a href="https://github.com/monaccode/astromesh/actions/workflows/release-cli.yml"><img src="https://github.com/monaccode/astromesh/actions/workflows/release-cli.yml/badge.svg" alt="CLI Release"></a>
7
+ <a href="https://pypi.org/project/astromesh-cli/"><img src="https://img.shields.io/pypi/v/astromesh-cli?label=astromeshctl%20PyPI" alt="CLI PyPI"></a>
8
+ <a href="https://test.pypi.org/project/astromesh-cli/"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftest.pypi.org%2Fpypi%2Fastromesh-cli%2Fjson&query=%24.info.version&label=CLI%20TestPyPI" alt="CLI TestPyPI"></a>
9
+ <a href="https://github.com/monaccode/astromesh/blob/develop/LICENSE"><img src="https://img.shields.io/github/license/monaccode/astromesh" alt="License"></a>
10
+ <a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+"></a>
11
+ </p>
12
+
13
+ The terminal interface to an Astromesh node: agents, config, mesh state, profiles, and more.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install astromesh-cli
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ # Check versions
25
+ astromeshctl version
26
+
27
+ # Talk to a running node
28
+ astromeshctl status
29
+ astromeshctl agents list
30
+ astromeshctl ask "What agents are running?"
31
+
32
+ # Run an agent
33
+ astromeshctl run my-agent "Hello"
34
+
35
+ # Inspect traces and metrics
36
+ astromeshctl traces list
37
+ astromeshctl metrics
38
+ astromeshctl cost
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ | Command | Purpose |
44
+ |---------|---------|
45
+ | `status` | Node health and summary |
46
+ | `doctor` | Diagnostics |
47
+ | `agents` | List, create, and manage agents |
48
+ | `providers` | Manage LLM providers |
49
+ | `mesh` / `peers` | Mesh networking |
50
+ | `services` | Service discovery |
51
+ | `run` | Run an agent |
52
+ | `dev` | Local development server |
53
+ | `traces` / `trace` | Execution traces |
54
+ | `tools` | Tool registry |
55
+ | `metrics` / `cost` | Usage and cost |
56
+ | `ask` | Ask the node a question |
57
+ | `new` | Scaffold new resources |
58
+
59
+ Plugins can extend `astromeshctl` via the `astromeshctl.plugins` entry point. For example, `astromesh-node` and `astromesh-orbit` register themselves this way.
60
+
61
+ ## Documentation
62
+
63
+ - [CLI Commands](../docs-site/src/content/docs/reference/cli-commands.md)
64
+ - [Astromesh Docs](https://monaccode.github.io/astromesh/)
65
+
66
+ ## License
67
+
68
+ Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """Astromesh CLI — manage Astromesh nodes and clusters."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.3.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
- response_text = data.get("response", "")
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(response_text),
107
+ Markdown(text),
107
108
  title="[cyan]Astromesh Copilot[/cyan]",
108
- subtitle=f"trace: {trace_id}",
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
- response_text = data.get("response", "")
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
- response_text,
78
+ text,
58
79
  title=f"[cyan]{name}[/cyan]",
59
- subtitle=f"trace: {trace_id} | tokens: {tokens}",
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
- typer.echo(f"astromesh {__version__}")
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.1.1"
7
+ version = "0.3.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.18.0",
12
+ "astromesh>=0.40.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()