conduct-cli 0.4.29__tar.gz → 0.4.30__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.29
3
+ Version: 0.4.30
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.4.29"
7
+ version = "0.4.30"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -832,6 +832,124 @@ def cmd_guard_join(args):
832
832
  )
833
833
 
834
834
 
835
+ def _report_tools_to_server() -> None:
836
+ """Detect AI coding tools on this machine and POST coverage to Guard API. Silent on failure."""
837
+ home = Path.home()
838
+
839
+ def _check_json_key(path: Path, *keys) -> bool:
840
+ try:
841
+ d = json.loads(path.read_text()) if path.exists() else {}
842
+ for k in keys:
843
+ d = d.get(k, {}) if isinstance(d, dict) else {}
844
+ return bool(d) and isinstance(d, dict) and len(d) > 0
845
+ except Exception:
846
+ return False
847
+
848
+ def _check_json_mcp(path: Path) -> bool:
849
+ try:
850
+ d = json.loads(path.read_text()) if path.exists() else {}
851
+ return "conduct" in d.get("mcpServers", {})
852
+ except Exception:
853
+ return False
854
+
855
+ def _check_json_hook(path: Path) -> bool:
856
+ try:
857
+ d = json.loads(path.read_text()) if path.exists() else {}
858
+ hooks = d.get("hooks", {})
859
+ pre = hooks.get("PreToolUse", [])
860
+ return any("conductguard" in str(h) or "conduct" in str(h).lower() for h in pre)
861
+ except Exception:
862
+ return False
863
+
864
+ def _check_toml_str(path: Path, needle: str) -> bool:
865
+ try:
866
+ return needle in (path.read_text() if path.exists() else "")
867
+ except Exception:
868
+ return False
869
+
870
+ tools = []
871
+
872
+ claude_dir = home / ".claude"
873
+ if claude_dir.exists():
874
+ settings = claude_dir / "settings.json"
875
+ tools.append({
876
+ "name": "claude-code",
877
+ "mcp_registered": _check_json_mcp(settings),
878
+ "hook_registered": _check_json_hook(settings),
879
+ })
880
+
881
+ codex_dir = home / ".codex"
882
+ if codex_dir.exists():
883
+ config = codex_dir / "config.toml"
884
+ tools.append({
885
+ "name": "codex",
886
+ "mcp_registered": _check_toml_str(config, "conduct-mcp"),
887
+ "hook_registered": _check_toml_str(config, "conductguard"),
888
+ })
889
+
890
+ cursor_dir = home / ".cursor"
891
+ if cursor_dir.exists():
892
+ tools.append({
893
+ "name": "cursor",
894
+ "mcp_registered": _check_json_mcp(cursor_dir / "mcp.json"),
895
+ "hook_registered": False,
896
+ })
897
+
898
+ windsurf_dir = home / ".codeium" / "windsurf"
899
+ if windsurf_dir.exists():
900
+ tools.append({
901
+ "name": "windsurf",
902
+ "mcp_registered": _check_json_mcp(windsurf_dir / "mcp_config.json"),
903
+ "hook_registered": False,
904
+ })
905
+
906
+ vscode_candidates = [
907
+ home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
908
+ home / ".config" / "Code" / "User" / "settings.json",
909
+ home / ".vscode" / "settings.json",
910
+ ]
911
+ vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
912
+ if vscode_settings:
913
+ try:
914
+ d = json.loads(vscode_settings.read_text())
915
+ mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
916
+ except Exception:
917
+ mcp_reg = False
918
+ tools.append({
919
+ "name": "vscode",
920
+ "mcp_registered": mcp_reg,
921
+ "hook_registered": False,
922
+ })
923
+
924
+ if not tools:
925
+ return
926
+
927
+ try:
928
+ cfg = _load_guard_config()
929
+ base_url = _api_url(cfg)
930
+ email = cfg.get("user_email", "")
931
+ token = cfg.get("member_token", "")
932
+ api_key = cfg.get("api_key", "")
933
+
934
+ if not email:
935
+ return
936
+
937
+ payload = json.dumps({"email": email, "tools": tools}).encode()
938
+ auth = token or api_key
939
+ req = urllib.request.Request(
940
+ f"{base_url}/guard/developer-tools",
941
+ data=payload,
942
+ headers={
943
+ "Content-Type": "application/json",
944
+ "Authorization": f"Bearer {auth}",
945
+ },
946
+ method="POST",
947
+ )
948
+ urllib.request.urlopen(req, timeout=8)
949
+ except Exception:
950
+ pass # Never surface errors — this is background telemetry
951
+
952
+
835
953
  def cmd_guard_sync(args):
836
954
  cfg = _require_guard_config()
837
955
  workspace_id = cfg.get("workspace_id")
@@ -861,6 +979,12 @@ def cmd_guard_sync(args):
861
979
  # Capture savings from RTK and Agent Booster
862
980
  _report_savings(cfg, base_url, api_key)
863
981
 
982
+ # Report AI tool coverage
983
+ try:
984
+ _report_tools_to_server()
985
+ except Exception:
986
+ pass
987
+
864
988
  print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
865
989
 
866
990
 
@@ -283,6 +283,138 @@ def _write_vscode_mcp_config() -> bool:
283
283
  return False
284
284
 
285
285
 
286
+ def _detect_ai_tools() -> list:
287
+ """
288
+ Detect which AI coding tools are installed and whether Guard/conduct-mcp is registered.
289
+ Returns list of {name, mcp_registered, hook_registered} for each detected tool.
290
+ Only includes tools whose config directory exists on this machine.
291
+ """
292
+ home = Path.home()
293
+ results = []
294
+
295
+ def _check_json_mcp(path: Path) -> bool:
296
+ try:
297
+ d = json.loads(path.read_text()) if path.exists() else {}
298
+ return "conduct" in d.get("mcpServers", {})
299
+ except Exception:
300
+ return False
301
+
302
+ def _check_json_hook(path: Path, hook_key: str = "hooks") -> bool:
303
+ try:
304
+ d = json.loads(path.read_text()) if path.exists() else {}
305
+ hooks = d.get(hook_key, {})
306
+ pre = hooks.get("PreToolUse", [])
307
+ return any("conductguard" in str(h) or "conduct" in str(h).lower() for h in pre)
308
+ except Exception:
309
+ return False
310
+
311
+ def _check_toml_str(path: Path, needle: str) -> bool:
312
+ try:
313
+ return needle in (path.read_text() if path.exists() else "")
314
+ except Exception:
315
+ return False
316
+
317
+ # Claude Code
318
+ claude_dir = home / ".claude"
319
+ if claude_dir.exists():
320
+ settings = claude_dir / "settings.json"
321
+ results.append({
322
+ "name": "claude-code",
323
+ "mcp_registered": _check_json_mcp(settings),
324
+ "hook_registered": _check_json_hook(settings),
325
+ })
326
+
327
+ # Codex
328
+ codex_dir = home / ".codex"
329
+ if codex_dir.exists():
330
+ config = codex_dir / "config.toml"
331
+ results.append({
332
+ "name": "codex",
333
+ "mcp_registered": _check_toml_str(config, "conduct-mcp"),
334
+ "hook_registered": _check_toml_str(config, "conductguard"),
335
+ })
336
+
337
+ # Cursor
338
+ cursor_dir = home / ".cursor"
339
+ if cursor_dir.exists():
340
+ results.append({
341
+ "name": "cursor",
342
+ "mcp_registered": _check_json_mcp(cursor_dir / "mcp.json"),
343
+ "hook_registered": False, # Cursor uses MCP only, no hook
344
+ })
345
+
346
+ # Windsurf
347
+ windsurf_dir = home / ".codeium" / "windsurf"
348
+ if windsurf_dir.exists():
349
+ results.append({
350
+ "name": "windsurf",
351
+ "mcp_registered": _check_json_mcp(windsurf_dir / "mcp_config.json"),
352
+ "hook_registered": False, # Windsurf uses MCP only
353
+ })
354
+
355
+ # VS Code (Copilot)
356
+ vscode_settings_candidates = [
357
+ home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
358
+ home / ".config" / "Code" / "User" / "settings.json",
359
+ home / ".vscode" / "settings.json",
360
+ ]
361
+ vscode_settings = next((p for p in vscode_settings_candidates if p.exists()), None)
362
+ if vscode_settings:
363
+ try:
364
+ d = json.loads(vscode_settings.read_text())
365
+ mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
366
+ except Exception:
367
+ mcp_reg = False
368
+ results.append({
369
+ "name": "vscode",
370
+ "mcp_registered": mcp_reg,
371
+ "hook_registered": False, # VS Code uses MCP only
372
+ })
373
+
374
+ return results
375
+
376
+
377
+ def _report_tool_coverage() -> None:
378
+ """Detect AI tools on this machine and POST coverage to Guard API. Silent on failure."""
379
+ try:
380
+ cfg = _load_config()
381
+ server = cfg.get("server", "").rstrip("/")
382
+ api_key = cfg.get("api_key", "")
383
+ token = cfg.get("token", "")
384
+ email = cfg.get("email", "")
385
+
386
+ # also check guard config for email/token
387
+ guard_cfg_path = Path.home() / ".conductguard" / "config.json"
388
+ if guard_cfg_path.exists():
389
+ gcfg = json.loads(guard_cfg_path.read_text())
390
+ if not email:
391
+ email = gcfg.get("user_email", "")
392
+ if not token:
393
+ token = gcfg.get("member_token", "")
394
+
395
+ if not server or not email:
396
+ return
397
+
398
+ tools = _detect_ai_tools()
399
+ if not tools:
400
+ return
401
+
402
+ payload = json.dumps({"email": email, "tools": tools}).encode()
403
+ auth = token or api_key
404
+ req = urllib.request.Request(
405
+ f"{server}/guard/developer-tools",
406
+ data=payload,
407
+ headers={
408
+ "Content-Type": "application/json",
409
+ "Authorization": f"Bearer {auth}",
410
+ },
411
+ method="POST",
412
+ )
413
+ urllib.request.urlopen(req, timeout=8)
414
+ except Exception:
415
+ pass # Never surface errors — this is background telemetry
416
+
417
+
286
418
  def cmd_mcp_install(args):
287
419
  """Register conduct-mcp in Claude Code, Codex, Cursor, Windsurf, and VS Code."""
288
420
  import shutil
@@ -333,6 +465,16 @@ def cmd_mcp_install(args):
333
465
  print(f"{GRAY} Supported: Claude Code, Codex, Cursor, Windsurf, VS Code{RESET}")
334
466
  print(f"{GRAY} After installing any of these, re-run: conduct mcp install{RESET}")
335
467
 
468
+ tools = _detect_ai_tools()
469
+ if tools:
470
+ print(f"{GRAY} Detected tools: {', '.join(t['name'] for t in tools)}{RESET}")
471
+ covered = [t['name'] for t in tools if t['mcp_registered']]
472
+ if covered:
473
+ print(f"{GREEN} MCP registered: {', '.join(covered)}{RESET}")
474
+ uncovered = [t['name'] for t in tools if not t['mcp_registered']]
475
+ if uncovered:
476
+ print(f"{YELLOW} Not covered: {', '.join(uncovered)} — run: conduct mcp install{RESET}")
477
+
336
478
 
337
479
  def cmd_login(args):
338
480
  server = args.server
@@ -403,6 +545,12 @@ def cmd_login(args):
403
545
  except Exception:
404
546
  pass # Never block login on MCP registration errors
405
547
 
548
+ # Report tool coverage to Guard
549
+ try:
550
+ _report_tool_coverage()
551
+ except Exception:
552
+ pass
553
+
406
554
 
407
555
  def cmd_agents(args):
408
556
  server, workspace_id, api_key, token = _require_auth(args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.29
3
+ Version: 0.4.30
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