conduct-cli 0.4.70__tar.gz → 0.4.72__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.
Files changed (26) hide show
  1. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/PKG-INFO +4 -1
  2. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/README.md +2 -0
  3. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/pyproject.toml +2 -2
  4. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/guard.py +139 -0
  5. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_session_start_template.py +23 -0
  6. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_template.py +24 -0
  7. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/main.py +32 -0
  8. conduct_cli-0.4.72/src/conduct_cli/memory.py +96 -0
  9. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/PKG-INFO +4 -1
  10. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
  11. conduct_cli-0.4.72/src/conduct_cli.egg-info/requires.txt +3 -0
  12. conduct_cli-0.4.70/src/conduct_cli.egg-info/requires.txt +0 -2
  13. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/setup.cfg +0 -0
  14. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/setup.py +0 -0
  15. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/__init__.py +0 -0
  16. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/api.py +0 -0
  17. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/guardmcp.py +0 -0
  18. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/hook_precompact_template.py +0 -0
  19. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli/mcp_server.py +0 -0
  20. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  21. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  22. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/src/conduct_cli.egg-info/top_level.txt +0 -0
  23. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_guard_policy.py +0 -0
  24. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_guard_savings.py +0 -0
  25. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_hook_syntax.py +0 -0
  26. {conduct_cli-0.4.70 → conduct_cli-0.4.72}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.70
3
+ Version: 0.4.72
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
@@ -22,11 +22,14 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  Requires-Dist: pyyaml>=6.0
24
24
  Requires-Dist: rich>=13.0
25
+ Requires-Dist: agent-booster[watch]>=0.2.23
25
26
 
26
27
  # conduct-cli
27
28
 
28
29
  Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
29
30
 
31
+ ![Conduct CLI demo — whoami, switch workspaces with Guard policy sync, and run an agent](assets/conduct-cli-demo.gif)
32
+
30
33
  ## Install
31
34
 
32
35
  ```bash
@@ -2,6 +2,8 @@
2
2
 
3
3
  Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
4
4
 
5
+ ![Conduct CLI demo — whoami, switch workspaces with Guard policy sync, and run an agent](assets/conduct-cli-demo.gif)
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.4.70"
7
+ version = "0.4.72"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Topic :: Software Development :: Libraries :: Application Frameworks",
25
25
  ]
26
- dependencies = ["pyyaml>=6.0", "rich>=13.0"]
26
+ dependencies = ["pyyaml>=6.0", "rich>=13.0", "agent-booster[watch]>=0.2.23"]
27
27
 
28
28
  [project.urls]
29
29
  Homepage = "https://conductai.ai"
@@ -740,6 +740,9 @@ def cmd_guard_sync(args):
740
740
  pass
741
741
  print(f" {GREEN}Hook script updated{RESET}")
742
742
 
743
+ # Auto-init Agent Booster if installed but not yet set up in this project
744
+ _ensure_booster(Path.cwd())
745
+
743
746
  # Capture savings from RTK and Agent Booster
744
747
  _report_savings(cfg, base_url, api_key)
745
748
 
@@ -752,6 +755,48 @@ def cmd_guard_sync(args):
752
755
  print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
753
756
 
754
757
 
758
+ def _ensure_booster(root: Path) -> None:
759
+ """Auto-init and background-index booster if installed but not yet set up."""
760
+ import shutil
761
+ import subprocess
762
+
763
+ if not shutil.which("booster"):
764
+ return # not installed — conduct-cli 0.4.71+ installs it, but may not be on PATH yet
765
+
766
+ db_path = root / ".booster" / "symbols.db"
767
+ hooks_path = root / ".claude" / "hooks" / "booster-gate.py"
768
+
769
+ # Init (writes hook scripts + wires settings.json) — fast, idempotent
770
+ if not hooks_path.exists():
771
+ try:
772
+ subprocess.run(["booster", "init", "--yes"], capture_output=True, timeout=15, cwd=str(root))
773
+ print(f" {GREEN}Agent Booster:{RESET} hooks installed")
774
+ except Exception:
775
+ return
776
+
777
+ # Index in background — may take 10-60s on large repos, never blocks sync
778
+ if not db_path.exists():
779
+ try:
780
+ subprocess.Popen(
781
+ ["booster", "index", "--embed"],
782
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
783
+ cwd=str(root),
784
+ )
785
+ print(f" {GREEN}Agent Booster:{RESET} indexing in background (Read/Grep intercept active shortly)")
786
+ except Exception:
787
+ pass
788
+ else:
789
+ symbols_count = 0
790
+ try:
791
+ import sqlite3
792
+ conn = sqlite3.connect(str(db_path))
793
+ symbols_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
794
+ conn.close()
795
+ except Exception:
796
+ pass
797
+ print(f" {GREEN}Agent Booster:{RESET} {symbols_count} symbols indexed — Read/Grep intercept active")
798
+
799
+
755
800
  def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
756
801
  import subprocess
757
802
 
@@ -1054,9 +1099,101 @@ def register_guard_parser(sub):
1054
1099
  help="Time window: 1h, 24h, 7d, 30d (default: 24h)",
1055
1100
  )
1056
1101
 
1102
+ # conduct guard booster-status
1103
+ guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
1104
+
1057
1105
  return guard_p, guard_sub
1058
1106
 
1059
1107
 
1108
+ def cmd_guard_booster_status(args):
1109
+ """Show whether booster is intercepting Read/Grep in this project."""
1110
+ import shutil, sqlite3, subprocess
1111
+
1112
+ root = Path.cwd()
1113
+ db_path = root / ".booster" / "symbols.db"
1114
+ hooks_path = root / ".claude" / "hooks" / "booster-gate.py"
1115
+ settings_p = root / ".claude" / "settings.json"
1116
+
1117
+ booster_bin = shutil.which("booster")
1118
+ print(f"\n{BOLD}Agent Booster intercept status — {root.name}{RESET}\n")
1119
+
1120
+ # 1. Binary
1121
+ if booster_bin:
1122
+ print(f" {GREEN}✓{RESET} booster installed ({booster_bin})")
1123
+ else:
1124
+ print(f" {RED}✗{RESET} booster not found on PATH — run: pip install agent-booster")
1125
+ return
1126
+
1127
+ # 2. Hook scripts written
1128
+ if hooks_path.exists():
1129
+ print(f" {GREEN}✓{RESET} hook scripts present (.claude/hooks/booster-gate.py)")
1130
+ else:
1131
+ print(f" {RED}✗{RESET} hook scripts missing — run: conduct guard sync")
1132
+
1133
+ # 3. Wired in settings.json
1134
+ wired = False
1135
+ if settings_p.exists():
1136
+ import json as _json
1137
+ s = _json.loads(settings_p.read_text())
1138
+ for h in s.get("hooks", {}).get("PreToolUse", []):
1139
+ if h.get("matcher") == "Read":
1140
+ wired = True
1141
+ break
1142
+ if wired:
1143
+ print(f" {GREEN}✓{RESET} Read hook wired in .claude/settings.json")
1144
+ else:
1145
+ print(f" {RED}✗{RESET} Read hook NOT in .claude/settings.json — run: conduct guard sync")
1146
+
1147
+ # 4. Index
1148
+ if db_path.exists():
1149
+ try:
1150
+ conn = sqlite3.connect(str(db_path))
1151
+ count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
1152
+ files = conn.execute("SELECT COUNT(DISTINCT file) FROM symbols").fetchone()[0]
1153
+ conn.close()
1154
+ print(f" {GREEN}✓{RESET} symbols.db — {count} symbols across {files} files")
1155
+ except Exception:
1156
+ print(f" {YELLOW}?{RESET} symbols.db exists but could not be read")
1157
+ else:
1158
+ print(f" {RED}✗{RESET} symbols.db missing — Read calls fall through unintercepted")
1159
+ print(f" run: booster index --embed (or: conduct guard sync to trigger it)")
1160
+ return
1161
+
1162
+ # 5. Live intercept test — try reading a known file and check if smart-read fires
1163
+ print(f"\n {BOLD}Live intercept test:{RESET}")
1164
+ try:
1165
+ import tempfile, json as _json
1166
+ # Pick the first indexed file
1167
+ conn = sqlite3.connect(str(db_path))
1168
+ row = conn.execute("SELECT file FROM symbols LIMIT 1").fetchone()
1169
+ conn.close()
1170
+ if row:
1171
+ # Prefer a .py/.ts file — more likely to have symbols and trigger smart-read
1172
+ conn = sqlite3.connect(str(db_path))
1173
+ src = conn.execute(
1174
+ "SELECT file FROM symbols WHERE file LIKE '%.py' OR file LIKE '%.ts' LIMIT 1"
1175
+ ).fetchone() or row
1176
+ conn.close()
1177
+ test_file = str(root / src[0])
1178
+ payload = _json.dumps({"tool_name": "Read", "tool_input": {"file_path": test_file}})
1179
+ r = subprocess.run(
1180
+ ["python3", str(hooks_path)],
1181
+ input=payload, capture_output=True, text=True, timeout=10,
1182
+ )
1183
+ if r.returncode == 2:
1184
+ lines = r.stdout.strip().splitlines()
1185
+ print(f" {GREEN}✓{RESET} Read intercepted → smart-read fired ({len(lines)} lines returned)")
1186
+ print(f" tested on: {row[0]}")
1187
+ elif r.returncode == 0:
1188
+ print(f" {YELLOW}~{RESET} Hook ran but passed through (file may not have symbols)")
1189
+ else:
1190
+ print(f" {RED}✗{RESET} Hook errored (exit {r.returncode})")
1191
+ except Exception as e:
1192
+ print(f" {YELLOW}?{RESET} Could not run live test: {e}")
1193
+
1194
+ print()
1195
+
1196
+
1060
1197
  def dispatch_guard(args, guard_p):
1061
1198
  """Dispatch to the correct guard handler. Called from main()."""
1062
1199
  guard_command = getattr(args, "guard_command", None)
@@ -1068,6 +1205,8 @@ def dispatch_guard(args, guard_p):
1068
1205
  cmd_guard_savings(args)
1069
1206
  elif guard_command == "audit":
1070
1207
  cmd_guard_audit(args)
1208
+ elif guard_command == "booster-status":
1209
+ cmd_guard_booster_status(args)
1071
1210
  else:
1072
1211
  guard_p.print_help()
1073
1212
  sys.exit(1)
@@ -46,6 +46,29 @@ def main():
46
46
  else:
47
47
  lines.append("- Memory index:\n (none)")
48
48
 
49
+ # Inject relevant team memories for the current repo
50
+ try:
51
+ repo = None
52
+ try:
53
+ import subprocess
54
+ out = subprocess.check_output(["git", "remote", "get-url", "origin"],
55
+ stderr=subprocess.DEVNULL, text=True).strip()
56
+ if "github.com" in out:
57
+ repo = out.split("github.com")[-1].lstrip("/:").rstrip(".git")
58
+ except Exception:
59
+ pass
60
+
61
+ from conduct_cli.memory import search_team_memory
62
+ results = search_team_memory("recent learnings patterns bugs", repo=repo, limit=3)
63
+ if results:
64
+ lines.append("- Team knowledge:")
65
+ for r in results[:3]:
66
+ dev = r.get("developer_id", "teammate")[:8]
67
+ summary = r.get("summary", "")[:120]
68
+ lines.append(f" {dev}: {summary}")
69
+ except Exception:
70
+ pass
71
+
49
72
  print("\n".join(lines))
50
73
  except Exception:
51
74
  pass
@@ -131,6 +131,20 @@ except Exception:
131
131
  return None, "allow", None, None
132
132
 
133
133
 
134
+ def _detect_repo() -> str | None:
135
+ try:
136
+ import subprocess
137
+ out = subprocess.check_output(["git", "remote", "get-url", "origin"],
138
+ stderr=subprocess.DEVNULL, text=True).strip()
139
+ # github.com/owner/repo or git@github.com:owner/repo
140
+ if "github.com" in out:
141
+ parts = out.split("github.com")[-1].lstrip("/:").rstrip(".git")
142
+ return parts # owner/repo
143
+ except Exception:
144
+ pass
145
+ return None
146
+
147
+
134
148
  def _detect_ai_tool():
135
149
  import os
136
150
  if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_ENTRYPOINT"):
@@ -535,6 +549,16 @@ def main():
535
549
  except Exception:
536
550
  sys.exit(0)
537
551
 
552
+ # Stop hook — session ended, capture for team memory
553
+ if data.get("hook_event_name") == "Stop" or data.get("stop_hook_active"):
554
+ session_id = data.get("session_id", "")
555
+ transcript_path = data.get("transcript_path")
556
+ # Detect repo from CWD git remote
557
+ repo = _detect_repo()
558
+ from conduct_cli.memory import post_session_to_api
559
+ post_session_to_api(session_id, transcript_path, repo)
560
+ sys.exit(0)
561
+
538
562
  # Policy version check (cached 60s) — auto-syncs if server version differs
539
563
  _maybe_sync_policy()
540
564
 
@@ -2708,6 +2708,28 @@ def cmd_test_guard(args):
2708
2708
  print(f"\n {CYAN}→ View events: {api_url.replace('api.', 'app.')}/guard/activity{RESET}\n")
2709
2709
 
2710
2710
 
2711
+ def cmd_memory(args):
2712
+ from conduct_cli.memory import search_team_memory
2713
+ memory_command = getattr(args, "memory_command", None)
2714
+ if memory_command == "search":
2715
+ query = " ".join(args.query)
2716
+ results = search_team_memory(query, repo=getattr(args, "repo", None), limit=args.limit)
2717
+ if not results:
2718
+ print("No team memories found.")
2719
+ return
2720
+ for r in results:
2721
+ repo = r.get("repo_full_name", "unknown")
2722
+ summary = r.get("summary", "")
2723
+ tags = ", ".join(r.get("topic_tags") or [])
2724
+ created = r.get("created_at", "")[:10]
2725
+ print(f"\n{BOLD}{repo}{RESET} {GRAY}{created}{RESET}")
2726
+ if tags:
2727
+ print(f" Tags: {tags}")
2728
+ print(f" {summary}")
2729
+ else:
2730
+ print("Usage: conduct memory search <query>")
2731
+
2732
+
2711
2733
  # ── Entry point ───────────────────────────────────────────────────────────────
2712
2734
 
2713
2735
  def main():
@@ -2860,6 +2882,14 @@ def main():
2860
2882
  sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
2861
2883
  sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
2862
2884
 
2885
+ # conduct memory
2886
+ memory_p = sub.add_parser("memory", help="Search team session memories")
2887
+ memory_sub = memory_p.add_subparsers(dest="memory_command")
2888
+ mem_search_p = memory_sub.add_parser("search", help="Search team memories")
2889
+ mem_search_p.add_argument("query", nargs="+", help="Search query")
2890
+ mem_search_p.add_argument("--repo", help="Filter by repo (owner/repo)")
2891
+ mem_search_p.add_argument("--limit", type=int, default=5, help="Max results")
2892
+
2863
2893
  args = parser.parse_args()
2864
2894
 
2865
2895
  if args.command == "login":
@@ -2937,6 +2967,8 @@ def main():
2937
2967
  cmd_test_security_verify(args)
2938
2968
  elif args.command == "session-report":
2939
2969
  cmd_session_report(args)
2970
+ elif args.command == "memory":
2971
+ cmd_memory(args)
2940
2972
  else:
2941
2973
  parser.print_help()
2942
2974
 
@@ -0,0 +1,96 @@
1
+ """Team session memory helpers for Conduct CLI."""
2
+ import json
3
+ import threading
4
+ import urllib.request
5
+ from pathlib import Path
6
+
7
+
8
+ def _load_config():
9
+ cfg_path = Path.home() / ".conduct" / "config.json"
10
+ if not cfg_path.exists():
11
+ return None
12
+ try:
13
+ return json.loads(cfg_path.read_text())
14
+ except Exception:
15
+ return None
16
+
17
+
18
+ def post_session_to_api(session_id: str, transcript_path: str | None, repo: str | None) -> bool:
19
+ """Fire-and-forget POST to /team-memory/sessions. Returns True if thread started."""
20
+ cfg = _load_config()
21
+ if not cfg:
22
+ return False
23
+ server = cfg.get("server", "").rstrip("/")
24
+ api_key = cfg.get("api_key", "")
25
+ workspace_id = cfg.get("workspace_id", "")
26
+ if not server or not workspace_id:
27
+ return False
28
+
29
+ raw_transcript = None
30
+ if transcript_path:
31
+ try:
32
+ text = Path(transcript_path).read_text(errors="ignore")
33
+ raw_transcript = text[:12000]
34
+ except Exception:
35
+ pass
36
+
37
+ payload = json.dumps({
38
+ "session_id": session_id,
39
+ "tool": "claude_code",
40
+ "repo_full_name": repo,
41
+ "raw_transcript": raw_transcript,
42
+ "files_touched": [],
43
+ }).encode()
44
+
45
+ def _send():
46
+ try:
47
+ req = urllib.request.Request(
48
+ f"{server}/team-memory/sessions",
49
+ data=payload,
50
+ headers={
51
+ "Content-Type": "application/json",
52
+ "X-API-Key": api_key,
53
+ "X-Workspace-ID": workspace_id,
54
+ },
55
+ method="POST",
56
+ )
57
+ urllib.request.urlopen(req, timeout=10)
58
+ except Exception:
59
+ pass
60
+
61
+ t = threading.Thread(target=_send, daemon=True)
62
+ t.start()
63
+ return True
64
+
65
+
66
+ def search_team_memory(query: str, repo: str | None = None, limit: int = 5) -> list[dict]:
67
+ """Search team session memories. Returns list of result dicts, never raises."""
68
+ cfg = _load_config()
69
+ if not cfg:
70
+ return []
71
+ server = cfg.get("server", "").rstrip("/")
72
+ api_key = cfg.get("api_key", "")
73
+ workspace_id = cfg.get("workspace_id", "")
74
+ if not server or not workspace_id:
75
+ return []
76
+
77
+ try:
78
+ import urllib.parse
79
+ params = {"q": query, "limit": str(limit)}
80
+ if repo:
81
+ params["repo"] = repo
82
+ url = f"{server}/team-memory/search?{urllib.parse.urlencode(params)}"
83
+ req = urllib.request.Request(
84
+ url,
85
+ headers={
86
+ "X-API-Key": api_key,
87
+ "X-Workspace-ID": workspace_id,
88
+ },
89
+ )
90
+ with urllib.request.urlopen(req, timeout=5) as resp:
91
+ data = json.loads(resp.read())
92
+ if isinstance(data, list):
93
+ return data
94
+ return data.get("results", []) if isinstance(data, dict) else []
95
+ except Exception:
96
+ return []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.70
3
+ Version: 0.4.72
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
@@ -22,11 +22,14 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  Requires-Dist: pyyaml>=6.0
24
24
  Requires-Dist: rich>=13.0
25
+ Requires-Dist: agent-booster[watch]>=0.2.23
25
26
 
26
27
  # conduct-cli
27
28
 
28
29
  Official CLI for [Conduct AI](https://conductai.ai) — install AI agents, manage projects, run end-to-end tests, and enforce team AI policies with ConductGuard.
29
30
 
31
+ ![Conduct CLI demo — whoami, switch workspaces with Guard policy sync, and run an agent](assets/conduct-cli-demo.gif)
32
+
30
33
  ## Install
31
34
 
32
35
  ```bash
@@ -10,6 +10,7 @@ src/conduct_cli/hook_session_start_template.py
10
10
  src/conduct_cli/hook_template.py
11
11
  src/conduct_cli/main.py
12
12
  src/conduct_cli/mcp_server.py
13
+ src/conduct_cli/memory.py
13
14
  src/conduct_cli.egg-info/PKG-INFO
14
15
  src/conduct_cli.egg-info/SOURCES.txt
15
16
  src/conduct_cli.egg-info/dependency_links.txt
@@ -0,0 +1,3 @@
1
+ pyyaml>=6.0
2
+ rich>=13.0
3
+ agent-booster[watch]>=0.2.23
@@ -1,2 +0,0 @@
1
- pyyaml>=6.0
2
- rich>=13.0
File without changes
File without changes