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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.43
3
+ Version: 0.4.44
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.4.43"
7
+ version = "0.4.44"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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(" " + "─" * 95)
1739
+ lines.append(" " + "─" * 100)
1624
1740
 
1625
1741
  for r in rows:
1626
- status_str = f"{GREEN}active{RESET}" if r["alive"] else f"{GRAY}idle{RESET}"
1627
- guard_str = f"{GREEN}✓{RESET}" if r["guard"] else f"{GRAY}—{RESET}"
1628
- model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"] != "—" else "—"
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.43
3
+ Version: 0.4.44
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
File without changes
File without changes
File without changes