conduct-cli 0.4.43__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.43 → conduct_cli-0.4.44}/PKG-INFO +1 -1
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/pyproject.toml +1 -1
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/main.py +124 -6
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/README.md +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/setup.cfg +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/setup.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.43 → conduct_cli-0.4.44}/tests/test_switch.py +0 -0
|
@@ -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
|
|
|
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
|