tweek 0.3.1__py3-none-any.whl → 0.4.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.
Files changed (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +170 -74
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
  61. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/cli_uninstall.py ADDED
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek CLI Uninstall Command
4
+
5
+ Full removal of Tweek from the system:
6
+ tweek uninstall Interactive full removal
7
+ tweek uninstall --all Remove ALL Tweek data system-wide
8
+ tweek uninstall --all --confirm Remove everything without prompts
9
+
10
+ IMPORTANT: Users MUST run ``tweek unprotect --all`` (or ``tweek uninstall``)
11
+ BEFORE removing the pip package with ``pip uninstall tweek``. If the package
12
+ is removed first, the Claude Code hooks in ~/.claude/settings.json become
13
+ orphaned — they still reference the hook scripts on disk but the ``tweek``
14
+ CLI is no longer available to clean them up. Orphaned hooks cause spurious
15
+ security warnings in every Claude Code session.
16
+
17
+ To manually clean up orphaned hooks, remove the ``PreToolUse`` and
18
+ ``PostToolUse`` entries that reference ``tweek`` from:
19
+ ~/.claude/settings.json (global hooks)
20
+ <project>/.claude/settings.json (project-level hooks)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import click
25
+ import json
26
+ import shutil
27
+ import subprocess
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ from tweek.cli_helpers import (
32
+ console,
33
+ TWEEK_BANNER,
34
+ _has_tweek_hooks,
35
+ _has_tweek_at,
36
+ )
37
+
38
+
39
+ # =============================================================================
40
+ # UNINSTALL COMMAND
41
+ # =============================================================================
42
+
43
+ @click.command(
44
+ epilog="""\b
45
+ Examples:
46
+ tweek uninstall Interactive full removal
47
+ tweek uninstall --all Remove ALL Tweek data system-wide
48
+ tweek uninstall --all --confirm Remove everything without prompts
49
+ """
50
+ )
51
+ @click.option("--all", "remove_all", is_flag=True, default=False,
52
+ help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
53
+ @click.option("--confirm", is_flag=True, help="Skip confirmation prompts")
54
+ def uninstall(remove_all: bool, confirm: bool):
55
+ """Fully remove Tweek from your system.
56
+
57
+ Removes all hooks, skills, configuration, data, and optionally
58
+ the Tweek package itself. For removing protection from a single
59
+ tool without uninstalling, use `tweek unprotect` instead.
60
+
61
+ This command can only be run from an interactive terminal.
62
+ AI agents are blocked from running it.
63
+ """
64
+ # ─────────────────────────────────────────────────────────────
65
+ # HUMAN-ONLY GATE: Block non-interactive execution
66
+ # ─────────────────────────────────────────────────────────────
67
+ if not sys.stdin.isatty():
68
+ console.print("[red]ERROR: tweek uninstall must be run from an interactive terminal.[/red]")
69
+ console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
70
+ console.print("[white]Open a terminal and run the command directly.[/white]")
71
+ raise SystemExit(1)
72
+
73
+ console.print(TWEEK_BANNER, style="cyan")
74
+
75
+ tweek_dir = Path("~/.tweek").expanduser()
76
+ global_target = Path("~/.claude").expanduser()
77
+ project_target = Path.cwd() / ".claude"
78
+
79
+ if not remove_all:
80
+ # Interactive: ask what to remove
81
+ console.print("[bold]What would you like to remove?[/bold]")
82
+ console.print()
83
+ console.print(" [bold]1.[/bold] Everything (all hooks, data, config, and package)")
84
+ console.print(" [bold]2.[/bold] Cancel")
85
+ console.print()
86
+ choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
87
+ if choice == 2:
88
+ console.print("[white]Cancelled[/white]")
89
+ return
90
+ console.print()
91
+
92
+ _uninstall_everything(global_target, project_target, tweek_dir, confirm)
93
+ _show_package_removal_hint()
94
+
95
+
96
+ # =============================================================================
97
+ # UNINSTALL HELPERS
98
+ # =============================================================================
99
+
100
+ def _detect_all_package_managers() -> list:
101
+ """Detect all places tweek is installed. Returns list of uninstall commands."""
102
+ found = []
103
+
104
+ # Check pipx
105
+ try:
106
+ result = subprocess.run(
107
+ ["pipx", "list"], capture_output=True, text=True, timeout=5
108
+ )
109
+ if "tweek" in result.stdout:
110
+ found.append("pipx uninstall tweek")
111
+ except (FileNotFoundError, subprocess.TimeoutExpired):
112
+ pass
113
+
114
+ # Check uv
115
+ try:
116
+ result = subprocess.run(
117
+ ["uv", "tool", "list"], capture_output=True, text=True, timeout=5
118
+ )
119
+ if "tweek" in result.stdout:
120
+ found.append("uv tool uninstall tweek")
121
+ except (FileNotFoundError, subprocess.TimeoutExpired):
122
+ pass
123
+
124
+ # Check pip
125
+ try:
126
+ result = subprocess.run(
127
+ [sys.executable, "-m", "pip", "show", "tweek"],
128
+ capture_output=True, text=True, timeout=5
129
+ )
130
+ if result.returncode == 0:
131
+ found.append("pip uninstall tweek -y")
132
+ except (FileNotFoundError, subprocess.TimeoutExpired):
133
+ pass
134
+
135
+ return found
136
+
137
+
138
+ def _show_package_removal_hint():
139
+ """Offer to remove all tweek CLI package installations for the user."""
140
+ pkg_cmds = _detect_all_package_managers()
141
+ if not pkg_cmds:
142
+ return
143
+
144
+ console.print()
145
+ console.print("[bold yellow]The tweek CLI binary is still installed on your system.[/bold yellow]")
146
+
147
+ if len(pkg_cmds) > 1:
148
+ console.print(f"[white]Found {len(pkg_cmds)} installations:[/white]")
149
+ for cmd in pkg_cmds:
150
+ console.print(f" [white]\u2022 {cmd}[/white]")
151
+
152
+ console.print()
153
+ label = " + ".join(f"[bold]{cmd}[/bold]" for cmd in pkg_cmds)
154
+ console.print(f" [bold]1.[/bold] Remove all now ({label})")
155
+ console.print(f" [bold]2.[/bold] Keep (you can remove later)")
156
+ console.print()
157
+ choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
158
+
159
+ if choice == 1:
160
+ for pkg_cmd in pkg_cmds:
161
+ console.print()
162
+ console.print(f"[cyan]Running:[/cyan] {pkg_cmd}")
163
+ try:
164
+ result = subprocess.run(
165
+ pkg_cmd.split(), capture_output=True, text=True, timeout=30
166
+ )
167
+ if result.returncode == 0:
168
+ console.print(f"[green]\u2713[/green] Removed ({pkg_cmd})")
169
+ else:
170
+ console.print(f"[red]\u2717[/red] Failed: {result.stderr.strip()}")
171
+ console.print(f" [white]Run manually: {pkg_cmd}[/white]")
172
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
173
+ console.print(f"[red]\u2717[/red] Could not run: {e}")
174
+ console.print(f" [white]Run manually: {pkg_cmd}[/white]")
175
+
176
+
177
+ def _remove_hooks_from_settings(settings_file: Path) -> list:
178
+ """Remove Tweek hooks from a settings.json file.
179
+
180
+ Returns list of hook types removed (e.g. ['PreToolUse', 'PostToolUse']).
181
+ """
182
+ removed = []
183
+
184
+ if not settings_file.exists():
185
+ return removed
186
+
187
+ try:
188
+ with open(settings_file) as f:
189
+ settings = json.load(f)
190
+ except (json.JSONDecodeError, IOError):
191
+ return removed
192
+
193
+ if not _has_tweek_hooks(settings):
194
+ return removed
195
+
196
+ for hook_type in ("PreToolUse", "PostToolUse"):
197
+ if "hooks" not in settings or hook_type not in settings["hooks"]:
198
+ continue
199
+
200
+ tool_hooks = settings["hooks"][hook_type]
201
+ filtered_hooks = []
202
+ for hook_config in tool_hooks:
203
+ filtered_inner = []
204
+ for hook in hook_config.get("hooks", []):
205
+ if "tweek" not in hook.get("command", "").lower():
206
+ filtered_inner.append(hook)
207
+ if filtered_inner:
208
+ hook_config["hooks"] = filtered_inner
209
+ filtered_hooks.append(hook_config)
210
+
211
+ if filtered_hooks:
212
+ settings["hooks"][hook_type] = filtered_hooks
213
+ else:
214
+ del settings["hooks"][hook_type]
215
+ removed.append(hook_type)
216
+
217
+ # Clean up empty hooks dict
218
+ if "hooks" in settings and not settings["hooks"]:
219
+ del settings["hooks"]
220
+
221
+ with open(settings_file, "w") as f:
222
+ json.dump(settings, f, indent=2)
223
+
224
+ return removed
225
+
226
+
227
+ def _remove_skill_directory(target: Path) -> bool:
228
+ """Remove the Tweek skill directory from a .claude/ target. Returns True if removed."""
229
+ skill_dir = target / "skills" / "tweek"
230
+ if skill_dir.exists() and skill_dir.is_dir():
231
+ shutil.rmtree(skill_dir)
232
+ return True
233
+ return False
234
+
235
+
236
+ def _remove_backup_file(target: Path) -> bool:
237
+ """Remove the settings.json.tweek-backup file. Returns True if removed."""
238
+ backup = target / "settings.json.tweek-backup"
239
+ if backup.exists():
240
+ backup.unlink()
241
+ return True
242
+ return False
243
+
244
+
245
+ def _remove_whitelist_entries(target: Path, tweek_dir: Path) -> int:
246
+ """Remove whitelist entries for target path from overrides.yaml. Returns count removed."""
247
+ import yaml
248
+
249
+ overrides_path = tweek_dir / "overrides.yaml"
250
+ if not overrides_path.exists():
251
+ return 0
252
+
253
+ try:
254
+ with open(overrides_path) as f:
255
+ data = yaml.safe_load(f) or {}
256
+ except (yaml.YAMLError, IOError):
257
+ return 0
258
+
259
+ whitelist = data.get("whitelist", [])
260
+ if not whitelist:
261
+ return 0
262
+
263
+ target_str = str(target.resolve())
264
+ original_count = len(whitelist)
265
+
266
+ data["whitelist"] = [
267
+ entry for entry in whitelist
268
+ if not (isinstance(entry, dict) and
269
+ str(Path(entry.get("path", "")).resolve()).startswith(target_str))
270
+ ]
271
+
272
+ removed_count = original_count - len(data["whitelist"])
273
+ if removed_count > 0:
274
+ with open(overrides_path, "w") as f:
275
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
276
+
277
+ return removed_count
278
+
279
+
280
+ def _remove_tweek_data_dir(tweek_dir: Path) -> list:
281
+ """Remove ~/.tweek/ directory and all contents. Returns list of items removed."""
282
+ removed = []
283
+
284
+ if not tweek_dir.exists():
285
+ return removed
286
+
287
+ # Remove known items with individual feedback
288
+ items = [
289
+ ("config.yaml", "configuration"),
290
+ ("overrides.yaml", "security overrides"),
291
+ ("security.db", "security log database"),
292
+ ]
293
+ for filename, label in items:
294
+ filepath = tweek_dir / filename
295
+ if filepath.exists():
296
+ filepath.unlink()
297
+ removed.append(label)
298
+
299
+ dirs = [
300
+ ("patterns", "pattern repository"),
301
+ ("chamber", "skill isolation chamber"),
302
+ ("jail", "skill jail"),
303
+ ("skills", "managed skills"),
304
+ ]
305
+ for dirname, label in dirs:
306
+ dirpath = tweek_dir / dirname
307
+ if dirpath.exists() and dirpath.is_dir():
308
+ shutil.rmtree(dirpath)
309
+ removed.append(label)
310
+
311
+ # Remove any remaining files
312
+ remaining = list(tweek_dir.iterdir()) if tweek_dir.exists() else []
313
+ for item in remaining:
314
+ if item.is_dir():
315
+ shutil.rmtree(item)
316
+ else:
317
+ item.unlink()
318
+
319
+ # Remove the directory itself
320
+ if tweek_dir.exists():
321
+ try:
322
+ tweek_dir.rmdir()
323
+ removed.append("data directory (~/.tweek/)")
324
+ except OSError:
325
+ # Not empty for some reason
326
+ shutil.rmtree(tweek_dir, ignore_errors=True)
327
+ removed.append("data directory (~/.tweek/)")
328
+
329
+ return removed
330
+
331
+
332
+ def _remove_mcp_integrations() -> list:
333
+ """Remove MCP integrations for all known clients. Returns list of clients removed."""
334
+ removed = []
335
+ clients = {
336
+ "claude-desktop": Path("~/Library/Application Support/Claude/claude_desktop_config.json").expanduser(),
337
+ "chatgpt": Path("~/Library/Application Support/com.openai.chat/config.json").expanduser(),
338
+ }
339
+
340
+ for client_name, config_path in clients.items():
341
+ if not config_path.exists():
342
+ continue
343
+ try:
344
+ with open(config_path) as f:
345
+ config = json.load(f)
346
+ mcp_servers = config.get("mcpServers", {})
347
+ tweek_keys = [k for k in mcp_servers if "tweek" in k.lower()]
348
+ if tweek_keys:
349
+ for key in tweek_keys:
350
+ del mcp_servers[key]
351
+ with open(config_path, "w") as f:
352
+ json.dump(config, f, indent=2)
353
+ removed.append(client_name)
354
+ except (json.JSONDecodeError, IOError, KeyError):
355
+ continue
356
+
357
+ return removed
358
+
359
+
360
+ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label: str):
361
+ """Uninstall Tweek from a single scope (project or global)."""
362
+ settings_file = target / "settings.json"
363
+ has_hooks = False
364
+ has_skills = (target / "skills" / "tweek").exists()
365
+ has_backup = (target / "settings.json.tweek-backup").exists()
366
+
367
+ if settings_file.exists():
368
+ try:
369
+ with open(settings_file) as f:
370
+ settings = json.load(f)
371
+ has_hooks = _has_tweek_hooks(settings)
372
+ except (json.JSONDecodeError, IOError):
373
+ pass
374
+
375
+ if not has_hooks and not has_skills and not has_backup:
376
+ console.print(f"[yellow]No Tweek installation found at {target}[/yellow]")
377
+ return
378
+
379
+ console.print(f"[bold]Found Tweek installation at:[/bold] {target}")
380
+ console.print()
381
+ console.print("[bold]The following will be removed:[/bold]")
382
+ if has_hooks:
383
+ console.print(" [white]\u2022[/white] PreToolUse and PostToolUse hooks from settings.json")
384
+ if has_skills:
385
+ console.print(" [white]\u2022[/white] Tweek skill directory (skills/tweek/)")
386
+ if has_backup:
387
+ console.print(" [white]\u2022[/white] Backup file (settings.json.tweek-backup)")
388
+ console.print(" [white]\u2022[/white] Project whitelist entries from overrides")
389
+ console.print()
390
+
391
+ if not confirm:
392
+ console.print(f"[yellow]Remove Tweek from this {scope_label}?[/yellow] ", end="")
393
+ if not click.confirm(""):
394
+ console.print("[white]Cancelled[/white]")
395
+ return
396
+
397
+ console.print()
398
+
399
+ # 1. Remove hooks
400
+ removed_hooks = _remove_hooks_from_settings(settings_file)
401
+ for hook_type in removed_hooks:
402
+ console.print(f" [green]\u2713[/green] Removed {hook_type} hook from settings.json")
403
+ if has_hooks and not removed_hooks:
404
+ console.print(f" [red]\u2717[/red] Failed to remove hooks from settings.json")
405
+
406
+ # 2. Remove skill directory
407
+ if _remove_skill_directory(target):
408
+ console.print(f" [green]\u2713[/green] Removed Tweek skill directory (skills/tweek/)")
409
+ else:
410
+ console.print(f" [white]-[/white] Skipped: Tweek skill directory not found")
411
+
412
+ # 3. Remove backup file
413
+ if _remove_backup_file(target):
414
+ console.print(f" [green]\u2713[/green] Removed backup file (settings.json.tweek-backup)")
415
+ else:
416
+ console.print(f" [white]-[/white] Skipped: no backup file found")
417
+
418
+ # 4. Remove whitelist entries
419
+ wl_count = _remove_whitelist_entries(target, tweek_dir)
420
+ if wl_count > 0:
421
+ console.print(f" [green]\u2713[/green] Removed {wl_count} whitelist entry(s) from overrides")
422
+ else:
423
+ console.print(f" [white]-[/white] Skipped: no whitelist entries found for this {scope_label}")
424
+
425
+ console.print()
426
+ console.print(f"[green]Uninstall complete.[/green] Tweek is no longer active for this {scope_label}.")
427
+ if scope_label == "project":
428
+ console.print("[white]Global installation (~/.claude/) was not affected.[/white]")
429
+ else:
430
+ console.print("[white]Project installations were not affected.[/white]")
431
+
432
+ # Offer to remove data directory
433
+ if tweek_dir.exists() and not confirm:
434
+ # Check if the OTHER scope still has tweek installed
435
+ global_target = Path("~/.claude").expanduser()
436
+ project_target = Path.cwd() / ".claude"
437
+ other_target = global_target if scope_label == "project" else project_target
438
+ other_label = "global" if scope_label == "project" else "project"
439
+ other_has_tweek = _has_tweek_at(other_target)
440
+
441
+ console.print()
442
+ console.print("[yellow]Also remove Tweek data directory (~/.tweek/)?[/yellow]")
443
+ console.print("[white]This contains config, patterns, security logs, and overrides.[/white]")
444
+ if other_has_tweek:
445
+ console.print(f"[bold red]Warning:[/bold red] Tweek is still installed at {other_label} scope ({other_target}).")
446
+ console.print(f" Removing ~/.tweek/ will affect that installation (no config, patterns, or logs).")
447
+ console.print()
448
+ console.print(f" [bold]1.[/bold] Keep data (recommended)" if other_has_tweek else f" [bold]1.[/bold] Keep data (can reinstall later without re-downloading patterns)")
449
+ console.print(f" [bold]2.[/bold] Remove data (~/.tweek/)")
450
+ console.print()
451
+ remove_choice = click.prompt("Select", type=click.IntRange(1, 2), default=1)
452
+ if remove_choice == 2:
453
+ console.print()
454
+ data_removed = _remove_tweek_data_dir(tweek_dir)
455
+ for item in data_removed:
456
+ console.print(f" [green]\u2713[/green] Removed {item}")
457
+ if not data_removed:
458
+ console.print(f" [white]-[/white] No data to remove")
459
+ elif tweek_dir.exists():
460
+ console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
461
+
462
+
463
+ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
464
+ """Full system removal of all Tweek data."""
465
+ console.print("[bold yellow]FULL REMOVAL[/bold yellow] \u2014 This will remove ALL Tweek data:\n")
466
+ console.print(" [white]\u2022[/white] Hooks from current project (.claude/settings.json)")
467
+ console.print(" [white]\u2022[/white] Hooks from global installation (~/.claude/settings.json)")
468
+ console.print(" [white]\u2022[/white] Tweek skill directories (project + global)")
469
+ console.print(" [white]\u2022[/white] All backup files")
470
+ console.print(" [white]\u2022[/white] Tweek data directory (~/.tweek/)")
471
+
472
+ # Show what exists in ~/.tweek/
473
+ if tweek_dir.exists():
474
+ for item in sorted(tweek_dir.iterdir()):
475
+ if item.is_dir():
476
+ console.print(f" [white]\u251c\u2500\u2500 {item.name}/ [/white]")
477
+ else:
478
+ console.print(f" [white]\u251c\u2500\u2500 {item.name}[/white]")
479
+
480
+ console.print(" [white]\u2022[/white] MCP integrations (Claude Desktop, ChatGPT)")
481
+ console.print()
482
+
483
+ if not confirm:
484
+ console.print("[bold red]Type 'yes' to confirm full removal[/bold red]: ", end="")
485
+ response = input()
486
+ if response.strip().lower() != "yes":
487
+ console.print("[white]Cancelled[/white]")
488
+ return
489
+
490
+ console.print()
491
+
492
+ # ── Project scope ──
493
+ console.print("[bold]Project scope (.claude/):[/bold]")
494
+ removed_hooks = _remove_hooks_from_settings(project_target / "settings.json")
495
+ for hook_type in removed_hooks:
496
+ console.print(f" [green]\u2713[/green] Removed {hook_type} hook from project settings.json")
497
+ if not removed_hooks:
498
+ console.print(f" [white]-[/white] Skipped: no project hooks found")
499
+
500
+ if _remove_skill_directory(project_target):
501
+ console.print(f" [green]\u2713[/green] Removed Tweek skill from project")
502
+ else:
503
+ console.print(f" [white]-[/white] Skipped: no project skill directory")
504
+
505
+ if _remove_backup_file(project_target):
506
+ console.print(f" [green]\u2713[/green] Removed project backup file")
507
+ else:
508
+ console.print(f" [white]-[/white] Skipped: no project backup file")
509
+
510
+ console.print()
511
+
512
+ # ── Global scope ──
513
+ console.print("[bold]Global scope (~/.claude/):[/bold]")
514
+ removed_hooks = _remove_hooks_from_settings(global_target / "settings.json")
515
+ for hook_type in removed_hooks:
516
+ console.print(f" [green]\u2713[/green] Removed {hook_type} hook from global settings.json")
517
+ if not removed_hooks:
518
+ console.print(f" [white]-[/white] Skipped: no global hooks found")
519
+
520
+ if _remove_skill_directory(global_target):
521
+ console.print(f" [green]\u2713[/green] Removed Tweek skill from global installation")
522
+ else:
523
+ console.print(f" [white]-[/white] Skipped: no global skill directory")
524
+
525
+ if _remove_backup_file(global_target):
526
+ console.print(f" [green]\u2713[/green] Removed global backup file")
527
+ else:
528
+ console.print(f" [white]-[/white] Skipped: no global backup file")
529
+
530
+ console.print()
531
+
532
+ # ── Tweek data directory ──
533
+ console.print("[bold]Tweek data (~/.tweek/):[/bold]")
534
+ data_removed = _remove_tweek_data_dir(tweek_dir)
535
+ for item in data_removed:
536
+ console.print(f" [green]\u2713[/green] Removed {item}")
537
+ if not data_removed:
538
+ console.print(f" [white]-[/white] Skipped: no data directory found")
539
+
540
+ console.print()
541
+
542
+ # ── MCP integrations ──
543
+ console.print("[bold]MCP integrations:[/bold]")
544
+ mcp_removed = _remove_mcp_integrations()
545
+ for client in mcp_removed:
546
+ console.print(f" [green]\u2713[/green] Removed {client} MCP integration")
547
+ if not mcp_removed:
548
+ console.print(f" [white]-[/white] Skipped: no MCP integrations found")
549
+
550
+ console.print()
551
+ console.print("[green]All Tweek data has been removed.[/green]")