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.
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/PKG-INFO +1 -1
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/pyproject.toml +1 -1
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/guard.py +124 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/main.py +148 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/README.md +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/setup.cfg +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/setup.py +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.29 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|