claude-jacked 0.2.9__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.
jacked/cli.py CHANGED
@@ -51,6 +51,19 @@ def get_config(quiet: bool = False) -> Optional[SmartForkConfig]:
51
51
  sys.exit(1)
52
52
 
53
53
 
54
+ def _require_search(command_name: str) -> bool:
55
+ """Check if qdrant-client is installed. If not, print helpful error and return False."""
56
+ try:
57
+ import qdrant_client # noqa: F401
58
+ return True
59
+ except ImportError:
60
+ console.print(f"[red]Error:[/red] '{command_name}' requires the search extra.")
61
+ console.print('\nInstall it with:')
62
+ console.print(' [bold]pip install "claude-jacked\[search]"[/bold]')
63
+ console.print(' [bold]pipx install "claude-jacked\[search]"[/bold]')
64
+ return False
65
+
66
+
54
67
  @click.group()
55
68
  @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
56
69
  def main(verbose: bool):
@@ -66,8 +79,24 @@ def index(session: Optional[str], repo: Optional[str]):
66
79
  Index a Claude session to Qdrant.
67
80
 
68
81
  If SESSION is not provided, indexes the current session (from CLAUDE_SESSION_ID).
82
+ Requires: pip install "claude-jacked[search]"
69
83
  """
70
84
  import os
85
+
86
+ # Check if qdrant is available
87
+ try:
88
+ import qdrant_client # noqa: F401
89
+ except ImportError:
90
+ # If called from Stop hook (CLAUDE_SESSION_ID set), exit silently
91
+ # If called manually, show helpful message
92
+ if os.getenv("CLAUDE_SESSION_ID") and not session:
93
+ sys.exit(0)
94
+ else:
95
+ console.print("[red]Error:[/red] 'index' requires the search extra.")
96
+ console.print('\nInstall it with:')
97
+ console.print(' [bold]pip install "claude-jacked\[search]"[/bold]')
98
+ sys.exit(1)
99
+
71
100
  from jacked.indexer import SessionIndexer
72
101
 
73
102
  # Try to get config quietly - if not configured, nudge and exit cleanly
@@ -145,7 +174,10 @@ def index(session: Optional[str], repo: Optional[str]):
145
174
  @click.option("--repo", "-r", help="Filter by repository name pattern")
146
175
  @click.option("--force", "-f", is_flag=True, help="Re-index all sessions")
147
176
  def backfill(repo: Optional[str], force: bool):
148
- """Index all existing Claude sessions."""
177
+ """Index all existing Claude sessions. Requires: pip install "claude-jacked[search]" """
178
+ if not _require_search("backfill"):
179
+ sys.exit(1)
180
+
149
181
  from jacked.indexer import SessionIndexer
150
182
 
151
183
  config = get_config()
@@ -187,9 +219,11 @@ def backfill(repo: Optional[str], force: bool):
187
219
  def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Optional[str], content_types: tuple):
188
220
  """Search for sessions by semantic similarity with multi-factor ranking.
189
221
 
190
- By default, searches plan, subagent_summary, summary_label, and user_message content.
191
- Use --type to filter to specific content types.
222
+ Requires: pip install "claude-jacked[search]"
192
223
  """
224
+ if not _require_search("search"):
225
+ sys.exit(1)
226
+
193
227
  import os
194
228
  from jacked.searcher import SessionSearcher
195
229
 
@@ -304,13 +338,11 @@ def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Option
304
338
  def retrieve(session_id: str, output: Optional[str], summary: bool, mode: str, max_tokens: int, inject: bool):
305
339
  """Retrieve a session's context with smart mode support.
306
340
 
307
- Modes:
308
- smart - Plan + agent summaries + labels + user messages (default)
309
- plan - Just the plan file
310
- labels - Just summary labels (tiny)
311
- agents - All subagent summaries
312
- full - Everything including full transcript
341
+ Requires: pip install "claude-jacked[search]"
313
342
  """
343
+ if not _require_search("retrieve"):
344
+ sys.exit(1)
345
+
314
346
  from jacked.retriever import SessionRetriever
315
347
 
316
348
  config = get_config()
@@ -381,7 +413,10 @@ def retrieve(session_id: str, output: Optional[str], summary: bool, mode: str, m
381
413
  @click.option("--repo", "-r", help="Filter by repository path")
382
414
  @click.option("--limit", "-n", default=20, help="Maximum results")
383
415
  def list_sessions(repo: Optional[str], limit: int):
384
- """List indexed sessions."""
416
+ """List indexed sessions. Requires: pip install "claude-jacked[search]" """
417
+ if not _require_search("sessions"):
418
+ sys.exit(1)
419
+
385
420
  from jacked.client import QdrantSessionClient
386
421
 
387
422
  config = get_config()
@@ -419,7 +454,10 @@ def list_sessions(repo: Optional[str], limit: int):
419
454
  @click.argument("session_id")
420
455
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
421
456
  def delete(session_id: str, yes: bool):
422
- """Delete a session from the index."""
457
+ """Delete a session from the index. Requires: pip install "claude-jacked[search]" """
458
+ if not _require_search("delete"):
459
+ sys.exit(1)
460
+
423
461
  from jacked.client import QdrantSessionClient
424
462
 
425
463
  config = get_config()
@@ -439,9 +477,11 @@ def cleardb():
439
477
  """
440
478
  Delete ALL your indexed data from Qdrant.
441
479
 
442
- Only deletes YOUR data (matching your user_name), not teammates' data.
443
- Use this before re-indexing with a new schema or to start fresh.
480
+ Requires: pip install "claude-jacked[search]"
444
481
  """
482
+ if not _require_search("cleardb"):
483
+ sys.exit(1)
484
+
445
485
  from jacked.client import QdrantSessionClient
446
486
 
447
487
  config = get_config()
@@ -480,7 +520,10 @@ def cleardb():
480
520
 
481
521
  @main.command()
482
522
  def status():
483
- """Show indexing health and Qdrant connectivity."""
523
+ """Show indexing health and Qdrant connectivity. Requires: pip install "claude-jacked[search]" """
524
+ if not _require_search("status"):
525
+ sys.exit(1)
526
+
484
527
  from jacked.client import QdrantSessionClient
485
528
 
486
529
  config = get_config()
@@ -582,8 +625,6 @@ def configure(show: bool):
582
625
  console.print("[dim]Run 'jacked configure --show' to see current values[/dim]")
583
626
 
584
627
 
585
- # Import for configure command
586
- from jacked.config import SmartForkConfig
587
628
 
588
629
 
589
630
  def _get_data_root() -> Path:
@@ -712,7 +753,7 @@ def _behavioral_rules_end_marker() -> str:
712
753
  return "# end-jacked-behaviors"
713
754
 
714
755
 
715
- def _install_behavioral_rules(claude_md_path: Path):
756
+ def _install_behavioral_rules(claude_md_path: Path, force: bool = False):
716
757
  """Install behavioral rules into CLAUDE.md with marker boundaries.
717
758
 
718
759
  - Show rules before writing, require confirmation
@@ -766,7 +807,7 @@ def _install_behavioral_rules(claude_md_path: Path):
766
807
  # Version upgrade needed
767
808
  console.print("\n[bold]Behavioral rules update available:[/bold]")
768
809
  console.print(f"[dim]{rules_text}[/dim]")
769
- if not click.confirm("Update behavioral rules in CLAUDE.md?"):
810
+ if not force and not click.confirm("Update behavioral rules in CLAUDE.md?"):
770
811
  console.print("[yellow][-][/yellow] Skipped behavioral rules update")
771
812
  return
772
813
 
@@ -797,7 +838,7 @@ def _install_behavioral_rules(claude_md_path: Path):
797
838
  # Fresh install - show and confirm
798
839
  console.print("\n[bold]Proposed behavioral rules for ~/.claude/CLAUDE.md:[/bold]")
799
840
  console.print(f"[dim]{rules_text}[/dim]")
800
- if not click.confirm("Add these behavioral rules to your global CLAUDE.md?"):
841
+ if not force and not click.confirm("Add these behavioral rules to your global CLAUDE.md?"):
801
842
  console.print("[yellow][-][/yellow] Skipped behavioral rules")
802
843
  return
803
844
 
@@ -871,44 +912,61 @@ def _security_hook_marker() -> str:
871
912
  return "# jacked-security"
872
913
 
873
914
 
874
- def _get_security_prompt() -> str:
875
- """Load security gatekeeper prompt from data file."""
876
- prompt_path = _get_data_root() / "prompts" / "security_gatekeeper.txt"
877
- if not prompt_path.exists():
878
- raise FileNotFoundError(f"Security prompt not found: {prompt_path}")
879
- return prompt_path.read_text(encoding="utf-8")
880
-
881
915
 
882
916
  def _install_security_hook(existing: dict, settings_path: Path):
883
- """Install Opus-powered security gatekeeper hook for Bash commands.
917
+ """Install security gatekeeper command hook for Bash PreToolUse events.
884
918
 
885
- Handles fresh install and version upgrades (detects stale prompts).
919
+ Uses a PreToolUse command hook (blocking) that calls a Python script.
920
+ The script evaluates commands via local rules, Anthropic API, or claude -p
921
+ and returns permissionDecision:"allow" to auto-approve safe commands.
922
+
923
+ Handles fresh install, version upgrades, and migration from PermissionRequest.
886
924
  """
887
925
  import json
926
+ import shutil
888
927
 
889
928
  marker = _security_hook_marker()
929
+ script_path = _get_data_root() / "hooks" / "security_gatekeeper.py"
890
930
 
891
- try:
892
- prompt_text = _get_security_prompt()
893
- except FileNotFoundError as e:
894
- console.print(f"[red][FAIL][/red] {e}")
931
+ if not script_path.exists():
932
+ console.print(f"[red][FAIL][/red] Security gatekeeper script not found: {script_path}")
895
933
  console.print("[yellow]Skipping security gatekeeper installation[/yellow]")
896
934
  return
897
935
 
898
- if "PermissionRequest" not in existing["hooks"]:
899
- existing["hooks"]["PermissionRequest"] = []
936
+ # Find python executable — prefer the one running this process
937
+ python_exe = sys.executable
938
+ if not python_exe or not Path(python_exe).exists():
939
+ python_exe = shutil.which("python3") or shutil.which("python") or "python"
940
+
941
+ # Use forward slashes for the command (works on Windows too)
942
+ python_path = str(Path(python_exe)).replace("\\", "/")
943
+ script_str = str(script_path).replace("\\", "/")
944
+ command_str = f"{python_path} {script_str}"
945
+
946
+ # Migrate: remove old PermissionRequest hooks with our marker
947
+ if "PermissionRequest" in existing.get("hooks", {}):
948
+ old_hooks = existing["hooks"]["PermissionRequest"]
949
+ before = len(old_hooks)
950
+ existing["hooks"]["PermissionRequest"] = [
951
+ h for h in old_hooks
952
+ if marker not in str(h) and "security_gatekeeper" not in str(h)
953
+ ]
954
+ if len(existing["hooks"]["PermissionRequest"]) < before:
955
+ console.print("[green][OK][/green] Migrated security hook from PermissionRequest to PreToolUse")
956
+
957
+ if "PreToolUse" not in existing["hooks"]:
958
+ existing["hooks"]["PreToolUse"] = []
900
959
 
901
960
  # Check if already installed and whether it needs upgrading
902
961
  hook_index = None
903
962
  needs_upgrade = False
904
- for i, hook_entry in enumerate(existing["hooks"]["PermissionRequest"]):
963
+ for i, hook_entry in enumerate(existing["hooks"]["PreToolUse"]):
905
964
  hook_str = str(hook_entry)
906
- if marker in hook_str:
965
+ if marker in hook_str or "security_gatekeeper" in hook_str:
907
966
  hook_index = i
908
- # Check if installed prompt matches current version
909
967
  for h in hook_entry.get("hooks", []):
910
- installed_prompt = h.get("prompt", "")
911
- if installed_prompt != prompt_text:
968
+ installed_cmd = h.get("command", "")
969
+ if installed_cmd != command_str:
912
970
  needs_upgrade = True
913
971
  break
914
972
 
@@ -919,26 +977,28 @@ def _install_security_hook(existing: dict, settings_path: Path):
919
977
  hook_entry = {
920
978
  "matcher": "Bash",
921
979
  "hooks": [{
922
- "type": "prompt",
923
- "prompt": prompt_text,
924
- "model": "opus",
925
- "timeout": 60,
980
+ "type": "command",
981
+ "command": command_str,
982
+ "timeout": 30,
926
983
  }]
927
984
  }
928
985
 
929
986
  if hook_index is not None and needs_upgrade:
930
- existing["hooks"]["PermissionRequest"][hook_index] = hook_entry
987
+ existing["hooks"]["PreToolUse"][hook_index] = hook_entry
931
988
  settings_path.write_text(json.dumps(existing, indent=2))
932
- console.print("[green][OK][/green] Updated security gatekeeper prompt to latest version")
989
+ console.print("[green][OK][/green] Updated security gatekeeper to latest version")
933
990
  else:
934
- existing["hooks"]["PermissionRequest"].append(hook_entry)
991
+ existing["hooks"]["PreToolUse"].append(hook_entry)
935
992
  settings_path.parent.mkdir(parents=True, exist_ok=True)
936
993
  settings_path.write_text(json.dumps(existing, indent=2))
937
- console.print("[green][OK][/green] Installed security gatekeeper (Opus evaluates Bash commands)")
994
+ console.print("[green][OK][/green] Installed security gatekeeper (PreToolUse, blocking)")
938
995
 
939
996
 
940
997
  def _remove_security_hook(settings_path: Path) -> bool:
941
- """Remove jacked security gatekeeper hook. Returns True if removed."""
998
+ """Remove jacked security gatekeeper hook. Returns True if removed.
999
+
1000
+ Checks both PreToolUse (current) and PermissionRequest (legacy).
1001
+ """
942
1002
  import json
943
1003
 
944
1004
  if not settings_path.exists():
@@ -946,16 +1006,20 @@ def _remove_security_hook(settings_path: Path) -> bool:
946
1006
 
947
1007
  settings = json.loads(settings_path.read_text())
948
1008
  marker = _security_hook_marker()
1009
+ modified = False
949
1010
 
950
- if "PermissionRequest" not in settings.get("hooks", {}):
951
- return False
1011
+ for hook_type in ["PreToolUse", "PermissionRequest"]:
1012
+ if hook_type not in settings.get("hooks", {}):
1013
+ continue
1014
+ before = len(settings["hooks"][hook_type])
1015
+ settings["hooks"][hook_type] = [
1016
+ h for h in settings["hooks"][hook_type]
1017
+ if marker not in str(h)
1018
+ ]
1019
+ if len(settings["hooks"][hook_type]) < before:
1020
+ modified = True
952
1021
 
953
- before = len(settings["hooks"]["PermissionRequest"])
954
- settings["hooks"]["PermissionRequest"] = [
955
- h for h in settings["hooks"]["PermissionRequest"]
956
- if marker not in str(h)
957
- ]
958
- if len(settings["hooks"]["PermissionRequest"]) < before:
1022
+ if modified:
959
1023
  settings_path.write_text(json.dumps(settings, indent=2))
960
1024
  console.print("[green][OK][/green] Removed security gatekeeper hook")
961
1025
  return True
@@ -965,10 +1029,17 @@ def _remove_security_hook(settings_path: Path) -> bool:
965
1029
 
966
1030
  @main.command()
967
1031
  @click.option("--sounds", is_flag=True, help="Install sound notification hooks")
968
- @click.option("--no-security", is_flag=True, help="Skip security gatekeeper hook")
1032
+ @click.option("--search", is_flag=True, help="Install session indexing hook (requires [search] extra)")
1033
+ @click.option("--security", is_flag=True, help="Install security gatekeeper hook (requires [security] extra)")
969
1034
  @click.option("--no-rules", is_flag=True, help="Skip behavioral rules in CLAUDE.md")
970
- def install(sounds: bool, no_security: bool, no_rules: bool):
971
- """Auto-install hook config, skill, agents, and commands."""
1035
+ @click.option("--force", "-f", is_flag=True, help="Overwrite existing agents/commands without prompting")
1036
+ def install(sounds: bool, search: bool, security: bool, no_rules: bool, force: bool):
1037
+ """Auto-install skill, agents, commands, and optional hooks.
1038
+
1039
+ Base install: agents, commands, behavioral rules, /jacked skill.
1040
+ Use --search to add session indexing (requires qdrant-client).
1041
+ Use --security to add security gatekeeper (requires anthropic SDK).
1042
+ """
972
1043
  import os
973
1044
  import json
974
1045
  import shutil
@@ -976,24 +1047,16 @@ def install(sounds: bool, no_security: bool, no_rules: bool):
976
1047
  home = Path.home()
977
1048
  pkg_root = _get_data_root()
978
1049
 
979
- # Hook configuration - assumes jacked is on PATH (installed via pipx)
980
- # async: True runs indexing in background so Claude Code doesn't wait
981
- hook_config = {
982
- "hooks": {
983
- "Stop": [
984
- {
985
- "matcher": "",
986
- "hooks": [
987
- {
988
- "type": "command",
989
- "command": 'jacked index --repo "$CLAUDE_PROJECT_DIR"',
990
- "async": True
991
- }
992
- ]
993
- }
994
- ]
995
- }
996
- }
1050
+ # Auto-detect extras: if the package is installed, enable by default
1051
+ has_qdrant = False
1052
+ try:
1053
+ import qdrant_client # noqa: F401
1054
+ has_qdrant = True
1055
+ except ImportError:
1056
+ pass
1057
+
1058
+ install_search = search or has_qdrant
1059
+ install_security = security
997
1060
 
998
1061
  console.print("[bold]Installing Jacked...[/bold]\n")
999
1062
 
@@ -1007,37 +1070,47 @@ def install(sounds: bool, no_security: bool, no_rules: bool):
1007
1070
  else:
1008
1071
  existing = {}
1009
1072
 
1010
- # Merge hook config
1011
1073
  if "hooks" not in existing:
1012
1074
  existing["hooks"] = {}
1013
1075
  if "Stop" not in existing["hooks"]:
1014
1076
  existing["hooks"]["Stop"] = []
1015
1077
 
1016
- # Check if hook already exists and if it needs updating
1017
- hook_index = None
1018
- needs_async_update = False
1019
- for i, hook_entry in enumerate(existing["hooks"]["Stop"]):
1020
- for h in hook_entry.get("hooks", []):
1021
- if "jacked" in h.get("command", ""):
1022
- hook_index = i
1023
- # Check if async is missing or false
1024
- if not h.get("async"):
1025
- needs_async_update = True
1026
- break
1027
-
1028
- if hook_index is None:
1029
- # No hook exists - add it
1030
- existing["hooks"]["Stop"].append(hook_config["hooks"]["Stop"][0])
1031
- settings_path.parent.mkdir(parents=True, exist_ok=True)
1032
- settings_path.write_text(json.dumps(existing, indent=2))
1033
- console.print(f"[green][OK][/green] Added Stop hook to {settings_path}")
1034
- elif needs_async_update:
1035
- # Hook exists but needs async: true
1036
- existing["hooks"]["Stop"][hook_index] = hook_config["hooks"]["Stop"][0]
1037
- settings_path.write_text(json.dumps(existing, indent=2))
1038
- console.print(f"[green][OK][/green] Updated Stop hook with async: true")
1078
+ # Stop hook for session indexing — only if search extra available
1079
+ if install_search:
1080
+ hook_config_stop = {
1081
+ "matcher": "",
1082
+ "hooks": [
1083
+ {
1084
+ "type": "command",
1085
+ "command": 'jacked index --repo "$CLAUDE_PROJECT_DIR"',
1086
+ "async": True
1087
+ }
1088
+ ]
1089
+ }
1090
+
1091
+ hook_index = None
1092
+ needs_async_update = False
1093
+ for i, hook_entry in enumerate(existing["hooks"]["Stop"]):
1094
+ for h in hook_entry.get("hooks", []):
1095
+ if "jacked" in h.get("command", ""):
1096
+ hook_index = i
1097
+ if not h.get("async"):
1098
+ needs_async_update = True
1099
+ break
1100
+
1101
+ if hook_index is None:
1102
+ existing["hooks"]["Stop"].append(hook_config_stop)
1103
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
1104
+ settings_path.write_text(json.dumps(existing, indent=2))
1105
+ console.print(f"[green][OK][/green] Added Stop hook (session indexing)")
1106
+ elif needs_async_update:
1107
+ existing["hooks"]["Stop"][hook_index] = hook_config_stop
1108
+ settings_path.write_text(json.dumps(existing, indent=2))
1109
+ console.print(f"[green][OK][/green] Updated Stop hook with async: true")
1110
+ else:
1111
+ console.print(f"[yellow][-][/yellow] Stop hook already configured")
1039
1112
  else:
1040
- console.print(f"[yellow][-][/yellow] Stop hook already configured correctly")
1113
+ console.print("[dim][-][/dim] Skipping session indexing hook (install \[search] extra to enable)")
1041
1114
 
1042
1115
  # Copy skill file with Python path templating
1043
1116
  # Claude Code expects skills in subdirectories with SKILL.md
@@ -1068,8 +1141,8 @@ def install(sounds: bool, no_security: bool, no_rules: bool):
1068
1141
  if src_content == dst_content:
1069
1142
  skipped += 1
1070
1143
  continue # Same content, skip silently
1071
- # Different content - ask before overwriting
1072
- if not click.confirm(f"Agent '{agent_file.name}' exists with different content. Overwrite?"):
1144
+ # Different content - ask before overwriting (unless --force)
1145
+ if not force and not click.confirm(f"Agent '{agent_file.name}' exists with different content. Overwrite?"):
1073
1146
  console.print(f"[yellow][-][/yellow] Skipped {agent_file.name}")
1074
1147
  continue
1075
1148
  shutil.copy(agent_file, dst_file)
@@ -1096,8 +1169,8 @@ def install(sounds: bool, no_security: bool, no_rules: bool):
1096
1169
  if src_content == dst_content:
1097
1170
  skipped += 1
1098
1171
  continue # Same content, skip silently
1099
- # Different content - ask before overwriting
1100
- if not click.confirm(f"Command '{cmd_file.name}' exists with different content. Overwrite?"):
1172
+ # Different content - ask before overwriting (unless --force)
1173
+ if not force and not click.confirm(f"Command '{cmd_file.name}' exists with different content. Overwrite?"):
1101
1174
  console.print(f"[yellow][-][/yellow] Skipped {cmd_file.name}")
1102
1175
  continue
1103
1176
  shutil.copy(cmd_file, dst_file)
@@ -1113,35 +1186,47 @@ def install(sounds: bool, no_security: bool, no_rules: bool):
1113
1186
  if sounds:
1114
1187
  _install_sound_hooks(existing, settings_path)
1115
1188
 
1116
- # Install security gatekeeper (default on, --no-security to skip)
1117
- if not no_security:
1189
+ # Install security gatekeeper only if --security flag passed
1190
+ if install_security:
1118
1191
  _install_security_hook(existing, settings_path)
1192
+ else:
1193
+ console.print("[dim][-][/dim] Skipping security gatekeeper (use --security to enable)")
1119
1194
 
1120
1195
  # Install behavioral rules in CLAUDE.md (default on, --no-rules to skip)
1121
1196
  if not no_rules:
1122
1197
  claude_md_path = home / ".claude" / "CLAUDE.md"
1123
- _install_behavioral_rules(claude_md_path)
1198
+ _install_behavioral_rules(claude_md_path, force=force)
1124
1199
 
1125
1200
  console.print("\n[bold]Installation complete![/bold]")
1126
1201
  console.print("\n[yellow]IMPORTANT: Restart Claude Code for new commands to take effect![/yellow]")
1127
1202
  console.print("\nWhat you get:")
1128
1203
  console.print(" - /jacked - Search past Claude sessions")
1129
- console.print(" - /dc - Double-check reviewer (with grill mode)")
1204
+ console.print(" - /dc - Double-check reviewer")
1130
1205
  console.print(" - /pr - PR workflow helper")
1131
1206
  console.print(" - /learn - Distill lessons into CLAUDE.md rules")
1132
1207
  console.print(" - /techdebt - Project tech debt audit")
1133
1208
  console.print(" - /redo - Scrap and re-implement with hindsight")
1134
1209
  console.print(" - /audit-rules - CLAUDE.md quality audit")
1135
1210
  console.print(" - 10 specialized agents (readme, wiki, tests, etc.)")
1136
- if not no_security:
1137
- console.print(" - Security gatekeeper (Opus evaluates Bash commands)")
1211
+ if install_search:
1212
+ console.print(" - Session indexing hook (auto-indexes after each response)")
1213
+ if install_security:
1214
+ console.print(" - Security gatekeeper (auto-approves safe Bash commands)")
1138
1215
  if not no_rules:
1139
- console.print(" - Behavioral rules in CLAUDE.md (auto-triggers for jacked commands)")
1216
+ console.print(" - Behavioral rules in CLAUDE.md")
1217
+
1218
+ # Show next steps based on what's installed
1140
1219
  console.print("\nNext steps:")
1141
1220
  console.print(" 1. Restart Claude Code (exit and run 'claude' again)")
1142
- console.print(" 2. Set environment variables (run 'jacked configure' for help)")
1143
- console.print(" 3. Run 'jacked backfill' to index existing sessions")
1144
- console.print(" 4. Use '/jacked <description>' in Claude to search past sessions")
1221
+ if install_search:
1222
+ console.print(" 2. Set Qdrant credentials (run 'jacked configure' for help)")
1223
+ console.print(" 3. Run 'jacked backfill' to index existing sessions")
1224
+ console.print(" 4. Use '/jacked <description>' to search past sessions")
1225
+ else:
1226
+ console.print("\nOptional extras:")
1227
+ console.print(' pip install "claude-jacked\[search]" # Session search via Qdrant')
1228
+ console.print(' pip install "claude-jacked\[security]" # Auto-approve safe Bash commands')
1229
+ console.print(' pip install "claude-jacked\[all]" # Everything')
1145
1230
 
1146
1231
 
1147
1232
  @main.command()