conduct-cli 0.5.2__tar.gz → 0.5.4__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.2 → conduct_cli-0.5.4}/PKG-INFO +1 -1
  2. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/pyproject.toml +1 -1
  3. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/guard.py +112 -9
  4. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/main.py +20 -9
  5. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  6. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/README.md +0 -0
  7. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/setup.cfg +0 -0
  8. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/setup.py +0 -0
  9. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/__init__.py +0 -0
  10. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/api.py +0 -0
  11. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/guardmcp.py +0 -0
  12. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_precompact_template.py +0 -0
  13. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_session_start_template.py +0 -0
  14. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_stop_template.py +0 -0
  15. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_template.py +0 -0
  16. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/mcp_server.py +0 -0
  17. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/memory.py +0 -0
  18. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/paxel.py +0 -0
  19. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  20. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  21. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  22. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/requires.txt +0 -0
  23. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/top_level.txt +0 -0
  24. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_guard_policy.py +0 -0
  25. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_guard_savings.py +0 -0
  26. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_hook_syntax.py +0 -0
  27. {conduct_cli-0.5.2 → conduct_cli-0.5.4}/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.2
3
+ Version: 0.5.4
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.2"
7
+ version = "0.5.4"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -159,6 +159,61 @@ def _install_session_hooks() -> None:
159
159
 
160
160
  # ── Guard config helpers ──────────────────────────────────────────────────────
161
161
 
162
+ _PERSONA_LABELS = {
163
+ "conservative": "Conservative — production-safe, default deny",
164
+ "standard": "Standard — engineering teams, balanced",
165
+ "developer": "Developer — local dev, audit-first",
166
+ }
167
+
168
+
169
+ def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
170
+ """Prompt for persona if none is set yet. Saves choice to guard config and API.
171
+
172
+ Returns the active persona name. Skips prompt silently if already set.
173
+ """
174
+ cfg = _load_guard_config()
175
+ if cfg.get("persona"):
176
+ return cfg["persona"]
177
+
178
+ print(f"\n{BOLD}Choose a policy persona for your agents:{RESET}")
179
+ choices = list(_PERSONA_LABELS.keys())
180
+ for i, key in enumerate(choices, 1):
181
+ print(f" {i}. {_PERSONA_LABELS[key]}")
182
+
183
+ while True:
184
+ try:
185
+ raw = input(f"\nEnter 1-{len(choices)} [default: 2 — Standard]: ").strip()
186
+ if raw == "":
187
+ raw = "2"
188
+ idx = int(raw) - 1
189
+ if 0 <= idx < len(choices):
190
+ chosen = choices[idx]
191
+ break
192
+ print(f" Enter a number between 1 and {len(choices)}")
193
+ except (ValueError, EOFError):
194
+ chosen = "standard"
195
+ break
196
+
197
+ # Push to API
198
+ try:
199
+ _req(
200
+ "PATCH",
201
+ f"{base_url}/guard/config/persona",
202
+ body={"persona": chosen},
203
+ api_key=api_key,
204
+ )
205
+ except Exception:
206
+ pass # non-fatal — local config still records the choice
207
+
208
+ # Persist locally so we skip the prompt on subsequent syncs
209
+ cfg = _load_guard_config()
210
+ cfg["persona"] = chosen
211
+ _save_guard_config(cfg)
212
+
213
+ print(f" {GREEN}Persona set:{RESET} {chosen.capitalize()}")
214
+ return chosen
215
+
216
+
162
217
  def _load_guard_config() -> dict:
163
218
  if CONFIG_PATH.exists():
164
219
  return json.loads(CONFIG_PATH.read_text())
@@ -484,6 +539,9 @@ def cmd_guard_install(args):
484
539
  except Exception:
485
540
  pass
486
541
 
542
+ # Persona selection — prompt once, skip if already chosen
543
+ _ensure_persona(workspace_id, api_key, server)
544
+
487
545
  # Persist guard config — include api_key so CLI commands can authenticate
488
546
  import time as _time
489
547
  _save_guard_config({
@@ -655,15 +713,19 @@ def _report_tools_to_server() -> None:
655
713
  "hook_registered": False,
656
714
  })
657
715
 
658
- vscode_candidates = [
659
- home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
660
- home / ".config" / "Code" / "User" / "settings.json",
661
- home / ".vscode" / "settings.json",
662
- ]
663
- vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
664
- if vscode_settings:
716
+ vscode_ext_dir = home / ".vscode" / "extensions"
717
+ copilot_installed = vscode_ext_dir.exists() and any(
718
+ p.name.startswith("github.copilot") for p in vscode_ext_dir.iterdir() if p.is_dir()
719
+ )
720
+ if copilot_installed:
721
+ vscode_candidates = [
722
+ home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
723
+ home / ".config" / "Code" / "User" / "settings.json",
724
+ home / ".vscode" / "settings.json",
725
+ ]
726
+ vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
665
727
  try:
666
- d = json.loads(vscode_settings.read_text())
728
+ d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
667
729
  mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
668
730
  except Exception:
669
731
  mcp_reg = False
@@ -718,6 +780,9 @@ def cmd_guard_sync(args):
718
780
  api_key = cfg.get("api_key", "")
719
781
  base_url = _api_url(cfg)
720
782
 
783
+ # Persona selection — prompt once, skip if already chosen
784
+ _ensure_persona(workspace_id, api_key, base_url)
785
+
721
786
  print(f"Syncing policy…")
722
787
 
723
788
  try:
@@ -733,6 +798,9 @@ def cmd_guard_sync(args):
733
798
  rule_count = len(policy.get("rules", []))
734
799
  print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
735
800
 
801
+ if getattr(args, "cursor", False):
802
+ _write_cursorrules(policy)
803
+
736
804
  # Re-check Security Loop install status
737
805
  try:
738
806
  sec = _req("GET", f"{base_url}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
@@ -785,6 +853,40 @@ def cmd_guard_sync(args):
785
853
  print(f"\n {CYAN}{mcp_url}{RESET}\n")
786
854
 
787
855
 
856
+ def _write_cursorrules(policy: dict) -> None:
857
+ """Write active Guard policies into .cursorrules in the current directory."""
858
+ rules = policy.get("rules", [])
859
+ enabled = [r for r in rules if r.get("enabled", True)]
860
+ lines = [
861
+ "# .cursorrules — generated by Conduct AI Guard",
862
+ "# Run `conduct guard sync --cursor` to refresh.",
863
+ "# Do not edit manually — changes will be overwritten.",
864
+ "",
865
+ "## Conduct Guard Policies",
866
+ f"# {len(enabled)} active rule(s) enforced by ConductGuard.",
867
+ "",
868
+ ]
869
+ for r in enabled:
870
+ action = r.get("action", "warn").upper()
871
+ rule_id = r.get("rule_id", "")
872
+ desc = r.get("description") or r.get("message") or ""
873
+ lines.append(f"# [{action}] {rule_id}" + (f" — {desc}" if desc else ""))
874
+ pattern = r.get("pattern")
875
+ if pattern:
876
+ lines.append(f"# pattern: {pattern}")
877
+ lines += [
878
+ "",
879
+ "## General",
880
+ "# Never include secrets, API keys, or credentials in prompts.",
881
+ "# PII (emails, SSNs, phone numbers) is redacted by Conduct before reaching any model.",
882
+ "# Conduct AI governance is active — all tool calls are audited.",
883
+ "# Independent of Cursor's ownership — policies enforced by your team, not the IDE vendor.",
884
+ ]
885
+ out = Path(".cursorrules")
886
+ out.write_text("\n".join(lines) + "\n")
887
+ print(f" {GREEN}.cursorrules written:{RESET} {len(enabled)} rule(s) → {out.resolve()}")
888
+
889
+
788
890
  def _ensure_booster(root: Path) -> None:
789
891
  """Auto-init and background-index booster if installed but not yet set up."""
790
892
  import shutil
@@ -1159,7 +1261,8 @@ def register_guard_parser(sub):
1159
1261
  guard_sub = guard_p.add_subparsers(dest="guard_command")
1160
1262
 
1161
1263
  # conduct guard sync
1162
- guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
1264
+ sync_p = guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
1265
+ sync_p.add_argument("--cursor", action="store_true", help="Write active Guard policies to .cursorrules")
1163
1266
 
1164
1267
  # conduct guard status
1165
1268
  guard_sub.add_parser("status", help="Show today's spend and violations")
@@ -302,16 +302,21 @@ def _detect_ai_tools() -> list:
302
302
  "hook_registered": False, # Windsurf uses MCP only
303
303
  })
304
304
 
305
- # VS Code (Copilot)
306
- vscode_settings_candidates = [
307
- home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
308
- home / ".config" / "Code" / "User" / "settings.json",
309
- home / ".vscode" / "settings.json",
310
- ]
311
- vscode_settings = next((p for p in vscode_settings_candidates if p.exists()), None)
312
- if vscode_settings:
305
+ # VS Code (Copilot) — only report if Copilot extension is actually installed
306
+ vscode_ext_dir = home / ".vscode" / "extensions"
307
+ copilot_installed = vscode_ext_dir.exists() and any(
308
+ p.name.startswith("github.copilot") for p in vscode_ext_dir.iterdir()
309
+ if p.is_dir()
310
+ )
311
+ if copilot_installed:
312
+ vscode_settings_candidates = [
313
+ home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
314
+ home / ".config" / "Code" / "User" / "settings.json",
315
+ home / ".vscode" / "settings.json",
316
+ ]
317
+ vscode_settings = next((p for p in vscode_settings_candidates if p.exists()), None)
313
318
  try:
314
- d = json.loads(vscode_settings.read_text())
319
+ d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
315
320
  mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
316
321
  except Exception:
317
322
  mcp_reg = False
@@ -409,6 +414,12 @@ def cmd_mcp_install(args):
409
414
  if uncovered:
410
415
  print(f"{YELLOW} Not covered: {', '.join(uncovered)} — run: conduct mcp install{RESET}")
411
416
 
417
+ # Push updated coverage to Guard so the dashboard reflects the new state immediately
418
+ try:
419
+ _report_tool_coverage()
420
+ except Exception:
421
+ pass
422
+
412
423
 
413
424
  def cmd_login(args):
414
425
  server = args.server
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.2
3
+ Version: 0.5.4
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