conduct-cli 0.4.45__tar.gz → 0.4.47__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.
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/PKG-INFO +1 -1
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/pyproject.toml +1 -1
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/main.py +130 -1
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/README.md +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/setup.cfg +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/setup.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.45 → conduct_cli-0.4.47}/tests/test_switch.py +0 -0
|
@@ -1756,9 +1756,61 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1756
1756
|
return "\n".join(lines)
|
|
1757
1757
|
|
|
1758
1758
|
|
|
1759
|
+
def _fetch_runs(cfg: dict) -> list[dict]:
|
|
1760
|
+
"""Fetch recent runs from GET /runs."""
|
|
1761
|
+
server = cfg.get("server", "").rstrip("/")
|
|
1762
|
+
api_key = cfg.get("api_key", "")
|
|
1763
|
+
ws_id = cfg.get("workspace", "")
|
|
1764
|
+
if not all([server, api_key, ws_id]):
|
|
1765
|
+
return []
|
|
1766
|
+
try:
|
|
1767
|
+
hdrs = {"Content-Type": "application/json", "X-Api-Key": api_key}
|
|
1768
|
+
data = api.req("GET", f"{server}/runs?limit=15&workspace_id={ws_id}", hdrs)
|
|
1769
|
+
runs = data if isinstance(data, list) else data.get("runs", data.get("items", []))
|
|
1770
|
+
return runs[:15]
|
|
1771
|
+
except Exception:
|
|
1772
|
+
return []
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _fetch_guard_activity(cfg: dict, guard_cfg: dict) -> list[dict]:
|
|
1776
|
+
"""Fetch recent Guard events grouped by developer."""
|
|
1777
|
+
server = cfg.get("server", "").rstrip("/")
|
|
1778
|
+
api_key = cfg.get("api_key", "")
|
|
1779
|
+
ws_id = cfg.get("workspace", "")
|
|
1780
|
+
if not all([server, api_key, ws_id]):
|
|
1781
|
+
return []
|
|
1782
|
+
try:
|
|
1783
|
+
hdrs = {"Content-Type": "application/json", "X-Api-Key": api_key}
|
|
1784
|
+
data = api.req("GET", f"{server}/guard/events?workspace_id={ws_id}&limit=200", hdrs)
|
|
1785
|
+
events = data if isinstance(data, list) else data.get("events", [])
|
|
1786
|
+
# Group by user_email
|
|
1787
|
+
devs: dict[str, dict] = {}
|
|
1788
|
+
for e in events:
|
|
1789
|
+
email = e.get("user_email") or e.get("email") or "unknown"
|
|
1790
|
+
blocked = e.get("decision") == "block"
|
|
1791
|
+
tokens = e.get("tokens_used") or e.get("tokens", 0) or 0
|
|
1792
|
+
if email not in devs:
|
|
1793
|
+
devs[email] = {"email": email, "calls": 0, "blocked": 0, "tokens": 0}
|
|
1794
|
+
devs[email]["calls"] += 1
|
|
1795
|
+
devs[email]["blocked"] += int(blocked)
|
|
1796
|
+
devs[email]["tokens"] += tokens
|
|
1797
|
+
return sorted(devs.values(), key=lambda d: d["calls"], reverse=True)
|
|
1798
|
+
except Exception:
|
|
1799
|
+
return []
|
|
1800
|
+
|
|
1801
|
+
|
|
1759
1802
|
def _render_tui(rows: list[dict]) -> None:
|
|
1760
1803
|
"""Full-screen TUI with live refresh. Uses rich.live if available, else ANSI loop."""
|
|
1761
1804
|
|
|
1805
|
+
cfg = _load_config()
|
|
1806
|
+
guard_cfg = {}
|
|
1807
|
+
_gp = Path.home() / ".conductguard" / "config.json"
|
|
1808
|
+
if _gp.exists():
|
|
1809
|
+
try:
|
|
1810
|
+
guard_cfg = json.loads(_gp.read_text())
|
|
1811
|
+
except Exception:
|
|
1812
|
+
pass
|
|
1813
|
+
|
|
1762
1814
|
def _build_rich_display(rows):
|
|
1763
1815
|
from rich.table import Table
|
|
1764
1816
|
from rich.panel import Panel
|
|
@@ -1812,7 +1864,7 @@ def _render_tui(rows: list[dict]) -> None:
|
|
|
1812
1864
|
# Summary header
|
|
1813
1865
|
summary = f"[bold]{active}[/] active · [bold]{guard_on}[/] with Guard · [dim]refreshing every 5s — Ctrl+C to quit[/]"
|
|
1814
1866
|
|
|
1815
|
-
panels = [Panel(tbl, title="[bold blue]
|
|
1867
|
+
panels = [Panel(tbl, title="[bold blue]● My Sessions[/]", subtitle=summary, expand=True)]
|
|
1816
1868
|
|
|
1817
1869
|
# Context pressure panel
|
|
1818
1870
|
if high_ctx:
|
|
@@ -1823,6 +1875,83 @@ def _render_tui(rows: list[dict]) -> None:
|
|
|
1823
1875
|
)
|
|
1824
1876
|
panels.append(Panel(warnings, title="[yellow bold]⚠ Context pressure[/]", expand=True))
|
|
1825
1877
|
|
|
1878
|
+
# Agent Runs panel
|
|
1879
|
+
run_data = _fetch_runs(cfg)
|
|
1880
|
+
runs_tbl = Table(box=box.SIMPLE, expand=True, show_header=True, header_style="bold white")
|
|
1881
|
+
runs_tbl.add_column("Agent", min_width=22)
|
|
1882
|
+
runs_tbl.add_column("Status", width=12)
|
|
1883
|
+
runs_tbl.add_column("Duration", width=10, justify="right")
|
|
1884
|
+
runs_tbl.add_column("Project", min_width=14)
|
|
1885
|
+
runs_tbl.add_column("Triggered", width=12)
|
|
1886
|
+
|
|
1887
|
+
if run_data:
|
|
1888
|
+
for r in run_data:
|
|
1889
|
+
status = r.get("status", "")
|
|
1890
|
+
scolor = {"running": "green", "succeeded": "dim green", "failed": "red",
|
|
1891
|
+
"paused": "yellow", "pending": "cyan", "cancelled": "dim"}.get(status, "white")
|
|
1892
|
+
sicon = {"running": "●", "succeeded": "✓", "failed": "✗",
|
|
1893
|
+
"paused": "⏸", "pending": "○", "cancelled": "—"}.get(status, "?")
|
|
1894
|
+
# Duration — timestamps may be ISO strings or unix floats
|
|
1895
|
+
import datetime as _dt
|
|
1896
|
+
def _to_ts(val):
|
|
1897
|
+
if not val:
|
|
1898
|
+
return None
|
|
1899
|
+
if isinstance(val, (int, float)):
|
|
1900
|
+
return float(val)
|
|
1901
|
+
try:
|
|
1902
|
+
return _dt.datetime.fromisoformat(str(val).replace("Z", "+00:00")).timestamp()
|
|
1903
|
+
except Exception:
|
|
1904
|
+
return None
|
|
1905
|
+
created_ts = _to_ts(r.get("created_at") or r.get("started_at"))
|
|
1906
|
+
ended_ts = _to_ts(r.get("completed_at") or r.get("finished_at"))
|
|
1907
|
+
if created_ts and ended_ts:
|
|
1908
|
+
secs = int(ended_ts - created_ts)
|
|
1909
|
+
dur = f"{secs//60}:{secs%60:02d}"
|
|
1910
|
+
elif created_ts and status == "running":
|
|
1911
|
+
secs = int(time.time() - created_ts)
|
|
1912
|
+
dur = f"{secs//60}:{secs%60:02d}"
|
|
1913
|
+
else:
|
|
1914
|
+
dur = "—"
|
|
1915
|
+
agent_name = r.get("workflow_name") or r.get("name") or "—"
|
|
1916
|
+
project_name = r.get("project_name") or r.get("project") or "—"
|
|
1917
|
+
trigger = r.get("trigger_type") or r.get("triggered_by") or "manual"
|
|
1918
|
+
runs_tbl.add_row(
|
|
1919
|
+
agent_name[:28],
|
|
1920
|
+
Text(f"{sicon} {status}", style=scolor),
|
|
1921
|
+
dur,
|
|
1922
|
+
project_name[:18],
|
|
1923
|
+
trigger[:12],
|
|
1924
|
+
)
|
|
1925
|
+
else:
|
|
1926
|
+
runs_tbl.add_row("[dim]No runs yet or not connected[/]", "", "", "", "")
|
|
1927
|
+
|
|
1928
|
+
panels.append(Panel(runs_tbl, title="[bold green]▶ Agent Runs[/]", expand=True))
|
|
1929
|
+
|
|
1930
|
+
# Team Activity panel (Guard)
|
|
1931
|
+
team_data = _fetch_guard_activity(cfg, guard_cfg)
|
|
1932
|
+
team_tbl = Table(box=box.SIMPLE, expand=True, show_header=True, header_style="bold white")
|
|
1933
|
+
team_tbl.add_column("Developer", min_width=28)
|
|
1934
|
+
team_tbl.add_column("Calls", width=8, justify="right")
|
|
1935
|
+
team_tbl.add_column("Blocked", width=8, justify="right")
|
|
1936
|
+
team_tbl.add_column("Tokens", width=10, justify="right")
|
|
1937
|
+
team_tbl.add_column("Guard", width=7, justify="center")
|
|
1938
|
+
|
|
1939
|
+
if team_data:
|
|
1940
|
+
for d in team_data:
|
|
1941
|
+
blocked_text = Text(str(d["blocked"]), style="red bold" if d["blocked"] else "dim")
|
|
1942
|
+
guard_text = Text("✓", style="green")
|
|
1943
|
+
team_tbl.add_row(
|
|
1944
|
+
d["email"][:32],
|
|
1945
|
+
str(d["calls"]),
|
|
1946
|
+
blocked_text,
|
|
1947
|
+
_fmt_tokens(d["tokens"]),
|
|
1948
|
+
guard_text,
|
|
1949
|
+
)
|
|
1950
|
+
else:
|
|
1951
|
+
team_tbl.add_row("[dim]No team activity or not connected[/]", "", "", "", "")
|
|
1952
|
+
|
|
1953
|
+
panels.append(Panel(team_tbl, title="[bold magenta]👥 Team Activity (Guard)[/]", expand=True))
|
|
1954
|
+
|
|
1826
1955
|
from rich.console import Group
|
|
1827
1956
|
return Group(*panels)
|
|
1828
1957
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|