conduct-cli 0.4.44__tar.gz → 0.4.46__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.44 → conduct_cli-0.4.46}/PKG-INFO +2 -1
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/pyproject.toml +2 -2
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/main.py +248 -70
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/PKG-INFO +2 -1
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/requires.txt +1 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/README.md +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/setup.cfg +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/setup.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.44 → conduct_cli-0.4.46}/tests/test_switch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.46
|
|
4
4
|
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
5
5
|
Author-email: Conduct AI <hello@conductai.ai>
|
|
6
6
|
License: MIT
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
24
25
|
|
|
25
26
|
# conduct-cli
|
|
26
27
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.46"
|
|
8
8
|
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
24
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
25
25
|
]
|
|
26
|
-
dependencies = ["pyyaml>=6.0"]
|
|
26
|
+
dependencies = ["pyyaml>=6.0", "rich>=13.0"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Homepage = "https://conductai.ai"
|
|
@@ -1756,80 +1756,258 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1756
1756
|
return "\n".join(lines)
|
|
1757
1757
|
|
|
1758
1758
|
|
|
1759
|
-
def
|
|
1760
|
-
"""
|
|
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 []
|
|
1761
1766
|
try:
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
sys.stdout.flush()
|
|
1805
|
-
|
|
1806
|
-
# Use raw terminal input for 'q' detection (non-blocking)
|
|
1807
|
-
import select, tty, termios
|
|
1808
|
-
old = termios.tcgetattr(sys.stdin)
|
|
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
|
+
|
|
1802
|
+
def _render_tui(rows: list[dict]) -> None:
|
|
1803
|
+
"""Full-screen TUI with live refresh. Uses rich.live if available, else ANSI loop."""
|
|
1804
|
+
|
|
1805
|
+
cfg = _load_config()
|
|
1806
|
+
guard_cfg = {}
|
|
1807
|
+
_gp = Path.home() / ".conductguard" / "config.json"
|
|
1808
|
+
if _gp.exists():
|
|
1809
1809
|
try:
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
rows = _load_sessions()
|
|
1814
|
-
_render_frame(rows)
|
|
1815
|
-
# Wait up to 5s, exit on 'q'
|
|
1816
|
-
for _ in range(50):
|
|
1817
|
-
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
1818
|
-
ch = sys.stdin.read(1)
|
|
1819
|
-
if ch in ("q", "Q"):
|
|
1820
|
-
stop = True
|
|
1821
|
-
break
|
|
1822
|
-
if stop:
|
|
1823
|
-
break
|
|
1824
|
-
finally:
|
|
1825
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
1826
|
-
_clear()
|
|
1827
|
-
print("conduct sessions exited.")
|
|
1810
|
+
guard_cfg = json.loads(_gp.read_text())
|
|
1811
|
+
except Exception:
|
|
1812
|
+
pass
|
|
1828
1813
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1814
|
+
def _build_rich_display(rows):
|
|
1815
|
+
from rich.table import Table
|
|
1816
|
+
from rich.panel import Panel
|
|
1817
|
+
from rich.columns import Columns
|
|
1818
|
+
from rich import box
|
|
1819
|
+
from rich.text import Text
|
|
1820
|
+
|
|
1821
|
+
active = sum(1 for r in rows if r["alive"])
|
|
1822
|
+
guard_on = sum(1 for r in rows if r["guard"])
|
|
1823
|
+
high_ctx = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
|
|
1824
|
+
|
|
1825
|
+
# Main sessions table
|
|
1826
|
+
tbl = Table(box=box.ROUNDED, expand=True, show_header=True, header_style="bold white")
|
|
1827
|
+
tbl.add_column("AI", width=4, style="bold")
|
|
1828
|
+
tbl.add_column("Project", min_width=14)
|
|
1829
|
+
tbl.add_column("Session", width=10, style="dim")
|
|
1830
|
+
tbl.add_column("Model", min_width=14)
|
|
1831
|
+
tbl.add_column("CTX", width=16)
|
|
1832
|
+
tbl.add_column("Tokens", width=10, justify="right")
|
|
1833
|
+
tbl.add_column("Turns", width=6, justify="right")
|
|
1834
|
+
tbl.add_column("Guard", width=6, justify="center")
|
|
1835
|
+
tbl.add_column("Status", width=8)
|
|
1836
|
+
|
|
1837
|
+
for r in rows:
|
|
1838
|
+
ai_text = Text(r.get("ai", "CC"), style="bold cyan" if r.get("ai") == "CD" else "bold blue")
|
|
1839
|
+
status_text = Text("active", style="green") if r["alive"] else Text("idle", style="dim")
|
|
1840
|
+
guard_text = Text("✓", style="green") if r["guard"] else Text("—", style="dim")
|
|
1841
|
+
model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"] not in ("—", "") else "—"
|
|
1842
|
+
|
|
1843
|
+
pct = r["ctx_pct"]
|
|
1844
|
+
if pct:
|
|
1845
|
+
filled = round(pct / 100 * 10)
|
|
1846
|
+
bar = "█" * filled + "░" * (10 - filled)
|
|
1847
|
+
color = "red" if pct >= 80 else "yellow" if pct >= 60 else "green"
|
|
1848
|
+
ctx_text = Text(f"{bar} {pct}%", style=color)
|
|
1849
|
+
else:
|
|
1850
|
+
ctx_text = Text("—", style="dim")
|
|
1851
|
+
|
|
1852
|
+
tbl.add_row(
|
|
1853
|
+
ai_text,
|
|
1854
|
+
r["project"],
|
|
1855
|
+
r["session_id"],
|
|
1856
|
+
model_short,
|
|
1857
|
+
ctx_text,
|
|
1858
|
+
_fmt_tokens(r["ctx_tokens"]) if r["ctx_tokens"] else "—",
|
|
1859
|
+
str(r["turns"]),
|
|
1860
|
+
guard_text,
|
|
1861
|
+
status_text,
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
# Summary header
|
|
1865
|
+
summary = f"[bold]{active}[/] active · [bold]{guard_on}[/] with Guard · [dim]refreshing every 5s — Ctrl+C to quit[/]"
|
|
1866
|
+
|
|
1867
|
+
panels = [Panel(tbl, title="[bold blue]● My Sessions[/]", subtitle=summary, expand=True)]
|
|
1868
|
+
|
|
1869
|
+
# Context pressure panel
|
|
1870
|
+
if high_ctx:
|
|
1871
|
+
warnings = "\n".join(
|
|
1872
|
+
f"[{'red' if r['ctx_pct'] >= 80 else 'yellow'}]{r['project']}[/] "
|
|
1873
|
+
f"[dim]{r['session_id']}[/] {r['ctx_pct']}% — consider /compact"
|
|
1874
|
+
for r in high_ctx
|
|
1875
|
+
)
|
|
1876
|
+
panels.append(Panel(warnings, title="[yellow bold]⚠ Context pressure[/]", expand=True))
|
|
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
|
|
1895
|
+
created = r.get("created_at") or r.get("started_at") or 0
|
|
1896
|
+
ended = r.get("completed_at") or r.get("finished_at") or 0
|
|
1897
|
+
if created and ended:
|
|
1898
|
+
secs = int(ended - created)
|
|
1899
|
+
dur = f"{secs//60}:{secs%60:02d}"
|
|
1900
|
+
elif created and status == "running":
|
|
1901
|
+
import datetime as _dt
|
|
1902
|
+
secs = int(time.time() - (created if created > 1e10 else created))
|
|
1903
|
+
dur = f"{secs//60}:{secs%60:02d}"
|
|
1904
|
+
else:
|
|
1905
|
+
dur = "—"
|
|
1906
|
+
agent_name = r.get("workflow_name") or r.get("name") or "—"
|
|
1907
|
+
project_name = r.get("project_name") or r.get("project") or "—"
|
|
1908
|
+
trigger = r.get("trigger_type") or r.get("triggered_by") or "manual"
|
|
1909
|
+
runs_tbl.add_row(
|
|
1910
|
+
agent_name[:28],
|
|
1911
|
+
Text(f"{sicon} {status}", style=scolor),
|
|
1912
|
+
dur,
|
|
1913
|
+
project_name[:18],
|
|
1914
|
+
trigger[:12],
|
|
1915
|
+
)
|
|
1916
|
+
else:
|
|
1917
|
+
runs_tbl.add_row("[dim]No runs yet or not connected[/]", "", "", "", "")
|
|
1918
|
+
|
|
1919
|
+
panels.append(Panel(runs_tbl, title="[bold green]▶ Agent Runs[/]", expand=True))
|
|
1920
|
+
|
|
1921
|
+
# Team Activity panel (Guard)
|
|
1922
|
+
team_data = _fetch_guard_activity(cfg, guard_cfg)
|
|
1923
|
+
team_tbl = Table(box=box.SIMPLE, expand=True, show_header=True, header_style="bold white")
|
|
1924
|
+
team_tbl.add_column("Developer", min_width=28)
|
|
1925
|
+
team_tbl.add_column("Calls", width=8, justify="right")
|
|
1926
|
+
team_tbl.add_column("Blocked", width=8, justify="right")
|
|
1927
|
+
team_tbl.add_column("Tokens", width=10, justify="right")
|
|
1928
|
+
team_tbl.add_column("Guard", width=7, justify="center")
|
|
1929
|
+
|
|
1930
|
+
if team_data:
|
|
1931
|
+
for d in team_data:
|
|
1932
|
+
blocked_text = Text(str(d["blocked"]), style="red bold" if d["blocked"] else "dim")
|
|
1933
|
+
guard_text = Text("✓", style="green")
|
|
1934
|
+
team_tbl.add_row(
|
|
1935
|
+
d["email"][:32],
|
|
1936
|
+
str(d["calls"]),
|
|
1937
|
+
blocked_text,
|
|
1938
|
+
_fmt_tokens(d["tokens"]),
|
|
1939
|
+
guard_text,
|
|
1940
|
+
)
|
|
1941
|
+
else:
|
|
1942
|
+
team_tbl.add_row("[dim]No team activity or not connected[/]", "", "", "", "")
|
|
1943
|
+
|
|
1944
|
+
panels.append(Panel(team_tbl, title="[bold magenta]👥 Team Activity (Guard)[/]", expand=True))
|
|
1945
|
+
|
|
1946
|
+
from rich.console import Group
|
|
1947
|
+
return Group(*panels)
|
|
1948
|
+
|
|
1949
|
+
# Try rich.live first (works without raw TTY)
|
|
1950
|
+
try:
|
|
1951
|
+
from rich.live import Live
|
|
1952
|
+
from rich.console import Console
|
|
1953
|
+
console = Console()
|
|
1954
|
+
with Live(console=console, refresh_per_second=0.2, screen=True) as live:
|
|
1955
|
+
while True:
|
|
1956
|
+
live.update(_build_rich_display(_load_sessions()))
|
|
1957
|
+
time.sleep(5)
|
|
1958
|
+
return
|
|
1959
|
+
except KeyboardInterrupt:
|
|
1960
|
+
return
|
|
1961
|
+
except ImportError:
|
|
1962
|
+
pass # fall through to ANSI loop
|
|
1963
|
+
|
|
1964
|
+
# ANSI fallback — requires real TTY
|
|
1965
|
+
if not sys.stdin.isatty():
|
|
1966
|
+
print(f"{YELLOW}TUI requires a real terminal. Run directly in your shell, not via a subprocess.{RESET}")
|
|
1832
1967
|
print(_render_table(rows))
|
|
1968
|
+
return
|
|
1969
|
+
|
|
1970
|
+
import shutil, signal, select, tty, termios
|
|
1971
|
+
stop = False
|
|
1972
|
+
def _sig(s, f): nonlocal stop; stop = True
|
|
1973
|
+
signal.signal(signal.SIGINT, _sig)
|
|
1974
|
+
|
|
1975
|
+
def _frame(rows):
|
|
1976
|
+
cols, _ = shutil.get_terminal_size((120, 40))
|
|
1977
|
+
title = " conduct sessions (Ctrl+C or q to quit) "
|
|
1978
|
+
pad = max(0, cols - len(title))
|
|
1979
|
+
active = sum(1 for r in rows if r["alive"])
|
|
1980
|
+
guard_on = sum(1 for r in rows if r["guard"])
|
|
1981
|
+
out = [
|
|
1982
|
+
f"{BOLD}\033[44m{title}{' ' * pad}\033[0m",
|
|
1983
|
+
f"\n {BOLD}{active}{RESET} active · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s\n",
|
|
1984
|
+
_render_table(rows),
|
|
1985
|
+
]
|
|
1986
|
+
high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
|
|
1987
|
+
if high:
|
|
1988
|
+
out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
|
|
1989
|
+
for r in high:
|
|
1990
|
+
c = RED if r["ctx_pct"] >= 80 else YELLOW
|
|
1991
|
+
out.append(f" {c}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
|
|
1992
|
+
sys.stdout.write("\033[2J\033[H" + "\n".join(out))
|
|
1993
|
+
sys.stdout.flush()
|
|
1994
|
+
|
|
1995
|
+
old = termios.tcgetattr(sys.stdin)
|
|
1996
|
+
try:
|
|
1997
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
1998
|
+
while not stop:
|
|
1999
|
+
_frame(_load_sessions())
|
|
2000
|
+
for _ in range(50):
|
|
2001
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
2002
|
+
if sys.stdin.read(1) in ("q", "Q"):
|
|
2003
|
+
stop = True
|
|
2004
|
+
break
|
|
2005
|
+
if stop:
|
|
2006
|
+
break
|
|
2007
|
+
finally:
|
|
2008
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
2009
|
+
sys.stdout.write("\033[2J\033[H")
|
|
2010
|
+
print("conduct sessions exited.")
|
|
1833
2011
|
|
|
1834
2012
|
|
|
1835
2013
|
def cmd_sessions(args):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.46
|
|
4
4
|
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
5
5
|
Author-email: Conduct AI <hello@conductai.ai>
|
|
6
6
|
License: MIT
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
24
25
|
|
|
25
26
|
# conduct-cli
|
|
26
27
|
|
|
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
|