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.
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/PKG-INFO +1 -1
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/pyproject.toml +1 -1
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/guard.py +184 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/main.py +16 -6
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/README.md +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/setup.cfg +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/setup.py +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.37 → conduct_cli-0.4.40}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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",
|
|
1433
|
-
run_p.add_argument("--project",
|
|
1434
|
-
run_p.add_argument("--input",
|
|
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)
|
|
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
|