conduct-cli 0.4.38__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.38
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.38"
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
@@ -1327,10 +1327,13 @@ def cmd_run(args):
1327
1327
  print(f" {GRAY}{k}={v}{RESET}")
1328
1328
  print()
1329
1329
 
1330
- run = api.req("POST", f"{server}/workflows/{workflow_id}/runs", json_h, {
1330
+ body: dict = {
1331
1331
  "triggered_by": "cli",
1332
1332
  "initial_state": {"__manual": True, "inputs": initial_state},
1333
- })
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)
1334
1337
  _stream_run(server, workflow_id, run["id"], workspace_id, token, api_key)
1335
1338
 
1336
1339
 
@@ -1435,9 +1438,10 @@ def main():
1435
1438
 
1436
1439
  # conduct run (existing)
1437
1440
  run_p = sub.add_parser("run", help="Run an installed agent by name")
1438
- run_p.add_argument("agent", help="Agent name (e.g. 'security_autopilot_fix')")
1439
- run_p.add_argument("--project", metavar="name", help="Narrow to a specific project")
1440
- 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)")
1441
1445
 
1442
1446
  # conduct guard
1443
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.38
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