conduct-cli 0.4.43__tar.gz → 0.4.45__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.43 → conduct_cli-0.4.45}/PKG-INFO +2 -1
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/pyproject.toml +2 -2
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/main.py +252 -76
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/PKG-INFO +2 -1
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/requires.txt +1 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/README.md +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/setup.cfg +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/setup.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.45}/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.45
|
|
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.45"
|
|
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"
|
|
@@ -1460,6 +1460,7 @@ def cmd_whoami(args):
|
|
|
1460
1460
|
|
|
1461
1461
|
_CLAUDE_SESSIONS = Path.home() / ".claude" / "sessions"
|
|
1462
1462
|
_CLAUDE_PROJECTS = Path.home() / ".claude" / "projects"
|
|
1463
|
+
_CODEX_SESSIONS = Path.home() / ".codex" / "sessions"
|
|
1463
1464
|
|
|
1464
1465
|
# Context window limits by model prefix (tokens)
|
|
1465
1466
|
_CTX_LIMITS = {
|
|
@@ -1539,6 +1540,116 @@ def _session_stats(session_id: str, project_dir: Path) -> dict:
|
|
|
1539
1540
|
}
|
|
1540
1541
|
|
|
1541
1542
|
|
|
1543
|
+
def _codex_running_cwds() -> set[str]:
|
|
1544
|
+
"""Return cwds of any running codex processes via /proc or ps."""
|
|
1545
|
+
cwds: set[str] = set()
|
|
1546
|
+
try:
|
|
1547
|
+
import subprocess as _sp
|
|
1548
|
+
out = _sp.run(["ps", "aux"], capture_output=True, text=True).stdout
|
|
1549
|
+
for line in out.splitlines():
|
|
1550
|
+
if "codex" in line and "grep" not in line:
|
|
1551
|
+
# Extract cwd from lsof for each codex PID
|
|
1552
|
+
parts = line.split()
|
|
1553
|
+
if parts:
|
|
1554
|
+
pid = parts[1]
|
|
1555
|
+
try:
|
|
1556
|
+
r = _sp.run(["lsof", "-p", pid, "-a", "-d", "cwd", "-Fn"],
|
|
1557
|
+
capture_output=True, text=True, timeout=1)
|
|
1558
|
+
for l in r.stdout.splitlines():
|
|
1559
|
+
if l.startswith("n"):
|
|
1560
|
+
cwds.add(l[1:])
|
|
1561
|
+
except Exception:
|
|
1562
|
+
pass
|
|
1563
|
+
except Exception:
|
|
1564
|
+
pass
|
|
1565
|
+
return cwds
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def _codex_session_stats(jsonl_path: Path) -> dict:
|
|
1569
|
+
"""Parse a Codex JSONL session file for model, ctx window, and turn count."""
|
|
1570
|
+
model = ""
|
|
1571
|
+
ctx_limit = 0
|
|
1572
|
+
turns = 0
|
|
1573
|
+
cwd = ""
|
|
1574
|
+
try:
|
|
1575
|
+
for raw in jsonl_path.read_bytes().splitlines():
|
|
1576
|
+
try:
|
|
1577
|
+
entry = json.loads(raw)
|
|
1578
|
+
except Exception:
|
|
1579
|
+
continue
|
|
1580
|
+
t = entry.get("type", "")
|
|
1581
|
+
p = entry.get("payload", {})
|
|
1582
|
+
if t == "session_meta":
|
|
1583
|
+
cwd = p.get("cwd", "")
|
|
1584
|
+
if t == "turn_context":
|
|
1585
|
+
model = p.get("model", model)
|
|
1586
|
+
turns += 1
|
|
1587
|
+
if t == "event_msg" and not ctx_limit:
|
|
1588
|
+
ctx_limit = p.get("model_context_window", 0)
|
|
1589
|
+
except Exception:
|
|
1590
|
+
pass
|
|
1591
|
+
|
|
1592
|
+
return {"model": model, "ctx_limit": ctx_limit, "turns": turns, "cwd": cwd}
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _load_codex_sessions(active_cwds: set[str]) -> list[dict]:
|
|
1596
|
+
"""Find recent Codex sessions from ~/.codex/sessions/YYYY/MM/DD/."""
|
|
1597
|
+
if not _CODEX_SESSIONS.exists():
|
|
1598
|
+
return []
|
|
1599
|
+
|
|
1600
|
+
guard_cfg = Path.home() / ".conductguard" / "config.json"
|
|
1601
|
+
guard_on = guard_cfg.exists()
|
|
1602
|
+
|
|
1603
|
+
rows = []
|
|
1604
|
+
# Walk the last 2 days of session dirs
|
|
1605
|
+
from datetime import datetime, timedelta
|
|
1606
|
+
today = datetime.now()
|
|
1607
|
+
date_dirs = []
|
|
1608
|
+
for delta in (0, 1):
|
|
1609
|
+
d = today - timedelta(days=delta)
|
|
1610
|
+
date_dirs.append(_CODEX_SESSIONS / str(d.year) / f"{d.month:02d}" / f"{d.day:02d}")
|
|
1611
|
+
|
|
1612
|
+
seen: set[str] = set()
|
|
1613
|
+
for date_dir in date_dirs:
|
|
1614
|
+
if not date_dir.exists():
|
|
1615
|
+
continue
|
|
1616
|
+
for f in sorted(date_dir.iterdir(), reverse=True):
|
|
1617
|
+
if not f.suffix == ".jsonl":
|
|
1618
|
+
continue
|
|
1619
|
+
session_id = f.stem.split("-", 1)[-1] if "-" in f.stem else f.stem
|
|
1620
|
+
if session_id in seen:
|
|
1621
|
+
continue
|
|
1622
|
+
seen.add(session_id)
|
|
1623
|
+
|
|
1624
|
+
stats = _codex_session_stats(f)
|
|
1625
|
+
cwd = stats.get("cwd", "")
|
|
1626
|
+
alive = cwd in active_cwds
|
|
1627
|
+
|
|
1628
|
+
ctx_limit = stats.get("ctx_limit", 0) or 200_000
|
|
1629
|
+
# Codex doesn't expose per-turn token counts in JSONL — show turns only
|
|
1630
|
+
ctx_pct = 0
|
|
1631
|
+
|
|
1632
|
+
rows.append({
|
|
1633
|
+
"pid": "—",
|
|
1634
|
+
"session_id": session_id[:8],
|
|
1635
|
+
"project": Path(cwd).name if cwd else "—",
|
|
1636
|
+
"cwd": cwd,
|
|
1637
|
+
"model": stats.get("model", "—"),
|
|
1638
|
+
"turns": stats.get("turns", 0),
|
|
1639
|
+
"ctx_pct": ctx_pct,
|
|
1640
|
+
"ctx_tokens": 0,
|
|
1641
|
+
"total_in": 0,
|
|
1642
|
+
"total_out": 0,
|
|
1643
|
+
"guard": guard_on,
|
|
1644
|
+
"alive": alive,
|
|
1645
|
+
"kind": "codex",
|
|
1646
|
+
"started_at": int(f.stat().st_mtime * 1000),
|
|
1647
|
+
"ai": "CD",
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1650
|
+
return rows
|
|
1651
|
+
|
|
1652
|
+
|
|
1542
1653
|
def _load_sessions() -> list[dict]:
|
|
1543
1654
|
"""Read ~/.claude/sessions/*.json and join with JSONL stats."""
|
|
1544
1655
|
if not _CLAUDE_SESSIONS.exists():
|
|
@@ -1590,8 +1701,13 @@ def _load_sessions() -> list[dict]:
|
|
|
1590
1701
|
"alive": alive,
|
|
1591
1702
|
"kind": kind,
|
|
1592
1703
|
"started_at": started_at,
|
|
1704
|
+
"ai": "CC",
|
|
1593
1705
|
})
|
|
1594
1706
|
|
|
1707
|
+
# Merge Codex sessions
|
|
1708
|
+
active_cwds = _codex_running_cwds()
|
|
1709
|
+
rows += _load_codex_sessions(active_cwds)
|
|
1710
|
+
|
|
1595
1711
|
return sorted(rows, key=lambda r: r["started_at"], reverse=True)
|
|
1596
1712
|
|
|
1597
1713
|
|
|
@@ -1616,21 +1732,23 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1616
1732
|
|
|
1617
1733
|
lines = []
|
|
1618
1734
|
header = (
|
|
1619
|
-
f" {BOLD}{'PROJECT':<18} {'SESSION':<10} {'MODEL':<18} "
|
|
1735
|
+
f" {BOLD}{'AI':<4} {'PROJECT':<18} {'SESSION':<10} {'MODEL':<18} "
|
|
1620
1736
|
f"{'CTX':>14} {'TOKENS IN':>10} {'TURNS':>6} {'GUARD':>6} {'STATUS':>8}{RESET}"
|
|
1621
1737
|
)
|
|
1622
1738
|
lines.append(header)
|
|
1623
|
-
lines.append(" " + "─" *
|
|
1739
|
+
lines.append(" " + "─" * 100)
|
|
1624
1740
|
|
|
1625
1741
|
for r in rows:
|
|
1626
|
-
status_str
|
|
1627
|
-
guard_str
|
|
1628
|
-
model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"]
|
|
1742
|
+
status_str = f"{GREEN}active{RESET}" if r["alive"] else f"{GRAY}idle{RESET}"
|
|
1743
|
+
guard_str = f"{GREEN}✓{RESET}" if r["guard"] else f"{GRAY}—{RESET}"
|
|
1744
|
+
model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"] not in ("—", "") else "—"
|
|
1629
1745
|
ctx_display = _ctx_bar(r["ctx_pct"]) if r["ctx_pct"] else f"{GRAY}{'—':>14}{RESET}"
|
|
1630
1746
|
tokens_str = _fmt_tokens(r["ctx_tokens"]) if r["ctx_tokens"] else "—"
|
|
1747
|
+
ai_label = r.get("ai", "CC")
|
|
1748
|
+
ai_color = CYAN if ai_label == "CD" else BLUE
|
|
1631
1749
|
|
|
1632
1750
|
lines.append(
|
|
1633
|
-
f" {r['project']:<18} {r['session_id']:<10} {model_short:<18} "
|
|
1751
|
+
f" {ai_color}{ai_label:<4}{RESET} {r['project']:<18} {r['session_id']:<10} {model_short:<18} "
|
|
1634
1752
|
f"{ctx_display} {tokens_str:>10} {r['turns']:>6} {guard_str:>6} {status_str}"
|
|
1635
1753
|
)
|
|
1636
1754
|
|
|
@@ -1639,79 +1757,137 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1639
1757
|
|
|
1640
1758
|
|
|
1641
1759
|
def _render_tui(rows: list[dict]) -> None:
|
|
1642
|
-
"""Full-screen TUI with live refresh
|
|
1760
|
+
"""Full-screen TUI with live refresh. Uses rich.live if available, else ANSI loop."""
|
|
1761
|
+
|
|
1762
|
+
def _build_rich_display(rows):
|
|
1763
|
+
from rich.table import Table
|
|
1764
|
+
from rich.panel import Panel
|
|
1765
|
+
from rich.columns import Columns
|
|
1766
|
+
from rich import box
|
|
1767
|
+
from rich.text import Text
|
|
1768
|
+
|
|
1769
|
+
active = sum(1 for r in rows if r["alive"])
|
|
1770
|
+
guard_on = sum(1 for r in rows if r["guard"])
|
|
1771
|
+
high_ctx = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
|
|
1772
|
+
|
|
1773
|
+
# Main sessions table
|
|
1774
|
+
tbl = Table(box=box.ROUNDED, expand=True, show_header=True, header_style="bold white")
|
|
1775
|
+
tbl.add_column("AI", width=4, style="bold")
|
|
1776
|
+
tbl.add_column("Project", min_width=14)
|
|
1777
|
+
tbl.add_column("Session", width=10, style="dim")
|
|
1778
|
+
tbl.add_column("Model", min_width=14)
|
|
1779
|
+
tbl.add_column("CTX", width=16)
|
|
1780
|
+
tbl.add_column("Tokens", width=10, justify="right")
|
|
1781
|
+
tbl.add_column("Turns", width=6, justify="right")
|
|
1782
|
+
tbl.add_column("Guard", width=6, justify="center")
|
|
1783
|
+
tbl.add_column("Status", width=8)
|
|
1784
|
+
|
|
1785
|
+
for r in rows:
|
|
1786
|
+
ai_text = Text(r.get("ai", "CC"), style="bold cyan" if r.get("ai") == "CD" else "bold blue")
|
|
1787
|
+
status_text = Text("active", style="green") if r["alive"] else Text("idle", style="dim")
|
|
1788
|
+
guard_text = Text("✓", style="green") if r["guard"] else Text("—", style="dim")
|
|
1789
|
+
model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"] not in ("—", "") else "—"
|
|
1790
|
+
|
|
1791
|
+
pct = r["ctx_pct"]
|
|
1792
|
+
if pct:
|
|
1793
|
+
filled = round(pct / 100 * 10)
|
|
1794
|
+
bar = "█" * filled + "░" * (10 - filled)
|
|
1795
|
+
color = "red" if pct >= 80 else "yellow" if pct >= 60 else "green"
|
|
1796
|
+
ctx_text = Text(f"{bar} {pct}%", style=color)
|
|
1797
|
+
else:
|
|
1798
|
+
ctx_text = Text("—", style="dim")
|
|
1799
|
+
|
|
1800
|
+
tbl.add_row(
|
|
1801
|
+
ai_text,
|
|
1802
|
+
r["project"],
|
|
1803
|
+
r["session_id"],
|
|
1804
|
+
model_short,
|
|
1805
|
+
ctx_text,
|
|
1806
|
+
_fmt_tokens(r["ctx_tokens"]) if r["ctx_tokens"] else "—",
|
|
1807
|
+
str(r["turns"]),
|
|
1808
|
+
guard_text,
|
|
1809
|
+
status_text,
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
# Summary header
|
|
1813
|
+
summary = f"[bold]{active}[/] active · [bold]{guard_on}[/] with Guard · [dim]refreshing every 5s — Ctrl+C to quit[/]"
|
|
1814
|
+
|
|
1815
|
+
panels = [Panel(tbl, title="[bold blue]conduct sessions[/]", subtitle=summary, expand=True)]
|
|
1816
|
+
|
|
1817
|
+
# Context pressure panel
|
|
1818
|
+
if high_ctx:
|
|
1819
|
+
warnings = "\n".join(
|
|
1820
|
+
f"[{'red' if r['ctx_pct'] >= 80 else 'yellow'}]{r['project']}[/] "
|
|
1821
|
+
f"[dim]{r['session_id']}[/] {r['ctx_pct']}% — consider /compact"
|
|
1822
|
+
for r in high_ctx
|
|
1823
|
+
)
|
|
1824
|
+
panels.append(Panel(warnings, title="[yellow bold]⚠ Context pressure[/]", expand=True))
|
|
1825
|
+
|
|
1826
|
+
from rich.console import Group
|
|
1827
|
+
return Group(*panels)
|
|
1828
|
+
|
|
1829
|
+
# Try rich.live first (works without raw TTY)
|
|
1643
1830
|
try:
|
|
1644
|
-
import
|
|
1645
|
-
import
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
def _render_frame(rows):
|
|
1658
|
-
cols, _ = shutil.get_terminal_size((120, 40))
|
|
1659
|
-
out = []
|
|
1660
|
-
|
|
1661
|
-
# Header bar
|
|
1662
|
-
title = " conduct sessions (TUI — q or Ctrl+C to quit) "
|
|
1663
|
-
pad = max(0, cols - len(title))
|
|
1664
|
-
out.append(f"{BOLD}\033[44m{title}{' ' * pad}\033[0m")
|
|
1665
|
-
out.append("")
|
|
1666
|
-
|
|
1667
|
-
# Summary line
|
|
1668
|
-
active = sum(1 for r in rows if r["alive"])
|
|
1669
|
-
guard_on = sum(1 for r in rows if r["guard"])
|
|
1670
|
-
out.append(f" {BOLD}{active}{RESET} active session(s) · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s")
|
|
1671
|
-
out.append("")
|
|
1672
|
-
|
|
1673
|
-
# Table
|
|
1674
|
-
out.append(_render_table(rows))
|
|
1675
|
-
|
|
1676
|
-
# Context pressure panel — sessions above 60%
|
|
1677
|
-
high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
|
|
1678
|
-
if high:
|
|
1679
|
-
out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
|
|
1680
|
-
for r in high:
|
|
1681
|
-
color = RED if r["ctx_pct"] >= 80 else YELLOW
|
|
1682
|
-
out.append(f" {color}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
|
|
1683
|
-
out.append("")
|
|
1684
|
-
|
|
1685
|
-
sys.stdout.write("\n".join(out))
|
|
1686
|
-
sys.stdout.flush()
|
|
1687
|
-
|
|
1688
|
-
# Use raw terminal input for 'q' detection (non-blocking)
|
|
1689
|
-
import select, tty, termios
|
|
1690
|
-
old = termios.tcgetattr(sys.stdin)
|
|
1691
|
-
try:
|
|
1692
|
-
tty.setcbreak(sys.stdin.fileno())
|
|
1693
|
-
while not stop:
|
|
1694
|
-
_clear()
|
|
1695
|
-
rows = _load_sessions()
|
|
1696
|
-
_render_frame(rows)
|
|
1697
|
-
# Wait up to 5s, exit on 'q'
|
|
1698
|
-
for _ in range(50):
|
|
1699
|
-
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
1700
|
-
ch = sys.stdin.read(1)
|
|
1701
|
-
if ch in ("q", "Q"):
|
|
1702
|
-
stop = True
|
|
1703
|
-
break
|
|
1704
|
-
if stop:
|
|
1705
|
-
break
|
|
1706
|
-
finally:
|
|
1707
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
1708
|
-
_clear()
|
|
1709
|
-
print("conduct sessions exited.")
|
|
1831
|
+
from rich.live import Live
|
|
1832
|
+
from rich.console import Console
|
|
1833
|
+
console = Console()
|
|
1834
|
+
with Live(console=console, refresh_per_second=0.2, screen=True) as live:
|
|
1835
|
+
while True:
|
|
1836
|
+
live.update(_build_rich_display(_load_sessions()))
|
|
1837
|
+
time.sleep(5)
|
|
1838
|
+
return
|
|
1839
|
+
except KeyboardInterrupt:
|
|
1840
|
+
return
|
|
1841
|
+
except ImportError:
|
|
1842
|
+
pass # fall through to ANSI loop
|
|
1710
1843
|
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
print(f"{YELLOW}TUI
|
|
1844
|
+
# ANSI fallback — requires real TTY
|
|
1845
|
+
if not sys.stdin.isatty():
|
|
1846
|
+
print(f"{YELLOW}TUI requires a real terminal. Run directly in your shell, not via a subprocess.{RESET}")
|
|
1714
1847
|
print(_render_table(rows))
|
|
1848
|
+
return
|
|
1849
|
+
|
|
1850
|
+
import shutil, signal, select, tty, termios
|
|
1851
|
+
stop = False
|
|
1852
|
+
def _sig(s, f): nonlocal stop; stop = True
|
|
1853
|
+
signal.signal(signal.SIGINT, _sig)
|
|
1854
|
+
|
|
1855
|
+
def _frame(rows):
|
|
1856
|
+
cols, _ = shutil.get_terminal_size((120, 40))
|
|
1857
|
+
title = " conduct sessions (Ctrl+C or q to quit) "
|
|
1858
|
+
pad = max(0, cols - len(title))
|
|
1859
|
+
active = sum(1 for r in rows if r["alive"])
|
|
1860
|
+
guard_on = sum(1 for r in rows if r["guard"])
|
|
1861
|
+
out = [
|
|
1862
|
+
f"{BOLD}\033[44m{title}{' ' * pad}\033[0m",
|
|
1863
|
+
f"\n {BOLD}{active}{RESET} active · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s\n",
|
|
1864
|
+
_render_table(rows),
|
|
1865
|
+
]
|
|
1866
|
+
high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
|
|
1867
|
+
if high:
|
|
1868
|
+
out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
|
|
1869
|
+
for r in high:
|
|
1870
|
+
c = RED if r["ctx_pct"] >= 80 else YELLOW
|
|
1871
|
+
out.append(f" {c}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
|
|
1872
|
+
sys.stdout.write("\033[2J\033[H" + "\n".join(out))
|
|
1873
|
+
sys.stdout.flush()
|
|
1874
|
+
|
|
1875
|
+
old = termios.tcgetattr(sys.stdin)
|
|
1876
|
+
try:
|
|
1877
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
1878
|
+
while not stop:
|
|
1879
|
+
_frame(_load_sessions())
|
|
1880
|
+
for _ in range(50):
|
|
1881
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
1882
|
+
if sys.stdin.read(1) in ("q", "Q"):
|
|
1883
|
+
stop = True
|
|
1884
|
+
break
|
|
1885
|
+
if stop:
|
|
1886
|
+
break
|
|
1887
|
+
finally:
|
|
1888
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
|
|
1889
|
+
sys.stdout.write("\033[2J\033[H")
|
|
1890
|
+
print("conduct sessions exited.")
|
|
1715
1891
|
|
|
1716
1892
|
|
|
1717
1893
|
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.45
|
|
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
|