conduct-cli 0.4.28__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.28 → conduct_cli-0.4.30}/PKG-INFO +1 -1
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/pyproject.toml +1 -1
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/guard.py +124 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/main.py +226 -11
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/README.md +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/setup.cfg +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/setup.py +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.28 → conduct_cli-0.4.30}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.28 → 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
|
|
|
@@ -217,7 +217,7 @@ def _write_codex_mcp_config() -> bool:
|
|
|
217
217
|
try:
|
|
218
218
|
content = config_path.read_text() if config_path.exists() else ""
|
|
219
219
|
if "conduct-mcp" in content:
|
|
220
|
-
return True
|
|
220
|
+
return True
|
|
221
221
|
mcp_block = '\n[[mcp_servers]]\nname = "conduct"\ncommand = "conduct-mcp"\nargs = []\n'
|
|
222
222
|
config_path.write_text(content + mcp_block)
|
|
223
223
|
return True
|
|
@@ -225,8 +225,198 @@ def _write_codex_mcp_config() -> bool:
|
|
|
225
225
|
return False
|
|
226
226
|
|
|
227
227
|
|
|
228
|
+
def _write_cursor_mcp_config() -> bool:
|
|
229
|
+
"""Write conduct-mcp into ~/.cursor/mcp.json. Returns True if written."""
|
|
230
|
+
cursor_dir = Path.home() / ".cursor"
|
|
231
|
+
if not cursor_dir.exists():
|
|
232
|
+
return False
|
|
233
|
+
config_path = cursor_dir / "mcp.json"
|
|
234
|
+
try:
|
|
235
|
+
existing = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
236
|
+
servers = existing.setdefault("mcpServers", {})
|
|
237
|
+
if "conduct" in servers:
|
|
238
|
+
return True
|
|
239
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
240
|
+
config_path.write_text(json.dumps(existing, indent=2))
|
|
241
|
+
return True
|
|
242
|
+
except Exception:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_windsurf_mcp_config() -> bool:
|
|
247
|
+
"""Write conduct-mcp into ~/.codeium/windsurf/mcp_config.json. Returns True if written."""
|
|
248
|
+
config_path = Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
|
|
249
|
+
if not config_path.parent.exists():
|
|
250
|
+
return False
|
|
251
|
+
try:
|
|
252
|
+
existing = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
253
|
+
servers = existing.setdefault("mcpServers", {})
|
|
254
|
+
if "conduct" in servers:
|
|
255
|
+
return True
|
|
256
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
257
|
+
config_path.write_text(json.dumps(existing, indent=2))
|
|
258
|
+
return True
|
|
259
|
+
except Exception:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _write_vscode_mcp_config() -> bool:
|
|
264
|
+
"""Write conduct-mcp into VS Code settings.json (mcp.servers). Returns True if written."""
|
|
265
|
+
# Check both standard locations
|
|
266
|
+
candidates = [
|
|
267
|
+
Path.home() / ".vscode" / "settings.json",
|
|
268
|
+
Path.home() / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
269
|
+
Path.home() / ".config" / "Code" / "User" / "settings.json",
|
|
270
|
+
]
|
|
271
|
+
settings_path = next((p for p in candidates if p.exists()), None)
|
|
272
|
+
if not settings_path:
|
|
273
|
+
return False
|
|
274
|
+
try:
|
|
275
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
276
|
+
servers = existing.setdefault("mcp", {}).setdefault("servers", {})
|
|
277
|
+
if "conduct" in servers:
|
|
278
|
+
return True
|
|
279
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
280
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
281
|
+
return True
|
|
282
|
+
except Exception:
|
|
283
|
+
return False
|
|
284
|
+
|
|
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
|
+
|
|
228
418
|
def cmd_mcp_install(args):
|
|
229
|
-
"""Register conduct-mcp in Claude Code and
|
|
419
|
+
"""Register conduct-mcp in Claude Code, Codex, Cursor, Windsurf, and VS Code."""
|
|
230
420
|
import shutil
|
|
231
421
|
import subprocess
|
|
232
422
|
|
|
@@ -242,29 +432,48 @@ def cmd_mcp_install(args):
|
|
|
242
432
|
if result.returncode == 0:
|
|
243
433
|
registered.append("Claude Code")
|
|
244
434
|
else:
|
|
245
|
-
# claude mcp add is idempotent; also try writing settings.json directly as fallback
|
|
246
435
|
_write_claude_mcp_settings()
|
|
247
436
|
registered.append("Claude Code (settings.json)")
|
|
248
437
|
except Exception:
|
|
249
438
|
_write_claude_mcp_settings()
|
|
250
439
|
registered.append("Claude Code (settings.json)")
|
|
251
440
|
else:
|
|
252
|
-
|
|
253
|
-
written = _write_claude_mcp_settings()
|
|
254
|
-
if written:
|
|
441
|
+
if _write_claude_mcp_settings():
|
|
255
442
|
registered.append("Claude Code (settings.json)")
|
|
256
443
|
|
|
257
444
|
# --- Codex CLI ---
|
|
258
|
-
|
|
259
|
-
if written:
|
|
445
|
+
if _write_codex_mcp_config():
|
|
260
446
|
registered.append("Codex")
|
|
261
447
|
|
|
448
|
+
# --- Cursor ---
|
|
449
|
+
if _write_cursor_mcp_config():
|
|
450
|
+
registered.append("Cursor")
|
|
451
|
+
|
|
452
|
+
# --- Windsurf ---
|
|
453
|
+
if _write_windsurf_mcp_config():
|
|
454
|
+
registered.append("Windsurf")
|
|
455
|
+
|
|
456
|
+
# --- VS Code (Copilot) ---
|
|
457
|
+
if _write_vscode_mcp_config():
|
|
458
|
+
registered.append("VS Code (Copilot)")
|
|
459
|
+
|
|
262
460
|
if registered:
|
|
263
461
|
print(f"{GREEN}✓ conduct-mcp registered in: {', '.join(registered)}{RESET}")
|
|
264
|
-
print(f"{GRAY} Restart
|
|
462
|
+
print(f"{GRAY} Restart your AI tools to pick up the new MCP server.{RESET}")
|
|
265
463
|
else:
|
|
266
|
-
print(f"{YELLOW}⚠ No supported AI tools detected
|
|
267
|
-
print(f"{GRAY}
|
|
464
|
+
print(f"{YELLOW}⚠ No supported AI tools detected on this machine.{RESET}")
|
|
465
|
+
print(f"{GRAY} Supported: Claude Code, Codex, Cursor, Windsurf, VS Code{RESET}")
|
|
466
|
+
print(f"{GRAY} After installing any of these, re-run: conduct mcp install{RESET}")
|
|
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}")
|
|
268
477
|
|
|
269
478
|
|
|
270
479
|
def cmd_login(args):
|
|
@@ -336,6 +545,12 @@ def cmd_login(args):
|
|
|
336
545
|
except Exception:
|
|
337
546
|
pass # Never block login on MCP registration errors
|
|
338
547
|
|
|
548
|
+
# Report tool coverage to Guard
|
|
549
|
+
try:
|
|
550
|
+
_report_tool_coverage()
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
|
|
339
554
|
|
|
340
555
|
def cmd_agents(args):
|
|
341
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
|