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.
- {claude_jacked-0.2.9.dist-info → claude_jacked-0.3.1.dist-info}/METADATA +200 -56
- {claude_jacked-0.2.9.dist-info → claude_jacked-0.3.1.dist-info}/RECORD +8 -8
- jacked/__init__.py +34 -14
- jacked/cli.py +201 -116
- jacked/data/hooks/security_gatekeeper.py +415 -0
- jacked/data/prompts/security_gatekeeper.txt +0 -58
- {claude_jacked-0.2.9.dist-info → claude_jacked-0.3.1.dist-info}/WHEEL +0 -0
- {claude_jacked-0.2.9.dist-info → claude_jacked-0.3.1.dist-info}/entry_points.txt +0 -0
- {claude_jacked-0.2.9.dist-info → claude_jacked-0.3.1.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
917
|
+
"""Install security gatekeeper command hook for Bash PreToolUse events.
|
|
884
918
|
|
|
885
|
-
|
|
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
|
-
|
|
892
|
-
|
|
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
|
-
|
|
899
|
-
|
|
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"]["
|
|
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
|
-
|
|
911
|
-
if
|
|
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": "
|
|
923
|
-
"
|
|
924
|
-
"
|
|
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"]["
|
|
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
|
|
989
|
+
console.print("[green][OK][/green] Updated security gatekeeper to latest version")
|
|
933
990
|
else:
|
|
934
|
-
existing["hooks"]["
|
|
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 (
|
|
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
|
-
|
|
951
|
-
|
|
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
|
-
|
|
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("--
|
|
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
|
-
|
|
971
|
-
|
|
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
|
-
#
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
#
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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(
|
|
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
|
|
1117
|
-
if
|
|
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
|
|
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
|
|
1137
|
-
console.print(" -
|
|
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
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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()
|