conduct-cli 0.5.4__tar.gz → 0.5.5__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.4 → conduct_cli-0.5.5}/PKG-INFO +1 -1
  2. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/pyproject.toml +1 -1
  3. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/api.py +2 -2
  4. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/guard.py +61 -31
  5. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/guardmcp.py +44 -19
  6. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/hook_template.py +22 -4
  7. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/main.py +82 -0
  8. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  9. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/README.md +0 -0
  10. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/setup.cfg +0 -0
  11. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/setup.py +0 -0
  12. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/__init__.py +0 -0
  13. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/hook_precompact_template.py +0 -0
  14. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/hook_session_start_template.py +0 -0
  15. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/hook_stop_template.py +0 -0
  16. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/mcp_server.py +0 -0
  17. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/memory.py +0 -0
  18. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli/paxel.py +0 -0
  19. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  20. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  21. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  22. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/requires.txt +0 -0
  23. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/src/conduct_cli.egg-info/top_level.txt +0 -0
  24. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/tests/test_guard_policy.py +0 -0
  25. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/tests/test_guard_savings.py +0 -0
  26. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/tests/test_hook_syntax.py +0 -0
  27. {conduct_cli-0.5.4 → conduct_cli-0.5.5}/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.4
3
+ Version: 0.5.5
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.4"
7
+ version = "0.5.5"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -31,7 +31,7 @@ def req(method: str, url: str, hdrs: dict, body=None, timeout: int = 30) -> dict
31
31
  detail = json.loads(raw).get("detail", raw)
32
32
  except Exception:
33
33
  detail = raw
34
- print(f"{RED}HTTP {e.code}: {detail}{RESET}")
34
+ print(f"{RED}HTTP {e.code}: {detail} [{url}]{RESET}")
35
35
  sys.exit(1)
36
36
  except (socket.timeout, TimeoutError):
37
37
  print(f"{RED}Request timed out: {url}{RESET}")
@@ -50,7 +50,7 @@ def req_text(method: str, url: str, hdrs: dict, body_text: str, timeout: int = 3
50
50
  detail = json.loads(raw).get("detail", raw)
51
51
  except Exception:
52
52
  detail = raw
53
- print(f"{RED}HTTP {e.code}: {detail}{RESET}")
53
+ print(f"{RED}HTTP {e.code}: {detail} [{url}]{RESET}")
54
54
  sys.exit(1)
55
55
  except (socket.timeout, TimeoutError):
56
56
  print(f"{RED}Request timed out: {url}{RESET}")
@@ -245,6 +245,17 @@ def _api_url(cfg: dict) -> str:
245
245
  # ── MCP registration ──────────────────────────────────────────────────────────
246
246
 
247
247
  # Tools that support mcpServers JSON — only write if the config file already exists
248
+ def _vscode_mcp_paths() -> list[tuple[Path, str]]:
249
+ """VS Code Copilot MCP config locations (macOS + Linux). Only if Copilot is installed."""
250
+ ext_dir = Path.home() / ".vscode" / "extensions"
251
+ if not ext_dir.exists() or not any(p.name.startswith("github.copilot") for p in ext_dir.iterdir() if p.is_dir()):
252
+ return []
253
+ candidates = [
254
+ Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json",
255
+ Path.home() / ".config" / "Code" / "User" / "mcp.json",
256
+ ]
257
+ return [(p, "VS Code Copilot") for p in candidates if p.parent.exists()]
258
+
248
259
  _MCP_TARGETS = [
249
260
  (Path.home() / ".claude" / "settings.json", "Claude Code"),
250
261
  (Path.home() / ".cursor" / "mcp.json", "Cursor"),
@@ -267,8 +278,9 @@ def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
267
278
  if shutil.which("booster"):
268
279
  servers["agent-booster"] = {"command": "booster", "args": ["serve"]}
269
280
 
281
+ targets = list(_MCP_TARGETS) + _vscode_mcp_paths()
270
282
  found_any = False
271
- for cfg_path, label in _MCP_TARGETS:
283
+ for cfg_path, label in targets:
272
284
  if not cfg_path.exists():
273
285
  continue
274
286
  found_any = True
@@ -382,7 +394,7 @@ def _req(method: str, url: str, body=None, token: str = None, api_key: str = Non
382
394
  detail = json.loads(raw).get("detail", raw)
383
395
  except Exception:
384
396
  detail = raw
385
- print(f"{RED}HTTP {e.code}: {detail}{RESET}")
397
+ print(f"{RED}HTTP {e.code}: {detail} [{url}]{RESET}")
386
398
  sys.exit(1)
387
399
  except Exception:
388
400
  print(f"{RED}Could not reach ConductAI API. Check your connection.{RESET}")
@@ -395,6 +407,13 @@ def _save_policy(policy: dict):
395
407
  POLICY_PATH.write_text(json.dumps(policy, indent=2))
396
408
 
397
409
 
410
+ def _load_policy() -> dict:
411
+ try:
412
+ return json.loads(POLICY_PATH.read_text())
413
+ except Exception:
414
+ return {"rules": []}
415
+
416
+
398
417
  # ── since-string parser ───────────────────────────────────────────────────────
399
418
 
400
419
  def _parse_since(since_str: str) -> str:
@@ -530,15 +549,6 @@ def cmd_guard_install(args):
530
549
  user_email = result.get("user_email") or ""
531
550
  clerk_user_id = result.get("clerk_user_id") or ""
532
551
 
533
- # Check if Security Loop module is installed for this workspace
534
- security_emit = False
535
- try:
536
- sec = _req("GET", f"{server}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
537
- if sec.get("installed"):
538
- security_emit = True
539
- except Exception:
540
- pass
541
-
542
552
  # Persona selection — prompt once, skip if already chosen
543
553
  _ensure_persona(workspace_id, api_key, server)
544
554
 
@@ -551,11 +561,8 @@ def cmd_guard_install(args):
551
561
  "clerk_user_id": clerk_user_id,
552
562
  "api_key": api_key,
553
563
  "api_url": server,
554
- "security_emit_enabled": security_emit,
555
564
  "last_synced_at": _time.time(),
556
565
  })
557
- if security_emit:
558
- print(f" {GREEN}Security Loop:{RESET} installed — classifier active")
559
566
 
560
567
  # Download policies
561
568
  try:
@@ -783,7 +790,8 @@ def cmd_guard_sync(args):
783
790
  # Persona selection — prompt once, skip if already chosen
784
791
  _ensure_persona(workspace_id, api_key, base_url)
785
792
 
786
- print(f"Syncing policy…")
793
+ dry_run = getattr(args, "dry_run", False)
794
+ print(f"{'[dry-run] ' if dry_run else ''}Syncing policy…")
787
795
 
788
796
  try:
789
797
  policy = _req(
@@ -794,28 +802,49 @@ def cmd_guard_sync(args):
794
802
  except Exception as e:
795
803
  print(f"Guard sync skipped: {e}", file=sys.stderr)
796
804
  sys.exit(0)
805
+
806
+ rules = policy.get("rules", [])
807
+ rule_count = len(rules)
808
+
809
+ if dry_run:
810
+ current_policy = _load_policy()
811
+ current_rules = {r["rule_id"]: r for r in current_policy.get("rules", [])}
812
+ new_rules = {r["rule_id"]: r for r in rules}
813
+
814
+ added = [r for r in new_rules if r not in current_rules]
815
+ removed = [r for r in current_rules if r not in new_rules]
816
+ changed = [
817
+ r for r in new_rules
818
+ if r in current_rules and new_rules[r].get("action") != current_rules[r].get("action")
819
+ ]
820
+
821
+ print(f"\n {BOLD}Dry run — no files written{RESET}")
822
+ print(f" Rules in policy: {rule_count}")
823
+ if added:
824
+ print(f"\n {GREEN}+ Added ({len(added)}){RESET}")
825
+ for rid in added:
826
+ r = new_rules[rid]
827
+ print(f" {rid} [{r.get('action','?').upper()}]")
828
+ if removed:
829
+ print(f"\n {RED}- Removed ({len(removed)}){RESET}")
830
+ for rid in removed:
831
+ print(f" {rid}")
832
+ if changed:
833
+ print(f"\n ~ Changed action ({len(changed)})")
834
+ for rid in changed:
835
+ old_a = current_rules[rid].get("action", "?")
836
+ new_a = new_rules[rid].get("action", "?")
837
+ print(f" {rid} {old_a} → {new_a}")
838
+ if not added and not removed and not changed:
839
+ print(f" {GREEN}No changes{RESET}")
840
+ return
841
+
797
842
  _save_policy(policy)
798
- rule_count = len(policy.get("rules", []))
799
843
  print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
800
844
 
801
845
  if getattr(args, "cursor", False):
802
846
  _write_cursorrules(policy)
803
847
 
804
- # Re-check Security Loop install status
805
- try:
806
- sec = _req("GET", f"{base_url}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
807
- cfg["security_emit_enabled"] = bool(sec.get("installed"))
808
- _save_guard_config(cfg)
809
- if sec.get("installed"):
810
- print(f" {GREEN}Security Loop:{RESET} installed — classifier active")
811
- try:
812
- policies = _req("GET", f"{base_url}/secure/policies?workspace_id={workspace_id}", api_key=api_key)
813
- policy_count = len(policies) if isinstance(policies, list) else 0
814
- print(f" {GREEN}Security Loop policies:{RESET} {policy_count} rule(s) synced")
815
- except Exception:
816
- pass
817
- except Exception:
818
- pass
819
848
 
820
849
  # Refresh hook script + re-register in all tools
821
850
  hook_path = GUARD_DIR / "hook.py"
@@ -1263,6 +1292,7 @@ def register_guard_parser(sub):
1263
1292
  # conduct guard sync
1264
1293
  sync_p = guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
1265
1294
  sync_p.add_argument("--cursor", action="store_true", help="Write active Guard policies to .cursorrules")
1295
+ sync_p.add_argument("--dry-run", action="store_true", help="Preview policy changes without writing anything")
1266
1296
 
1267
1297
  # conduct guard status
1268
1298
  guard_sub.add_parser("status", help="Show today's spend and violations")
@@ -139,12 +139,31 @@ def _load_config() -> dict:
139
139
  return {}
140
140
 
141
141
 
142
- _sync_lock = threading.Lock()
142
+ _DAEMON_URL = "http://127.0.0.1:7878"
143
+ _sync_lock = threading.Lock()
143
144
  _SYNC_INTERVAL = 300 # 5 minutes
144
145
 
145
146
 
147
+ def _daemon_alive() -> bool:
148
+ try:
149
+ urllib.request.urlopen(f"{_DAEMON_URL}/health", timeout=0.3)
150
+ return True
151
+ except Exception:
152
+ return False
153
+
154
+
146
155
  def _maybe_sync() -> None:
147
- """If config is stale (>5 min since last sync), run `conduct guard sync` in background."""
156
+ """Sync policy: daemon (instant) conduct guard sync (every 5 min). Never raises."""
157
+ try:
158
+ if _daemon_alive():
159
+ cfg = _load_config()
160
+ ws_id = cfg.get("workspace_id", "")
161
+ if ws_id:
162
+ with urllib.request.urlopen(f"{_DAEMON_URL}/policy?workspace_id={ws_id}", timeout=1) as r:
163
+ POLICY_PATH.write_text(r.read().decode())
164
+ return
165
+ except Exception:
166
+ pass # fall through to CLI sync
148
167
  cfg = _load_config()
149
168
  last_sync = cfg.get("last_synced_at", 0)
150
169
  if time.time() - last_sync < _SYNC_INTERVAL:
@@ -153,11 +172,7 @@ def _maybe_sync() -> None:
153
172
  return # another thread is already syncing
154
173
  def _run():
155
174
  try:
156
- subprocess.run(
157
- ["conduct", "guard", "sync"],
158
- timeout=15,
159
- capture_output=True,
160
- )
175
+ subprocess.run(["conduct", "guard", "sync"], timeout=15, capture_output=True)
161
176
  except Exception:
162
177
  pass
163
178
  finally:
@@ -192,12 +207,11 @@ def _post_audit_event(
192
207
  token: str,
193
208
  ai_tool: str,
194
209
  ) -> None:
195
- """Fire-and-forget: post a guard audit event to the Conduct API."""
210
+ """Fire-and-forget: queue event via daemon (batch) or direct API fallback."""
196
211
  if not workspace_id:
197
212
  return
198
- cfg = _load_config()
199
- api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
200
- payload = json.dumps({
213
+ cfg = _load_config()
214
+ event = {
201
215
  "workspace_id": workspace_id,
202
216
  "clerk_user_id": cfg.get("user_email", ""),
203
217
  "user_email": cfg.get("user_email", ""),
@@ -207,15 +221,26 @@ def _post_audit_event(
207
221
  "decision": decision,
208
222
  "rule_id": rule_id,
209
223
  "hook_session_id": _SESSION_ID,
210
- }).encode()
224
+ }
211
225
  try:
212
- req = urllib.request.Request(
213
- f"{api_url}/guard/events",
214
- data=payload,
215
- headers={
216
- "Content-Type": "application/json",
217
- "Authorization": f"Bearer {token}",
218
- },
226
+ # Fast path: daemon batches events, reducing API calls 30:1
227
+ if _daemon_alive():
228
+ body = json.dumps(event).encode()
229
+ req = urllib.request.Request(
230
+ f"{_DAEMON_URL}/event", data=body,
231
+ headers={"Content-Type": "application/json"}, method="POST",
232
+ )
233
+ urllib.request.urlopen(req, timeout=1)
234
+ return
235
+ except Exception:
236
+ pass
237
+ # Fallback: direct API post
238
+ try:
239
+ api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
240
+ body = json.dumps(event).encode()
241
+ req = urllib.request.Request(
242
+ f"{api_url}/guard/events", data=body,
243
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
219
244
  method="POST",
220
245
  )
221
246
  urllib.request.urlopen(req, timeout=5)
@@ -39,8 +39,19 @@ VERSION_CACHE_TTL = 60 # 1 minute — matches server poll window
39
39
  WARNED_RULES_PATH = GUARD_DIR / "warned_rules.json"
40
40
 
41
41
 
42
+ _DAEMON_URL = "http://127.0.0.1:7878"
43
+
44
+
45
+ def _daemon_alive() -> bool:
46
+ try:
47
+ with urllib.request.urlopen(f"{_DAEMON_URL}/health", timeout=0.3):
48
+ return True
49
+ except Exception:
50
+ return False
51
+
52
+
42
53
  def _maybe_sync_policy():
43
- """Check server policy version once per minute; re-download if stale. Never raises."""
54
+ """Sync policy from daemon (instant) or remote API (once per minute). Never raises."""
44
55
  try:
45
56
  cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
46
57
  workspace_id = cfg.get("workspace_id")
@@ -48,18 +59,25 @@ def _maybe_sync_policy():
48
59
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
49
60
  if not workspace_id:
50
61
  return
51
- # Check cache TTL
62
+
63
+ # Fast path: daemon is running — it keeps policy fresh via WebSocket push
64
+ if _daemon_alive():
65
+ url = f"{_DAEMON_URL}/policy?workspace_id={workspace_id}"
66
+ with urllib.request.urlopen(url, timeout=1) as resp:
67
+ remote = json.loads(resp.read())
68
+ POLICY_PATH.write_text(json.dumps(remote, indent=2))
69
+ return
70
+
71
+ # Slow path: no daemon — poll remote API once per minute
52
72
  if VERSION_CACHE_PATH.exists():
53
73
  cache = json.loads(VERSION_CACHE_PATH.read_text())
54
74
  if time.time() - cache.get("ts", 0) < VERSION_CACHE_TTL:
55
75
  return
56
- # Fetch current version from server
57
76
  url = f"{api_url}/guard/policies/sync?workspace_id={workspace_id}"
58
77
  req = urllib.request.Request(url, headers={"Authorization": f"Bearer {api_key}"} if api_key else {})
59
78
  with urllib.request.urlopen(req, timeout=2) as resp:
60
79
  remote = json.loads(resp.read())
61
80
  remote_version = remote.get("version", "")
62
- # Compare to local
63
81
  local_version = ""
64
82
  if POLICY_PATH.exists():
65
83
  local_version = json.loads(POLICY_PATH.read_text()).get("version", "")
@@ -2858,6 +2858,77 @@ def _check_guard_setup(command: str) -> None:
2858
2858
  )
2859
2859
 
2860
2860
 
2861
+
2862
+ def cmd_skill(args):
2863
+ """conduct skill list | install <slug> | uninstall <slug>"""
2864
+ skill_command = getattr(args, "skill_command", None)
2865
+ server, workspace, api_key, token = _require_auth(args)
2866
+
2867
+ headers = {"Content-Type": "application/json"}
2868
+ if api_key:
2869
+ headers["X-Api-Key"] = api_key
2870
+ elif token:
2871
+ headers["Authorization"] = f"Bearer {token}"
2872
+
2873
+ def _req(method, path, expect=None):
2874
+ url = f"{server}{path}"
2875
+ req = urllib.request.Request(url, headers=headers, method=method)
2876
+ try:
2877
+ with urllib.request.urlopen(req, timeout=10) as r:
2878
+ data = json.loads(r.read())
2879
+ return data
2880
+ except urllib.error.HTTPError as e:
2881
+ body = e.read().decode()
2882
+ try:
2883
+ msg = json.loads(body).get("detail", body)
2884
+ except Exception:
2885
+ msg = body
2886
+ print(f"{RED}Error {e.code}: {msg}{RESET}")
2887
+ sys.exit(1)
2888
+
2889
+ if skill_command == "list":
2890
+ data = _req("GET", f"/compliance/packs/available?workspace_id={workspace}")
2891
+ installed_data = _req("GET", f"/compliance/packs/installed?workspace_id={workspace}")
2892
+ installed = set(installed_data.get("installed", []))
2893
+
2894
+ packs = data.get("packs", [])
2895
+ if not packs:
2896
+ print("No skill packs available.")
2897
+ return
2898
+
2899
+ print(f"\n{'Slug':<20} {'Name':<22} {'Tier':<8} {'Rules':<7} Status")
2900
+ print("─" * 70)
2901
+ for p in packs:
2902
+ slug = p.get("slug", "")
2903
+ name = p.get("name", "")
2904
+ tier = p.get("tier", "")
2905
+ rules = len(p.get("rules", []))
2906
+ status = f"{GREEN}installed{RESET}" if slug in installed else f"{GRAY}available{RESET}"
2907
+ print(f"{slug:<20} {name:<22} {tier:<8} {rules:<7} {status}")
2908
+ print()
2909
+
2910
+ elif skill_command == "install":
2911
+ slug = getattr(args, "slug", None)
2912
+ if not slug:
2913
+ print(f"{RED}Usage: conduct skill install <slug>{RESET}")
2914
+ sys.exit(1)
2915
+ _req("POST", f"/compliance/packs/{slug}/install?workspace_id={workspace}")
2916
+ print(f"{GREEN}✓{RESET} Installed {BOLD}{slug}{RESET}")
2917
+ print(f" Run {BOLD}conduct guard sync{RESET} to push the updated policy to your hook.")
2918
+
2919
+ elif skill_command == "uninstall":
2920
+ slug = getattr(args, "slug", None)
2921
+ if not slug:
2922
+ print(f"{RED}Usage: conduct skill uninstall <slug>{RESET}")
2923
+ sys.exit(1)
2924
+ _req("DELETE", f"/compliance/packs/{slug}/uninstall?workspace_id={workspace}")
2925
+ print(f"{GREEN}✓{RESET} Uninstalled {BOLD}{slug}{RESET}")
2926
+ print(f" Run {BOLD}conduct guard sync{RESET} to push the updated policy to your hook.")
2927
+
2928
+ else:
2929
+ print("Usage: conduct skill <list|install|uninstall> [slug]")
2930
+
2931
+
2861
2932
  def main():
2862
2933
  _auto_update()
2863
2934
 
@@ -2998,6 +3069,15 @@ def main():
2998
3069
  mcp_sub = mcp_p.add_subparsers(dest="mcp_command")
2999
3070
  mcp_sub.add_parser("install", help="Register conduct-mcp in Claude Code and Codex")
3000
3071
 
3072
+ # conduct skill
3073
+ skill_p = sub.add_parser("skill", help="Manage Guard skill packs")
3074
+ skill_sub = skill_p.add_subparsers(dest="skill_command")
3075
+ skill_sub.add_parser("list", help="List available and installed skill packs")
3076
+ skill_install_p = skill_sub.add_parser("install", help="Install a skill pack")
3077
+ skill_install_p.add_argument("slug", help="Pack slug, e.g. conduct-owasp")
3078
+ skill_uninstall_p = skill_sub.add_parser("uninstall", help="Uninstall a skill pack")
3079
+ skill_uninstall_p.add_argument("slug", help="Pack slug, e.g. conduct-owasp")
3080
+
3001
3081
  # conduct sync
3002
3082
  sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
3003
3083
 
@@ -3091,6 +3171,8 @@ def main():
3091
3171
  cmd_mcp_install(args)
3092
3172
  else:
3093
3173
  mcp_p.print_help()
3174
+ elif args.command == "skill":
3175
+ cmd_skill(args)
3094
3176
  elif args.command == "sync":
3095
3177
  cmd_sync(args)
3096
3178
  elif args.command == "test-guard":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.4
3
+ Version: 0.5.5
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