conduct-cli 0.5.0__tar.gz → 0.5.3__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 (27) hide show
  1. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/PKG-INFO +1 -1
  2. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/pyproject.toml +1 -1
  3. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/guard.py +78 -12
  4. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/guardmcp.py +35 -0
  5. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_template.py +6 -2
  6. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/main.py +32 -0
  7. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  8. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/README.md +0 -0
  9. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/setup.cfg +0 -0
  10. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/setup.py +0 -0
  11. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/__init__.py +0 -0
  12. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/api.py +0 -0
  13. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_precompact_template.py +0 -0
  14. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_session_start_template.py +0 -0
  15. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_stop_template.py +0 -0
  16. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/mcp_server.py +0 -0
  17. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/memory.py +0 -0
  18. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/paxel.py +0 -0
  19. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  20. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  21. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  22. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/requires.txt +0 -0
  23. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/top_level.txt +0 -0
  24. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_guard_policy.py +0 -0
  25. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_guard_savings.py +0 -0
  26. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_hook_syntax.py +0 -0
  27. {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.0
3
+ Version: 0.5.3
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.5.0"
7
+ version = "0.5.3"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -199,12 +199,19 @@ _MCP_TARGETS = [
199
199
 
200
200
 
201
201
  def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
202
- """Write conductguard MCP entry into every AI tool config found on this machine.
202
+ """Write conductguard + agent-booster MCP entries into every AI tool config found.
203
203
 
204
204
  Credentials are NOT stored in the MCP config — the server reads them from
205
205
  ~/.conductguard/config.json at startup, which is written by guard sync.
206
206
  """
207
- entry = {"command": "conductguard-mcp"}
207
+ import shutil
208
+ servers: dict[str, dict] = {
209
+ "conductguard": {"command": "conductguard-mcp"},
210
+ }
211
+ # Register agent-booster only if the binary is available
212
+ if shutil.which("booster"):
213
+ servers["agent-booster"] = {"command": "booster", "args": ["serve"]}
214
+
208
215
  found_any = False
209
216
  for cfg_path, label in _MCP_TARGETS:
210
217
  if not cfg_path.exists():
@@ -215,12 +222,16 @@ def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
215
222
  except (json.JSONDecodeError, OSError):
216
223
  existing = {}
217
224
  mcp = existing.setdefault("mcpServers", {})
218
- if mcp.get("conductguard") == entry:
219
- print(f" {GRAY}Guard MCP already registered in {label}{RESET}")
220
- continue
221
- mcp["conductguard"] = entry
222
- cfg_path.write_text(json.dumps(existing, indent=2))
223
- print(f" {GREEN}Guard MCP registered in {label}{RESET}")
225
+ changed = False
226
+ for name, entry in servers.items():
227
+ if mcp.get(name) == entry:
228
+ print(f" {GRAY}{name} MCP already registered in {label}{RESET}")
229
+ else:
230
+ mcp[name] = entry
231
+ changed = True
232
+ print(f" {GREEN}{name} MCP registered in {label}{RESET}")
233
+ if changed:
234
+ cfg_path.write_text(json.dumps(existing, indent=2))
224
235
  if not found_any:
225
236
  print(f" {GRAY}No AI tool configs found for MCP registration{RESET}")
226
237
 
@@ -474,6 +485,7 @@ def cmd_guard_install(args):
474
485
  pass
475
486
 
476
487
  # Persist guard config — include api_key so CLI commands can authenticate
488
+ import time as _time
477
489
  _save_guard_config({
478
490
  "workspace_id": workspace_id,
479
491
  "member_token": member_token,
@@ -482,6 +494,7 @@ def cmd_guard_install(args):
482
494
  "api_key": api_key,
483
495
  "api_url": server,
484
496
  "security_emit_enabled": security_emit,
497
+ "last_synced_at": _time.time(),
485
498
  })
486
499
  if security_emit:
487
500
  print(f" {GREEN}Security Loop:{RESET} installed — classifier active")
@@ -545,10 +558,12 @@ def cmd_guard_join(args):
545
558
  print(f" {GREEN}Policy downloaded:{RESET} {rule_count} rule(s)")
546
559
 
547
560
  # Persist guard config
561
+ import time as _time
548
562
  cfg = {
549
- "workspace_id": workspace_id,
550
- "user_email": email,
551
- "api_url": base_url,
563
+ "workspace_id": workspace_id,
564
+ "user_email": email,
565
+ "api_url": base_url,
566
+ "last_synced_at": _time.time(),
552
567
  }
553
568
  if member_token:
554
569
  cfg["member_token"] = member_token
@@ -718,6 +733,9 @@ def cmd_guard_sync(args):
718
733
  rule_count = len(policy.get("rules", []))
719
734
  print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
720
735
 
736
+ if getattr(args, "cursor", False):
737
+ _write_cursorrules(policy)
738
+
721
739
  # Re-check Security Loop install status
722
740
  try:
723
741
  sec = _req("GET", f"{base_url}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
@@ -770,6 +788,40 @@ def cmd_guard_sync(args):
770
788
  print(f"\n {CYAN}{mcp_url}{RESET}\n")
771
789
 
772
790
 
791
+ def _write_cursorrules(policy: dict) -> None:
792
+ """Write active Guard policies into .cursorrules in the current directory."""
793
+ rules = policy.get("rules", [])
794
+ enabled = [r for r in rules if r.get("enabled", True)]
795
+ lines = [
796
+ "# .cursorrules — generated by Conduct AI Guard",
797
+ "# Run `conduct guard sync --cursor` to refresh.",
798
+ "# Do not edit manually — changes will be overwritten.",
799
+ "",
800
+ "## Conduct Guard Policies",
801
+ f"# {len(enabled)} active rule(s) enforced by ConductGuard.",
802
+ "",
803
+ ]
804
+ for r in enabled:
805
+ action = r.get("action", "warn").upper()
806
+ rule_id = r.get("rule_id", "")
807
+ desc = r.get("description") or r.get("message") or ""
808
+ lines.append(f"# [{action}] {rule_id}" + (f" — {desc}" if desc else ""))
809
+ pattern = r.get("pattern")
810
+ if pattern:
811
+ lines.append(f"# pattern: {pattern}")
812
+ lines += [
813
+ "",
814
+ "## General",
815
+ "# Never include secrets, API keys, or credentials in prompts.",
816
+ "# PII (emails, SSNs, phone numbers) is redacted by Conduct before reaching any model.",
817
+ "# Conduct AI governance is active — all tool calls are audited.",
818
+ "# Independent of Cursor's ownership — policies enforced by your team, not the IDE vendor.",
819
+ ]
820
+ out = Path(".cursorrules")
821
+ out.write_text("\n".join(lines) + "\n")
822
+ print(f" {GREEN}.cursorrules written:{RESET} {len(enabled)} rule(s) → {out.resolve()}")
823
+
824
+
773
825
  def _ensure_booster(root: Path) -> None:
774
826
  """Auto-init and background-index booster if installed but not yet set up."""
775
827
  import shutil
@@ -878,6 +930,8 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
878
930
  "saved_tokens": raw.get("saved_tokens", 0),
879
931
  "savings_pct": raw.get("savings_pct", 0.0),
880
932
  "total_reads": raw.get("total_reads", 0),
933
+ "crusher": raw.get("crusher", {}),
934
+ "cache_align": raw.get("cache_align", {}),
881
935
  }
882
936
  except Exception:
883
937
  pass
@@ -929,6 +983,14 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
929
983
  except Exception:
930
984
  pass # Never fail sync because savings POST failed
931
985
 
986
+ # Push booster symbol index to team workspace (best-effort)
987
+ try:
988
+ r = subprocess.run(["booster", "index-push"], capture_output=True, text=True, timeout=30)
989
+ if r.returncode == 0 and r.stdout.strip():
990
+ print(f" {GREEN}Booster index:{RESET} {r.stdout.strip()}")
991
+ except Exception:
992
+ pass
993
+
932
994
 
933
995
  def cmd_guard_status(args):
934
996
  cfg = _require_guard_config()
@@ -1134,7 +1196,8 @@ def register_guard_parser(sub):
1134
1196
  guard_sub = guard_p.add_subparsers(dest="guard_command")
1135
1197
 
1136
1198
  # conduct guard sync
1137
- guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
1199
+ sync_p = guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
1200
+ sync_p.add_argument("--cursor", action="store_true", help="Write active Guard policies to .cursorrules")
1138
1201
 
1139
1202
  # conduct guard status
1140
1203
  guard_sub.add_parser("status", help="Show today's spend and violations")
@@ -1154,6 +1217,9 @@ def register_guard_parser(sub):
1154
1217
  # conduct guard booster-status
1155
1218
  guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
1156
1219
 
1220
+ # conduct guard skip-setup
1221
+ guard_sub.add_parser("skip-setup", help="Suppress the Guard setup reminder (does not disable Guard)")
1222
+
1157
1223
  return guard_p, guard_sub
1158
1224
 
1159
1225
 
@@ -15,7 +15,10 @@ from __future__ import annotations
15
15
  import argparse
16
16
  import json
17
17
  import re
18
+ import subprocess
18
19
  import sys
20
+ import threading
21
+ import time
19
22
  import uuid
20
23
  import urllib.request
21
24
  import urllib.error
@@ -136,6 +139,32 @@ def _load_config() -> dict:
136
139
  return {}
137
140
 
138
141
 
142
+ _sync_lock = threading.Lock()
143
+ _SYNC_INTERVAL = 300 # 5 minutes
144
+
145
+
146
+ def _maybe_sync() -> None:
147
+ """If config is stale (>5 min since last sync), run `conduct guard sync` in background."""
148
+ cfg = _load_config()
149
+ last_sync = cfg.get("last_synced_at", 0)
150
+ if time.time() - last_sync < _SYNC_INTERVAL:
151
+ return
152
+ if not _sync_lock.acquire(blocking=False):
153
+ return # another thread is already syncing
154
+ def _run():
155
+ try:
156
+ subprocess.run(
157
+ ["conduct", "guard", "sync"],
158
+ timeout=15,
159
+ capture_output=True,
160
+ )
161
+ except Exception:
162
+ pass
163
+ finally:
164
+ _sync_lock.release()
165
+ threading.Thread(target=_run, daemon=True, name="guard-auto-sync").start()
166
+
167
+
139
168
  def _detect_surface(client_info: dict) -> str:
140
169
  """Map MCP clientInfo.name → ai_tool label sent to Guard API."""
141
170
  name = (client_info.get("name") or "").lower()
@@ -293,6 +322,12 @@ def _handle_guard_sync(workspace_id: str, token: str) -> str:
293
322
 
294
323
 
295
324
  def _dispatch_tool(name: str, arguments: dict, workspace_id: str, token: str, ai_tool: str) -> str:
325
+ # Always re-read config so workspace_id/token stay fresh between syncs
326
+ _maybe_sync()
327
+ cfg = _load_config()
328
+ workspace_id = cfg.get("workspace_id") or workspace_id
329
+ token = cfg.get("member_token") or token
330
+
296
331
  if name == "guard_status":
297
332
  return _handle_guard_status(workspace_id)
298
333
  if name == "guard_check":
@@ -219,6 +219,8 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, ses
219
219
  if not workspace_id:
220
220
  return
221
221
 
222
+ import platform as _platform
223
+ _os = f"{_platform.system()} {_platform.release()} {_platform.machine()}".strip()
222
224
  payload = json.dumps({
223
225
  "workspace_id": workspace_id,
224
226
  "clerk_user_id": cfg.get("user_email"),
@@ -228,8 +230,10 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, ses
228
230
  "input_summary": json.dumps(tool_input)[:200],
229
231
  "decision": decision,
230
232
  "rule_id": rule_id,
231
- "rule_message": message,
232
- "hook_session_id": session_id,
233
+ "rule_message": message,
234
+ "hook_session_id": session_id,
235
+ "os_info": _os,
236
+ "hostname": _platform.node(),
233
237
  })
234
238
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
235
239
  script = (
@@ -2823,6 +2823,30 @@ def cmd_memory(args):
2823
2823
 
2824
2824
  # ── Entry point ───────────────────────────────────────────────────────────────
2825
2825
 
2826
+ _GUARD_CONFIG = Path.home() / ".conductguard" / "config.json"
2827
+ _GUARD_SKIP = Path.home() / ".conductguard" / ".setup_skip"
2828
+
2829
+ GREEN = "\033[32m"
2830
+ YELLOW = "\033[33m"
2831
+ BOLD = "\033[1m"
2832
+ RESET = "\033[0m"
2833
+
2834
+
2835
+ def _check_guard_setup(command: str) -> None:
2836
+ """On first run after install, prompt user to run conduct guard sync."""
2837
+ # Skip if: already set up, user said skip, or running guard sync/login itself
2838
+ if command in ("guard", "login", "whoami", "version"):
2839
+ return
2840
+ if _GUARD_CONFIG.exists() or _GUARD_SKIP.exists():
2841
+ return
2842
+ print(
2843
+ f"\n{YELLOW}{BOLD}⚡ Conduct Guard is not set up on this machine.{RESET}\n"
2844
+ f" Run {BOLD}conduct guard sync{RESET} to register policy hooks and MCP servers.\n"
2845
+ f" (This takes ~5 seconds and only needs to happen once per machine.)\n"
2846
+ f" To skip this reminder: {BOLD}conduct guard skip-setup{RESET}\n"
2847
+ )
2848
+
2849
+
2826
2850
  def main():
2827
2851
  _auto_update()
2828
2852
 
@@ -2983,6 +3007,14 @@ def main():
2983
3007
 
2984
3008
  args = parser.parse_args()
2985
3009
 
3010
+ _check_guard_setup(args.command or "")
3011
+
3012
+ if args.command == "guard" and getattr(args, "guard_command", None) == "skip-setup":
3013
+ _GUARD_SKIP.parent.mkdir(parents=True, exist_ok=True)
3014
+ _GUARD_SKIP.touch()
3015
+ print(f"{GREEN}✓ Setup reminder suppressed.{RESET} Run `conduct guard sync` anytime to enable Guard.")
3016
+ return
3017
+
2986
3018
  if args.command == "login":
2987
3019
  cmd_login(args)
2988
3020
  elif args.command == "agents":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.0
3
+ Version: 0.5.3
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