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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.44
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.44"
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 _render_tui(rows: list[dict]) -> None:
1760
- """Full-screen TUI with live refresh using ANSI escape codes."""
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
- import shutil
1763
- import signal
1764
-
1765
- stop = False
1766
- def _sigint(sig, frame):
1767
- nonlocal stop
1768
- stop = True
1769
- signal.signal(signal.SIGINT, _sigint)
1770
-
1771
- def _clear():
1772
- sys.stdout.write("\033[2J\033[H")
1773
- sys.stdout.flush()
1774
-
1775
- def _render_frame(rows):
1776
- cols, _ = shutil.get_terminal_size((120, 40))
1777
- out = []
1778
-
1779
- # Header bar
1780
- title = " conduct sessions (TUI q or Ctrl+C to quit) "
1781
- pad = max(0, cols - len(title))
1782
- out.append(f"{BOLD}\033[44m{title}{' ' * pad}\033[0m")
1783
- out.append("")
1784
-
1785
- # Summary line
1786
- active = sum(1 for r in rows if r["alive"])
1787
- guard_on = sum(1 for r in rows if r["guard"])
1788
- out.append(f" {BOLD}{active}{RESET} active session(s) · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s")
1789
- out.append("")
1790
-
1791
- # Table
1792
- out.append(_render_table(rows))
1793
-
1794
- # Context pressure panel — sessions above 60%
1795
- high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
1796
- if high:
1797
- out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
1798
- for r in high:
1799
- color = RED if r["ctx_pct"] >= 80 else YELLOW
1800
- out.append(f" {color}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
1801
- out.append("")
1802
-
1803
- sys.stdout.write("\n".join(out))
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
- tty.setcbreak(sys.stdin.fileno())
1811
- while not stop:
1812
- _clear()
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
- except Exception as e:
1830
- # Graceful fallback to table if TUI setup fails
1831
- print(f"{YELLOW}TUI unavailable ({e}), showing table:{RESET}")
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.44
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