tweek 0.2.0__py3-none-any.whl → 0.3.0__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.
tweek/cli.py CHANGED
@@ -5,7 +5,7 @@ Tweek CLI - GAH! Security for your AI agents.
5
5
  Usage:
6
6
  tweek install [--scope global|project]
7
7
  tweek uninstall [--scope global|project]
8
- tweek status
8
+ tweek doctor
9
9
  tweek config [--skill NAME] [--preset paranoid|cautious|trusted]
10
10
  tweek vault store SKILL KEY VALUE
11
11
  tweek vault get SKILL KEY
@@ -152,41 +152,7 @@ def _has_tweek_hooks(settings: dict) -> bool:
152
152
  return False
153
153
 
154
154
 
155
- @main.command(
156
- epilog="""\b
157
- Examples:
158
- tweek install Install for current project
159
- tweek install --global Install globally (all projects)
160
- tweek install --interactive Walk through configuration prompts
161
- tweek install --preset paranoid Apply paranoid security preset
162
- tweek install --quick Zero-prompt install with defaults
163
- tweek install --with-sandbox Install sandbox tool if needed (Linux)
164
- tweek install --force-proxy Override existing proxy configurations
165
- """
166
- )
167
- @click.option("--global", "install_global", is_flag=True, default=False,
168
- help="Install globally to ~/.claude/ (protects all projects)")
169
- @click.option("--dev-test", is_flag=True, hidden=True,
170
- help="Install to test environment (for Tweek development only)")
171
- @click.option("--backup/--no-backup", default=True,
172
- help="Backup existing hooks before installation")
173
- @click.option("--skip-env-scan", is_flag=True,
174
- help="Skip scanning for .env files to migrate")
175
- @click.option("--interactive", "-i", is_flag=True,
176
- help="Interactively configure security settings")
177
- @click.option("--preset", type=click.Choice(["paranoid", "cautious", "trusted"]),
178
- help="Apply a security preset (skip interactive)")
179
- @click.option("--ai-defaults", is_flag=True,
180
- help="Let AI suggest default settings based on detected skills")
181
- @click.option("--with-sandbox", is_flag=True,
182
- help="Prompt to install sandbox tool if not available (Linux only)")
183
- @click.option("--force-proxy", is_flag=True,
184
- help="Force Tweek proxy to override existing proxy configurations (e.g., openclaw)")
185
- @click.option("--skip-proxy-check", is_flag=True,
186
- help="Skip checking for existing proxy configurations")
187
- @click.option("--quick", is_flag=True,
188
- help="Zero-prompt install with cautious defaults (skips env scan and proxy check)")
189
- def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: bool, interactive: bool, preset: str, ai_defaults: bool, with_sandbox: bool, force_proxy: bool, skip_proxy_check: bool, quick: bool):
155
+ def _install_claude_code_hooks(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: bool, interactive: bool, preset: str, ai_defaults: bool, with_sandbox: bool, force_proxy: bool, skip_proxy_check: bool, quick: bool):
190
156
  """Install Tweek hooks into Claude Code.
191
157
 
192
158
  By default, installs to the current project (./.claude/).
@@ -236,13 +202,13 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
236
202
  else:
237
203
  console.print()
238
204
  console.print("[yellow]⚠ Claude Code not detected on this system[/yellow]")
239
- console.print(" [dim]Tweek hooks require Claude Code to function.[/dim]")
240
- console.print(" [dim]https://docs.anthropic.com/en/docs/claude-code[/dim]")
205
+ console.print(" [white]Tweek hooks require Claude Code to function.[/white]")
206
+ console.print(" [white]https://docs.anthropic.com/en/docs/claude-code[/white]")
241
207
  console.print()
242
208
  if quick or not click.confirm("Continue installing hooks anyway?", default=False):
243
209
  if not quick:
244
210
  console.print()
245
- console.print("[dim]Run 'tweek install' later after installing Claude Code.[/dim]")
211
+ console.print("[white]Run 'tweek install' later after installing Claude Code.[/white]")
246
212
  return
247
213
  console.print()
248
214
 
@@ -250,25 +216,16 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
250
216
  # Step 2: Scope selection (always shown unless --global or --quick)
251
217
  # ─────────────────────────────────────────────────────────────
252
218
  if not install_global and not dev_test and not quick:
253
- # Smart default: if in a git repo, default to project; otherwise global
254
- in_git_repo = (Path.cwd() / ".git").exists()
255
- default_scope = 1 if in_git_repo else 2
256
-
257
219
  console.print()
258
220
  console.print("[bold]Installation Scope[/bold]")
259
221
  console.print()
260
- console.print(" [cyan]1.[/cyan] This project only (./.claude/)")
261
- console.print(" [dim]Protects only the current project[/dim]")
262
- console.print(" [cyan]2.[/cyan] All projects globally (~/.claude/)")
263
- console.print(" [dim]Protects every project on this machine[/dim]")
222
+ console.print(" [cyan]1.[/cyan] All projects globally (~/.claude/) [green](recommended)[/green]")
223
+ console.print(" [white]Protects every project on this machine[/white]")
224
+ console.print(" [cyan]2.[/cyan] This directory only (./.claude/)")
225
+ console.print(" [white]Protects only the current directory[/white]")
264
226
  console.print()
265
- if in_git_repo:
266
- console.print(f" [dim]Git repo detected — defaulting to project scope[/dim]")
267
- else:
268
- console.print(f" [dim]No git repo — defaulting to global scope[/dim]")
269
- console.print()
270
- scope_choice = click.prompt("Select", type=click.IntRange(1, 2), default=default_scope)
271
- if scope_choice == 2:
227
+ scope_choice = click.prompt("Select", type=click.IntRange(1, 2), default=1)
228
+ if scope_choice == 1:
272
229
  install_global = True
273
230
  console.print()
274
231
 
@@ -298,8 +255,8 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
298
255
  with open(project_settings) as f:
299
256
  project_config = json.load(f)
300
257
  if _has_tweek_hooks(project_config):
301
- console.print("[dim]Note: Tweek is also installed in this project.[/dim]")
302
- console.print("[dim]Project-level settings take precedence over global.[/dim]")
258
+ console.print("[white]Note: Tweek is also installed in this project.[/white]")
259
+ console.print("[white]Project-level settings take precedence over global.[/white]")
303
260
  console.print()
304
261
  else:
305
262
  # Installing per-project — check if global hooks exist
@@ -308,8 +265,8 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
308
265
  with open(global_settings) as f:
309
266
  global_config = json.load(f)
310
267
  if _has_tweek_hooks(global_config):
311
- console.print("[dim]Note: Tweek is also installed globally.[/dim]")
312
- console.print("[dim]Project-level settings will take precedence in this directory.[/dim]")
268
+ console.print("[white]Note: Tweek is also installed globally.[/white]")
269
+ console.print("[white]Project-level settings will take precedence in this directory.[/white]")
313
270
  console.print()
314
271
  except (json.JSONDecodeError, IOError):
315
272
  pass
@@ -336,12 +293,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
336
293
  if openclaw_status["gateway_active"]:
337
294
  console.print(f" Gateway running on port {openclaw_status['port']}")
338
295
  elif openclaw_status["running"]:
339
- console.print(f" [dim]Process running (gateway may start on port {openclaw_status['port']})[/dim]")
296
+ console.print(f" [white]Process running (gateway may start on port {openclaw_status['port']})[/white]")
340
297
  else:
341
- console.print(f" [dim]Installed but not currently running[/dim]")
298
+ console.print(f" [white]Installed but not currently running[/white]")
342
299
 
343
300
  if openclaw_status["config_path"]:
344
- console.print(f" [dim]Config: {openclaw_status['config_path']}[/dim]")
301
+ console.print(f" [white]Config: {openclaw_status['config_path']}[/white]")
345
302
 
346
303
  console.print()
347
304
 
@@ -353,11 +310,11 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
353
310
  console.print("[cyan]Tweek can protect OpenClaw tool calls. Choose a method:[/cyan]")
354
311
  console.print()
355
312
  console.print(" [cyan]1.[/cyan] Protect via [bold]tweek-security[/bold] ClawHub skill")
356
- console.print(" [dim]Screens tool calls through Tweek as a ClawHub skill[/dim]")
313
+ console.print(" [white]Screens tool calls through Tweek as a ClawHub skill[/white]")
357
314
  console.print(" [cyan]2.[/cyan] Protect via [bold]tweek protect openclaw[/bold]")
358
- console.print(" [dim]Wraps the OpenClaw gateway with Tweek's proxy[/dim]")
315
+ console.print(" [white]Wraps the OpenClaw gateway with Tweek's proxy[/white]")
359
316
  console.print(" [cyan]3.[/cyan] Skip for now")
360
- console.print(" [dim]You can set up OpenClaw protection later[/dim]")
317
+ console.print(" [white]You can set up OpenClaw protection later[/white]")
361
318
  console.print()
362
319
 
363
320
  choice = click.prompt(
@@ -375,12 +332,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
375
332
  proxy_override_enabled = True
376
333
  console.print()
377
334
  console.print("[green]✓[/green] OpenClaw proxy protection will be configured")
378
- console.print(f" [dim]Run 'tweek protect openclaw' after installation to complete setup[/dim]")
335
+ console.print(f" [white]Run 'tweek protect openclaw' after installation to complete setup[/white]")
379
336
  console.print()
380
337
  else:
381
338
  console.print()
382
- console.print("[dim]Skipped. Run 'tweek protect openclaw' or add the[/dim]")
383
- console.print("[dim]tweek-security skill later to protect OpenClaw.[/dim]")
339
+ console.print("[white]Skipped. Run 'tweek protect openclaw' or add the[/white]")
340
+ console.print("[white]tweek-security skill later to protect OpenClaw.[/white]")
384
341
  console.print()
385
342
 
386
343
  # Check for other proxy conflicts
@@ -397,7 +354,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
397
354
  # Proxy module not fully available, skip detection
398
355
  pass
399
356
  except Exception as e:
400
- console.print(f"[dim]Warning: Could not check for proxy conflicts: {e}[/dim]")
357
+ console.print(f"[white]Warning: Could not check for proxy conflicts: {e}[/white]")
401
358
 
402
359
  # ─────────────────────────────────────────────────────────────
403
360
  # Step 5: Install hooks into settings.json
@@ -411,7 +368,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
411
368
  if settings_file.exists():
412
369
  backup_path = settings_file.with_suffix(".json.tweek-backup")
413
370
  shutil.copy(settings_file, backup_path)
414
- console.print(f"[dim]Backed up existing settings to {backup_path}[/dim]")
371
+ console.print(f"[white]Backed up existing settings to {backup_path}[/white]")
415
372
 
416
373
  # Create target directory
417
374
  target.mkdir(parents=True, exist_ok=True)
@@ -486,7 +443,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
486
443
  shutil.rmtree(skill_target)
487
444
  shutil.copytree(skill_source, skill_target)
488
445
  console.print(f"[green]✓[/green] Tweek skill installed to: {skill_target}")
489
- console.print(f" [dim]Claude now understands Tweek warnings and commands[/dim]")
446
+ console.print(f" [white]Claude now understands Tweek warnings and commands[/white]")
490
447
 
491
448
  # Add whitelist entry for the skill directory in overrides
492
449
  try:
@@ -522,12 +479,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
522
479
  console.print(f"[green]✓[/green] Skill directory whitelisted in overrides")
523
480
 
524
481
  except ImportError:
525
- console.print(f"[dim]Note: PyYAML not available — skill whitelist not added to overrides[/dim]")
482
+ console.print(f"[white]Note: PyYAML not available — skill whitelist not added to overrides[/white]")
526
483
  except Exception as e:
527
- console.print(f"[dim]Warning: Could not update overrides whitelist: {e}[/dim]")
484
+ console.print(f"[white]Warning: Could not update overrides whitelist: {e}[/white]")
528
485
  else:
529
- console.print(f"[dim]Tweek skill source not found — skill not installed[/dim]")
530
- console.print(f" [dim]Skill can be installed manually from the tweek repository[/dim]")
486
+ console.print(f"[white]Tweek skill source not found — skill not installed[/white]")
487
+ console.print(f" [white]Skill can be installed manually from the tweek repository[/white]")
531
488
 
532
489
  # ─────────────────────────────────────────────────────────────
533
490
  # Step 7: Security Configuration
@@ -597,7 +554,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
597
554
 
598
555
  console.print(f"\n[green]✓[/green] Configured {len(unknown_skills)} skills")
599
556
  else:
600
- console.print("[dim]All detected skills already configured[/dim]")
557
+ console.print("[white]All detected skills already configured[/white]")
601
558
 
602
559
  # Apply cautious preset as base
603
560
  cfg.apply_preset("cautious")
@@ -631,7 +588,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
631
588
  else:
632
589
  # Custom: ask about key tools
633
590
  console.print("\n[bold]Configure key tools:[/bold]")
634
- console.print("[dim](safe/default/risky/dangerous)[/dim]\n")
591
+ console.print("[white](safe/default/risky/dangerous)[/white]\n")
635
592
 
636
593
  for tool in ["Bash", "WebFetch", "Edit"]:
637
594
  current = cfg.get_tool_tier(tool)
@@ -650,7 +607,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
650
607
  if not cfg.export_config("user"):
651
608
  cfg.apply_preset("cautious")
652
609
  console.print("\n[green]✓[/green] Applied default [bold]cautious[/bold] security preset")
653
- console.print("[dim]Run 'tweek config interactive' to customize[/dim]")
610
+ console.print("[white]Run 'tweek config interactive' to customize[/white]")
654
611
  install_summary["preset"] = "cautious"
655
612
  else:
656
613
  install_summary["preset"] = "existing"
@@ -672,7 +629,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
672
629
 
673
630
  if env_files:
674
631
  table = Table(title="Found .env Files")
675
- table.add_column("#", style="dim")
632
+ table.add_column("#", style="white")
676
633
  table.add_column("Path")
677
634
  table.add_column("Credentials", justify="right")
678
635
 
@@ -714,7 +671,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
714
671
  )
715
672
 
716
673
  # Show dry-run preview
717
- console.print(f" [dim]Preview - credentials to migrate:[/dim]")
674
+ console.print(f" [white]Preview - credentials to migrate:[/white]")
718
675
  for key in keys:
719
676
  console.print(f" • {key}")
720
677
 
@@ -727,9 +684,9 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
727
684
  except Exception as e:
728
685
  console.print(f" [red]✗[/red] Migration failed: {e}")
729
686
  else:
730
- console.print(f" [dim]Skipped[/dim]")
687
+ console.print(f" [white]Skipped[/white]")
731
688
  else:
732
- console.print("[dim]No .env files with credentials found[/dim]")
689
+ console.print("[white]No .env files with credentials found[/white]")
733
690
 
734
691
  # ─────────────────────────────────────────────────────────────
735
692
  # Step 10: Linux: Prompt for firejail installation
@@ -742,8 +699,8 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
742
699
  prompt_install_firejail(console)
743
700
  else:
744
701
  console.print("\n[yellow]Note:[/yellow] Sandbox (firejail) not installed.")
745
- console.print(f"[dim]Install with: {caps.sandbox_install_hint}[/dim]")
746
- console.print("[dim]Or run 'tweek install --with-sandbox' to install now[/dim]")
702
+ console.print(f"[white]Install with: {caps.sandbox_install_hint}[/white]")
703
+ console.print("[white]Or run 'tweek install --with-sandbox' to install now[/white]")
747
704
 
748
705
  # ─────────────────────────────────────────────────────────────
749
706
  # Step 11: Configure Tweek proxy if override was enabled
@@ -773,7 +730,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
773
730
  yaml.dump(tweek_config, f, default_flow_style=False)
774
731
 
775
732
  console.print("\n[green]✓[/green] Proxy override configured")
776
- console.print(f" [dim]Config saved to: {proxy_config_path}[/dim]")
733
+ console.print(f" [white]Config saved to: {proxy_config_path}[/white]")
777
734
  console.print(" [yellow]Run 'tweek proxy start' to begin intercepting API calls[/yellow]")
778
735
  install_summary["proxy"] = True
779
736
  except Exception as e:
@@ -804,14 +761,14 @@ def _check_python_version(console: Console, quick: bool) -> None:
804
761
  resolved_system = Path(system_python3).resolve()
805
762
 
806
763
  if resolved_install != resolved_system:
807
- console.print(f"[dim] Note: system python3 is {resolved_system}[/dim]")
808
- console.print(f"[dim] Hooks will use {resolved_install} (the Python running this install)[/dim]")
764
+ console.print(f"[white] Note: system python3 is {resolved_system}[/white]")
765
+ console.print(f"[white] Hooks will use {resolved_install} (the Python running this install)[/white]")
809
766
  except (OSError, ValueError):
810
767
  pass
811
768
  else:
812
769
  if not quick:
813
770
  console.print("[yellow] Note: python3 not found on PATH[/yellow]")
814
- console.print(f"[dim] Hooks will use {sys.executable} directly[/dim]")
771
+ console.print(f"[white] Hooks will use {sys.executable} directly[/white]")
815
772
 
816
773
 
817
774
  def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) -> dict:
@@ -861,9 +818,9 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
861
818
  console.print()
862
819
  console.print(" [cyan]1.[/cyan] Auto-detect (recommended)")
863
820
  if local_model_ready:
864
- console.print(f" [dim]Local model installed ({local_model_name}) — will use it first[/dim]")
821
+ console.print(f" [white]Local model installed ({local_model_name}) — will use it first[/white]")
865
822
  else:
866
- console.print(" [dim]Uses first available: Local model > Anthropic > OpenAI > Google[/dim]")
823
+ console.print(" [white]Uses first available: Local model > Anthropic > OpenAI > Google[/white]")
867
824
  console.print(" [cyan]2.[/cyan] Anthropic (Claude Haiku)")
868
825
  console.print(" [cyan]3.[/cyan] OpenAI (GPT-4o-mini)")
869
826
  console.print(" [cyan]4.[/cyan] Google (Gemini 2.0 Flash)")
@@ -871,8 +828,8 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
871
828
  console.print(" [cyan]6.[/cyan] Disable screening")
872
829
  if not local_model_ready:
873
830
  console.print()
874
- console.print(" [dim]Tip: Run 'tweek model download' to install the local model[/dim]")
875
- console.print(" [dim] (on-device, no API key, ~45MB download)[/dim]")
831
+ console.print(" [white]Tip: Run 'tweek model download' to install the local model[/white]")
832
+ console.print(" [white] (on-device, no API key, ~45MB download)[/white]")
876
833
  console.print()
877
834
 
878
835
  choice = click.prompt("Select", type=click.IntRange(1, 6), default=1)
@@ -892,8 +849,8 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
892
849
  # Custom endpoint configuration
893
850
  console.print()
894
851
  console.print("[bold]Custom Endpoint Configuration[/bold]")
895
- console.print("[dim]Most local servers (Ollama, LM Studio, vLLM) and cloud providers[/dim]")
896
- console.print("[dim](Together, Groq, Mistral) expose an OpenAI-compatible API.[/dim]")
852
+ console.print("[white]Most local servers (Ollama, LM Studio, vLLM) and cloud providers[/white]")
853
+ console.print("[white](Together, Groq, Mistral) expose an OpenAI-compatible API.[/white]")
897
854
  console.print()
898
855
 
899
856
  result["provider"] = "openai"
@@ -914,7 +871,7 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
914
871
  console.print()
915
872
  elif choice == 6:
916
873
  result["provider"] = "disabled"
917
- console.print("[dim]Screening disabled. Pattern matching and other layers remain active.[/dim]")
874
+ console.print("[white]Screening disabled. Pattern matching and other layers remain active.[/white]")
918
875
  # else: quick mode — leave as auto
919
876
 
920
877
  # Resolve display names for summary
@@ -980,7 +937,7 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
980
937
  else:
981
938
  console.print(f"[green]✓[/green] LLM provider configured: {result['provider_display']}")
982
939
  except Exception as e:
983
- console.print(f"[dim]Warning: Could not save LLM config: {e}[/dim]")
940
+ console.print(f"[white]Warning: Could not save LLM config: {e}[/white]")
984
941
  else:
985
942
  if result["provider_display"] and "disabled" not in (result["provider_display"] or ""):
986
943
  console.print(f"[green]✓[/green] LLM provider: {result['provider_display']} ({result.get('model_display', 'auto')})")
@@ -1047,7 +1004,7 @@ def _validate_llm_provider(llm_config: dict) -> None:
1047
1004
  expected_vars = [llm_config["api_key_env"]]
1048
1005
  elif llm_config.get("base_url"):
1049
1006
  # Local endpoints (Ollama etc.) don't need an API key
1050
- console.print(f" [dim]Checking endpoint: {llm_config['base_url']}[/dim]")
1007
+ console.print(f" [white]Checking endpoint: {llm_config['base_url']}[/white]")
1051
1008
  try:
1052
1009
  from tweek.security.llm_reviewer import resolve_provider
1053
1010
  test_provider = resolve_provider(
@@ -1060,10 +1017,10 @@ def _validate_llm_provider(llm_config: dict) -> None:
1060
1017
  console.print(f" [green]✓[/green] Endpoint reachable")
1061
1018
  else:
1062
1019
  console.print(f" [yellow]⚠[/yellow] Could not verify endpoint")
1063
- console.print(f" [dim]Tweek will try this endpoint at runtime[/dim]")
1020
+ console.print(f" [white]Tweek will try this endpoint at runtime[/white]")
1064
1021
  except Exception:
1065
1022
  console.print(f" [yellow]⚠[/yellow] Could not verify endpoint")
1066
- console.print(f" [dim]Tweek will try this endpoint at runtime[/dim]")
1023
+ console.print(f" [white]Tweek will try this endpoint at runtime[/white]")
1067
1024
  return
1068
1025
  else:
1069
1026
  expected_vars = env_var_map.get(provider, [])
@@ -1082,8 +1039,8 @@ def _validate_llm_provider(llm_config: dict) -> None:
1082
1039
  if not found_key:
1083
1040
  var_list = " or ".join(expected_vars)
1084
1041
  console.print(f" [yellow]⚠[/yellow] {var_list} not set in environment")
1085
- console.print(f" [dim]LLM review will be disabled until the key is available.[/dim]")
1086
- console.print(f" [dim]Set it in your shell profile or .env file, then restart Claude Code.[/dim]")
1042
+ console.print(f" [white]LLM review will be disabled until the key is available.[/white]")
1043
+ console.print(f" [white]Set it in your shell profile or .env file, then restart Claude Code.[/white]")
1087
1044
 
1088
1045
  # Offer fallback
1089
1046
  console.print()
@@ -1102,7 +1059,7 @@ def _validate_llm_provider(llm_config: dict) -> None:
1102
1059
  else:
1103
1060
  llm_config["provider_display"] = "disabled (no API key found)"
1104
1061
  llm_config["model_display"] = None
1105
- console.print(f" [dim]No API keys found — LLM review will be disabled[/dim]")
1062
+ console.print(f" [white]No API keys found — LLM review will be disabled[/white]")
1106
1063
 
1107
1064
 
1108
1065
  def _print_install_summary(
@@ -1142,7 +1099,7 @@ def _print_install_summary(
1142
1099
  console.print(f" [green]✓[/green] Hook Python: {hook_python}")
1143
1100
  else:
1144
1101
  console.print(f" [yellow]⚠[/yellow] Hook Python not found: {hook_python}")
1145
- console.print(f" [dim]Run 'tweek install' again if Python was reinstalled[/dim]")
1102
+ console.print(f" [white]Run 'tweek install' again if Python was reinstalled[/white]")
1146
1103
  except (IndexError, KeyError):
1147
1104
  pass
1148
1105
  elif has_pre:
@@ -1178,14 +1135,14 @@ def _print_install_summary(
1178
1135
  elif llm_display and "disabled" not in llm_display:
1179
1136
  console.print(f" [green]✓[/green] LLM reviewer: {llm_display}")
1180
1137
  else:
1181
- console.print(f" [dim]○[/dim] LLM reviewer: {llm_display}")
1138
+ console.print(f" [white]○[/white] LLM reviewer: {llm_display}")
1182
1139
 
1183
1140
  # Sandbox status
1184
1141
  caps = get_capabilities()
1185
1142
  if caps.sandbox_available:
1186
1143
  console.print(f" [green]✓[/green] Sandbox: {caps.sandbox_tool}")
1187
1144
  else:
1188
- console.print(f" [dim]○[/dim] Sandbox: not available ({caps.platform.value})")
1145
+ console.print(f" [white]○[/white] Sandbox: not available ({caps.platform.value})")
1189
1146
 
1190
1147
  # Summary table
1191
1148
  console.print()
@@ -1213,34 +1170,37 @@ def _print_install_summary(
1213
1170
 
1214
1171
  # Next steps
1215
1172
  console.print()
1216
- console.print("[dim]Next steps:[/dim]")
1217
- console.print("[dim] tweek status — Verify installation[/dim]")
1218
- console.print("[dim] tweek update — Get latest attack patterns[/dim]")
1219
- console.print("[dim] tweek config list — See security settings[/dim]")
1173
+ console.print("[white]Next steps:[/white]")
1174
+ console.print("[white] tweek doctor — Verify installation[/white]")
1175
+ console.print("[white] tweek update — Get latest attack patterns[/white]")
1176
+ console.print("[white] tweek config list — See security settings[/white]")
1220
1177
  if proxy_override_enabled:
1221
- console.print("[dim] tweek proxy start — Enable API interception[/dim]")
1178
+ console.print("[white] tweek proxy start — Enable API interception[/white]")
1222
1179
 
1223
1180
 
1224
1181
  @main.command(
1225
1182
  epilog="""\b
1226
1183
  Examples:
1227
- tweek uninstall Interactive — choose what to remove
1228
- tweek uninstall --global Remove global installation directly
1229
- tweek uninstall --everything Remove ALL Tweek data system-wide
1230
- tweek uninstall --confirm Skip confirmation prompt
1184
+ tweek unprotect Interactive — choose what to remove
1185
+ tweek unprotect claude-code Remove Claude Code hooks
1186
+ tweek unprotect claude-code --global Remove global Claude Code hooks
1187
+ tweek unprotect claude-desktop Remove from Claude Desktop
1188
+ tweek unprotect --all Remove ALL Tweek data system-wide
1231
1189
  """
1232
1190
  )
1233
- @click.option("--global", "uninstall_global", is_flag=True, default=False,
1234
- help="Uninstall from ~/.claude/ (global installation)")
1235
- @click.option("--everything", is_flag=True, default=False,
1191
+ @click.argument("tool", required=False, type=click.Choice(
1192
+ ["claude-code", "openclaw", "claude-desktop", "chatgpt", "gemini"]))
1193
+ @click.option("--all", "remove_all", is_flag=True, default=False,
1236
1194
  help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
1195
+ @click.option("--global", "unprotect_global", is_flag=True, default=False,
1196
+ help="Remove from ~/.claude/ (global installation)")
1237
1197
  @click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
1238
- def uninstall(uninstall_global: bool, everything: bool, confirm: bool):
1239
- """Remove Tweek hooks and data from Claude Code.
1198
+ def unprotect(tool: str, remove_all: bool, unprotect_global: bool, confirm: bool):
1199
+ """Remove Tweek protection from an AI tool.
1240
1200
 
1241
- When run without flags, presents an interactive menu to choose scope.
1242
- Use --global to remove from ~/.claude/ directly.
1243
- Use --everything to remove ALL Tweek data system-wide.
1201
+ When run without arguments, launches an interactive wizard
1202
+ that walks through each protected tool asking if you want
1203
+ to remove protection. Use --all to remove everything at once.
1244
1204
 
1245
1205
  This command can only be run from an interactive terminal.
1246
1206
  AI agents are blocked from running it.
@@ -1252,64 +1212,71 @@ def uninstall(uninstall_global: bool, everything: bool, confirm: bool):
1252
1212
  # This is Layer 2 of protection (Layer 1 is the PreToolUse hook)
1253
1213
  # ─────────────────────────────────────────────────────────────
1254
1214
  if not sys.stdin.isatty():
1255
- console.print("[red]ERROR: tweek uninstall must be run from an interactive terminal.[/red]")
1256
- console.print("[dim]This command cannot be run by AI agents or automated scripts.[/dim]")
1257
- console.print("[dim]Open a terminal and run the command directly.[/dim]")
1215
+ console.print("[red]ERROR: tweek unprotect must be run from an interactive terminal.[/red]")
1216
+ console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
1217
+ console.print("[white]Open a terminal and run the command directly.[/white]")
1258
1218
  raise SystemExit(1)
1259
1219
 
1220
+ # No tool and no --all: run interactive wizard
1221
+ if not tool and not remove_all:
1222
+ _run_unprotect_wizard()
1223
+ return
1224
+
1260
1225
  console.print(TWEEK_BANNER, style="cyan")
1261
1226
 
1262
1227
  tweek_dir = Path("~/.tweek").expanduser()
1263
1228
  global_target = Path("~/.claude").expanduser()
1264
1229
  project_target = Path.cwd() / ".claude"
1265
1230
 
1266
- if everything:
1231
+ if remove_all:
1267
1232
  _uninstall_everything(global_target, project_target, tweek_dir, confirm)
1268
- elif uninstall_global:
1269
- _uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
1270
- else:
1271
- # ── Interactive scope selection ──
1272
- # Detect what's installed
1273
- has_project = _has_tweek_at(project_target)
1274
- has_global = _has_tweek_at(global_target)
1275
- has_data = tweek_dir.exists() and any(tweek_dir.iterdir()) if tweek_dir.exists() else False
1276
-
1277
- if not has_project and not has_global and not has_data:
1278
- console.print("[yellow]No Tweek installation found.[/yellow]")
1279
- console.print(f" Checked project: {project_target}")
1280
- console.print(f" Checked global: {global_target}")
1281
- console.print(f" Checked data: {tweek_dir}")
1282
- _show_package_removal_hint()
1283
- return
1233
+ _show_package_removal_hint()
1234
+ return
1284
1235
 
1285
- console.print("[bold]What would you like to remove?[/bold]\n")
1236
+ if tool == "claude-code":
1237
+ if unprotect_global:
1238
+ _uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
1239
+ else:
1240
+ _uninstall_scope(project_target, tweek_dir, confirm, scope_label="project")
1241
+ _show_package_removal_hint()
1242
+ return
1286
1243
 
1287
- options = []
1288
- if has_project:
1289
- options.append(("project", f"This project only ({project_target})"))
1290
- if has_global:
1291
- options.append(("global", f"Global installation (~/.claude/)"))
1292
- if has_project or has_global or has_data:
1293
- options.append(("everything", "Everything — all hooks, skills, config, data, and MCP integrations"))
1244
+ if tool in ("claude-desktop", "chatgpt", "gemini"):
1245
+ try:
1246
+ from tweek.mcp.clients import get_client
1247
+ handler = get_client(tool)
1248
+ result = handler.uninstall()
1249
+ if result.get("success"):
1250
+ console.print(f"[green]{result.get('message', 'Uninstalled successfully')}[/green]")
1251
+ if result.get("backup"):
1252
+ console.print(f" Backup: {result['backup']}")
1253
+ if result.get("instructions"):
1254
+ console.print()
1255
+ for line in result["instructions"]:
1256
+ console.print(f" {line}")
1257
+ else:
1258
+ console.print(f"[red]{result.get('error', 'Uninstallation failed')}[/red]")
1259
+ except Exception as e:
1260
+ console.print(f"[red]Error: {e}[/red]")
1261
+ return
1294
1262
 
1295
- for i, (_, label) in enumerate(options, 1):
1296
- console.print(f" [bold]{i}.[/bold] {label}")
1263
+ if tool == "openclaw":
1264
+ console.print("[yellow]OpenClaw unprotect: removing Tweek proxy configuration...[/yellow]")
1265
+ # TODO: implement openclaw unprotect
1266
+ console.print("[white]Manual step: remove tweek plugin from openclaw.json[/white]")
1267
+ return
1297
1268
 
1298
- console.print()
1299
- choice = click.prompt("Select", type=click.IntRange(1, len(options)), default=len(options))
1269
+ _show_package_removal_hint()
1300
1270
 
1301
- selected = options[choice - 1][0]
1302
- console.print()
1303
1271
 
1304
- if selected == "project":
1305
- _uninstall_scope(project_target, tweek_dir, confirm, scope_label="project")
1306
- elif selected == "global":
1307
- _uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
1308
- elif selected == "everything":
1309
- _uninstall_everything(global_target, project_target, tweek_dir, confirm)
1272
+ @main.command()
1273
+ def status():
1274
+ """Show Tweek protection status dashboard.
1310
1275
 
1311
- # Always show how to remove the CLI binary itself
1312
- _show_package_removal_hint()
1276
+ Scans for all supported AI tools and displays which are
1277
+ detected, which are protected by Tweek, and configuration details.
1278
+ """
1279
+ _show_protection_status()
1313
1280
 
1314
1281
 
1315
1282
  # ─────────────────────────────────────────────────────────────
@@ -1369,9 +1336,9 @@ def _show_package_removal_hint():
1369
1336
  console.print("[bold yellow]The tweek CLI binary is still installed on your system.[/bold yellow]")
1370
1337
 
1371
1338
  if len(pkg_cmds) > 1:
1372
- console.print(f"[dim]Found {len(pkg_cmds)} installations:[/dim]")
1339
+ console.print(f"[white]Found {len(pkg_cmds)} installations:[/white]")
1373
1340
  for cmd in pkg_cmds:
1374
- console.print(f" [dim]• {cmd}[/dim]")
1341
+ console.print(f" [white]• {cmd}[/white]")
1375
1342
 
1376
1343
  console.print()
1377
1344
  label = " + ".join(f"[bold]{cmd}[/bold]" for cmd in pkg_cmds)
@@ -1392,10 +1359,10 @@ def _show_package_removal_hint():
1392
1359
  console.print(f"[green]✓[/green] Removed ({pkg_cmd})")
1393
1360
  else:
1394
1361
  console.print(f"[red]✗[/red] Failed: {result.stderr.strip()}")
1395
- console.print(f" [dim]Run manually: {pkg_cmd}[/dim]")
1362
+ console.print(f" [white]Run manually: {pkg_cmd}[/white]")
1396
1363
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
1397
1364
  console.print(f"[red]✗[/red] Could not run: {e}")
1398
- console.print(f" [dim]Run manually: {pkg_cmd}[/dim]")
1365
+ console.print(f" [white]Run manually: {pkg_cmd}[/white]")
1399
1366
 
1400
1367
 
1401
1368
  def _has_tweek_at(target: Path) -> bool:
@@ -1629,18 +1596,18 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
1629
1596
  console.print()
1630
1597
  console.print("[bold]The following will be removed:[/bold]")
1631
1598
  if has_hooks:
1632
- console.print(" [dim]•[/dim] PreToolUse and PostToolUse hooks from settings.json")
1599
+ console.print(" [white]•[/white] PreToolUse and PostToolUse hooks from settings.json")
1633
1600
  if has_skills:
1634
- console.print(" [dim]•[/dim] Tweek skill directory (skills/tweek/)")
1601
+ console.print(" [white]•[/white] Tweek skill directory (skills/tweek/)")
1635
1602
  if has_backup:
1636
- console.print(" [dim]•[/dim] Backup file (settings.json.tweek-backup)")
1637
- console.print(" [dim]•[/dim] Project whitelist entries from overrides")
1603
+ console.print(" [white]•[/white] Backup file (settings.json.tweek-backup)")
1604
+ console.print(" [white]•[/white] Project whitelist entries from overrides")
1638
1605
  console.print()
1639
1606
 
1640
1607
  if not confirm:
1641
1608
  console.print(f"[yellow]Remove Tweek from this {scope_label}?[/yellow] ", end="")
1642
1609
  if not click.confirm(""):
1643
- console.print("[dim]Cancelled[/dim]")
1610
+ console.print("[white]Cancelled[/white]")
1644
1611
  return
1645
1612
 
1646
1613
  console.print()
@@ -1656,27 +1623,27 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
1656
1623
  if _remove_skill_directory(target):
1657
1624
  console.print(f" [green]✓[/green] Removed Tweek skill directory (skills/tweek/)")
1658
1625
  else:
1659
- console.print(f" [dim]-[/dim] Skipped: Tweek skill directory not found")
1626
+ console.print(f" [white]-[/white] Skipped: Tweek skill directory not found")
1660
1627
 
1661
1628
  # 3. Remove backup file
1662
1629
  if _remove_backup_file(target):
1663
1630
  console.print(f" [green]✓[/green] Removed backup file (settings.json.tweek-backup)")
1664
1631
  else:
1665
- console.print(f" [dim]-[/dim] Skipped: no backup file found")
1632
+ console.print(f" [white]-[/white] Skipped: no backup file found")
1666
1633
 
1667
1634
  # 4. Remove whitelist entries
1668
1635
  wl_count = _remove_whitelist_entries(target, tweek_dir)
1669
1636
  if wl_count > 0:
1670
1637
  console.print(f" [green]✓[/green] Removed {wl_count} whitelist entry(s) from overrides")
1671
1638
  else:
1672
- console.print(f" [dim]-[/dim] Skipped: no whitelist entries found for this {scope_label}")
1639
+ console.print(f" [white]-[/white] Skipped: no whitelist entries found for this {scope_label}")
1673
1640
 
1674
1641
  console.print()
1675
1642
  console.print(f"[green]Uninstall complete.[/green] Tweek is no longer active for this {scope_label}.")
1676
1643
  if scope_label == "project":
1677
- console.print("[dim]Global installation (~/.claude/) was not affected.[/dim]")
1644
+ console.print("[white]Global installation (~/.claude/) was not affected.[/white]")
1678
1645
  else:
1679
- console.print("[dim]Project installations were not affected.[/dim]")
1646
+ console.print("[white]Project installations were not affected.[/white]")
1680
1647
 
1681
1648
  # Offer to remove data directory
1682
1649
  if tweek_dir.exists() and not confirm:
@@ -1689,7 +1656,7 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
1689
1656
 
1690
1657
  console.print()
1691
1658
  console.print("[yellow]Also remove Tweek data directory (~/.tweek/)?[/yellow]")
1692
- console.print("[dim]This contains config, patterns, security logs, and overrides.[/dim]")
1659
+ console.print("[white]This contains config, patterns, security logs, and overrides.[/white]")
1693
1660
  if other_has_tweek:
1694
1661
  console.print(f"[bold red]Warning:[/bold red] Tweek is still installed at {other_label} scope ({other_target}).")
1695
1662
  console.print(f" Removing ~/.tweek/ will affect that installation (no config, patterns, or logs).")
@@ -1704,9 +1671,9 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
1704
1671
  for item in data_removed:
1705
1672
  console.print(f" [green]✓[/green] Removed {item}")
1706
1673
  if not data_removed:
1707
- console.print(f" [dim]-[/dim] No data to remove")
1674
+ console.print(f" [white]-[/white] No data to remove")
1708
1675
  elif tweek_dir.exists():
1709
- console.print("[dim]Tweek data directory (~/.tweek/) was preserved.[/dim]")
1676
+ console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
1710
1677
 
1711
1678
 
1712
1679
  def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
@@ -1714,28 +1681,28 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
1714
1681
  import json
1715
1682
 
1716
1683
  console.print("[bold yellow]FULL REMOVAL[/bold yellow] — This will remove ALL Tweek data:\n")
1717
- console.print(" [dim]•[/dim] Hooks from current project (.claude/settings.json)")
1718
- console.print(" [dim]•[/dim] Hooks from global installation (~/.claude/settings.json)")
1719
- console.print(" [dim]•[/dim] Tweek skill directories (project + global)")
1720
- console.print(" [dim]•[/dim] All backup files")
1721
- console.print(" [dim]•[/dim] Tweek data directory (~/.tweek/)")
1684
+ console.print(" [white]•[/white] Hooks from current project (.claude/settings.json)")
1685
+ console.print(" [white]•[/white] Hooks from global installation (~/.claude/settings.json)")
1686
+ console.print(" [white]•[/white] Tweek skill directories (project + global)")
1687
+ console.print(" [white]•[/white] All backup files")
1688
+ console.print(" [white]•[/white] Tweek data directory (~/.tweek/)")
1722
1689
 
1723
1690
  # Show what exists in ~/.tweek/
1724
1691
  if tweek_dir.exists():
1725
1692
  for item in sorted(tweek_dir.iterdir()):
1726
1693
  if item.is_dir():
1727
- console.print(f" [dim]├── {item.name}/ [/dim]")
1694
+ console.print(f" [white]├── {item.name}/ [/white]")
1728
1695
  else:
1729
- console.print(f" [dim]├── {item.name}[/dim]")
1696
+ console.print(f" [white]├── {item.name}[/white]")
1730
1697
 
1731
- console.print(" [dim]•[/dim] MCP integrations (Claude Desktop, ChatGPT)")
1698
+ console.print(" [white]•[/white] MCP integrations (Claude Desktop, ChatGPT)")
1732
1699
  console.print()
1733
1700
 
1734
1701
  if not confirm:
1735
1702
  console.print("[bold red]Type 'yes' to confirm full removal[/bold red]: ", end="")
1736
1703
  response = input()
1737
1704
  if response.strip().lower() != "yes":
1738
- console.print("[dim]Cancelled[/dim]")
1705
+ console.print("[white]Cancelled[/white]")
1739
1706
  return
1740
1707
 
1741
1708
  console.print()
@@ -1746,17 +1713,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
1746
1713
  for hook_type in removed_hooks:
1747
1714
  console.print(f" [green]✓[/green] Removed {hook_type} hook from project settings.json")
1748
1715
  if not removed_hooks:
1749
- console.print(f" [dim]-[/dim] Skipped: no project hooks found")
1716
+ console.print(f" [white]-[/white] Skipped: no project hooks found")
1750
1717
 
1751
1718
  if _remove_skill_directory(project_target):
1752
1719
  console.print(f" [green]✓[/green] Removed Tweek skill from project")
1753
1720
  else:
1754
- console.print(f" [dim]-[/dim] Skipped: no project skill directory")
1721
+ console.print(f" [white]-[/white] Skipped: no project skill directory")
1755
1722
 
1756
1723
  if _remove_backup_file(project_target):
1757
1724
  console.print(f" [green]✓[/green] Removed project backup file")
1758
1725
  else:
1759
- console.print(f" [dim]-[/dim] Skipped: no project backup file")
1726
+ console.print(f" [white]-[/white] Skipped: no project backup file")
1760
1727
 
1761
1728
  console.print()
1762
1729
 
@@ -1766,17 +1733,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
1766
1733
  for hook_type in removed_hooks:
1767
1734
  console.print(f" [green]✓[/green] Removed {hook_type} hook from global settings.json")
1768
1735
  if not removed_hooks:
1769
- console.print(f" [dim]-[/dim] Skipped: no global hooks found")
1736
+ console.print(f" [white]-[/white] Skipped: no global hooks found")
1770
1737
 
1771
1738
  if _remove_skill_directory(global_target):
1772
1739
  console.print(f" [green]✓[/green] Removed Tweek skill from global installation")
1773
1740
  else:
1774
- console.print(f" [dim]-[/dim] Skipped: no global skill directory")
1741
+ console.print(f" [white]-[/white] Skipped: no global skill directory")
1775
1742
 
1776
1743
  if _remove_backup_file(global_target):
1777
1744
  console.print(f" [green]✓[/green] Removed global backup file")
1778
1745
  else:
1779
- console.print(f" [dim]-[/dim] Skipped: no global backup file")
1746
+ console.print(f" [white]-[/white] Skipped: no global backup file")
1780
1747
 
1781
1748
  console.print()
1782
1749
 
@@ -1786,7 +1753,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
1786
1753
  for item in data_removed:
1787
1754
  console.print(f" [green]✓[/green] Removed {item}")
1788
1755
  if not data_removed:
1789
- console.print(f" [dim]-[/dim] Skipped: no data directory found")
1756
+ console.print(f" [white]-[/white] Skipped: no data directory found")
1790
1757
 
1791
1758
  console.print()
1792
1759
 
@@ -1796,7 +1763,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
1796
1763
  for client in mcp_removed:
1797
1764
  console.print(f" [green]✓[/green] Removed {client} MCP integration")
1798
1765
  if not mcp_removed:
1799
- console.print(f" [dim]-[/dim] Skipped: no MCP integrations found")
1766
+ console.print(f" [white]-[/white] Skipped: no MCP integrations found")
1800
1767
 
1801
1768
  console.print()
1802
1769
  console.print("[green]All Tweek data has been removed.[/green]")
@@ -1875,8 +1842,8 @@ def trust(path: str, reason: str, list_trusted: bool):
1875
1842
  ]
1876
1843
 
1877
1844
  if not whitelist:
1878
- console.print("[dim]No trusted paths configured.[/dim]")
1879
- console.print("[dim]Use 'tweek trust' to trust the current project.[/dim]")
1845
+ console.print("[white]No trusted paths configured.[/white]")
1846
+ console.print("[white]Use 'tweek trust' to trust the current project.[/white]")
1880
1847
  return
1881
1848
 
1882
1849
  if trusted_entries:
@@ -1885,16 +1852,16 @@ def trust(path: str, reason: str, list_trusted: bool):
1885
1852
  entry_reason = entry.get("reason", "")
1886
1853
  console.print(f" [green]✓[/green] {entry['path']}")
1887
1854
  if entry_reason:
1888
- console.print(f" [dim]{entry_reason}[/dim]")
1855
+ console.print(f" [white]{entry_reason}[/white]")
1889
1856
 
1890
1857
  if tool_scoped:
1891
1858
  console.print("\n[bold]Tool-scoped whitelist entries:[/bold]\n")
1892
1859
  for entry in tool_scoped:
1893
1860
  tools = ", ".join(entry.get("tools", []))
1894
1861
  entry_reason = entry.get("reason", "")
1895
- console.print(f" [cyan]○[/cyan] {entry['path']} [dim]({tools})[/dim]")
1862
+ console.print(f" [cyan]○[/cyan] {entry['path']} [white]({tools})[/white]")
1896
1863
  if entry_reason:
1897
- console.print(f" [dim]{entry_reason}[/dim]")
1864
+ console.print(f" [white]{entry_reason}[/white]")
1898
1865
 
1899
1866
  if other_entries:
1900
1867
  console.print("\n[bold]Other whitelist entries:[/bold]\n")
@@ -1905,9 +1872,9 @@ def trust(path: str, reason: str, list_trusted: bool):
1905
1872
  console.print(f" [cyan]○[/cyan] Command: {entry['command_prefix']}")
1906
1873
  entry_reason = entry.get("reason", "")
1907
1874
  if entry_reason:
1908
- console.print(f" [dim]{entry_reason}[/dim]")
1875
+ console.print(f" [white]{entry_reason}[/white]")
1909
1876
 
1910
- console.print(f"\n[dim]Config: {overrides_path}[/dim]")
1877
+ console.print(f"\n[white]Config: {overrides_path}[/white]")
1911
1878
  return
1912
1879
 
1913
1880
  # Resolve path to absolute
@@ -1924,7 +1891,7 @@ def trust(path: str, reason: str, list_trusted: bool):
1924
1891
 
1925
1892
  if already_trusted:
1926
1893
  console.print(f"[green]✓[/green] Already trusted: {resolved}")
1927
- console.print("[dim]Use 'tweek untrust' to remove.[/dim]")
1894
+ console.print("[white]Use 'tweek untrust' to remove.[/white]")
1928
1895
  return
1929
1896
 
1930
1897
  # Add whitelist entry (no tools restriction = all tools exempt)
@@ -1942,8 +1909,8 @@ def trust(path: str, reason: str, list_trusted: bool):
1942
1909
  return
1943
1910
 
1944
1911
  console.print(f"[green]✓[/green] Trusted: {resolved}")
1945
- console.print(f" [dim]All screening is now skipped for files in this directory.[/dim]")
1946
- console.print(f" [dim]To resume screening: tweek untrust {path}[/dim]")
1912
+ console.print(f" [white]All screening is now skipped for files in this directory.[/white]")
1913
+ console.print(f" [white]To resume screening: tweek untrust {path}[/white]")
1947
1914
 
1948
1915
 
1949
1916
  @main.command(
@@ -1991,7 +1958,7 @@ def untrust(path: str):
1991
1958
 
1992
1959
  if len(whitelist) == original_len:
1993
1960
  console.print(f"[yellow]This path is not currently trusted:[/yellow] {resolved}")
1994
- console.print("[dim]Use 'tweek trust --list' to see all trusted paths.[/dim]")
1961
+ console.print("[white]Use 'tweek trust --list' to see all trusted paths.[/white]")
1995
1962
  return
1996
1963
 
1997
1964
  overrides["whitelist"] = whitelist
@@ -2007,7 +1974,7 @@ def untrust(path: str):
2007
1974
  return
2008
1975
 
2009
1976
  console.print(f"[green]✓[/green] Removed trust: {resolved}")
2010
- console.print(f" [dim]Tweek will now screen tool calls for files in this directory.[/dim]")
1977
+ console.print(f" [white]Tweek will now screen tool calls for files in this directory.[/white]")
2011
1978
 
2012
1979
 
2013
1980
  @main.command(
@@ -2038,7 +2005,7 @@ def update(check: bool):
2038
2005
  # First time: clone the repo
2039
2006
  if check:
2040
2007
  console.print("[yellow]Patterns not installed.[/yellow]")
2041
- console.print(f"[dim]Run 'tweek update' to install from {patterns_repo}[/dim]")
2008
+ console.print(f"[white]Run 'tweek update' to install from {patterns_repo}[/white]")
2042
2009
  return
2043
2010
 
2044
2011
  console.print(f"[cyan]Installing patterns from {patterns_repo}...[/cyan]")
@@ -2060,15 +2027,15 @@ def update(check: bool):
2060
2027
  data = yaml.safe_load(f)
2061
2028
  count = data.get("pattern_count", len(data.get("patterns", [])))
2062
2029
  free_max = data.get("free_tier_max", 23)
2063
- console.print(f"[dim]Installed {count} patterns ({free_max} free, {count - free_max} pro)[/dim]")
2030
+ console.print(f"[white]Installed {count} patterns ({free_max} free, {count - free_max} pro)[/white]")
2064
2031
 
2065
2032
  except subprocess.CalledProcessError as e:
2066
2033
  console.print(f"[red]✗[/red] Failed to clone patterns: {e.stderr}")
2067
2034
  return
2068
2035
  except FileNotFoundError:
2069
2036
  console.print("[red]\u2717[/red] git not found.")
2070
- console.print(" [dim]Hint: Install git from https://git-scm.com/downloads[/dim]")
2071
- console.print(" [dim]On macOS: xcode-select --install[/dim]")
2037
+ console.print(" [white]Hint: Install git from https://git-scm.com/downloads[/white]")
2038
+ console.print(" [white]On macOS: xcode-select --install[/white]")
2072
2039
  return
2073
2040
 
2074
2041
  else:
@@ -2089,7 +2056,7 @@ def update(check: bool):
2089
2056
  )
2090
2057
  if "behind" in result2.stdout:
2091
2058
  console.print("[yellow]Updates available.[/yellow]")
2092
- console.print("[dim]Run 'tweek update' to install[/dim]")
2059
+ console.print("[white]Run 'tweek update' to install[/white]")
2093
2060
  else:
2094
2061
  console.print("[green]✓[/green] Patterns are up to date")
2095
2062
  except Exception as e:
@@ -2113,11 +2080,11 @@ def update(check: bool):
2113
2080
 
2114
2081
  # Show what changed
2115
2082
  if result.stdout.strip():
2116
- console.print(f"[dim]{result.stdout.strip()}[/dim]")
2083
+ console.print(f"[white]{result.stdout.strip()}[/white]")
2117
2084
 
2118
2085
  except subprocess.CalledProcessError as e:
2119
2086
  console.print(f"[red]✗[/red] Failed to update patterns: {e.stderr}")
2120
- console.print("[dim]Try: rm -rf ~/.tweek/patterns && tweek update[/dim]")
2087
+ console.print("[white]Try: rm -rf ~/.tweek/patterns && tweek update[/white]")
2121
2088
  return
2122
2089
 
2123
2090
  # Show current version info
@@ -2135,7 +2102,7 @@ def update(check: bool):
2135
2102
  console.print(f"[cyan]Total patterns:[/cyan] {count} (all included free)")
2136
2103
 
2137
2104
  console.print(f"[cyan]All features:[/cyan] LLM review, session analysis, rate limiting, sandbox (open source)")
2138
- console.print(f"[dim]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/dim]")
2105
+ console.print(f"[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
2139
2106
 
2140
2107
  except Exception:
2141
2108
  pass
@@ -2168,21 +2135,113 @@ def doctor(verbose: bool, json_out: bool):
2168
2135
  print_doctor_results(checks)
2169
2136
 
2170
2137
 
2171
- # `tweek status` — alias for `tweek doctor`
2172
- @main.command("status")
2173
- @click.option("--verbose", "-v", is_flag=True, help="Show detailed check information")
2174
- @click.option("--json-output", "--json", "json_out", is_flag=True, help="Output results as JSON")
2175
- def status(verbose: bool, json_out: bool):
2176
- """Show Tweek protection status (alias for 'tweek doctor')."""
2177
- from tweek.diagnostics import run_health_checks
2178
- from tweek.cli_helpers import print_doctor_results, print_doctor_json
2179
2138
 
2180
- checks = run_health_checks(verbose=verbose)
2139
+ @main.command("upgrade")
2140
+ def upgrade():
2141
+ """Upgrade Tweek to the latest version from PyPI.
2181
2142
 
2182
- if json_out:
2183
- print_doctor_json(checks)
2184
- else:
2185
- print_doctor_results(checks)
2143
+ Detects how Tweek was installed (uv, pipx, or pip) and runs
2144
+ the appropriate upgrade command.
2145
+ """
2146
+ import subprocess
2147
+
2148
+ console.print("[cyan]Checking for updates...[/cyan]")
2149
+ console.print()
2150
+
2151
+ current_version = None
2152
+ try:
2153
+ from tweek import __version__
2154
+ current_version = __version__
2155
+ console.print(f" Current version: [bold]{current_version}[/bold]")
2156
+ except ImportError:
2157
+ pass
2158
+
2159
+ # Detect install method and upgrade
2160
+ upgraded = False
2161
+
2162
+ # Try uv first
2163
+ try:
2164
+ result = subprocess.run(
2165
+ ["uv", "tool", "list"], capture_output=True, text=True, timeout=10
2166
+ )
2167
+ if result.returncode == 0 and "tweek" in result.stdout:
2168
+ console.print(" Install method: [cyan]uv[/cyan]")
2169
+ console.print()
2170
+ console.print("[white]Upgrading via uv...[/white]")
2171
+ proc = subprocess.run(
2172
+ ["uv", "tool", "upgrade", "tweek"],
2173
+ capture_output=False, timeout=120
2174
+ )
2175
+ if proc.returncode == 0:
2176
+ upgraded = True
2177
+ else:
2178
+ console.print("[yellow]uv upgrade failed, trying reinstall...[/yellow]")
2179
+ subprocess.run(
2180
+ ["uv", "tool", "install", "--force", "tweek"],
2181
+ capture_output=False, timeout=120
2182
+ )
2183
+ upgraded = True
2184
+ except (FileNotFoundError, subprocess.TimeoutExpired):
2185
+ pass
2186
+
2187
+ # Try pipx
2188
+ if not upgraded:
2189
+ try:
2190
+ result = subprocess.run(
2191
+ ["pipx", "list"], capture_output=True, text=True, timeout=10
2192
+ )
2193
+ if result.returncode == 0 and "tweek" in result.stdout:
2194
+ console.print(" Install method: [cyan]pipx[/cyan]")
2195
+ console.print()
2196
+ console.print("[white]Upgrading via pipx...[/white]")
2197
+ proc = subprocess.run(
2198
+ ["pipx", "upgrade", "tweek"],
2199
+ capture_output=False, timeout=120
2200
+ )
2201
+ upgraded = proc.returncode == 0
2202
+ except (FileNotFoundError, subprocess.TimeoutExpired):
2203
+ pass
2204
+
2205
+ # Try pip
2206
+ if not upgraded:
2207
+ try:
2208
+ result = subprocess.run(
2209
+ [sys.executable, "-m", "pip", "show", "tweek"],
2210
+ capture_output=True, text=True, timeout=10
2211
+ )
2212
+ if result.returncode == 0:
2213
+ console.print(" Install method: [cyan]pip[/cyan]")
2214
+ console.print()
2215
+ console.print("[white]Upgrading via pip...[/white]")
2216
+ proc = subprocess.run(
2217
+ [sys.executable, "-m", "pip", "install", "--upgrade", "tweek"],
2218
+ capture_output=False, timeout=120
2219
+ )
2220
+ upgraded = proc.returncode == 0
2221
+ except (FileNotFoundError, subprocess.TimeoutExpired):
2222
+ pass
2223
+
2224
+ if not upgraded:
2225
+ console.print("[red]Could not determine install method.[/red]")
2226
+ console.print("[white]Try manually:[/white]")
2227
+ console.print(" uv tool upgrade tweek")
2228
+ console.print(" pipx upgrade tweek")
2229
+ console.print(" pip install --upgrade tweek")
2230
+ return
2231
+
2232
+ # Show new version
2233
+ console.print()
2234
+ try:
2235
+ result = subprocess.run(
2236
+ ["tweek", "--version"], capture_output=True, text=True, timeout=10
2237
+ )
2238
+ if result.returncode == 0:
2239
+ new_version = result.stdout.strip()
2240
+ console.print(f"[green]✓[/green] Updated to {new_version}")
2241
+ else:
2242
+ console.print("[green]✓[/green] Update complete")
2243
+ except (FileNotFoundError, subprocess.TimeoutExpired):
2244
+ console.print("[green]✓[/green] Update complete")
2186
2245
 
2187
2246
 
2188
2247
  @main.command(
@@ -2241,8 +2300,8 @@ def audit(path, translate, llm_review, json_out):
2241
2300
  skills = scan_installed_skills()
2242
2301
 
2243
2302
  if not skills:
2244
- console.print("[dim]No installed skills found.[/dim]")
2245
- console.print("[dim]Specify a file path to audit: tweek audit <path>[/dim]")
2303
+ console.print("[white]No installed skills found.[/white]")
2304
+ console.print("[white]Specify a file path to audit: tweek audit <path>[/white]")
2246
2305
  return
2247
2306
 
2248
2307
  console.print(f"Found {len(skills)} skill(s)")
@@ -2291,7 +2350,7 @@ def _print_audit_result(result):
2291
2350
  risk_icons = {"safe": "[green]SAFE[/green]", "suspicious": "[yellow]SUSPICIOUS[/yellow]", "dangerous": "[red]DANGEROUS[/red]"}
2292
2351
 
2293
2352
  console.print(f" [bold]{result.skill_name}[/bold] — {risk_icons.get(result.risk_level, result.risk_level)}")
2294
- console.print(f" [dim]{result.skill_path}[/dim]")
2353
+ console.print(f" [white]{result.skill_path}[/white]")
2295
2354
 
2296
2355
  if result.error:
2297
2356
  console.print(f" [red]Error: {result.error}[/red]")
@@ -2306,12 +2365,12 @@ def _print_audit_result(result):
2306
2365
 
2307
2366
  if result.findings:
2308
2367
  table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
2309
- table.add_column("Severity", style="dim")
2368
+ table.add_column("Severity", style="white")
2310
2369
  table.add_column("Pattern")
2311
2370
  table.add_column("Description")
2312
- table.add_column("Match", style="dim")
2371
+ table.add_column("Match", style="white")
2313
2372
 
2314
- severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "dim"}
2373
+ severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "white"}
2315
2374
 
2316
2375
  for finding in result.findings:
2317
2376
  table.add_row(
@@ -2410,7 +2469,7 @@ def quickstart():
2410
2469
  # Step 2: Security preset
2411
2470
  console.print("[bold cyan]Step 2/4: Security Preset[/bold cyan]")
2412
2471
  console.print(" [cyan]1.[/cyan] paranoid \u2014 Block everything suspicious, prompt on risky")
2413
- console.print(" [cyan]2.[/cyan] cautious \u2014 Block dangerous, prompt on risky [dim](recommended)[/dim]")
2472
+ console.print(" [cyan]2.[/cyan] cautious \u2014 Block dangerous, prompt on risky [white](recommended)[/white]")
2414
2473
  console.print(" [cyan]3.[/cyan] trusted \u2014 Allow most operations, block only dangerous")
2415
2474
  console.print()
2416
2475
 
@@ -2449,17 +2508,16 @@ def quickstart():
2449
2508
  if setup_mcp:
2450
2509
  try:
2451
2510
  import mcp # noqa: F401
2452
- console.print("[dim]MCP package available. Configure upstream servers in ~/.tweek/config.yaml[/dim]")
2453
- console.print("[dim]Then run: tweek mcp proxy[/dim]")
2511
+ console.print("[white]MCP package available. Configure upstream servers in ~/.tweek/config.yaml[/white]")
2512
+ console.print("[white]Then run: tweek mcp proxy[/white]")
2454
2513
  except ImportError:
2455
2514
  print_warning("MCP package not installed. Install with: pip install tweek[mcp]")
2456
2515
  else:
2457
- console.print("[dim]Skipped.[/dim]")
2516
+ console.print("[white]Skipped.[/white]")
2458
2517
 
2459
2518
  console.print()
2460
2519
  console.print("[bold green]Setup complete![/bold green]")
2461
2520
  console.print(" Run [cyan]tweek doctor[/cyan] to verify your installation")
2462
- console.print(" Run [cyan]tweek status[/cyan] to see protection status")
2463
2521
 
2464
2522
 
2465
2523
  def _quickstart_install_hooks(scope: str) -> None:
@@ -2527,21 +2585,31 @@ def _quickstart_install_hooks(scope: str) -> None:
2527
2585
  # =============================================================================
2528
2586
 
2529
2587
  @main.group(
2588
+ invoke_without_command=True,
2530
2589
  epilog="""\b
2531
2590
  Examples:
2532
- tweek protect openclaw One-command OpenClaw protection
2533
- tweek protect openclaw --paranoid Use paranoid security preset
2534
- tweek protect openclaw --port 9999 Override gateway port
2535
- tweek protect claude Install Claude Code hooks (alias for tweek install)
2591
+ tweek protect Interactive wizard detect & protect all tools
2592
+ tweek protect --status Show protection status for all tools
2593
+ tweek protect claude-code Install Claude Code hooks
2594
+ tweek protect openclaw One-command OpenClaw protection
2595
+ tweek protect claude-desktop Configure Claude Desktop integration
2596
+ tweek protect chatgpt Set up ChatGPT Desktop integration
2597
+ tweek protect gemini Configure Gemini CLI integration
2536
2598
  """
2537
2599
  )
2538
- def protect():
2539
- """Set up Tweek protection for a specific AI agent.
2600
+ @click.option("--status", is_flag=True, help="Show protection status for all tools")
2601
+ @click.pass_context
2602
+ def protect(ctx, status):
2603
+ """Set up Tweek protection for AI tools.
2540
2604
 
2541
- One-command setup that auto-detects, configures, and starts
2542
- screening all tool calls for your AI assistant.
2605
+ When run without a subcommand, launches an interactive wizard
2606
+ that auto-detects installed AI tools and offers to protect them.
2543
2607
  """
2544
- pass
2608
+ if status:
2609
+ _show_protection_status()
2610
+ return
2611
+ if ctx.invoked_subcommand is None:
2612
+ _run_protect_wizard()
2545
2613
 
2546
2614
 
2547
2615
  @protect.command(
@@ -2589,11 +2657,11 @@ def protect_openclaw(port, paranoid, preset):
2589
2657
  console.print()
2590
2658
  console.print("[red]OpenClaw not detected on this system.[/red]")
2591
2659
  console.print()
2592
- console.print("[dim]Install OpenClaw first:[/dim]")
2660
+ console.print("[white]Install OpenClaw first:[/white]")
2593
2661
  console.print(" npm install -g openclaw")
2594
2662
  console.print()
2595
- console.print("[dim]Or if OpenClaw is installed in a non-standard location,[/dim]")
2596
- console.print("[dim]specify the gateway port manually:[/dim]")
2663
+ console.print("[white]Or if OpenClaw is installed in a non-standard location,[/white]")
2664
+ console.print("[white]specify the gateway port manually:[/white]")
2597
2665
  console.print(" tweek protect openclaw --port 18789")
2598
2666
  return
2599
2667
 
@@ -2610,7 +2678,7 @@ def protect_openclaw(port, paranoid, preset):
2610
2678
  elif openclaw["process_running"]:
2611
2679
  console.print(" [yellow](process running, gateway inactive)[/yellow]")
2612
2680
  else:
2613
- console.print(" [dim](not running)[/dim]")
2681
+ console.print(" [white](not running)[/white]")
2614
2682
 
2615
2683
  if openclaw["config_path"]:
2616
2684
  console.print(f" Config: {openclaw['config_path']}")
@@ -2634,7 +2702,7 @@ def protect_openclaw(port, paranoid, preset):
2634
2702
  if anthropic_key:
2635
2703
  console.print(" LLM Review: [green]active[/green] (ANTHROPIC_API_KEY found)")
2636
2704
  else:
2637
- console.print(" LLM Review: [dim]available (set ANTHROPIC_API_KEY for semantic analysis)[/dim]")
2705
+ console.print(" LLM Review: [white]available (set ANTHROPIC_API_KEY for semantic analysis)[/white]")
2638
2706
 
2639
2707
  # Show warnings
2640
2708
  for warning in result.warnings:
@@ -2644,53 +2712,366 @@ def protect_openclaw(port, paranoid, preset):
2644
2712
 
2645
2713
  if not openclaw["gateway_active"]:
2646
2714
  console.print("[yellow]Note: OpenClaw gateway is not currently running.[/yellow]")
2647
- console.print("[dim]Protection will activate when OpenClaw starts.[/dim]")
2715
+ console.print("[white]Protection will activate when OpenClaw starts.[/white]")
2648
2716
  console.print()
2649
2717
 
2650
2718
  console.print("[green]Protection configured.[/green] Screening all OpenClaw tool calls.")
2651
2719
  console.print()
2652
- console.print("[dim]Verify: tweek doctor[/dim]")
2653
- console.print("[dim]Logs: tweek logs show[/dim]")
2654
- console.print("[dim]Stop: tweek proxy stop[/dim]")
2720
+ console.print("[white]Verify: tweek doctor[/white]")
2721
+ console.print("[white]Logs: tweek logs show[/white]")
2722
+ console.print("[white]Stop: tweek proxy stop[/white]")
2655
2723
 
2656
2724
 
2657
2725
  @protect.command(
2658
- "claude",
2726
+ "claude-code",
2659
2727
  epilog="""\b
2660
2728
  Examples:
2661
- tweek protect claude Install Claude Code hooks (current project)
2662
- tweek protect claude --global Install globally (all projects)
2729
+ tweek protect claude-code Install for current project
2730
+ tweek protect claude-code --global Install globally (all projects)
2731
+ tweek protect claude-code --quick Zero-prompt install with defaults
2732
+ tweek protect claude-code --preset paranoid Apply paranoid security preset
2663
2733
  """
2664
2734
  )
2665
2735
  @click.option("--global", "install_global", is_flag=True, default=False,
2666
2736
  help="Install globally to ~/.claude/ (protects all projects)")
2737
+ @click.option("--dev-test", is_flag=True, hidden=True,
2738
+ help="Install to test environment (for Tweek development only)")
2739
+ @click.option("--backup/--no-backup", default=True,
2740
+ help="Backup existing hooks before installation")
2741
+ @click.option("--skip-env-scan", is_flag=True,
2742
+ help="Skip scanning for .env files to migrate")
2743
+ @click.option("--interactive", "-i", is_flag=True,
2744
+ help="Interactively configure security settings")
2667
2745
  @click.option("--preset", type=click.Choice(["paranoid", "cautious", "trusted"]),
2668
- default=None, help="Security preset to apply")
2669
- @click.pass_context
2670
- def protect_claude(ctx, install_global, preset):
2746
+ help="Apply a security preset (skip interactive)")
2747
+ @click.option("--ai-defaults", is_flag=True,
2748
+ help="Let AI suggest default settings based on detected skills")
2749
+ @click.option("--with-sandbox", is_flag=True,
2750
+ help="Prompt to install sandbox tool if not available (Linux only)")
2751
+ @click.option("--force-proxy", is_flag=True,
2752
+ help="Force Tweek proxy to override existing proxy configurations (e.g., openclaw)")
2753
+ @click.option("--skip-proxy-check", is_flag=True,
2754
+ help="Skip checking for existing proxy configurations")
2755
+ @click.option("--quick", is_flag=True,
2756
+ help="Zero-prompt install with cautious defaults (skips env scan and proxy check)")
2757
+ def protect_claude_code(install_global, dev_test, backup, skip_env_scan, interactive, preset, ai_defaults, with_sandbox, force_proxy, skip_proxy_check, quick):
2671
2758
  """Install Tweek hooks for Claude Code.
2672
2759
 
2673
- This is equivalent to 'tweek install' -- installs PreToolUse
2674
- and PostToolUse hooks to screen all Claude Code tool calls.
2760
+ Installs PreToolUse and PostToolUse hooks to screen all
2761
+ Claude Code tool calls through Tweek's security pipeline.
2675
2762
  """
2676
- # Delegate to the main install command
2677
- # (use main.commands lookup to avoid name shadowing by mcp install)
2678
- install_cmd = main.commands['install']
2679
- ctx.invoke(
2680
- install_cmd,
2763
+ _install_claude_code_hooks(
2681
2764
  install_global=install_global,
2682
- dev_test=False,
2683
- backup=True,
2684
- skip_env_scan=False,
2685
- interactive=False,
2765
+ dev_test=dev_test,
2766
+ backup=backup,
2767
+ skip_env_scan=skip_env_scan,
2768
+ interactive=interactive,
2686
2769
  preset=preset,
2687
- ai_defaults=False,
2688
- with_sandbox=False,
2689
- force_proxy=False,
2690
- skip_proxy_check=False,
2770
+ ai_defaults=ai_defaults,
2771
+ with_sandbox=with_sandbox,
2772
+ force_proxy=force_proxy,
2773
+ skip_proxy_check=skip_proxy_check,
2774
+ quick=quick,
2691
2775
  )
2692
2776
 
2693
2777
 
2778
+ @protect.command("claude-desktop")
2779
+ def protect_claude_desktop():
2780
+ """Configure Tweek as MCP server for Claude Desktop."""
2781
+ _protect_mcp_client("claude-desktop")
2782
+
2783
+
2784
+ @protect.command("chatgpt")
2785
+ def protect_chatgpt():
2786
+ """Configure Tweek as MCP server for ChatGPT Desktop."""
2787
+ _protect_mcp_client("chatgpt")
2788
+
2789
+
2790
+ @protect.command("gemini")
2791
+ def protect_gemini():
2792
+ """Configure Tweek as MCP server for Gemini CLI."""
2793
+ _protect_mcp_client("gemini")
2794
+
2795
+
2796
+ def _protect_mcp_client(client_name: str):
2797
+ """Shared logic for MCP client protection commands."""
2798
+ try:
2799
+ from tweek.mcp.clients import get_client
2800
+
2801
+ handler = get_client(client_name)
2802
+ result = handler.install()
2803
+
2804
+ if result.get("success"):
2805
+ console.print(f"[green]{result.get('message', 'Installed successfully')}[/green]")
2806
+ if result.get("config_path"):
2807
+ console.print(f" Config: {result['config_path']}")
2808
+ if result.get("backup"):
2809
+ console.print(f" Backup: {result['backup']}")
2810
+ if result.get("instructions"):
2811
+ console.print()
2812
+ for line in result["instructions"]:
2813
+ console.print(f" {line}")
2814
+ else:
2815
+ console.print(f"[red]{result.get('error', 'Installation failed')}[/red]")
2816
+ except Exception as e:
2817
+ console.print(f"[red]Error: {e}[/red]")
2818
+
2819
+
2820
+ # =============================================================================
2821
+ # PROTECT WIZARD & STATUS HELPERS
2822
+ # =============================================================================
2823
+
2824
+
2825
+ def _detect_all_tools():
2826
+ """Detect all supported AI tools and their protection status.
2827
+
2828
+ Returns list of (tool_id, label, installed, protected, detail) tuples.
2829
+ """
2830
+ import shutil
2831
+ import json
2832
+
2833
+ tools = []
2834
+
2835
+ # Claude Code
2836
+ claude_installed = shutil.which("claude") is not None
2837
+ claude_protected = _has_tweek_at(Path("~/.claude").expanduser()) if claude_installed else False
2838
+ tools.append((
2839
+ "claude-code", "Claude Code", claude_installed, claude_protected,
2840
+ "Hooks in ~/.claude/settings.json" if claude_protected else "",
2841
+ ))
2842
+
2843
+ # OpenClaw
2844
+ oc_installed = False
2845
+ oc_protected = False
2846
+ oc_detail = ""
2847
+ try:
2848
+ from tweek.integrations.openclaw import detect_openclaw_installation
2849
+ openclaw = detect_openclaw_installation()
2850
+ oc_installed = openclaw.get("installed", False)
2851
+ if oc_installed:
2852
+ oc_protected = openclaw.get("tweek_configured", False)
2853
+ oc_detail = f"Gateway port {openclaw.get('gateway_port', '?')}"
2854
+ except Exception:
2855
+ pass
2856
+ tools.append(("openclaw", "OpenClaw", oc_installed, oc_protected, oc_detail))
2857
+
2858
+ # MCP clients
2859
+ mcp_configs = [
2860
+ ("claude-desktop", "Claude Desktop",
2861
+ Path("~/Library/Application Support/Claude/claude_desktop_config.json").expanduser()),
2862
+ ("chatgpt", "ChatGPT Desktop",
2863
+ Path("~/Library/Application Support/com.openai.chat/developer_settings.json").expanduser()),
2864
+ ("gemini", "Gemini CLI",
2865
+ Path("~/.gemini/settings.json").expanduser()),
2866
+ ]
2867
+ for tool_id, label, config_path in mcp_configs:
2868
+ installed = config_path.exists()
2869
+ protected = False
2870
+ if installed:
2871
+ try:
2872
+ with open(config_path) as f:
2873
+ data = json.load(f)
2874
+ mcp_servers = data.get("mcpServers", {})
2875
+ protected = "tweek-security" in mcp_servers or "tweek" in mcp_servers
2876
+ except Exception:
2877
+ pass
2878
+ detail = str(config_path) if protected else ""
2879
+ tools.append((tool_id, label, installed, protected, detail))
2880
+
2881
+ return tools
2882
+
2883
+
2884
+ def _run_protect_wizard():
2885
+ """Interactive wizard: detect tools and ask Y/n for each one."""
2886
+ console.print(TWEEK_BANNER, style="cyan")
2887
+ console.print("[bold]Tweek Protection Wizard[/bold]\n")
2888
+ console.print("Scanning for AI tools...\n")
2889
+
2890
+ tools = _detect_all_tools()
2891
+
2892
+ # Show detection summary
2893
+ detected = [(tid, label, prot) for tid, label, inst, prot, _ in tools if inst]
2894
+ not_detected = [label for _, label, inst, _, _ in tools if not inst]
2895
+
2896
+ if not_detected:
2897
+ for label in not_detected:
2898
+ console.print(f" [white]{label:<20}[/white] [white]not found[/white]")
2899
+
2900
+ if not detected:
2901
+ console.print("\n[yellow]No AI tools detected on this system.[/yellow]")
2902
+ return
2903
+
2904
+ # Show already-protected tools
2905
+ already_protected = [(tid, label) for tid, label, prot in detected if prot]
2906
+ unprotected = [(tid, label) for tid, label, prot in detected if not prot]
2907
+
2908
+ for _, label in already_protected:
2909
+ console.print(f" [green]{label:<20} protected[/green]")
2910
+
2911
+ if not unprotected:
2912
+ console.print(f"\n[green]All {len(already_protected)} detected tool(s) already protected.[/green]")
2913
+ console.print("Run 'tweek status' to see details.")
2914
+ return
2915
+
2916
+ for _, label in unprotected:
2917
+ console.print(f" [yellow]{label:<20} not protected[/yellow]")
2918
+
2919
+ # Ask for preset first (applies to all)
2920
+ console.print()
2921
+ console.print("[bold]Security preset:[/bold]")
2922
+ console.print(" [bold]1.[/bold] cautious [white](recommended)[/white] — screen risky & dangerous tools")
2923
+ console.print(" [bold]2.[/bold] paranoid — screen everything except safe tools")
2924
+ console.print(" [bold]3.[/bold] trusted — only screen dangerous tools")
2925
+ console.print()
2926
+ preset_choice = click.prompt("Select preset", type=click.IntRange(1, 3), default=1)
2927
+ preset = ["cautious", "paranoid", "trusted"][preset_choice - 1]
2928
+
2929
+ # Walk through each unprotected tool
2930
+ console.print()
2931
+ protected_count = 0
2932
+ skipped_count = 0
2933
+
2934
+ for tool_id, label in unprotected:
2935
+ protect_it = click.confirm(f" Protect {label}?", default=True)
2936
+
2937
+ if not protect_it:
2938
+ console.print(f" [white]skipped[/white]")
2939
+ skipped_count += 1
2940
+ continue
2941
+
2942
+ try:
2943
+ if tool_id == "claude-code":
2944
+ _install_claude_code_hooks(
2945
+ install_global=True, dev_test=False, backup=True,
2946
+ skip_env_scan=True, interactive=False, preset=preset,
2947
+ ai_defaults=False, with_sandbox=False, force_proxy=False,
2948
+ skip_proxy_check=True, quick=True,
2949
+ )
2950
+ elif tool_id == "openclaw":
2951
+ from tweek.integrations.openclaw import setup_openclaw_protection
2952
+ result = setup_openclaw_protection(preset=preset)
2953
+ if result.success:
2954
+ console.print(f" [green]done[/green]")
2955
+ else:
2956
+ console.print(f" [red]failed: {result.error}[/red]")
2957
+ continue
2958
+ elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
2959
+ _protect_mcp_client(tool_id)
2960
+ protected_count += 1
2961
+ except Exception as e:
2962
+ console.print(f" [red]error: {e}[/red]")
2963
+
2964
+ console.print()
2965
+ if protected_count:
2966
+ console.print(f"[green]Protected {protected_count} tool(s).[/green]", end="")
2967
+ if skipped_count:
2968
+ console.print(f" [white]Skipped {skipped_count}.[/white]", end="")
2969
+ console.print()
2970
+ console.print("Run 'tweek status' to see the full dashboard.")
2971
+
2972
+
2973
+ def _run_unprotect_wizard():
2974
+ """Interactive wizard: detect protected tools and ask Y/n to unprotect each."""
2975
+ console.print(TWEEK_BANNER, style="cyan")
2976
+ console.print("[bold]Tweek Unprotect Wizard[/bold]\n")
2977
+ console.print("Scanning for protected AI tools...\n")
2978
+
2979
+ tools = _detect_all_tools()
2980
+ tweek_dir = Path("~/.tweek").expanduser()
2981
+ global_target = Path("~/.claude").expanduser()
2982
+ project_target = Path.cwd() / ".claude"
2983
+
2984
+ protected = [(tid, label) for tid, label, inst, prot, _ in tools if inst and prot]
2985
+
2986
+ if not protected:
2987
+ console.print("[yellow]No protected tools found.[/yellow]")
2988
+ return
2989
+
2990
+ for _, label in protected:
2991
+ console.print(f" [green]{label:<20} protected[/green]")
2992
+
2993
+ console.print()
2994
+ removed_count = 0
2995
+ skipped_count = 0
2996
+
2997
+ for tool_id, label in protected:
2998
+ remove_it = click.confirm(f" Remove protection from {label}?", default=False)
2999
+
3000
+ if not remove_it:
3001
+ console.print(f" [white]kept[/white]")
3002
+ skipped_count += 1
3003
+ continue
3004
+
3005
+ try:
3006
+ if tool_id == "claude-code":
3007
+ _uninstall_scope(global_target, tweek_dir, confirm=True, scope_label="global")
3008
+ elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
3009
+ from tweek.mcp.clients import get_client
3010
+ handler = get_client(tool_id)
3011
+ result = handler.uninstall()
3012
+ if result.get("success"):
3013
+ console.print(f" [green]{result.get('message', 'removed')}[/green]")
3014
+ else:
3015
+ console.print(f" [red]{result.get('error', 'failed')}[/red]")
3016
+ continue
3017
+ elif tool_id == "openclaw":
3018
+ console.print(" [white]Manual step: remove tweek plugin from openclaw.json[/white]")
3019
+ removed_count += 1
3020
+ except Exception as e:
3021
+ console.print(f" [red]error: {e}[/red]")
3022
+
3023
+ console.print()
3024
+ if removed_count:
3025
+ console.print(f"[green]Removed protection from {removed_count} tool(s).[/green]", end="")
3026
+ if skipped_count:
3027
+ console.print(f" [white]Kept {skipped_count}.[/white]", end="")
3028
+ console.print()
3029
+
3030
+
3031
+ def _show_protection_status():
3032
+ """Show protection status dashboard for all AI tools."""
3033
+ console.print(TWEEK_BANNER, style="cyan")
3034
+
3035
+ tools = _detect_all_tools()
3036
+
3037
+ # Build status table
3038
+ table = Table(title="Protection Status", show_lines=False)
3039
+ table.add_column("Tool", style="cyan", min_width=18)
3040
+ table.add_column("Installed", justify="center", min_width=10)
3041
+ table.add_column("Protected", justify="center", min_width=10)
3042
+ table.add_column("Details")
3043
+
3044
+ detected_count = 0
3045
+ protected_count = 0
3046
+
3047
+ for tool_id, label, installed, protected, detail in tools:
3048
+ if installed:
3049
+ detected_count += 1
3050
+ if protected:
3051
+ protected_count += 1
3052
+
3053
+ table.add_row(
3054
+ label,
3055
+ "[green]yes[/green]" if installed else "[white]no[/white]",
3056
+ "[green]yes[/green]" if protected else "[yellow]no[/yellow]" if installed else "[white]—[/white]",
3057
+ detail,
3058
+ )
3059
+
3060
+ console.print(table)
3061
+ console.print()
3062
+
3063
+ # Summary line
3064
+ unprotected_count = detected_count - protected_count
3065
+ if detected_count == 0:
3066
+ console.print("[yellow]No AI tools detected.[/yellow]")
3067
+ elif unprotected_count == 0:
3068
+ console.print(f"[green]{protected_count}/{detected_count} detected tools protected.[/green]")
3069
+ else:
3070
+ console.print(f"[yellow]{protected_count}/{detected_count} detected tools protected. {unprotected_count} unprotected.[/yellow]")
3071
+ console.print("[white]Run 'tweek protect' to set up protection.[/white]")
3072
+ console.print()
3073
+
3074
+
2694
3075
  # =============================================================================
2695
3076
  # CONFIG COMMANDS
2696
3077
  # =============================================================================
@@ -2775,7 +3156,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
2775
3156
  }
2776
3157
 
2777
3158
  source_styles = {
2778
- "default": "dim",
3159
+ "default": "white",
2779
3160
  "user": "cyan",
2780
3161
  "project": "magenta",
2781
3162
  }
@@ -2784,7 +3165,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
2784
3165
  table = Table(title="Tool Security Tiers")
2785
3166
  table.add_column("Tool", style="bold")
2786
3167
  table.add_column("Tier")
2787
- table.add_column("Source", style="dim")
3168
+ table.add_column("Source", style="white")
2788
3169
  table.add_column("Description")
2789
3170
 
2790
3171
  for tool in cfg.list_tools():
@@ -2804,7 +3185,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
2804
3185
  table = Table(title="Skill Security Tiers")
2805
3186
  table.add_column("Skill", style="bold")
2806
3187
  table.add_column("Tier")
2807
- table.add_column("Source", style="dim")
3188
+ table.add_column("Source", style="white")
2808
3189
  table.add_column("Description")
2809
3190
 
2810
3191
  for skill in cfg.list_skills():
@@ -2819,8 +3200,8 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
2819
3200
 
2820
3201
  console.print(table)
2821
3202
 
2822
- console.print("\n[dim]Tiers: safe (no checks) → default (regex) → risky (+LLM) → dangerous (+sandbox)[/dim]")
2823
- console.print("[dim]Sources: default (built-in), user (~/.tweek/config.yaml), project (.tweek/config.yaml)[/dim]")
3203
+ console.print("\n[white]Tiers: safe (no checks) → default (regex) → risky (+LLM) → dangerous (+sandbox)[/white]")
3204
+ console.print("[white]Sources: default (built-in), user (~/.tweek/config.yaml), project (.tweek/config.yaml)[/white]")
2824
3205
 
2825
3206
 
2826
3207
  @config.command("set",
@@ -2883,11 +3264,11 @@ def config_preset(preset_name: str, scope: str):
2883
3264
  console.print(f"[green]✓[/green] Applied [bold]{preset_name}[/bold] preset ({scope} config)")
2884
3265
 
2885
3266
  if preset_name == "paranoid":
2886
- console.print("[dim]All tools require screening, Bash commands always sandboxed[/dim]")
3267
+ console.print("[white]All tools require screening, Bash commands always sandboxed[/white]")
2887
3268
  elif preset_name == "cautious":
2888
- console.print("[dim]Balanced: read-only tools safe, Bash dangerous[/dim]")
3269
+ console.print("[white]Balanced: read-only tools safe, Bash dangerous[/white]")
2889
3270
  elif preset_name == "trusted":
2890
- console.print("[dim]Minimal prompts: only high-risk patterns trigger alerts[/dim]")
3271
+ console.print("[white]Minimal prompts: only high-risk patterns trigger alerts[/white]")
2891
3272
 
2892
3273
 
2893
3274
  @config.command("reset",
@@ -2912,7 +3293,7 @@ def config_reset(skill: str, tool: str, reset_all: bool, scope: str, confirm: bo
2912
3293
 
2913
3294
  if reset_all:
2914
3295
  if not confirm and not click.confirm(f"Reset ALL {scope} configuration?"):
2915
- console.print("[dim]Cancelled[/dim]")
3296
+ console.print("[white]Cancelled[/white]")
2916
3297
  return
2917
3298
  cfg.reset_all(scope=scope)
2918
3299
  console.print(f"[green]✓[/green] Reset all {scope} configuration to defaults")
@@ -2970,7 +3351,7 @@ def config_validate(scope: str, json_out: bool):
2970
3351
  console.print()
2971
3352
  console.print("[bold]Configuration Validation[/bold]")
2972
3353
  console.print("\u2500" * 40)
2973
- console.print(f"[dim]Scope: {scope}[/dim]")
3354
+ console.print(f"[white]Scope: {scope}[/white]")
2974
3355
  console.print()
2975
3356
 
2976
3357
  if not issues:
@@ -2986,11 +3367,11 @@ def config_validate(scope: str, json_out: bool):
2986
3367
  level_styles = {
2987
3368
  "error": "[red]ERROR[/red]",
2988
3369
  "warning": "[yellow]WARN[/yellow] ",
2989
- "info": "[dim]INFO[/dim] ",
3370
+ "info": "[white]INFO[/white] ",
2990
3371
  }
2991
3372
 
2992
3373
  for issue in issues:
2993
- style = level_styles.get(issue.level, "[dim]???[/dim] ")
3374
+ style = level_styles.get(issue.level, "[white]???[/white] ")
2994
3375
  msg = f" {style} {issue.key} \u2192 {issue.message}"
2995
3376
  if issue.suggestion:
2996
3377
  msg += f" {issue.suggestion}"
@@ -3102,7 +3483,7 @@ def config_llm(verbose: bool, validate: bool):
3102
3483
  console.print()
3103
3484
  console.print(" [yellow]Status:[/yellow] Disabled (no provider available)")
3104
3485
  console.print()
3105
- console.print(" [dim]To enable, set one of:[/dim]")
3486
+ console.print(" [white]To enable, set one of:[/white]")
3106
3487
  console.print(" ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY")
3107
3488
  console.print(" Or install Ollama: [cyan]https://ollama.ai[/cyan]")
3108
3489
  console.print()
@@ -3137,8 +3518,8 @@ def config_llm(verbose: bool, validate: bool):
3137
3518
  for m in server.all_models:
3138
3519
  console.print(f" - {m}")
3139
3520
  else:
3140
- console.print(" [dim]No local LLM server detected[/dim]")
3141
- console.print(" [dim]Checked: Ollama (localhost:11434), LM Studio (localhost:1234)[/dim]")
3521
+ console.print(" [white]No local LLM server detected[/white]")
3522
+ console.print(" [white]Checked: Ollama (localhost:11434), LM Studio (localhost:1234)[/white]")
3142
3523
  except Exception as e:
3143
3524
  console.print(f" [yellow]Detection error: {e}[/yellow]")
3144
3525
 
@@ -3176,8 +3557,8 @@ def config_llm(verbose: bool, validate: bool):
3176
3557
  console.print(f" [green]PASSED[/green] ({score:.0%})")
3177
3558
  else:
3178
3559
  console.print(f" [red]FAILED[/red] ({score:.0%}, minimum: 60%)")
3179
- console.print(" [dim]This model may not reliably classify security threats.[/dim]")
3180
- console.print(" [dim]Try a larger model: ollama pull qwen2.5:7b-instruct[/dim]")
3560
+ console.print(" [white]This model may not reliably classify security threats.[/white]")
3561
+ console.print(" [white]Try a larger model: ollama pull qwen2.5:7b-instruct[/white]")
3181
3562
  except Exception as e:
3182
3563
  console.print(f" [red]Validation error: {e}[/red]")
3183
3564
 
@@ -3207,8 +3588,8 @@ def vault_store(skill: str, key: str, value: Optional[str]):
3207
3588
 
3208
3589
  if not VAULT_AVAILABLE:
3209
3590
  console.print("[red]\u2717[/red] Vault not available.")
3210
- console.print(" [dim]Hint: Install keyring support: pip install keyring[/dim]")
3211
- console.print(" [dim]On macOS, keyring uses Keychain. On Linux, install gnome-keyring or kwallet.[/dim]")
3591
+ console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
3592
+ console.print(" [white]On macOS, keyring uses Keychain. On Linux, install gnome-keyring or kwallet.[/white]")
3212
3593
  return
3213
3594
 
3214
3595
  caps = get_capabilities()
@@ -3224,13 +3605,13 @@ def vault_store(skill: str, key: str, value: Optional[str]):
3224
3605
  vault_instance = get_vault()
3225
3606
  if vault_instance.store(skill, key, value):
3226
3607
  console.print(f"[green]\u2713[/green] Stored {key} for skill '{skill}'")
3227
- console.print(f"[dim]Backend: {caps.vault_backend}[/dim]")
3608
+ console.print(f"[white]Backend: {caps.vault_backend}[/white]")
3228
3609
  else:
3229
3610
  console.print(f"[red]\u2717[/red] Failed to store credential")
3230
- console.print(" [dim]Hint: Check your keyring backend is unlocked and accessible[/dim]")
3611
+ console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
3231
3612
  except Exception as e:
3232
3613
  console.print(f"[red]\u2717[/red] Failed to store credential: {e}")
3233
- console.print(" [dim]Hint: Check your keyring backend is unlocked and accessible[/dim]")
3614
+ console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
3234
3615
 
3235
3616
 
3236
3617
  @vault.command("get",
@@ -3248,7 +3629,7 @@ def vault_get(skill: str, key: str):
3248
3629
 
3249
3630
  if not VAULT_AVAILABLE:
3250
3631
  console.print("[red]\u2717[/red] Vault not available.")
3251
- console.print(" [dim]Hint: Install keyring support: pip install keyring[/dim]")
3632
+ console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
3252
3633
  return
3253
3634
 
3254
3635
  vault_instance = get_vault()
@@ -3262,7 +3643,7 @@ def vault_get(skill: str, key: str):
3262
3643
  console.print(value)
3263
3644
  else:
3264
3645
  console.print(f"[red]\u2717[/red] Credential not found: {key} for skill '{skill}'")
3265
- console.print(" [dim]Hint: Store it with: tweek vault store {skill} {key} <value>[/dim]".format(skill=skill, key=key))
3646
+ console.print(" [white]Hint: Store it with: tweek vault store {skill} {key} <value>[/white]".format(skill=skill, key=key))
3266
3647
 
3267
3648
 
3268
3649
  @vault.command("migrate-env",
@@ -3302,7 +3683,7 @@ def vault_migrate_env(dry_run: bool, env_file: str, skill: str):
3302
3683
  successful = sum(1 for _, s in results if s)
3303
3684
  console.print(f"\n[green]✓[/green] {'Would migrate' if dry_run else 'Migrated'} {successful} credentials to skill '{skill}'")
3304
3685
  else:
3305
- console.print("[dim]No credentials found to migrate[/dim]")
3686
+ console.print("[white]No credentials found to migrate[/white]")
3306
3687
 
3307
3688
  except Exception as e:
3308
3689
  console.print(f"[red]✗[/red] Migration failed: {e}")
@@ -3369,16 +3750,16 @@ def license_status():
3369
3750
  console.print(f"[bold]License Tier:[/bold] [{tier_color}]{lic.tier.value.upper()}[/{tier_color}]")
3370
3751
 
3371
3752
  if info:
3372
- console.print(f"[dim]Licensed to: {info.email}[/dim]")
3753
+ console.print(f"[white]Licensed to: {info.email}[/white]")
3373
3754
  if info.expires_at:
3374
3755
  from datetime import datetime
3375
3756
  exp_date = datetime.fromtimestamp(info.expires_at).strftime("%Y-%m-%d")
3376
3757
  if info.is_expired:
3377
3758
  console.print(f"[red]Expired: {exp_date}[/red]")
3378
3759
  else:
3379
- console.print(f"[dim]Expires: {exp_date}[/dim]")
3760
+ console.print(f"[white]Expires: {exp_date}[/white]")
3380
3761
  else:
3381
- console.print("[dim]Expires: Never[/dim]")
3762
+ console.print("[white]Expires: Never[/white]")
3382
3763
  console.print()
3383
3764
 
3384
3765
  # Features table
@@ -3395,7 +3776,7 @@ def license_status():
3395
3776
 
3396
3777
  for feature, required_tier in feature_tiers.items():
3397
3778
  has_it = lic.has_feature(feature)
3398
- status = "[green]✓[/green]" if has_it else "[dim]○[/dim]"
3779
+ status = "[green]✓[/green]" if has_it else "[white]○[/white]"
3399
3780
  tier_display = required_tier.value.upper()
3400
3781
  if required_tier == Tier.PRO:
3401
3782
  tier_display = f"[cyan]{tier_display}[/cyan]"
@@ -3407,7 +3788,7 @@ def license_status():
3407
3788
  if lic.tier == Tier.FREE:
3408
3789
  console.print()
3409
3790
  console.print("[green]All security features are included free and open source.[/green]")
3410
- console.print("[dim]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/dim]")
3791
+ console.print("[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
3411
3792
 
3412
3793
 
3413
3794
  @license.command("activate",
@@ -3427,7 +3808,7 @@ def license_activate(license_key: str):
3427
3808
  if success:
3428
3809
  console.print(f"[green]✓[/green] {message}")
3429
3810
  console.print()
3430
- console.print("[dim]Run 'tweek license status' to see available features[/dim]")
3811
+ console.print("[white]Run 'tweek license status' to see available features[/white]")
3431
3812
  else:
3432
3813
  console.print(f"[red]✗[/red] {message}")
3433
3814
 
@@ -3447,7 +3828,7 @@ def license_deactivate(confirm: bool):
3447
3828
  if not confirm:
3448
3829
  console.print("[yellow]Deactivate license and revert to FREE tier?[/yellow] ", end="")
3449
3830
  if not click.confirm(""):
3450
- console.print("[dim]Cancelled[/dim]")
3831
+ console.print("[white]Cancelled[/white]")
3451
3832
  return
3452
3833
 
3453
3834
  lic = get_license()
@@ -3525,7 +3906,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
3525
3906
  table.add_column("Severity")
3526
3907
  table.add_column("Count", justify="right")
3527
3908
 
3528
- severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "dim"}
3909
+ severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "white"}
3529
3910
  for pattern in stat_data['top_patterns']:
3530
3911
  sev = pattern['severity'] or "unknown"
3531
3912
  style = severity_styles.get(sev, "white")
@@ -3562,7 +3943,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
3562
3943
  et = EventType(event_type)
3563
3944
  except ValueError:
3564
3945
  console.print(f"[red]Unknown event type: {event_type}[/red]")
3565
- console.print(f"[dim]Valid types: {', '.join(e.value for e in EventType)}[/dim]")
3946
+ console.print(f"[white]Valid types: {', '.join(e.value for e in EventType)}[/white]")
3566
3947
  return
3567
3948
 
3568
3949
  events = logger.get_recent_events(limit=limit, event_type=et, tool_name=tool)
@@ -3573,7 +3954,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
3573
3954
  return
3574
3955
 
3575
3956
  table = Table(title=title)
3576
- table.add_column("Time", style="dim")
3957
+ table.add_column("Time", style="white")
3577
3958
  table.add_column("Type", style="cyan")
3578
3959
  table.add_column("Tool", style="green")
3579
3960
  table.add_column("Tier")
@@ -3614,7 +3995,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
3614
3995
  )
3615
3996
 
3616
3997
  console.print(table)
3617
- console.print(f"\n[dim]Showing {len(events)} events. Use --limit to see more.[/dim]")
3998
+ console.print(f"\n[white]Showing {len(events)} events. Use --limit to see more.[/white]")
3618
3999
 
3619
4000
 
3620
4001
  @logs.command("export",
@@ -3665,7 +4046,7 @@ def logs_clear(days: int, confirm: bool):
3665
4046
 
3666
4047
  console.print(f"[yellow]{msg}[/yellow] ", end="")
3667
4048
  if not click.confirm(""):
3668
- console.print("[dim]Cancelled[/dim]")
4049
+ console.print("[white]Cancelled[/white]")
3669
4050
  return
3670
4051
 
3671
4052
  logger = get_logger()
@@ -3677,7 +4058,7 @@ def logs_clear(days: int, confirm: bool):
3677
4058
  else:
3678
4059
  console.print(f"[green]Cleared {deleted} event(s)[/green]")
3679
4060
  else:
3680
- console.print("[dim]No events to clear[/dim]")
4061
+ console.print("[white]No events to clear[/white]")
3681
4062
 
3682
4063
 
3683
4064
  @logs.command("bundle",
@@ -3715,11 +4096,11 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
3715
4096
  size = item.get("size")
3716
4097
  size_str = f" ({size:,} bytes)" if size else ""
3717
4098
  if "not found" in status:
3718
- console.print(f" [dim] SKIP {name} ({status})[/dim]")
4099
+ console.print(f" [white] SKIP {name} ({status})[/white]")
3719
4100
  else:
3720
4101
  console.print(f" [green] ADD {name}{size_str}[/green]")
3721
4102
  console.print()
3722
- console.print("[dim]No files will be collected in dry-run mode.[/dim]")
4103
+ console.print("[white]No files will be collected in dry-run mode.[/white]")
3723
4104
  return
3724
4105
 
3725
4106
  # Determine output path
@@ -3737,9 +4118,9 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
3737
4118
  result = collector.create_bundle(output_path)
3738
4119
  size = result.stat().st_size
3739
4120
  console.print(f"\n[green]Bundle created: {result}[/green]")
3740
- console.print(f"[dim]Size: {size:,} bytes[/dim]")
4121
+ console.print(f"[white]Size: {size:,} bytes[/white]")
3741
4122
  if not no_redact:
3742
- console.print("[dim]Sensitive data has been redacted.[/dim]")
4123
+ console.print("[white]Sensitive data has been redacted.[/white]")
3743
4124
  console.print(f"\n[bold]Send this file to Tweek support for analysis.[/bold]")
3744
4125
  except Exception as e:
3745
4126
  console.print(f"[red]Failed to create bundle: {e}[/red]")
@@ -3785,8 +4166,8 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
3785
4166
 
3786
4167
  if not PROXY_AVAILABLE:
3787
4168
  console.print("[red]\u2717[/red] Proxy dependencies not installed.")
3788
- console.print(" [dim]Hint: Install with: pip install tweek[proxy][/dim]")
3789
- console.print(" [dim]This adds mitmproxy for HTTP(S) interception.[/dim]")
4169
+ console.print(" [white]Hint: Install with: pip install tweek[proxy][/white]")
4170
+ console.print(" [white]This adds mitmproxy for HTTP(S) interception.[/white]")
3790
4171
  return
3791
4172
 
3792
4173
  from tweek.proxy.server import start_proxy
@@ -3807,7 +4188,7 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
3807
4188
  console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
3808
4189
  console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
3809
4190
  console.print()
3810
- console.print("[dim]Or use 'tweek proxy wrap' to create a wrapper script[/dim]")
4191
+ console.print("[white]Or use 'tweek proxy wrap' to create a wrapper script[/white]")
3811
4192
  else:
3812
4193
  console.print(f"[red]✗[/red] {message}")
3813
4194
 
@@ -3852,7 +4233,7 @@ def proxy_trust():
3852
4233
 
3853
4234
  if not PROXY_AVAILABLE:
3854
4235
  console.print("[red]✗[/red] Proxy dependencies not installed.")
3855
- console.print("[dim]Run: pip install tweek\\[proxy][/dim]")
4236
+ console.print("[white]Run: pip install tweek\\[proxy][/white]")
3856
4237
  return
3857
4238
 
3858
4239
  from tweek.proxy.server import install_ca_certificate, get_proxy_info
@@ -3864,11 +4245,11 @@ def proxy_trust():
3864
4245
  console.print("This will install a local CA certificate to enable HTTPS interception.")
3865
4246
  console.print("The certificate is generated on YOUR machine and never transmitted.")
3866
4247
  console.print()
3867
- console.print(f"[dim]Certificate location: {info['ca_cert']}[/dim]")
4248
+ console.print(f"[white]Certificate location: {info['ca_cert']}[/white]")
3868
4249
  console.print()
3869
4250
 
3870
4251
  if not click.confirm("Install certificate? (requires admin password)"):
3871
- console.print("[dim]Cancelled[/dim]")
4252
+ console.print("[white]Cancelled[/white]")
3872
4253
  return
3873
4254
 
3874
4255
  success, message = install_ca_certificate()
@@ -3920,7 +4301,7 @@ def proxy_config(set_enabled, set_disabled, port):
3920
4301
  yaml.dump(config, f, default_flow_style=False)
3921
4302
 
3922
4303
  console.print(f"[green]✓[/green] Proxy mode enabled (port {port})")
3923
- console.print("[dim]Run 'tweek proxy start' to start the proxy[/dim]")
4304
+ console.print("[white]Run 'tweek proxy start' to start the proxy[/white]")
3924
4305
 
3925
4306
  elif set_disabled:
3926
4307
  if "proxy" in config:
@@ -3962,10 +4343,10 @@ def proxy_wrap(app_name: str, command: str, output: str, port: int):
3962
4343
  console.print(f" chmod +x {output_path}")
3963
4344
  console.print(f" ./{output_path.name}")
3964
4345
  console.print()
3965
- console.print("[dim]The script will:[/dim]")
3966
- console.print("[dim] 1. Start Tweek proxy if not running[/dim]")
3967
- console.print("[dim] 2. Set proxy environment variables[/dim]")
3968
- console.print(f"[dim] 3. Run: {command}[/dim]")
4346
+ console.print("[white]The script will:[/white]")
4347
+ console.print("[white] 1. Start Tweek proxy if not running[/white]")
4348
+ console.print("[white] 2. Set proxy environment variables[/white]")
4349
+ console.print(f"[white] 3. Run: {command}[/white]")
3969
4350
 
3970
4351
 
3971
4352
  @proxy.command("setup",
@@ -4040,9 +4421,9 @@ def proxy_setup():
4040
4421
  print_warning("Certificate module not available. Run: tweek proxy trust")
4041
4422
  except Exception as e:
4042
4423
  print_warning(f"Could not set up certificate: {e}")
4043
- console.print(" [dim]You can do this later with: tweek proxy trust[/dim]")
4424
+ console.print(" [white]You can do this later with: tweek proxy trust[/white]")
4044
4425
  else:
4045
- console.print(" [dim]Skipped. Run 'tweek proxy trust' later.[/dim]")
4426
+ console.print(" [white]Skipped. Run 'tweek proxy trust' later.[/white]")
4046
4427
  console.print()
4047
4428
 
4048
4429
  # Step 3: Shell environment
@@ -4066,13 +4447,13 @@ def proxy_setup():
4066
4447
  f.write(f"export HTTP_PROXY=http://127.0.0.1:{port}\n")
4067
4448
  f.write(f"export HTTPS_PROXY=http://127.0.0.1:{port}\n")
4068
4449
  print_success(f"Added to {shell_rc}")
4069
- console.print(f" [dim]Restart your shell or run: source {shell_rc}[/dim]")
4450
+ console.print(f" [white]Restart your shell or run: source {shell_rc}[/white]")
4070
4451
  except Exception as e:
4071
4452
  print_warning(f"Could not write to {shell_rc}: {e}")
4072
4453
  else:
4073
- console.print(" [dim]Skipped. Set HTTP_PROXY and HTTPS_PROXY manually.[/dim]")
4454
+ console.print(" [white]Skipped. Set HTTP_PROXY and HTTPS_PROXY manually.[/white]")
4074
4455
  else:
4075
- console.print(" [dim]Could not detect shell config file.[/dim]")
4456
+ console.print(" [white]Could not detect shell config file.[/white]")
4076
4457
  console.print(f" Add these to your shell profile:")
4077
4458
  console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
4078
4459
  console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
@@ -4166,7 +4547,7 @@ def plugins_list(category: str, show_all: bool):
4166
4547
  license_style = "green" if license_tier == LicenseTier.FREE else "cyan"
4167
4548
 
4168
4549
  source_str = info.source.value if hasattr(info, 'source') else "builtin"
4169
- source_style = "blue" if source_str == "git" else "dim"
4550
+ source_style = "blue" if source_str == "git" else "white"
4170
4551
 
4171
4552
  table.add_row(
4172
4553
  info.name,
@@ -4180,6 +4561,18 @@ def plugins_list(category: str, show_all: bool):
4180
4561
  console.print(table)
4181
4562
  console.print()
4182
4563
 
4564
+ # Summary line across all categories
4565
+ total_count = 0
4566
+ enabled_count = 0
4567
+ for cat in list(PluginCategory):
4568
+ for info in registry.list_plugins(cat):
4569
+ total_count += 1
4570
+ if info.enabled:
4571
+ enabled_count += 1
4572
+ disabled_count = total_count - enabled_count
4573
+ console.print(f"Plugins: {total_count} registered, {enabled_count} enabled, {disabled_count} disabled")
4574
+ console.print()
4575
+
4183
4576
  except ImportError as e:
4184
4577
  console.print(f"[red]Plugin system not available: {e}[/red]")
4185
4578
 
@@ -4236,7 +4629,7 @@ def plugins_info(plugin_name: str, category: str):
4236
4629
  plugin_cfg = cfg.get_plugin_config(found_cat, plugin_name)
4237
4630
 
4238
4631
  console.print(f"\n[bold]{found_info.name}[/bold] ({found_cat})")
4239
- console.print(f"[dim]{found_info.metadata.description}[/dim]")
4632
+ console.print(f"[white]{found_info.metadata.description}[/white]")
4240
4633
  console.print()
4241
4634
 
4242
4635
  table = Table(show_header=False)
@@ -4408,7 +4801,7 @@ def plugins_scan(content: str, direction: str, plugin: str):
4408
4801
 
4409
4802
  if not plugins_to_use:
4410
4803
  console.print("[yellow]No compliance plugins enabled.[/yellow]")
4411
- console.print("[dim]Enable plugins with: tweek plugins enable <name> -c compliance[/dim]")
4804
+ console.print("[white]Enable plugins with: tweek plugins enable <name> -c compliance[/white]")
4412
4805
  return
4413
4806
 
4414
4807
  for p in plugins_to_use:
@@ -4422,12 +4815,12 @@ def plugins_scan(content: str, direction: str, plugin: str):
4422
4815
  "critical": "red bold",
4423
4816
  "high": "red",
4424
4817
  "medium": "yellow",
4425
- "low": "dim",
4818
+ "low": "white",
4426
4819
  }
4427
4820
  style = severity_styles.get(finding.severity.value, "white")
4428
4821
 
4429
4822
  console.print(f" [{style}]{finding.severity.value.upper()}[/{style}] {finding.pattern_name}")
4430
- console.print(f" [dim]Matched: {finding.matched_text[:60]}{'...' if len(finding.matched_text) > 60 else ''}[/dim]")
4823
+ console.print(f" [white]Matched: {finding.matched_text[:60]}{'...' if len(finding.matched_text) > 60 else ''}[/white]")
4431
4824
  if finding.description:
4432
4825
  console.print(f" {finding.description}")
4433
4826
 
@@ -4501,11 +4894,11 @@ def plugins_install(name: str, version: str, from_lockfile: bool, no_verify: boo
4501
4894
  console.print(f"[green]\u2713[/green] {msg}")
4502
4895
  else:
4503
4896
  console.print(f"[red]\u2717[/red] {msg}")
4504
- console.print(f" [dim]Hint: Check network connectivity or try: tweek plugins registry --refresh[/dim]")
4897
+ console.print(f" [white]Hint: Check network connectivity or try: tweek plugins registry --refresh[/white]")
4505
4898
 
4506
4899
  except Exception as e:
4507
4900
  console.print(f"[red]Error: {e}[/red]")
4508
- console.print(f" [dim]Hint: Check network connectivity and try again[/dim]")
4901
+ console.print(f" [white]Hint: Check network connectivity and try again[/white]")
4509
4902
 
4510
4903
 
4511
4904
  @plugins.command("update",
@@ -4873,87 +5266,6 @@ def serve():
4873
5266
  console.print(f"[red]MCP server error: {e}[/red]")
4874
5267
 
4875
5268
 
4876
- @mcp.command(
4877
- epilog="""\b
4878
- Examples:
4879
- tweek mcp install claude-desktop Configure Claude Desktop integration
4880
- tweek mcp install chatgpt Set up ChatGPT Desktop integration
4881
- tweek mcp install gemini Configure Gemini CLI integration
4882
- """
4883
- )
4884
- @click.argument("client", type=click.Choice(["claude-desktop", "chatgpt", "gemini"]))
4885
- def install(client):
4886
- """Install Tweek as MCP server for a desktop client.
4887
-
4888
- Supported clients:
4889
- claude-desktop - Auto-configures Claude Desktop
4890
- chatgpt - Provides Developer Mode setup instructions
4891
- gemini - Auto-configures Gemini CLI settings
4892
- """
4893
- try:
4894
- from tweek.mcp.clients import get_client
4895
-
4896
- handler = get_client(client)
4897
- result = handler.install()
4898
-
4899
- if result.get("success"):
4900
- console.print(f"[green]✅ {result.get('message', 'Installed successfully')}[/green]")
4901
-
4902
- if result.get("config_path"):
4903
- console.print(f" Config: {result['config_path']}")
4904
-
4905
- if result.get("backup"):
4906
- console.print(f" Backup: {result['backup']}")
4907
-
4908
- # Show instructions for manual setup clients
4909
- if result.get("instructions"):
4910
- console.print()
4911
- for line in result["instructions"]:
4912
- console.print(f" {line}")
4913
- else:
4914
- console.print(f"[red]❌ {result.get('error', 'Installation failed')}[/red]")
4915
-
4916
- except Exception as e:
4917
- console.print(f"[red]Error: {e}[/red]")
4918
-
4919
-
4920
- @mcp.command(
4921
- epilog="""\b
4922
- Examples:
4923
- tweek mcp uninstall claude-desktop Remove from Claude Desktop
4924
- tweek mcp uninstall chatgpt Remove from ChatGPT Desktop
4925
- tweek mcp uninstall gemini Remove from Gemini CLI
4926
- """
4927
- )
4928
- @click.argument("client", type=click.Choice(["claude-desktop", "chatgpt", "gemini"]))
4929
- def uninstall(client):
4930
- """Remove Tweek MCP server from a desktop client.
4931
-
4932
- Supported clients: claude-desktop, chatgpt, gemini
4933
- """
4934
- try:
4935
- from tweek.mcp.clients import get_client
4936
-
4937
- handler = get_client(client)
4938
- result = handler.uninstall()
4939
-
4940
- if result.get("success"):
4941
- console.print(f"[green]✅ {result.get('message', 'Uninstalled successfully')}[/green]")
4942
-
4943
- if result.get("backup"):
4944
- console.print(f" Backup: {result['backup']}")
4945
-
4946
- if result.get("instructions"):
4947
- console.print()
4948
- for line in result["instructions"]:
4949
- console.print(f" {line}")
4950
- else:
4951
- console.print(f"[red]❌ {result.get('error', 'Uninstallation failed')}[/red]")
4952
-
4953
- except Exception as e:
4954
- console.print(f"[red]Error: {e}[/red]")
4955
-
4956
-
4957
5269
  # =============================================================================
4958
5270
  # MCP PROXY COMMANDS
4959
5271
  # =============================================================================
@@ -5109,13 +5421,13 @@ def chamber_list():
5109
5421
  items = chamber.list_chamber()
5110
5422
 
5111
5423
  if not items:
5112
- console.print("[dim]Chamber is empty.[/dim]")
5424
+ console.print("[white]Chamber is empty.[/white]")
5113
5425
  return
5114
5426
 
5115
5427
  table = Table(title="Isolation Chamber")
5116
5428
  table.add_column("Name", style="cyan")
5117
5429
  table.add_column("Has SKILL.md", style="green")
5118
- table.add_column("Path", style="dim")
5430
+ table.add_column("Path", style="white")
5119
5431
 
5120
5432
  for item in items:
5121
5433
  has_md = "Yes" if item["has_skill_md"] else "[red]No[/red]"
@@ -5210,7 +5522,7 @@ def jail_list():
5210
5522
  items = chamber.list_jail()
5211
5523
 
5212
5524
  if not items:
5213
- console.print("[dim]Jail is empty.[/dim]")
5525
+ console.print("[white]Jail is empty.[/white]")
5214
5526
  return
5215
5527
 
5216
5528
  table = Table(title="Skill Jail")
@@ -5282,7 +5594,7 @@ def skills_report(name: str):
5282
5594
  report_data = chamber.get_report(name)
5283
5595
 
5284
5596
  if not report_data:
5285
- console.print(f"[dim]No report found for '{name}'.[/dim]")
5597
+ console.print(f"[white]No report found for '{name}'.[/white]")
5286
5598
  return
5287
5599
 
5288
5600
  console.print(Panel(
@@ -5404,7 +5716,7 @@ def sandbox_status():
5404
5716
  else:
5405
5717
  console.print(f"[bold]Project:[/bold] {project_dir}")
5406
5718
  console.print(f"[bold]Layer:[/bold] 0-1 (no project isolation)")
5407
- console.print("[dim]Run 'tweek sandbox init' to enable project isolation.[/dim]")
5719
+ console.print("[white]Run 'tweek sandbox init' to enable project isolation.[/white]")
5408
5720
 
5409
5721
 
5410
5722
  @sandbox.command("init")
@@ -5478,7 +5790,7 @@ def sandbox_list():
5478
5790
  projects = registry.list_projects()
5479
5791
 
5480
5792
  if not projects:
5481
- console.print("[dim]No projects registered. Run 'tweek sandbox init' in a project.[/dim]")
5793
+ console.print("[white]No projects registered. Run 'tweek sandbox init' in a project.[/white]")
5482
5794
  return
5483
5795
 
5484
5796
  table = Table(title="Registered Projects")
@@ -5550,12 +5862,12 @@ def sandbox_logs(show_global: bool, limit: int):
5550
5862
 
5551
5863
  events = logger.get_recent_events(limit=limit)
5552
5864
  if not events:
5553
- console.print("[dim]No events found.[/dim]")
5865
+ console.print("[white]No events found.[/white]")
5554
5866
  return
5555
5867
 
5556
5868
  from rich.table import Table
5557
5869
  table = Table()
5558
- table.add_column("Time", style="dim")
5870
+ table.add_column("Time", style="white")
5559
5871
  table.add_column("Type")
5560
5872
  table.add_column("Tool")
5561
5873
  table.add_column("Decision", style="green")
@@ -5635,7 +5947,7 @@ def sandbox_verify():
5635
5947
  checks_passed += 1
5636
5948
  else:
5637
5949
  console.print(" Sandbox initialized: [red]NO[/red]")
5638
- console.print(" [dim]Run 'tweek sandbox init' to enable.[/dim]")
5950
+ console.print(" [white]Run 'tweek sandbox init' to enable.[/white]")
5639
5951
 
5640
5952
  # Check 3: Layer
5641
5953
  checks_total += 1
@@ -5656,7 +5968,7 @@ def sandbox_verify():
5656
5968
  elif sandbox:
5657
5969
  console.print(" Project security.db: [yellow]NOT FOUND[/yellow]")
5658
5970
  else:
5659
- console.print(" Project security.db: [dim]N/A (sandbox inactive)[/dim]")
5971
+ console.print(" Project security.db: [white]N/A (sandbox inactive)[/white]")
5660
5972
 
5661
5973
  # Check 5: .gitignore
5662
5974
  checks_total += 1
@@ -5686,7 +5998,7 @@ def docker_init():
5686
5998
  bridge = DockerBridge()
5687
5999
  if not bridge.is_docker_available():
5688
6000
  console.print("[red]Docker is not installed or not running.[/red]")
5689
- console.print("[dim]Install Docker Desktop from https://www.docker.com/products/docker-desktop/[/dim]")
6001
+ console.print("[white]Install Docker Desktop from https://www.docker.com/products/docker-desktop/[/white]")
5690
6002
  raise SystemExit(1)
5691
6003
 
5692
6004
  from tweek.sandbox.project import _detect_project_dir
@@ -5697,7 +6009,7 @@ def docker_init():
5697
6009
 
5698
6010
  compose_path = bridge.init(project_dir)
5699
6011
  console.print(f"[green]Docker Sandbox config generated: {compose_path}[/green]")
5700
- console.print("[dim]Run 'tweek sandbox docker run' to start the container.[/dim]")
6012
+ console.print("[white]Run 'tweek sandbox docker run' to start the container.[/white]")
5701
6013
 
5702
6014
 
5703
6015
  @sandbox_docker.command("run")
@@ -5734,7 +6046,7 @@ def docker_status():
5734
6046
  compose = project_dir / ".tweek" / "docker-compose.yaml"
5735
6047
  console.print(f"[bold]Docker config:[/bold] {'exists' if compose.exists() else 'not generated'}")
5736
6048
  else:
5737
- console.print("[dim]Not in a project directory.[/dim]")
6049
+ console.print("[white]Not in a project directory.[/white]")
5738
6050
 
5739
6051
 
5740
6052
  # =========================================================================
@@ -5795,7 +6107,7 @@ def override_create(pattern: str, mode: str, duration_minutes: Optional[int], re
5795
6107
  if reason:
5796
6108
  console.print(f" Reason: {reason}")
5797
6109
  console.print()
5798
- console.print("[dim]Next time this pattern triggers, you'll see an 'ask' prompt instead of a hard block.[/dim]")
6110
+ console.print("[white]Next time this pattern triggers, you'll see an 'ask' prompt instead of a hard block.[/white]")
5799
6111
 
5800
6112
 
5801
6113
  @override_group.command("list")
@@ -5808,7 +6120,7 @@ def override_list():
5808
6120
  active_patterns = {o["pattern"] for o in active}
5809
6121
 
5810
6122
  if not all_overrides:
5811
- console.print("[dim]No break-glass overrides found.[/dim]")
6123
+ console.print("[white]No break-glass overrides found.[/white]")
5812
6124
  return
5813
6125
 
5814
6126
  table = Table(title="Break-Glass Overrides")
@@ -5822,9 +6134,9 @@ def override_list():
5822
6134
  if o["pattern"] in active_patterns and not o.get("used"):
5823
6135
  status = "[green]active[/green]"
5824
6136
  elif o.get("used"):
5825
- status = "[dim]consumed[/dim]"
6137
+ status = "[white]consumed[/white]"
5826
6138
  else:
5827
- status = "[dim]expired[/dim]"
6139
+ status = "[white]expired[/white]"
5828
6140
 
5829
6141
  table.add_row(
5830
6142
  o["pattern"],
@@ -5913,7 +6225,7 @@ def feedback_stats(above_threshold: bool):
5913
6225
 
5914
6226
  stats = get_stats()
5915
6227
  if not stats:
5916
- console.print("[dim]No feedback data recorded yet.[/dim]")
6228
+ console.print("[white]No feedback data recorded yet.[/white]")
5917
6229
  return
5918
6230
 
5919
6231
  table = Table(title="Pattern FP Statistics")
@@ -5954,7 +6266,7 @@ def feedback_reset(pattern_name: str):
5954
6266
  if result.get("was_demoted"):
5955
6267
  console.print(f" Restored severity: {result.get('original_severity')}")
5956
6268
  else:
5957
- console.print(f"[dim]No feedback data found for '{pattern_name}'.[/dim]")
6269
+ console.print(f"[white]No feedback data found for '{pattern_name}'.[/white]")
5958
6270
 
5959
6271
 
5960
6272
  # =========================================================================
@@ -5995,7 +6307,7 @@ def memory_status():
5995
6307
  if last_decay:
5996
6308
  console.print(f" Last decay: {last_decay}")
5997
6309
  else:
5998
- console.print(" Last decay: [dim]never[/dim]")
6310
+ console.print(" Last decay: [white]never[/white]")
5999
6311
 
6000
6312
  db_size = stats.get("db_size_bytes", 0)
6001
6313
  if db_size > 1024 * 1024:
@@ -6019,7 +6331,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
6019
6331
  patterns = store.get_pattern_stats(min_decisions=min_decisions, sort_by=sort_by)
6020
6332
 
6021
6333
  if not patterns:
6022
- console.print("[dim]No pattern decision data recorded yet.[/dim]")
6334
+ console.print("[white]No pattern decision data recorded yet.[/white]")
6023
6335
  return
6024
6336
 
6025
6337
  table = Table(title="Pattern Decision History")
@@ -6036,7 +6348,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
6036
6348
  ratio_style = "green" if ratio >= 0.9 else ("yellow" if ratio >= 0.5 else "red")
6037
6349
  table.add_row(
6038
6350
  p.get("pattern_name", "?"),
6039
- p.get("path_prefix") or "[dim]-[/dim]",
6351
+ p.get("path_prefix") or "[white]-[/white]",
6040
6352
  str(p.get("total_decisions", 0)),
6041
6353
  f"{p.get('weighted_approvals', 0):.1f}",
6042
6354
  f"{p.get('weighted_denials', 0):.1f}",
@@ -6057,7 +6369,7 @@ def memory_sources(suspicious: bool):
6057
6369
  sources = store.get_all_sources(suspicious_only=suspicious)
6058
6370
 
6059
6371
  if not sources:
6060
- console.print("[dim]No source trust data recorded yet.[/dim]")
6372
+ console.print("[white]No source trust data recorded yet.[/white]")
6061
6373
  return
6062
6374
 
6063
6375
  table = Table(title="Source Trust Scores")
@@ -6092,7 +6404,7 @@ def memory_suggestions(show_all: bool):
6092
6404
  suggestions = store.get_whitelist_suggestions(pending_only=not show_all)
6093
6405
 
6094
6406
  if not suggestions:
6095
- console.print("[dim]No whitelist suggestions available.[/dim]")
6407
+ console.print("[white]No whitelist suggestions available.[/white]")
6096
6408
  return
6097
6409
 
6098
6410
  table = Table(title="Learned Whitelist Suggestions")
@@ -6110,8 +6422,8 @@ def memory_suggestions(show_all: bool):
6110
6422
  table.add_row(
6111
6423
  str(s.id),
6112
6424
  s.pattern_name,
6113
- s.tool_name or "[dim]-[/dim]",
6114
- s.path_prefix or "[dim]-[/dim]",
6425
+ s.tool_name or "[white]-[/white]",
6426
+ s.path_prefix or "[white]-[/white]",
6115
6427
  str(s.approval_count),
6116
6428
  str(s.denial_count),
6117
6429
  f"{s.confidence:.0%}",
@@ -6130,7 +6442,7 @@ def memory_accept(suggestion_id: int):
6130
6442
  store = get_memory_store()
6131
6443
  if store.review_whitelist_suggestion(suggestion_id, accepted=True):
6132
6444
  console.print(f"[bold green]Accepted[/bold green] suggestion #{suggestion_id}")
6133
- console.print(" [dim]Note: To apply to overrides.yaml, manually add the whitelist rule.[/dim]")
6445
+ console.print(" [white]Note: To apply to overrides.yaml, manually add the whitelist rule.[/white]")
6134
6446
  else:
6135
6447
  console.print(f"[red]Suggestion #{suggestion_id} not found.[/red]")
6136
6448
 
@@ -6161,7 +6473,7 @@ def memory_baseline(project_hash: Optional[str]):
6161
6473
  baselines = store.get_workflow_baseline(project_hash)
6162
6474
 
6163
6475
  if not baselines:
6164
- console.print("[dim]No workflow baseline data for this project.[/dim]")
6476
+ console.print("[white]No workflow baseline data for this project.[/white]")
6165
6477
  return
6166
6478
 
6167
6479
  table = Table(title=f"Workflow Baseline (project: {project_hash[:8]}...)")
@@ -6177,7 +6489,7 @@ def memory_baseline(project_hash: Optional[str]):
6177
6489
  pct_style = "green" if denial_pct < 0.1 else ("yellow" if denial_pct < 0.3 else "red")
6178
6490
  table.add_row(
6179
6491
  b.tool_name,
6180
- str(b.hour_of_day) if b.hour_of_day is not None else "[dim]-[/dim]",
6492
+ str(b.hour_of_day) if b.hour_of_day is not None else "[white]-[/white]",
6181
6493
  str(b.invocation_count),
6182
6494
  str(b.denied_count),
6183
6495
  f"[{pct_style}]{denial_pct:.0%}[/{pct_style}]",
@@ -6196,7 +6508,7 @@ def memory_audit(limit: int):
6196
6508
  entries = store.get_audit_log(limit=limit)
6197
6509
 
6198
6510
  if not entries:
6199
- console.print("[dim]No audit entries.[/dim]")
6511
+ console.print("[white]No audit entries.[/white]")
6200
6512
  return
6201
6513
 
6202
6514
  table = Table(title=f"Memory Audit Log (last {limit})")
@@ -6238,7 +6550,7 @@ def memory_clear(table_name: Optional[str], confirm: bool):
6238
6550
  if not confirm:
6239
6551
  target = table_name or "ALL"
6240
6552
  if not click.confirm(f"Clear {target} memory data? This cannot be undone"):
6241
- console.print("[dim]Cancelled.[/dim]")
6553
+ console.print("[white]Cancelled.[/white]")
6242
6554
  return
6243
6555
 
6244
6556
  store = get_memory_store()