conduct-cli 0.4.42__tar.gz → 0.4.44__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.42 → conduct_cli-0.4.44}/PKG-INFO +1 -1
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/pyproject.toml +1 -1
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/main.py +412 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/README.md +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/setup.cfg +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/setup.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.42 → conduct_cli-0.4.44}/tests/test_switch.py +0 -0
|
@@ -1458,6 +1458,412 @@ def cmd_whoami(args):
|
|
|
1458
1458
|
print()
|
|
1459
1459
|
|
|
1460
1460
|
|
|
1461
|
+
_CLAUDE_SESSIONS = Path.home() / ".claude" / "sessions"
|
|
1462
|
+
_CLAUDE_PROJECTS = Path.home() / ".claude" / "projects"
|
|
1463
|
+
_CODEX_SESSIONS = Path.home() / ".codex" / "sessions"
|
|
1464
|
+
|
|
1465
|
+
# Context window limits by model prefix (tokens)
|
|
1466
|
+
_CTX_LIMITS = {
|
|
1467
|
+
"claude-opus-4": 200_000,
|
|
1468
|
+
"claude-sonnet-4": 200_000,
|
|
1469
|
+
"claude-haiku-4": 200_000,
|
|
1470
|
+
"claude-opus-3": 200_000,
|
|
1471
|
+
"claude-sonnet-3": 200_000,
|
|
1472
|
+
"claude-haiku-3": 200_000,
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
def _ctx_limit(model: str) -> int:
|
|
1476
|
+
for prefix, limit in _CTX_LIMITS.items():
|
|
1477
|
+
if model.startswith(prefix):
|
|
1478
|
+
return limit
|
|
1479
|
+
return 200_000
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _is_alive(pid: int) -> bool:
|
|
1483
|
+
try:
|
|
1484
|
+
os.kill(pid, 0)
|
|
1485
|
+
return True
|
|
1486
|
+
except OSError:
|
|
1487
|
+
return False
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
def _session_stats(session_id: str, project_dir: Path) -> dict:
|
|
1491
|
+
"""Parse the tail of a session JSONL for model, token counts, and turn count."""
|
|
1492
|
+
jsonl = project_dir / f"{session_id}.jsonl"
|
|
1493
|
+
if not jsonl.exists():
|
|
1494
|
+
return {}
|
|
1495
|
+
|
|
1496
|
+
model = ""
|
|
1497
|
+
total_input = 0
|
|
1498
|
+
total_output = 0
|
|
1499
|
+
turns = 0
|
|
1500
|
+
last_usage: dict = {}
|
|
1501
|
+
|
|
1502
|
+
try:
|
|
1503
|
+
lines = jsonl.read_bytes().splitlines()
|
|
1504
|
+
# Scan last 300 lines for efficiency
|
|
1505
|
+
for raw in lines[-300:]:
|
|
1506
|
+
try:
|
|
1507
|
+
entry = json.loads(raw)
|
|
1508
|
+
except Exception:
|
|
1509
|
+
continue
|
|
1510
|
+
msg = entry.get("message", {})
|
|
1511
|
+
if not isinstance(msg, dict):
|
|
1512
|
+
continue
|
|
1513
|
+
if msg.get("role") == "assistant":
|
|
1514
|
+
turns += 1
|
|
1515
|
+
if msg.get("model"):
|
|
1516
|
+
model = msg["model"]
|
|
1517
|
+
usage = msg.get("usage", {})
|
|
1518
|
+
if usage:
|
|
1519
|
+
last_usage = usage
|
|
1520
|
+
if msg.get("role") == "assistant" and "usage" in msg:
|
|
1521
|
+
u = msg["usage"]
|
|
1522
|
+
total_input += u.get("input_tokens", 0) + u.get("cache_read_input_tokens", 0)
|
|
1523
|
+
total_output += u.get("output_tokens", 0)
|
|
1524
|
+
except Exception:
|
|
1525
|
+
pass
|
|
1526
|
+
|
|
1527
|
+
cache_read = last_usage.get("cache_read_input_tokens", 0)
|
|
1528
|
+
fresh_in = last_usage.get("input_tokens", 0)
|
|
1529
|
+
ctx_tokens = cache_read + fresh_in
|
|
1530
|
+
limit = _ctx_limit(model)
|
|
1531
|
+
ctx_pct = round(ctx_tokens / limit * 100) if limit else 0
|
|
1532
|
+
|
|
1533
|
+
return {
|
|
1534
|
+
"model": model,
|
|
1535
|
+
"turns": turns,
|
|
1536
|
+
"ctx_tokens": ctx_tokens,
|
|
1537
|
+
"ctx_pct": ctx_pct,
|
|
1538
|
+
"total_in": total_input,
|
|
1539
|
+
"total_out": total_output,
|
|
1540
|
+
}
|
|
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
|
+
|
|
1653
|
+
def _load_sessions() -> list[dict]:
|
|
1654
|
+
"""Read ~/.claude/sessions/*.json and join with JSONL stats."""
|
|
1655
|
+
if not _CLAUDE_SESSIONS.exists():
|
|
1656
|
+
return []
|
|
1657
|
+
|
|
1658
|
+
guard_cfg = Path.home() / ".conductguard" / "config.json"
|
|
1659
|
+
guard_on = guard_cfg.exists()
|
|
1660
|
+
|
|
1661
|
+
rows = []
|
|
1662
|
+
seen_sessions: set[str] = set()
|
|
1663
|
+
for f in sorted(_CLAUDE_SESSIONS.iterdir()):
|
|
1664
|
+
if not f.suffix == ".json":
|
|
1665
|
+
continue
|
|
1666
|
+
try:
|
|
1667
|
+
s = json.loads(f.read_text())
|
|
1668
|
+
except Exception:
|
|
1669
|
+
continue
|
|
1670
|
+
|
|
1671
|
+
pid = s.get("pid", 0)
|
|
1672
|
+
session_id = s.get("sessionId", "")
|
|
1673
|
+
cwd = s.get("cwd", "")
|
|
1674
|
+
started_at = s.get("startedAt", 0)
|
|
1675
|
+
kind = s.get("kind", "")
|
|
1676
|
+
|
|
1677
|
+
if session_id in seen_sessions:
|
|
1678
|
+
continue
|
|
1679
|
+
seen_sessions.add(session_id)
|
|
1680
|
+
|
|
1681
|
+
alive = _is_alive(pid)
|
|
1682
|
+
|
|
1683
|
+
# Claude names project dirs by replacing / with - (keeping leading -)
|
|
1684
|
+
project_key = cwd.replace("/", "-").replace("\\", "-")
|
|
1685
|
+
project_dir = _CLAUDE_PROJECTS / project_key
|
|
1686
|
+
|
|
1687
|
+
stats = _session_stats(session_id, project_dir) if project_dir.exists() else {}
|
|
1688
|
+
|
|
1689
|
+
rows.append({
|
|
1690
|
+
"pid": pid,
|
|
1691
|
+
"session_id": session_id[:8],
|
|
1692
|
+
"project": Path(cwd).name if cwd else "—",
|
|
1693
|
+
"cwd": cwd,
|
|
1694
|
+
"model": stats.get("model", "—"),
|
|
1695
|
+
"turns": stats.get("turns", 0),
|
|
1696
|
+
"ctx_pct": stats.get("ctx_pct", 0),
|
|
1697
|
+
"ctx_tokens": stats.get("ctx_tokens", 0),
|
|
1698
|
+
"total_in": stats.get("total_in", 0),
|
|
1699
|
+
"total_out": stats.get("total_out", 0),
|
|
1700
|
+
"guard": guard_on,
|
|
1701
|
+
"alive": alive,
|
|
1702
|
+
"kind": kind,
|
|
1703
|
+
"started_at": started_at,
|
|
1704
|
+
"ai": "CC",
|
|
1705
|
+
})
|
|
1706
|
+
|
|
1707
|
+
# Merge Codex sessions
|
|
1708
|
+
active_cwds = _codex_running_cwds()
|
|
1709
|
+
rows += _load_codex_sessions(active_cwds)
|
|
1710
|
+
|
|
1711
|
+
return sorted(rows, key=lambda r: r["started_at"], reverse=True)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def _fmt_tokens(n: int) -> str:
|
|
1715
|
+
if n >= 1_000_000:
|
|
1716
|
+
return f"{n/1_000_000:.1f}M"
|
|
1717
|
+
if n >= 1_000:
|
|
1718
|
+
return f"{n/1_000:.1f}k"
|
|
1719
|
+
return str(n)
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def _ctx_bar(pct: int, width: int = 10) -> str:
|
|
1723
|
+
filled = round(pct / 100 * width)
|
|
1724
|
+
bar = "█" * filled + "░" * (width - filled)
|
|
1725
|
+
color = RED if pct >= 80 else YELLOW if pct >= 60 else GREEN
|
|
1726
|
+
return f"{color}{bar}{RESET} {pct}%"
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _render_table(rows: list[dict]) -> str:
|
|
1730
|
+
if not rows:
|
|
1731
|
+
return f"\n{GRAY} No Claude Code sessions found.{RESET}\n"
|
|
1732
|
+
|
|
1733
|
+
lines = []
|
|
1734
|
+
header = (
|
|
1735
|
+
f" {BOLD}{'AI':<4} {'PROJECT':<18} {'SESSION':<10} {'MODEL':<18} "
|
|
1736
|
+
f"{'CTX':>14} {'TOKENS IN':>10} {'TURNS':>6} {'GUARD':>6} {'STATUS':>8}{RESET}"
|
|
1737
|
+
)
|
|
1738
|
+
lines.append(header)
|
|
1739
|
+
lines.append(" " + "─" * 100)
|
|
1740
|
+
|
|
1741
|
+
for r in rows:
|
|
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 "—"
|
|
1745
|
+
ctx_display = _ctx_bar(r["ctx_pct"]) if r["ctx_pct"] else f"{GRAY}{'—':>14}{RESET}"
|
|
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
|
|
1749
|
+
|
|
1750
|
+
lines.append(
|
|
1751
|
+
f" {ai_color}{ai_label:<4}{RESET} {r['project']:<18} {r['session_id']:<10} {model_short:<18} "
|
|
1752
|
+
f"{ctx_display} {tokens_str:>10} {r['turns']:>6} {guard_str:>6} {status_str}"
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
lines.append("")
|
|
1756
|
+
return "\n".join(lines)
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _render_tui(rows: list[dict]) -> None:
|
|
1760
|
+
"""Full-screen TUI with live refresh using ANSI escape codes."""
|
|
1761
|
+
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)
|
|
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.")
|
|
1828
|
+
|
|
1829
|
+
except Exception as e:
|
|
1830
|
+
# Graceful fallback to table if TUI setup fails
|
|
1831
|
+
print(f"{YELLOW}TUI unavailable ({e}), showing table:{RESET}")
|
|
1832
|
+
print(_render_table(rows))
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
def cmd_sessions(args):
|
|
1836
|
+
tui = getattr(args, "tui", False)
|
|
1837
|
+
watch = getattr(args, "watch", False)
|
|
1838
|
+
|
|
1839
|
+
if tui:
|
|
1840
|
+
_render_tui(_load_sessions())
|
|
1841
|
+
return
|
|
1842
|
+
|
|
1843
|
+
if watch:
|
|
1844
|
+
try:
|
|
1845
|
+
import signal
|
|
1846
|
+
stop = False
|
|
1847
|
+
def _sig(s, f): nonlocal stop; stop = True
|
|
1848
|
+
signal.signal(signal.SIGINT, _sig)
|
|
1849
|
+
while not stop:
|
|
1850
|
+
sys.stdout.write("\033[2J\033[H")
|
|
1851
|
+
sys.stdout.flush()
|
|
1852
|
+
rows = _load_sessions()
|
|
1853
|
+
print(f"\n{BOLD}conduct sessions{RESET} {GRAY}(Ctrl+C to stop · refreshing every 5s){RESET}")
|
|
1854
|
+
print(_render_table(rows))
|
|
1855
|
+
for _ in range(50):
|
|
1856
|
+
time.sleep(0.1)
|
|
1857
|
+
if stop: break
|
|
1858
|
+
except KeyboardInterrupt:
|
|
1859
|
+
pass
|
|
1860
|
+
return
|
|
1861
|
+
|
|
1862
|
+
rows = _load_sessions()
|
|
1863
|
+
print(f"\n{BOLD}conduct sessions{RESET}")
|
|
1864
|
+
print(_render_table(rows))
|
|
1865
|
+
|
|
1866
|
+
|
|
1461
1867
|
def cmd_run(args):
|
|
1462
1868
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
1463
1869
|
json_h = api.headers(workspace_id, token, "application/json", api_key)
|
|
@@ -1624,6 +2030,10 @@ def main():
|
|
|
1624
2030
|
guard_p, _guard_sub = _guard.register_guard_parser(sub)
|
|
1625
2031
|
|
|
1626
2032
|
# conduct mcp
|
|
2033
|
+
sessions_p = sub.add_parser("sessions", help="Show active Claude Code / Codex sessions")
|
|
2034
|
+
sessions_p.add_argument("--watch", action="store_true", help="Refresh every 5s (table view)")
|
|
2035
|
+
sessions_p.add_argument("--tui", action="store_true", help="Full-screen TUI with live panels")
|
|
2036
|
+
|
|
1627
2037
|
mcp_p = sub.add_parser("mcp", help="Manage the Conduct MCP server")
|
|
1628
2038
|
mcp_sub = mcp_p.add_subparsers(dest="mcp_command")
|
|
1629
2039
|
mcp_sub.add_parser("install", help="Register conduct-mcp in Claude Code and Codex")
|
|
@@ -1676,6 +2086,8 @@ def main():
|
|
|
1676
2086
|
cmd_switch(args)
|
|
1677
2087
|
elif args.command == "whoami":
|
|
1678
2088
|
cmd_whoami(args)
|
|
2089
|
+
elif args.command == "sessions":
|
|
2090
|
+
cmd_sessions(args)
|
|
1679
2091
|
elif args.command == "guard":
|
|
1680
2092
|
_guard.dispatch_guard(args, guard_p)
|
|
1681
2093
|
elif args.command == "mcp":
|
|
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
|