conduct-cli 0.5.0__tar.gz → 0.5.2__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.2}/PKG-INFO +1 -1
  2. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/pyproject.toml +1 -1
  3. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/guard.py +39 -11
  4. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/guardmcp.py +35 -0
  5. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_template.py +6 -2
  6. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/main.py +32 -0
  7. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  8. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/README.md +0 -0
  9. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/setup.cfg +0 -0
  10. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/setup.py +0 -0
  11. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/__init__.py +0 -0
  12. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/api.py +0 -0
  13. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_precompact_template.py +0 -0
  14. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_session_start_template.py +0 -0
  15. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_stop_template.py +0 -0
  16. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/mcp_server.py +0 -0
  17. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/memory.py +0 -0
  18. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/paxel.py +0 -0
  19. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  20. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  21. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  22. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/requires.txt +0 -0
  23. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/top_level.txt +0 -0
  24. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_guard_policy.py +0 -0
  25. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_guard_savings.py +0 -0
  26. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_hook_syntax.py +0 -0
  27. {conduct_cli-0.5.0 → conduct_cli-0.5.2}/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.2
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.2"
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
@@ -878,6 +893,8 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
878
893
  "saved_tokens": raw.get("saved_tokens", 0),
879
894
  "savings_pct": raw.get("savings_pct", 0.0),
880
895
  "total_reads": raw.get("total_reads", 0),
896
+ "crusher": raw.get("crusher", {}),
897
+ "cache_align": raw.get("cache_align", {}),
881
898
  }
882
899
  except Exception:
883
900
  pass
@@ -929,6 +946,14 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
929
946
  except Exception:
930
947
  pass # Never fail sync because savings POST failed
931
948
 
949
+ # Push booster symbol index to team workspace (best-effort)
950
+ try:
951
+ r = subprocess.run(["booster", "index-push"], capture_output=True, text=True, timeout=30)
952
+ if r.returncode == 0 and r.stdout.strip():
953
+ print(f" {GREEN}Booster index:{RESET} {r.stdout.strip()}")
954
+ except Exception:
955
+ pass
956
+
932
957
 
933
958
  def cmd_guard_status(args):
934
959
  cfg = _require_guard_config()
@@ -1154,6 +1179,9 @@ def register_guard_parser(sub):
1154
1179
  # conduct guard booster-status
1155
1180
  guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
1156
1181
 
1182
+ # conduct guard skip-setup
1183
+ guard_sub.add_parser("skip-setup", help="Suppress the Guard setup reminder (does not disable Guard)")
1184
+
1157
1185
  return guard_p, guard_sub
1158
1186
 
1159
1187
 
@@ -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.2
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