conduct-cli 0.4.37__tar.gz → 0.4.40__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.37
3
+ Version: 0.4.40
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.37"
7
+ version = "0.4.40"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -446,6 +446,144 @@ if __name__ == "__main__":
446
446
  main()
447
447
  '''
448
448
 
449
+ _PRECOMPACT_HOOK_SCRIPT = '''\
450
+ #!/usr/bin/env python3
451
+ """ConductGuard PreCompact hook — persists session context before compaction."""
452
+ import json
453
+ import os
454
+ import subprocess
455
+ import sys
456
+ from datetime import datetime, timezone
457
+ from pathlib import Path
458
+
459
+ GUARD_DIR = Path.home() / ".conductguard"
460
+ SNAPSHOT_PATH = GUARD_DIR / "session_snapshot.json"
461
+
462
+
463
+ def _git(cmd):
464
+ try:
465
+ return subprocess.check_output(
466
+ ["git"] + cmd, stderr=subprocess.DEVNULL, text=True, timeout=3
467
+ ).strip()
468
+ except Exception:
469
+ return ""
470
+
471
+
472
+ def _guard_status():
473
+ try:
474
+ out = subprocess.check_output(
475
+ ["conductguard", "status", "--json"],
476
+ stderr=subprocess.DEVNULL, text=True, timeout=3,
477
+ )
478
+ return json.loads(out.strip())
479
+ except Exception:
480
+ return None
481
+
482
+
483
+ def _memory_headline():
484
+ try:
485
+ root = Path.cwd()
486
+ mem_key = str(root).replace("/", "-").lstrip("-")
487
+ mem_path = Path.home() / ".claude" / "projects" / mem_key / "memory" / "MEMORY.md"
488
+ if mem_path.exists():
489
+ return "\\n".join(mem_path.read_text().splitlines()[:10])
490
+ except Exception:
491
+ pass
492
+ return ""
493
+
494
+
495
+ def main():
496
+ try:
497
+ sys.stdin.read()
498
+ except Exception:
499
+ pass
500
+
501
+ try:
502
+ GUARD_DIR.mkdir(parents=True, exist_ok=True)
503
+ snapshot = {
504
+ "compacted_at": datetime.now(timezone.utc).isoformat(),
505
+ "tier1": {
506
+ "git_branch": _git(["branch", "--show-current"]),
507
+ "recent_commits": _git(["log", "--oneline", "-3"]),
508
+ "memory_headline": _memory_headline(),
509
+ },
510
+ "tier2": {"guard_status": _guard_status()},
511
+ "tier3": {"cwd": str(Path.cwd()), "python": sys.version.split()[0]},
512
+ }
513
+ tmp = GUARD_DIR / "session_snapshot.tmp"
514
+ tmp.write_text(json.dumps(snapshot, indent=2))
515
+ tmp.rename(SNAPSHOT_PATH)
516
+ except Exception:
517
+ pass
518
+
519
+ sys.exit(0)
520
+
521
+
522
+ if __name__ == "__main__":
523
+ main()
524
+ '''
525
+
526
+ _SESSION_START_HOOK_SCRIPT = '''\
527
+ #!/usr/bin/env python3
528
+ """ConductGuard SessionStart hook — prints context after compaction."""
529
+ import json
530
+ import sys
531
+ from datetime import datetime, timezone
532
+ from pathlib import Path
533
+
534
+ SNAPSHOT_PATH = Path.home() / ".conductguard" / "session_snapshot.json"
535
+ MAX_AGE_HOURS = 2
536
+
537
+
538
+ def main():
539
+ try:
540
+ sys.stdin.read()
541
+ except Exception:
542
+ pass
543
+
544
+ if not SNAPSHOT_PATH.exists():
545
+ sys.exit(0)
546
+
547
+ try:
548
+ snapshot = json.loads(SNAPSHOT_PATH.read_text())
549
+ compacted_at = datetime.fromisoformat(snapshot.get("compacted_at", ""))
550
+ age_hours = (datetime.now(timezone.utc) - compacted_at).total_seconds() / 3600
551
+ if age_hours > MAX_AGE_HOURS:
552
+ sys.exit(0)
553
+
554
+ t1 = snapshot.get("tier1", {})
555
+ branch = t1.get("git_branch", "")
556
+ commits = t1.get("recent_commits", "")
557
+ headline = t1.get("memory_headline", "")
558
+ t2 = snapshot.get("tier2", {})
559
+ guard = t2.get("guard_status") or {}
560
+
561
+ lines = [f"## Session resumed (snapshot from {compacted_at.strftime(\'%Y-%m-%d %H:%M\')} UTC)"]
562
+ if branch:
563
+ last = commits.splitlines()[0] if commits else ""
564
+ lines.append(f"- Branch: {branch}" + (f" | Last: {last}" if last else ""))
565
+ budget = guard.get("budget_pct")
566
+ if budget is not None:
567
+ lines.append(f"- Guard: {budget}% budget used")
568
+ else:
569
+ lines.append("- Guard: state unavailable")
570
+ if headline:
571
+ lines.append(f"- Memory index:\\n {headline}")
572
+ else:
573
+ lines.append("- Memory index:\\n (none)")
574
+
575
+ print("\\n".join(lines))
576
+ except Exception:
577
+ pass
578
+
579
+ sys.exit(0)
580
+
581
+
582
+ if __name__ == "__main__":
583
+ main()
584
+ '''
585
+
586
+
449
587
  # ── Python interpreter selection ─────────────────────────────────────────────
450
588
 
451
589
  def _best_python() -> str:
@@ -478,6 +616,42 @@ def _write_hook(path: Path) -> None:
478
616
  ) from exc
479
617
 
480
618
 
619
+ def _install_session_hooks() -> None:
620
+ """Write PreCompact + SessionStart hook scripts and register them in ~/.claude/settings.json."""
621
+ python = _best_python()
622
+
623
+ precompact_path = GUARD_DIR / "guard-precompact.py"
624
+ session_start_path = GUARD_DIR / "guard-session-start.py"
625
+
626
+ precompact_path.write_text(_PRECOMPACT_HOOK_SCRIPT)
627
+ precompact_path.chmod(0o755)
628
+ session_start_path.write_text(_SESSION_START_HOOK_SCRIPT)
629
+ session_start_path.chmod(0o755)
630
+
631
+ claude_settings = Path.home() / ".claude" / "settings.json"
632
+ settings: dict = {}
633
+ if claude_settings.exists():
634
+ try:
635
+ settings = json.loads(claude_settings.read_text())
636
+ except Exception:
637
+ pass
638
+
639
+ hooks = settings.setdefault("hooks", {})
640
+
641
+ pre_cmd = f"{python} {precompact_path}"
642
+ compact_hooks = hooks.setdefault("PreCompact", [])
643
+ if not any(pre_cmd in str(e) for h in compact_hooks for e in h.get("hooks", [])):
644
+ compact_hooks.append({"hooks": [{"type": "command", "command": pre_cmd}]})
645
+
646
+ start_cmd = f"{python} {session_start_path}"
647
+ start_hooks = hooks.setdefault("SessionStart", [])
648
+ if not any(start_cmd in str(e) for h in start_hooks for e in h.get("hooks", [])):
649
+ start_hooks.append({"hooks": [{"type": "command", "command": start_cmd}]})
650
+
651
+ claude_settings.parent.mkdir(parents=True, exist_ok=True)
652
+ claude_settings.write_text(json.dumps(settings, indent=2) + "\n")
653
+
654
+
481
655
  # ── Guard config helpers ──────────────────────────────────────────────────────
482
656
 
483
657
  def _load_guard_config() -> dict:
@@ -815,6 +989,12 @@ def cmd_guard_install(args):
815
989
  # Register MCP in all found AI tools — Cursor/Windsurf (advisory)
816
990
  _register_mcp(workspace_id, member_token or "", server)
817
991
 
992
+ # Install session persistence hooks (PreCompact + SessionStart)
993
+ try:
994
+ _install_session_hooks()
995
+ except Exception:
996
+ pass
997
+
818
998
 
819
999
  def cmd_guard_join(args):
820
1000
  invite_code = args.invite_code
@@ -1020,6 +1200,10 @@ def cmd_guard_sync(args):
1020
1200
  _install_codex_hook(hook_path)
1021
1201
  cfg2 = _load_guard_config()
1022
1202
  _register_mcp(workspace_id, cfg2.get("member_token", ""), base_url)
1203
+ try:
1204
+ _install_session_hooks()
1205
+ except Exception:
1206
+ pass
1023
1207
  print(f" {GREEN}Hook script updated{RESET}")
1024
1208
 
1025
1209
  # Capture savings from RTK and Agent Booster
@@ -131,7 +131,13 @@ def _require_auth(args):
131
131
 
132
132
  def _stream_run(server: str, workflow_id: str, run_id: str, workspace_id: str, token=None, api_key=None) -> bool:
133
133
  hdrs = api.headers(workspace_id, token, "application/json", api_key)
134
- url = f"{server}/workflows/{workflow_id}/runs/{run_id}/stream"
134
+ # SSE endpoint reads auth from query params (EventSource can't set headers)
135
+ qs_parts = [f"workspace_id={workspace_id}"]
136
+ if token:
137
+ qs_parts.append(f"token={token}")
138
+ if api_key:
139
+ qs_parts.append(f"api_key={api_key}")
140
+ url = f"{server}/workflows/{workflow_id}/runs/{run_id}/stream?{'&'.join(qs_parts)}"
135
141
 
136
142
  for data in api.stream(url, hdrs):
137
143
  kind = data.get("kind", "")
@@ -1321,10 +1327,13 @@ def cmd_run(args):
1321
1327
  print(f" {GRAY}{k}={v}{RESET}")
1322
1328
  print()
1323
1329
 
1324
- run = api.req("POST", f"{server}/workflows/{workflow_id}/runs", json_h, {
1330
+ body: dict = {
1325
1331
  "triggered_by": "cli",
1326
1332
  "initial_state": {"__manual": True, "inputs": initial_state},
1327
- })
1333
+ }
1334
+ if getattr(args, "max_turns", None):
1335
+ body["max_turns"] = args.max_turns
1336
+ run = api.req("POST", f"{server}/workflows/{workflow_id}/runs", json_h, body)
1328
1337
  _stream_run(server, workflow_id, run["id"], workspace_id, token, api_key)
1329
1338
 
1330
1339
 
@@ -1429,9 +1438,10 @@ def main():
1429
1438
 
1430
1439
  # conduct run (existing)
1431
1440
  run_p = sub.add_parser("run", help="Run an installed agent by name")
1432
- run_p.add_argument("agent", help="Agent name (e.g. 'security_autopilot_fix')")
1433
- run_p.add_argument("--project", metavar="name", help="Narrow to a specific project")
1434
- run_p.add_argument("--input", action="append", metavar="key=value", help="Runtime input (repeatable)")
1441
+ run_p.add_argument("agent", help="Agent name (e.g. 'security_autopilot_fix')")
1442
+ run_p.add_argument("--project", metavar="name", help="Narrow to a specific project")
1443
+ run_p.add_argument("--input", action="append", metavar="key=value", help="Runtime input (repeatable)")
1444
+ run_p.add_argument("--max-turns", dest="max_turns", type=int, metavar="N", help="Max agentic turns (default: auto)")
1435
1445
 
1436
1446
  # conduct guard
1437
1447
  guard_p, _guard_sub = _guard.register_guard_parser(sub)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.37
3
+ Version: 0.4.40
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