claude-jacked 0.2.3__py3-none-any.whl → 0.2.9__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/METADATA +523 -0
- claude_jacked-0.2.9.dist-info/RECORD +33 -0
- jacked/cli.py +752 -47
- jacked/client.py +196 -29
- jacked/data/agents/code-simplicity-reviewer.md +87 -0
- jacked/data/agents/defensive-error-handler.md +93 -0
- jacked/data/agents/double-check-reviewer.md +214 -0
- jacked/data/agents/git-pr-workflow-manager.md +149 -0
- jacked/data/agents/issue-pr-coordinator.md +131 -0
- jacked/data/agents/pr-workflow-checker.md +199 -0
- jacked/data/agents/readme-maintainer.md +123 -0
- jacked/data/agents/test-coverage-engineer.md +155 -0
- jacked/data/agents/test-coverage-improver.md +139 -0
- jacked/data/agents/wiki-documentation-architect.md +580 -0
- jacked/data/commands/audit-rules.md +103 -0
- jacked/data/commands/dc.md +155 -0
- jacked/data/commands/learn.md +89 -0
- jacked/data/commands/pr.md +4 -0
- jacked/data/commands/redo.md +85 -0
- jacked/data/commands/techdebt.md +115 -0
- jacked/data/prompts/security_gatekeeper.txt +58 -0
- jacked/data/rules/jacked_behaviors.md +11 -0
- jacked/data/skills/jacked/SKILL.md +162 -0
- jacked/index_write_tracker.py +227 -0
- jacked/indexer.py +255 -129
- jacked/retriever.py +389 -137
- jacked/searcher.py +65 -13
- jacked/transcript.py +339 -0
- claude_jacked-0.2.3.dist-info/METADATA +0 -483
- claude_jacked-0.2.3.dist-info/RECORD +0 -13
- {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/WHEEL +0 -0
- {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/entry_points.txt +0 -0
- {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/licenses/LICENSE +0 -0
jacked/cli.py
CHANGED
|
@@ -32,11 +32,18 @@ def setup_logging(verbose: bool = False):
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def get_config() -> SmartForkConfig:
|
|
36
|
-
"""Load configuration from environment.
|
|
35
|
+
def get_config(quiet: bool = False) -> Optional[SmartForkConfig]:
|
|
36
|
+
"""Load configuration from environment.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
quiet: If True, return None instead of printing error and exiting.
|
|
40
|
+
Used by hooks that should fail gracefully.
|
|
41
|
+
"""
|
|
37
42
|
try:
|
|
38
43
|
return SmartForkConfig.from_env()
|
|
39
44
|
except ValueError as e:
|
|
45
|
+
if quiet:
|
|
46
|
+
return None
|
|
40
47
|
console.print(f"[red]Configuration error:[/red] {e}")
|
|
41
48
|
console.print("\nSet these environment variables:")
|
|
42
49
|
console.print(" QDRANT_CLAUDE_SESSIONS_ENDPOINT=<your-qdrant-url>")
|
|
@@ -63,7 +70,12 @@ def index(session: Optional[str], repo: Optional[str]):
|
|
|
63
70
|
import os
|
|
64
71
|
from jacked.indexer import SessionIndexer
|
|
65
72
|
|
|
66
|
-
config
|
|
73
|
+
# Try to get config quietly - if not configured, nudge and exit cleanly
|
|
74
|
+
config = get_config(quiet=True)
|
|
75
|
+
if config is None:
|
|
76
|
+
print("[jacked] Indexing skipped - run 'jacked configure' to set up Qdrant")
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
|
|
67
79
|
indexer = SessionIndexer(config)
|
|
68
80
|
|
|
69
81
|
if session:
|
|
@@ -119,8 +131,8 @@ def index(session: Optional[str], repo: Optional[str]):
|
|
|
119
131
|
if result.get("indexed"):
|
|
120
132
|
console.print(
|
|
121
133
|
f"[green][OK][/green] Indexed session {result['session_id']}: "
|
|
122
|
-
f"{result['
|
|
123
|
-
f"{result['
|
|
134
|
+
f"{result['plans']}p {result['subagent_summaries']}a "
|
|
135
|
+
f"{result['summary_labels']}l {result['user_messages']}u {result['chunks']}c"
|
|
124
136
|
)
|
|
125
137
|
elif result.get("skipped"):
|
|
126
138
|
console.print(f"[yellow][-][/yellow] Session {result['session_id']} unchanged, skipped")
|
|
@@ -167,8 +179,17 @@ def backfill(repo: Optional[str], force: bool):
|
|
|
167
179
|
@click.option("--limit", "-n", default=5, help="Maximum results")
|
|
168
180
|
@click.option("--mine", "-m", is_flag=True, help="Only show my sessions")
|
|
169
181
|
@click.option("--user", "-u", help="Only show sessions from this user")
|
|
170
|
-
|
|
171
|
-
"""
|
|
182
|
+
@click.option(
|
|
183
|
+
"--type", "-t", "content_types",
|
|
184
|
+
multiple=True,
|
|
185
|
+
help="Filter by content type (plan, subagent_summary, summary_label, user_message, chunk)"
|
|
186
|
+
)
|
|
187
|
+
def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Optional[str], content_types: tuple):
|
|
188
|
+
"""Search for sessions by semantic similarity with multi-factor ranking.
|
|
189
|
+
|
|
190
|
+
By default, searches plan, subagent_summary, summary_label, and user_message content.
|
|
191
|
+
Use --type to filter to specific content types.
|
|
192
|
+
"""
|
|
172
193
|
import os
|
|
173
194
|
from jacked.searcher import SessionSearcher
|
|
174
195
|
|
|
@@ -178,6 +199,9 @@ def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Option
|
|
|
178
199
|
# Use current repo if not specified
|
|
179
200
|
current_repo = repo or os.getenv("CLAUDE_PROJECT_DIR")
|
|
180
201
|
|
|
202
|
+
# Convert tuple to list or None
|
|
203
|
+
type_filter = list(content_types) if content_types else None
|
|
204
|
+
|
|
181
205
|
with Progress(
|
|
182
206
|
SpinnerColumn(),
|
|
183
207
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -191,6 +215,7 @@ def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Option
|
|
|
191
215
|
limit=limit,
|
|
192
216
|
mine_only=mine,
|
|
193
217
|
user_filter=user,
|
|
218
|
+
content_types=type_filter,
|
|
194
219
|
)
|
|
195
220
|
|
|
196
221
|
progress.remove_task(task)
|
|
@@ -202,27 +227,61 @@ def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Option
|
|
|
202
227
|
table = Table(title="Search Results", show_header=True)
|
|
203
228
|
table.add_column("#", style="dim", width=3)
|
|
204
229
|
table.add_column("Score", style="cyan", width=6)
|
|
205
|
-
table.add_column("User", style="yellow", width=
|
|
206
|
-
table.add_column("
|
|
207
|
-
table.add_column("
|
|
230
|
+
table.add_column("User", style="yellow", width=10)
|
|
231
|
+
table.add_column("Age", style="green", width=12)
|
|
232
|
+
table.add_column("Repo", style="magenta", width=15)
|
|
233
|
+
table.add_column("Content", style="blue", width=8)
|
|
208
234
|
table.add_column("Preview")
|
|
209
235
|
|
|
210
236
|
for i, result in enumerate(results, 1):
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
# Format relative time
|
|
238
|
+
if result.timestamp:
|
|
239
|
+
from datetime import datetime, timezone
|
|
240
|
+
now = datetime.now(timezone.utc)
|
|
241
|
+
ts = result.timestamp
|
|
242
|
+
if ts.tzinfo is None:
|
|
243
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
244
|
+
days = (now - ts).days
|
|
245
|
+
if days == 0:
|
|
246
|
+
age_str = "today"
|
|
247
|
+
elif days == 1:
|
|
248
|
+
age_str = "yesterday"
|
|
249
|
+
elif days < 7:
|
|
250
|
+
age_str = f"{days}d ago"
|
|
251
|
+
elif days < 30:
|
|
252
|
+
age_str = f"{days // 7}w ago"
|
|
253
|
+
elif days < 365:
|
|
254
|
+
age_str = f"{days // 30}mo ago"
|
|
255
|
+
else:
|
|
256
|
+
age_str = f"{days // 365}y ago"
|
|
257
|
+
else:
|
|
258
|
+
age_str = "?"
|
|
259
|
+
|
|
260
|
+
preview = result.intent_preview[:40] + "..." if len(result.intent_preview) > 40 else result.intent_preview
|
|
213
261
|
user_display = "YOU" if result.is_own else f"@{result.user_name}"
|
|
262
|
+
|
|
263
|
+
# Content indicators
|
|
264
|
+
indicators = []
|
|
265
|
+
if result.has_plan:
|
|
266
|
+
indicators.append("📋")
|
|
267
|
+
if result.has_agent_summaries:
|
|
268
|
+
indicators.append("🤖")
|
|
269
|
+
content_str = " ".join(indicators) if indicators else "-"
|
|
270
|
+
|
|
214
271
|
table.add_row(
|
|
215
272
|
str(i),
|
|
216
273
|
f"{result.score:.0f}%",
|
|
217
274
|
user_display,
|
|
218
|
-
|
|
219
|
-
result.repo_name,
|
|
275
|
+
age_str,
|
|
276
|
+
result.repo_name[:15],
|
|
277
|
+
content_str,
|
|
220
278
|
preview,
|
|
221
279
|
)
|
|
222
280
|
|
|
223
281
|
console.print(table)
|
|
224
|
-
console.print(
|
|
225
|
-
console.print(f"[dim]Use 'jacked retrieve <
|
|
282
|
+
console.print("\n[dim]📋 = has plan file | 🤖 = has agent summaries[/dim]")
|
|
283
|
+
console.print(f"[dim]Use 'jacked retrieve <id> --mode smart' for optimized context (default)[/dim]")
|
|
284
|
+
console.print(f"[dim]Use 'jacked retrieve <id> --mode full' for complete transcript[/dim]")
|
|
226
285
|
|
|
227
286
|
# Print session IDs for easy copy
|
|
228
287
|
console.print("\nSession IDs:")
|
|
@@ -232,10 +291,26 @@ def search(query: str, repo: Optional[str], limit: int, mine: bool, user: Option
|
|
|
232
291
|
|
|
233
292
|
@main.command()
|
|
234
293
|
@click.argument("session_id")
|
|
235
|
-
@click.option("--output", "-o", type=click.Path(), help="Save
|
|
236
|
-
@click.option("--summary", "-s", is_flag=True, help="Show summary instead of
|
|
237
|
-
|
|
238
|
-
""
|
|
294
|
+
@click.option("--output", "-o", type=click.Path(), help="Save output to file")
|
|
295
|
+
@click.option("--summary", "-s", is_flag=True, help="Show summary instead of content")
|
|
296
|
+
@click.option(
|
|
297
|
+
"--mode", "-m",
|
|
298
|
+
type=click.Choice(["smart", "plan", "labels", "agents", "full"]),
|
|
299
|
+
default="smart",
|
|
300
|
+
help="Retrieval mode (default: smart)"
|
|
301
|
+
)
|
|
302
|
+
@click.option("--max-tokens", "-t", default=15000, help="Max token budget for smart mode")
|
|
303
|
+
@click.option("--inject", "-i", is_flag=True, help="Format for context injection")
|
|
304
|
+
def retrieve(session_id: str, output: Optional[str], summary: bool, mode: str, max_tokens: int, inject: bool):
|
|
305
|
+
"""Retrieve a session's context with smart mode support.
|
|
306
|
+
|
|
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
|
|
313
|
+
"""
|
|
239
314
|
from jacked.retriever import SessionRetriever
|
|
240
315
|
|
|
241
316
|
config = get_config()
|
|
@@ -248,7 +323,7 @@ def retrieve(session_id: str, output: Optional[str], summary: bool):
|
|
|
248
323
|
) as progress:
|
|
249
324
|
task = progress.add_task(f"Retrieving {session_id}...", total=None)
|
|
250
325
|
|
|
251
|
-
session = retriever.retrieve(session_id)
|
|
326
|
+
session = retriever.retrieve(session_id, mode=mode)
|
|
252
327
|
|
|
253
328
|
progress.remove_task(task)
|
|
254
329
|
|
|
@@ -256,12 +331,28 @@ def retrieve(session_id: str, output: Optional[str], summary: bool):
|
|
|
256
331
|
console.print(f"[red]Session {session_id} not found[/red]")
|
|
257
332
|
sys.exit(1)
|
|
258
333
|
|
|
259
|
-
# Show metadata
|
|
334
|
+
# Show metadata with content summary
|
|
335
|
+
tokens = session.content.estimate_tokens()
|
|
336
|
+
content_parts = []
|
|
337
|
+
if session.content.plan:
|
|
338
|
+
content_parts.append(f"Plan: {tokens['plan']} tokens")
|
|
339
|
+
if session.content.subagent_summaries:
|
|
340
|
+
content_parts.append(f"Agent summaries: {len(session.content.subagent_summaries)} ({tokens['subagent_summaries']} tokens)")
|
|
341
|
+
if session.content.summary_labels:
|
|
342
|
+
content_parts.append(f"Labels: {len(session.content.summary_labels)} ({tokens['summary_labels']} tokens)")
|
|
343
|
+
if session.content.user_messages:
|
|
344
|
+
content_parts.append(f"User messages: {len(session.content.user_messages)} ({tokens['user_messages']} tokens)")
|
|
345
|
+
if session.content.chunks:
|
|
346
|
+
content_parts.append(f"Transcript chunks: {len(session.content.chunks)} ({tokens['chunks']} tokens)")
|
|
347
|
+
|
|
260
348
|
console.print(Panel(
|
|
261
349
|
f"Session: {session.session_id}\n"
|
|
262
350
|
f"Repository: {session.repo_name}\n"
|
|
263
351
|
f"Machine: {session.machine}\n"
|
|
264
|
-
f"
|
|
352
|
+
f"Age: {session.format_relative_time()}\n"
|
|
353
|
+
f"Local: {'Yes' if session.is_local else 'No'}\n"
|
|
354
|
+
f"\nContent available:\n " + "\n ".join(content_parts) +
|
|
355
|
+
f"\n\nEstimated tokens (smart): {tokens['total']}",
|
|
265
356
|
title="Session Info",
|
|
266
357
|
))
|
|
267
358
|
|
|
@@ -272,21 +363,24 @@ def retrieve(session_id: str, output: Optional[str], summary: bool):
|
|
|
272
363
|
|
|
273
364
|
if summary:
|
|
274
365
|
text = retriever.get_summary(session)
|
|
366
|
+
elif inject:
|
|
367
|
+
text = retriever.format_for_injection(session, mode=mode, max_tokens=max_tokens)
|
|
275
368
|
else:
|
|
276
|
-
|
|
369
|
+
# Default: format based on mode
|
|
370
|
+
text = retriever.format_for_injection(session, mode=mode, max_tokens=max_tokens)
|
|
277
371
|
|
|
278
372
|
if output:
|
|
279
373
|
Path(output).write_text(text, encoding="utf-8")
|
|
280
374
|
console.print(f"\n[green]Saved to {output}[/green]")
|
|
281
375
|
else:
|
|
282
|
-
console.print(f"\n[bold]
|
|
376
|
+
console.print(f"\n[bold]Content (mode={mode}):[/bold]")
|
|
283
377
|
console.print(text)
|
|
284
378
|
|
|
285
379
|
|
|
286
|
-
@main.command()
|
|
380
|
+
@main.command(name="sessions")
|
|
287
381
|
@click.option("--repo", "-r", help="Filter by repository path")
|
|
288
382
|
@click.option("--limit", "-n", default=20, help="Maximum results")
|
|
289
|
-
def
|
|
383
|
+
def list_sessions(repo: Optional[str], limit: int):
|
|
290
384
|
"""List indexed sessions."""
|
|
291
385
|
from jacked.client import QdrantSessionClient
|
|
292
386
|
|
|
@@ -340,6 +434,50 @@ def delete(session_id: str, yes: bool):
|
|
|
340
434
|
console.print(f"[green][OK][/green] Deleted session {session_id}")
|
|
341
435
|
|
|
342
436
|
|
|
437
|
+
@main.command()
|
|
438
|
+
def cleardb():
|
|
439
|
+
"""
|
|
440
|
+
Delete ALL your indexed data from Qdrant.
|
|
441
|
+
|
|
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.
|
|
444
|
+
"""
|
|
445
|
+
from jacked.client import QdrantSessionClient
|
|
446
|
+
|
|
447
|
+
config = get_config()
|
|
448
|
+
client = QdrantSessionClient(config)
|
|
449
|
+
|
|
450
|
+
# Show what we're about to delete
|
|
451
|
+
user_name = config.user_name
|
|
452
|
+
count = client.count_by_user(user_name)
|
|
453
|
+
|
|
454
|
+
if count == 0:
|
|
455
|
+
console.print(f"[yellow]No data found for user '{user_name}'[/yellow]")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
console.print(Panel(
|
|
459
|
+
f"[bold red]WARNING: This will permanently delete ALL your indexed data![/bold red]\n\n"
|
|
460
|
+
f"User: [cyan]{user_name}[/cyan]\n"
|
|
461
|
+
f"Points to delete: [red]{count}[/red]\n\n"
|
|
462
|
+
f"This only affects YOUR data. Teammates' data will be untouched.\n"
|
|
463
|
+
f"After clearing, run 'jacked backfill' to re-index.",
|
|
464
|
+
title="Clear Database",
|
|
465
|
+
))
|
|
466
|
+
|
|
467
|
+
# Require typing confirmation phrase
|
|
468
|
+
console.print("\n[bold]To confirm, type: DELETE MY DATA[/bold]")
|
|
469
|
+
confirmation = click.prompt("Confirmation", default="", show_default=False)
|
|
470
|
+
|
|
471
|
+
if confirmation != "DELETE MY DATA":
|
|
472
|
+
console.print("[yellow]Cancelled - confirmation did not match[/yellow]")
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Do the delete
|
|
476
|
+
deleted = client.delete_by_user(user_name)
|
|
477
|
+
console.print(f"\n[green][OK][/green] Deleted {deleted} points for user '{user_name}'")
|
|
478
|
+
console.print("\n[dim]Run 'jacked backfill' to re-index your sessions[/dim]")
|
|
479
|
+
|
|
480
|
+
|
|
343
481
|
@main.command()
|
|
344
482
|
def status():
|
|
345
483
|
"""Show indexing health and Qdrant connectivity."""
|
|
@@ -448,17 +586,398 @@ def configure(show: bool):
|
|
|
448
586
|
from jacked.config import SmartForkConfig
|
|
449
587
|
|
|
450
588
|
|
|
589
|
+
def _get_data_root() -> Path:
|
|
590
|
+
"""Find the data root directory for skills/agents/commands.
|
|
591
|
+
|
|
592
|
+
Data is now inside the package at jacked/data/.
|
|
593
|
+
"""
|
|
594
|
+
return Path(__file__).parent / "data"
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _sound_hook_marker() -> str:
|
|
598
|
+
"""Marker to identify jacked sound hooks."""
|
|
599
|
+
return "# jacked-sound: "
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _get_sound_command(hook_type: str) -> str:
|
|
603
|
+
"""Generate cross-platform sound command (backgrounded, with fallbacks).
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
hook_type: 'notification' or 'complete'
|
|
607
|
+
"""
|
|
608
|
+
if hook_type == "notification":
|
|
609
|
+
win_sound = "Exclamation"
|
|
610
|
+
mac_sound = "Basso.aiff"
|
|
611
|
+
linux_sound = "dialog-warning.oga"
|
|
612
|
+
else: # complete
|
|
613
|
+
win_sound = "Asterisk"
|
|
614
|
+
mac_sound = "Glass.aiff"
|
|
615
|
+
linux_sound = "complete.oga"
|
|
616
|
+
|
|
617
|
+
# Use uname for detection, background with &, fallback to bell
|
|
618
|
+
return (
|
|
619
|
+
'('
|
|
620
|
+
'OS=$(uname -s); '
|
|
621
|
+
'case "$OS" in '
|
|
622
|
+
f'Darwin) afplay /System/Library/Sounds/{mac_sound} 2>/dev/null || printf "\\a";; '
|
|
623
|
+
'Linux) '
|
|
624
|
+
' if grep -qi microsoft /proc/version 2>/dev/null; then '
|
|
625
|
+
f' powershell.exe -Command "[System.Media.SystemSounds]::{win_sound}.Play()" 2>/dev/null || printf "\\a"; '
|
|
626
|
+
' else '
|
|
627
|
+
f' paplay /usr/share/sounds/freedesktop/stereo/{linux_sound} 2>/dev/null || printf "\\a"; '
|
|
628
|
+
' fi;; '
|
|
629
|
+
f'MINGW*|MSYS*|CYGWIN*) powershell -Command "[System.Media.SystemSounds]::{win_sound}.Play()" 2>/dev/null || printf "\\a";; '
|
|
630
|
+
'*) printf "\\a";; '
|
|
631
|
+
'esac'
|
|
632
|
+
') &'
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _install_sound_hooks(existing: dict, settings_path: Path):
|
|
637
|
+
"""Install sound notification hooks."""
|
|
638
|
+
import json
|
|
639
|
+
|
|
640
|
+
marker = _sound_hook_marker()
|
|
641
|
+
|
|
642
|
+
# Notification hook
|
|
643
|
+
if "Notification" not in existing["hooks"]:
|
|
644
|
+
existing["hooks"]["Notification"] = []
|
|
645
|
+
|
|
646
|
+
notif_exists = any(marker in str(h) for h in existing["hooks"]["Notification"])
|
|
647
|
+
if not notif_exists:
|
|
648
|
+
existing["hooks"]["Notification"].append({
|
|
649
|
+
"matcher": "",
|
|
650
|
+
"hooks": [{"type": "command", "command": marker + _get_sound_command("notification")}]
|
|
651
|
+
})
|
|
652
|
+
console.print("[green][OK][/green] Added Notification sound hook")
|
|
653
|
+
else:
|
|
654
|
+
console.print("[yellow][-][/yellow] Notification sound hook exists")
|
|
655
|
+
|
|
656
|
+
# Stop sound hook (separate from index)
|
|
657
|
+
stop_exists = any(marker in str(h) for h in existing["hooks"]["Stop"])
|
|
658
|
+
if not stop_exists:
|
|
659
|
+
existing["hooks"]["Stop"].append({
|
|
660
|
+
"matcher": "",
|
|
661
|
+
"hooks": [{"type": "command", "command": marker + _get_sound_command("complete")}]
|
|
662
|
+
})
|
|
663
|
+
console.print("[green][OK][/green] Added Stop sound hook")
|
|
664
|
+
else:
|
|
665
|
+
console.print("[yellow][-][/yellow] Stop sound hook exists")
|
|
666
|
+
|
|
667
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _remove_sound_hooks(settings_path: Path) -> bool:
|
|
671
|
+
"""Remove jacked sound hooks. Returns True if any removed."""
|
|
672
|
+
import json
|
|
673
|
+
|
|
674
|
+
if not settings_path.exists():
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
settings = json.loads(settings_path.read_text())
|
|
678
|
+
marker = _sound_hook_marker()
|
|
679
|
+
modified = False
|
|
680
|
+
|
|
681
|
+
for hook_type in ["Notification", "Stop"]:
|
|
682
|
+
if hook_type in settings.get("hooks", {}):
|
|
683
|
+
before = len(settings["hooks"][hook_type])
|
|
684
|
+
settings["hooks"][hook_type] = [
|
|
685
|
+
h for h in settings["hooks"][hook_type]
|
|
686
|
+
if marker not in str(h)
|
|
687
|
+
]
|
|
688
|
+
if len(settings["hooks"][hook_type]) < before:
|
|
689
|
+
console.print(f"[green][OK][/green] Removed {hook_type} sound hook")
|
|
690
|
+
modified = True
|
|
691
|
+
|
|
692
|
+
if modified:
|
|
693
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
694
|
+
return modified
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _get_behavioral_rules() -> str:
|
|
698
|
+
"""Load behavioral rules from data file."""
|
|
699
|
+
rules_path = _get_data_root() / "rules" / "jacked_behaviors.md"
|
|
700
|
+
if not rules_path.exists():
|
|
701
|
+
raise FileNotFoundError(f"Behavioral rules not found: {rules_path}")
|
|
702
|
+
return rules_path.read_text(encoding="utf-8").strip()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _behavioral_rules_marker() -> str:
|
|
706
|
+
"""Start marker for jacked behavioral rules block."""
|
|
707
|
+
return "# jacked-behaviors-v2"
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _behavioral_rules_end_marker() -> str:
|
|
711
|
+
"""End marker for jacked behavioral rules block."""
|
|
712
|
+
return "# end-jacked-behaviors"
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _install_behavioral_rules(claude_md_path: Path):
|
|
716
|
+
"""Install behavioral rules into CLAUDE.md with marker boundaries.
|
|
717
|
+
|
|
718
|
+
- Show rules before writing, require confirmation
|
|
719
|
+
- Backup file before first modification
|
|
720
|
+
- Atomic write (build in memory, write once)
|
|
721
|
+
- Skip if already installed with same version
|
|
722
|
+
"""
|
|
723
|
+
import shutil
|
|
724
|
+
|
|
725
|
+
try:
|
|
726
|
+
rules_text = _get_behavioral_rules()
|
|
727
|
+
except FileNotFoundError as e:
|
|
728
|
+
console.print(f"[red][FAIL][/red] {e}")
|
|
729
|
+
console.print("[yellow]Skipping behavioral rules installation[/yellow]")
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
start_marker = _behavioral_rules_marker()
|
|
733
|
+
end_marker = _behavioral_rules_end_marker()
|
|
734
|
+
|
|
735
|
+
# Read existing content
|
|
736
|
+
existing_content = ""
|
|
737
|
+
if claude_md_path.exists():
|
|
738
|
+
existing_content = claude_md_path.read_text(encoding="utf-8")
|
|
739
|
+
|
|
740
|
+
# Check if already installed (any version)
|
|
741
|
+
marker_prefix = "# jacked-behaviors-v"
|
|
742
|
+
has_start = marker_prefix in existing_content
|
|
743
|
+
has_end = end_marker in existing_content
|
|
744
|
+
|
|
745
|
+
# Orphaned marker detection: start without end (or end without start)
|
|
746
|
+
if has_start != has_end:
|
|
747
|
+
which = "start" if has_start else "end"
|
|
748
|
+
missing = "end" if has_start else "start"
|
|
749
|
+
console.print(f"[red][FAIL][/red] Found {which} marker but no {missing} marker in CLAUDE.md")
|
|
750
|
+
console.print("Your CLAUDE.md has a corrupted jacked rules block. Please fix it manually:")
|
|
751
|
+
console.print(f" Start marker: {start_marker}")
|
|
752
|
+
console.print(f" End marker: {end_marker}")
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
has_existing = has_start and has_end
|
|
756
|
+
if has_existing:
|
|
757
|
+
# Extract existing block (find the versioned start marker)
|
|
758
|
+
start_idx = existing_content.index(marker_prefix)
|
|
759
|
+
end_idx = existing_content.index(end_marker) + len(end_marker)
|
|
760
|
+
existing_block = existing_content[start_idx:end_idx].strip()
|
|
761
|
+
|
|
762
|
+
if existing_block == rules_text:
|
|
763
|
+
console.print("[yellow][-][/yellow] Behavioral rules already configured correctly")
|
|
764
|
+
return
|
|
765
|
+
else:
|
|
766
|
+
# Version upgrade needed
|
|
767
|
+
console.print("\n[bold]Behavioral rules update available:[/bold]")
|
|
768
|
+
console.print(f"[dim]{rules_text}[/dim]")
|
|
769
|
+
if not click.confirm("Update behavioral rules in CLAUDE.md?"):
|
|
770
|
+
console.print("[yellow][-][/yellow] Skipped behavioral rules update")
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
# Backup before modifying
|
|
774
|
+
backup_path = claude_md_path.with_suffix(".md.pre-jacked")
|
|
775
|
+
if not backup_path.exists():
|
|
776
|
+
shutil.copy2(claude_md_path, backup_path)
|
|
777
|
+
console.print(f"[dim]Backup: {backup_path}[/dim]")
|
|
778
|
+
|
|
779
|
+
# Replace the block (symmetric with _remove_behavioral_rules)
|
|
780
|
+
before = existing_content[:start_idx].rstrip("\n")
|
|
781
|
+
after = existing_content[end_idx:].lstrip("\n")
|
|
782
|
+
if before and after:
|
|
783
|
+
new_content = before + "\n\n" + rules_text + "\n\n" + after
|
|
784
|
+
elif before:
|
|
785
|
+
new_content = before + "\n\n" + rules_text + "\n"
|
|
786
|
+
else:
|
|
787
|
+
new_content = rules_text + "\n" + after if after else rules_text + "\n"
|
|
788
|
+
try:
|
|
789
|
+
claude_md_path.write_text(new_content, encoding="utf-8")
|
|
790
|
+
except PermissionError:
|
|
791
|
+
console.print(f"[red][FAIL][/red] Permission denied writing to {claude_md_path}")
|
|
792
|
+
console.print("Check file permissions and try again.")
|
|
793
|
+
return
|
|
794
|
+
console.print("[green][OK][/green] Updated behavioral rules to latest version")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
# Fresh install - show and confirm
|
|
798
|
+
console.print("\n[bold]Proposed behavioral rules for ~/.claude/CLAUDE.md:[/bold]")
|
|
799
|
+
console.print(f"[dim]{rules_text}[/dim]")
|
|
800
|
+
if not click.confirm("Add these behavioral rules to your global CLAUDE.md?"):
|
|
801
|
+
console.print("[yellow][-][/yellow] Skipped behavioral rules")
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
# Backup before modifying (if file exists and no backup yet)
|
|
805
|
+
if claude_md_path.exists():
|
|
806
|
+
backup_path = claude_md_path.with_suffix(".md.pre-jacked")
|
|
807
|
+
if not backup_path.exists():
|
|
808
|
+
shutil.copy2(claude_md_path, backup_path)
|
|
809
|
+
console.print(f"[dim]Backup: {backup_path}[/dim]")
|
|
810
|
+
|
|
811
|
+
# Ensure parent directory exists
|
|
812
|
+
claude_md_path.parent.mkdir(parents=True, exist_ok=True)
|
|
813
|
+
|
|
814
|
+
# Build new content atomically
|
|
815
|
+
if existing_content and not existing_content.endswith("\n\n"):
|
|
816
|
+
if existing_content.endswith("\n"):
|
|
817
|
+
new_content = existing_content + "\n" + rules_text + "\n"
|
|
818
|
+
else:
|
|
819
|
+
new_content = existing_content + "\n\n" + rules_text + "\n"
|
|
820
|
+
else:
|
|
821
|
+
new_content = existing_content + rules_text + "\n"
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
claude_md_path.write_text(new_content, encoding="utf-8")
|
|
825
|
+
except PermissionError:
|
|
826
|
+
console.print(f"[red][FAIL][/red] Permission denied writing to {claude_md_path}")
|
|
827
|
+
console.print("Check file permissions and try again.")
|
|
828
|
+
return
|
|
829
|
+
console.print("[green][OK][/green] Installed behavioral rules in CLAUDE.md")
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _remove_behavioral_rules(claude_md_path: Path) -> bool:
|
|
833
|
+
"""Remove jacked behavioral rules block from CLAUDE.md.
|
|
834
|
+
|
|
835
|
+
Returns True if rules were found and removed.
|
|
836
|
+
"""
|
|
837
|
+
if not claude_md_path.exists():
|
|
838
|
+
return False
|
|
839
|
+
|
|
840
|
+
content = claude_md_path.read_text(encoding="utf-8")
|
|
841
|
+
marker_prefix = "# jacked-behaviors-v"
|
|
842
|
+
end_marker = _behavioral_rules_end_marker()
|
|
843
|
+
|
|
844
|
+
if marker_prefix not in content or end_marker not in content:
|
|
845
|
+
return False
|
|
846
|
+
|
|
847
|
+
start_idx = content.index(marker_prefix)
|
|
848
|
+
end_idx = content.index(end_marker) + len(end_marker)
|
|
849
|
+
|
|
850
|
+
# Strip the block and any extra blank lines around it
|
|
851
|
+
before = content[:start_idx].rstrip("\n")
|
|
852
|
+
after = content[end_idx:].lstrip("\n")
|
|
853
|
+
|
|
854
|
+
if before and after:
|
|
855
|
+
new_content = before + "\n\n" + after
|
|
856
|
+
elif before:
|
|
857
|
+
new_content = before + "\n"
|
|
858
|
+
else:
|
|
859
|
+
new_content = after
|
|
860
|
+
|
|
861
|
+
try:
|
|
862
|
+
claude_md_path.write_text(new_content, encoding="utf-8")
|
|
863
|
+
except PermissionError:
|
|
864
|
+
console.print(f"[red][FAIL][/red] Permission denied writing to {claude_md_path}")
|
|
865
|
+
return False
|
|
866
|
+
return True
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _security_hook_marker() -> str:
|
|
870
|
+
"""Marker to identify jacked security gatekeeper hooks."""
|
|
871
|
+
return "# jacked-security"
|
|
872
|
+
|
|
873
|
+
|
|
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
|
+
|
|
882
|
+
def _install_security_hook(existing: dict, settings_path: Path):
|
|
883
|
+
"""Install Opus-powered security gatekeeper hook for Bash commands.
|
|
884
|
+
|
|
885
|
+
Handles fresh install and version upgrades (detects stale prompts).
|
|
886
|
+
"""
|
|
887
|
+
import json
|
|
888
|
+
|
|
889
|
+
marker = _security_hook_marker()
|
|
890
|
+
|
|
891
|
+
try:
|
|
892
|
+
prompt_text = _get_security_prompt()
|
|
893
|
+
except FileNotFoundError as e:
|
|
894
|
+
console.print(f"[red][FAIL][/red] {e}")
|
|
895
|
+
console.print("[yellow]Skipping security gatekeeper installation[/yellow]")
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
if "PermissionRequest" not in existing["hooks"]:
|
|
899
|
+
existing["hooks"]["PermissionRequest"] = []
|
|
900
|
+
|
|
901
|
+
# Check if already installed and whether it needs upgrading
|
|
902
|
+
hook_index = None
|
|
903
|
+
needs_upgrade = False
|
|
904
|
+
for i, hook_entry in enumerate(existing["hooks"]["PermissionRequest"]):
|
|
905
|
+
hook_str = str(hook_entry)
|
|
906
|
+
if marker in hook_str:
|
|
907
|
+
hook_index = i
|
|
908
|
+
# Check if installed prompt matches current version
|
|
909
|
+
for h in hook_entry.get("hooks", []):
|
|
910
|
+
installed_prompt = h.get("prompt", "")
|
|
911
|
+
if installed_prompt != prompt_text:
|
|
912
|
+
needs_upgrade = True
|
|
913
|
+
break
|
|
914
|
+
|
|
915
|
+
if hook_index is not None and not needs_upgrade:
|
|
916
|
+
console.print("[yellow][-][/yellow] Security gatekeeper hook already configured")
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
hook_entry = {
|
|
920
|
+
"matcher": "Bash",
|
|
921
|
+
"hooks": [{
|
|
922
|
+
"type": "prompt",
|
|
923
|
+
"prompt": prompt_text,
|
|
924
|
+
"model": "opus",
|
|
925
|
+
"timeout": 60,
|
|
926
|
+
}]
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if hook_index is not None and needs_upgrade:
|
|
930
|
+
existing["hooks"]["PermissionRequest"][hook_index] = hook_entry
|
|
931
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
932
|
+
console.print("[green][OK][/green] Updated security gatekeeper prompt to latest version")
|
|
933
|
+
else:
|
|
934
|
+
existing["hooks"]["PermissionRequest"].append(hook_entry)
|
|
935
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
936
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
937
|
+
console.print("[green][OK][/green] Installed security gatekeeper (Opus evaluates Bash commands)")
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _remove_security_hook(settings_path: Path) -> bool:
|
|
941
|
+
"""Remove jacked security gatekeeper hook. Returns True if removed."""
|
|
942
|
+
import json
|
|
943
|
+
|
|
944
|
+
if not settings_path.exists():
|
|
945
|
+
return False
|
|
946
|
+
|
|
947
|
+
settings = json.loads(settings_path.read_text())
|
|
948
|
+
marker = _security_hook_marker()
|
|
949
|
+
|
|
950
|
+
if "PermissionRequest" not in settings.get("hooks", {}):
|
|
951
|
+
return False
|
|
952
|
+
|
|
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:
|
|
959
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
960
|
+
console.print("[green][OK][/green] Removed security gatekeeper hook")
|
|
961
|
+
return True
|
|
962
|
+
|
|
963
|
+
return False
|
|
964
|
+
|
|
965
|
+
|
|
451
966
|
@main.command()
|
|
452
|
-
|
|
967
|
+
@click.option("--sounds", is_flag=True, help="Install sound notification hooks")
|
|
968
|
+
@click.option("--no-security", is_flag=True, help="Skip security gatekeeper hook")
|
|
969
|
+
@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):
|
|
453
971
|
"""Auto-install hook config, skill, agents, and commands."""
|
|
454
972
|
import os
|
|
455
973
|
import json
|
|
456
974
|
import shutil
|
|
457
975
|
|
|
458
976
|
home = Path.home()
|
|
459
|
-
pkg_root =
|
|
977
|
+
pkg_root = _get_data_root()
|
|
460
978
|
|
|
461
|
-
# Hook configuration
|
|
979
|
+
# Hook configuration - assumes jacked is on PATH (installed via pipx)
|
|
980
|
+
# async: True runs indexing in background so Claude Code doesn't wait
|
|
462
981
|
hook_config = {
|
|
463
982
|
"hooks": {
|
|
464
983
|
"Stop": [
|
|
@@ -467,7 +986,8 @@ def install():
|
|
|
467
986
|
"hooks": [
|
|
468
987
|
{
|
|
469
988
|
"type": "command",
|
|
470
|
-
"command": 'jacked index --repo "$CLAUDE_PROJECT_DIR"'
|
|
989
|
+
"command": 'jacked index --repo "$CLAUDE_PROJECT_DIR"',
|
|
990
|
+
"async": True
|
|
471
991
|
}
|
|
472
992
|
]
|
|
473
993
|
}
|
|
@@ -493,21 +1013,34 @@ def install():
|
|
|
493
1013
|
if "Stop" not in existing["hooks"]:
|
|
494
1014
|
existing["hooks"]["Stop"] = []
|
|
495
1015
|
|
|
496
|
-
# Check if hook already exists
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
503
1030
|
existing["hooks"]["Stop"].append(hook_config["hooks"]["Stop"][0])
|
|
504
1031
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
505
1032
|
settings_path.write_text(json.dumps(existing, indent=2))
|
|
506
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")
|
|
507
1039
|
else:
|
|
508
|
-
console.print(f"[yellow][-][/yellow] Stop hook already
|
|
1040
|
+
console.print(f"[yellow][-][/yellow] Stop hook already configured correctly")
|
|
509
1041
|
|
|
510
|
-
# Copy skill file
|
|
1042
|
+
# Copy skill file with Python path templating
|
|
1043
|
+
# Claude Code expects skills in subdirectories with SKILL.md
|
|
511
1044
|
skill_dir = home / ".claude" / "skills" / "jacked"
|
|
512
1045
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
513
1046
|
|
|
@@ -520,39 +1053,90 @@ def install():
|
|
|
520
1053
|
else:
|
|
521
1054
|
console.print(f"[yellow][-][/yellow] Skill file not found at {skill_src}")
|
|
522
1055
|
|
|
523
|
-
# Copy agents
|
|
1056
|
+
# Copy agents (with conflict detection)
|
|
524
1057
|
agents_src = pkg_root / "agents"
|
|
525
1058
|
agents_dst = home / ".claude" / "agents"
|
|
526
1059
|
if agents_src.exists():
|
|
527
1060
|
agents_dst.mkdir(parents=True, exist_ok=True)
|
|
528
1061
|
agent_count = 0
|
|
1062
|
+
skipped = 0
|
|
529
1063
|
for agent_file in agents_src.glob("*.md"):
|
|
530
|
-
|
|
1064
|
+
dst_file = agents_dst / agent_file.name
|
|
1065
|
+
src_content = agent_file.read_text(encoding="utf-8")
|
|
1066
|
+
if dst_file.exists():
|
|
1067
|
+
dst_content = dst_file.read_text(encoding="utf-8")
|
|
1068
|
+
if src_content == dst_content:
|
|
1069
|
+
skipped += 1
|
|
1070
|
+
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?"):
|
|
1073
|
+
console.print(f"[yellow][-][/yellow] Skipped {agent_file.name}")
|
|
1074
|
+
continue
|
|
1075
|
+
shutil.copy(agent_file, dst_file)
|
|
531
1076
|
agent_count += 1
|
|
532
|
-
|
|
1077
|
+
msg = f"[green][OK][/green] Installed {agent_count} agents"
|
|
1078
|
+
if skipped:
|
|
1079
|
+
msg += f" ({skipped} unchanged)"
|
|
1080
|
+
console.print(msg)
|
|
533
1081
|
else:
|
|
534
1082
|
console.print(f"[yellow][-][/yellow] Agents directory not found")
|
|
535
1083
|
|
|
536
|
-
# Copy commands
|
|
1084
|
+
# Copy commands (with conflict detection)
|
|
537
1085
|
commands_src = pkg_root / "commands"
|
|
538
1086
|
commands_dst = home / ".claude" / "commands"
|
|
539
1087
|
if commands_src.exists():
|
|
540
1088
|
commands_dst.mkdir(parents=True, exist_ok=True)
|
|
541
1089
|
cmd_count = 0
|
|
1090
|
+
skipped = 0
|
|
542
1091
|
for cmd_file in commands_src.glob("*.md"):
|
|
543
|
-
|
|
1092
|
+
dst_file = commands_dst / cmd_file.name
|
|
1093
|
+
src_content = cmd_file.read_text(encoding="utf-8")
|
|
1094
|
+
if dst_file.exists():
|
|
1095
|
+
dst_content = dst_file.read_text(encoding="utf-8")
|
|
1096
|
+
if src_content == dst_content:
|
|
1097
|
+
skipped += 1
|
|
1098
|
+
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?"):
|
|
1101
|
+
console.print(f"[yellow][-][/yellow] Skipped {cmd_file.name}")
|
|
1102
|
+
continue
|
|
1103
|
+
shutil.copy(cmd_file, dst_file)
|
|
544
1104
|
cmd_count += 1
|
|
545
|
-
|
|
1105
|
+
msg = f"[green][OK][/green] Installed {cmd_count} commands"
|
|
1106
|
+
if skipped:
|
|
1107
|
+
msg += f" ({skipped} unchanged)"
|
|
1108
|
+
console.print(msg)
|
|
546
1109
|
else:
|
|
547
1110
|
console.print(f"[yellow][-][/yellow] Commands directory not found")
|
|
548
1111
|
|
|
1112
|
+
# Install sound hooks if requested
|
|
1113
|
+
if sounds:
|
|
1114
|
+
_install_sound_hooks(existing, settings_path)
|
|
1115
|
+
|
|
1116
|
+
# Install security gatekeeper (default on, --no-security to skip)
|
|
1117
|
+
if not no_security:
|
|
1118
|
+
_install_security_hook(existing, settings_path)
|
|
1119
|
+
|
|
1120
|
+
# Install behavioral rules in CLAUDE.md (default on, --no-rules to skip)
|
|
1121
|
+
if not no_rules:
|
|
1122
|
+
claude_md_path = home / ".claude" / "CLAUDE.md"
|
|
1123
|
+
_install_behavioral_rules(claude_md_path)
|
|
1124
|
+
|
|
549
1125
|
console.print("\n[bold]Installation complete![/bold]")
|
|
550
1126
|
console.print("\n[yellow]IMPORTANT: Restart Claude Code for new commands to take effect![/yellow]")
|
|
551
1127
|
console.print("\nWhat you get:")
|
|
552
1128
|
console.print(" - /jacked - Search past Claude sessions")
|
|
553
|
-
console.print(" - /dc - Double-check reviewer")
|
|
1129
|
+
console.print(" - /dc - Double-check reviewer (with grill mode)")
|
|
554
1130
|
console.print(" - /pr - PR workflow helper")
|
|
1131
|
+
console.print(" - /learn - Distill lessons into CLAUDE.md rules")
|
|
1132
|
+
console.print(" - /techdebt - Project tech debt audit")
|
|
1133
|
+
console.print(" - /redo - Scrap and re-implement with hindsight")
|
|
1134
|
+
console.print(" - /audit-rules - CLAUDE.md quality audit")
|
|
555
1135
|
console.print(" - 10 specialized agents (readme, wiki, tests, etc.)")
|
|
1136
|
+
if not no_security:
|
|
1137
|
+
console.print(" - Security gatekeeper (Opus evaluates Bash commands)")
|
|
1138
|
+
if not no_rules:
|
|
1139
|
+
console.print(" - Behavioral rules in CLAUDE.md (auto-triggers for jacked commands)")
|
|
556
1140
|
console.print("\nNext steps:")
|
|
557
1141
|
console.print(" 1. Restart Claude Code (exit and run 'claude' again)")
|
|
558
1142
|
console.print(" 2. Set environment variables (run 'jacked configure' for help)")
|
|
@@ -560,5 +1144,126 @@ def install():
|
|
|
560
1144
|
console.print(" 4. Use '/jacked <description>' in Claude to search past sessions")
|
|
561
1145
|
|
|
562
1146
|
|
|
1147
|
+
@main.command()
|
|
1148
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
1149
|
+
@click.option("--sounds", is_flag=True, help="Remove only sound hooks")
|
|
1150
|
+
@click.option("--security", is_flag=True, help="Remove only security gatekeeper hook")
|
|
1151
|
+
@click.option("--rules", is_flag=True, help="Remove only behavioral rules from CLAUDE.md")
|
|
1152
|
+
def uninstall(yes: bool, sounds: bool, security: bool, rules: bool):
|
|
1153
|
+
"""Remove jacked hooks, skill, agents, and commands from Claude Code."""
|
|
1154
|
+
import json
|
|
1155
|
+
import shutil
|
|
1156
|
+
|
|
1157
|
+
home = Path.home()
|
|
1158
|
+
pkg_root = _get_data_root()
|
|
1159
|
+
settings_path = home / ".claude" / "settings.json"
|
|
1160
|
+
|
|
1161
|
+
# If --sounds flag, only remove sound hooks
|
|
1162
|
+
if sounds:
|
|
1163
|
+
if _remove_sound_hooks(settings_path):
|
|
1164
|
+
console.print("[bold]Sound hooks removed![/bold]")
|
|
1165
|
+
else:
|
|
1166
|
+
console.print("[yellow]No sound hooks found[/yellow]")
|
|
1167
|
+
return
|
|
1168
|
+
|
|
1169
|
+
# If --security flag, only remove security hook
|
|
1170
|
+
if security:
|
|
1171
|
+
if _remove_security_hook(settings_path):
|
|
1172
|
+
console.print("[bold]Security gatekeeper removed![/bold]")
|
|
1173
|
+
else:
|
|
1174
|
+
console.print("[yellow]No security gatekeeper hook found[/yellow]")
|
|
1175
|
+
return
|
|
1176
|
+
|
|
1177
|
+
# If --rules flag, only remove behavioral rules
|
|
1178
|
+
if rules:
|
|
1179
|
+
claude_md_path = home / ".claude" / "CLAUDE.md"
|
|
1180
|
+
if _remove_behavioral_rules(claude_md_path):
|
|
1181
|
+
console.print("[bold]Behavioral rules removed from CLAUDE.md![/bold]")
|
|
1182
|
+
else:
|
|
1183
|
+
console.print("[yellow]No behavioral rules found in CLAUDE.md[/yellow]")
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
if not yes:
|
|
1187
|
+
if not click.confirm("Remove jacked from Claude Code? (This won't delete your Qdrant index)"):
|
|
1188
|
+
console.print("Cancelled")
|
|
1189
|
+
return
|
|
1190
|
+
|
|
1191
|
+
console.print("[bold]Uninstalling Jacked...[/bold]\n")
|
|
1192
|
+
|
|
1193
|
+
# Also remove sound, security hooks, and behavioral rules during full uninstall
|
|
1194
|
+
_remove_sound_hooks(settings_path)
|
|
1195
|
+
_remove_security_hook(settings_path)
|
|
1196
|
+
claude_md_path = home / ".claude" / "CLAUDE.md"
|
|
1197
|
+
if _remove_behavioral_rules(claude_md_path):
|
|
1198
|
+
console.print("[green][OK][/green] Removed behavioral rules from CLAUDE.md")
|
|
1199
|
+
|
|
1200
|
+
# Remove Stop hook from settings.json
|
|
1201
|
+
if settings_path.exists():
|
|
1202
|
+
try:
|
|
1203
|
+
settings = json.loads(settings_path.read_text())
|
|
1204
|
+
if "hooks" in settings and "Stop" in settings["hooks"]:
|
|
1205
|
+
# Filter out jacked hooks
|
|
1206
|
+
original_count = len(settings["hooks"]["Stop"])
|
|
1207
|
+
settings["hooks"]["Stop"] = [
|
|
1208
|
+
h for h in settings["hooks"]["Stop"]
|
|
1209
|
+
if "jacked" not in str(h.get("hooks", []))
|
|
1210
|
+
]
|
|
1211
|
+
removed_count = original_count - len(settings["hooks"]["Stop"])
|
|
1212
|
+
if removed_count > 0:
|
|
1213
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
1214
|
+
console.print(f"[green][OK][/green] Removed Stop hook from {settings_path}")
|
|
1215
|
+
else:
|
|
1216
|
+
console.print(f"[yellow][-][/yellow] No jacked hook found in settings")
|
|
1217
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
1218
|
+
console.print(f"[red][FAIL][/red] Error reading settings: {e}")
|
|
1219
|
+
else:
|
|
1220
|
+
console.print(f"[yellow][-][/yellow] No settings.json found")
|
|
1221
|
+
|
|
1222
|
+
# Remove skill directory
|
|
1223
|
+
skill_dir = home / ".claude" / "skills" / "jacked"
|
|
1224
|
+
if skill_dir.exists():
|
|
1225
|
+
shutil.rmtree(skill_dir)
|
|
1226
|
+
console.print(f"[green][OK][/green] Removed skill: /jacked")
|
|
1227
|
+
else:
|
|
1228
|
+
console.print(f"[yellow][-][/yellow] Skill not found")
|
|
1229
|
+
|
|
1230
|
+
# Remove only jacked-installed agents (not the whole directory!)
|
|
1231
|
+
agents_src = pkg_root / "agents"
|
|
1232
|
+
agents_dst = home / ".claude" / "agents"
|
|
1233
|
+
if agents_src.exists() and agents_dst.exists():
|
|
1234
|
+
agent_count = 0
|
|
1235
|
+
for agent_file in agents_src.glob("*.md"):
|
|
1236
|
+
dst_file = agents_dst / agent_file.name
|
|
1237
|
+
if dst_file.exists():
|
|
1238
|
+
dst_file.unlink()
|
|
1239
|
+
agent_count += 1
|
|
1240
|
+
if agent_count > 0:
|
|
1241
|
+
console.print(f"[green][OK][/green] Removed {agent_count} agents")
|
|
1242
|
+
else:
|
|
1243
|
+
console.print(f"[yellow][-][/yellow] No jacked agents found")
|
|
1244
|
+
else:
|
|
1245
|
+
console.print(f"[yellow][-][/yellow] Agents directory not found")
|
|
1246
|
+
|
|
1247
|
+
# Remove only jacked-installed commands (not the whole directory!)
|
|
1248
|
+
commands_src = pkg_root / "commands"
|
|
1249
|
+
commands_dst = home / ".claude" / "commands"
|
|
1250
|
+
if commands_src.exists() and commands_dst.exists():
|
|
1251
|
+
cmd_count = 0
|
|
1252
|
+
for cmd_file in commands_src.glob("*.md"):
|
|
1253
|
+
dst_file = commands_dst / cmd_file.name
|
|
1254
|
+
if dst_file.exists():
|
|
1255
|
+
dst_file.unlink()
|
|
1256
|
+
cmd_count += 1
|
|
1257
|
+
if cmd_count > 0:
|
|
1258
|
+
console.print(f"[green][OK][/green] Removed {cmd_count} commands")
|
|
1259
|
+
else:
|
|
1260
|
+
console.print(f"[yellow][-][/yellow] No jacked commands found")
|
|
1261
|
+
else:
|
|
1262
|
+
console.print(f"[yellow][-][/yellow] Commands directory not found")
|
|
1263
|
+
|
|
1264
|
+
console.print("\n[bold]Uninstall complete![/bold]")
|
|
1265
|
+
console.print("\n[dim]Note: Your Qdrant index is still intact. Run 'pipx uninstall claude-jacked' to fully remove.[/dim]")
|
|
1266
|
+
|
|
1267
|
+
|
|
563
1268
|
if __name__ == "__main__":
|
|
564
1269
|
main()
|