tweek 0.2.1__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/__init__.py +1 -1
- tweek/cli.py +648 -435
- tweek/cli_helpers.py +6 -6
- tweek/cli_model.py +7 -7
- tweek/config/manager.py +1 -1
- tweek/diagnostics.py +59 -7
- tweek/hooks/pre_tool_use.py +3 -3
- tweek/mcp/approval_cli.py +4 -4
- tweek/sandbox/linux.py +5 -5
- tweek/skill_template/SKILL.md +2 -3
- tweek/skill_template/cli-reference.md +33 -18
- tweek/skill_template/scripts/check_installed.py +4 -4
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/METADATA +20 -14
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/RECORD +19 -18
- tweek-0.3.0.dist-info/licenses/NOTICE +199 -0
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/WHEEL +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.0.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
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/).
|
|
@@ -327,12 +293,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
327
293
|
if openclaw_status["gateway_active"]:
|
|
328
294
|
console.print(f" Gateway running on port {openclaw_status['port']}")
|
|
329
295
|
elif openclaw_status["running"]:
|
|
330
|
-
console.print(f" [
|
|
296
|
+
console.print(f" [white]Process running (gateway may start on port {openclaw_status['port']})[/white]")
|
|
331
297
|
else:
|
|
332
|
-
console.print(f" [
|
|
298
|
+
console.print(f" [white]Installed but not currently running[/white]")
|
|
333
299
|
|
|
334
300
|
if openclaw_status["config_path"]:
|
|
335
|
-
console.print(f" [
|
|
301
|
+
console.print(f" [white]Config: {openclaw_status['config_path']}[/white]")
|
|
336
302
|
|
|
337
303
|
console.print()
|
|
338
304
|
|
|
@@ -344,11 +310,11 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
344
310
|
console.print("[cyan]Tweek can protect OpenClaw tool calls. Choose a method:[/cyan]")
|
|
345
311
|
console.print()
|
|
346
312
|
console.print(" [cyan]1.[/cyan] Protect via [bold]tweek-security[/bold] ClawHub skill")
|
|
347
|
-
console.print(" [
|
|
313
|
+
console.print(" [white]Screens tool calls through Tweek as a ClawHub skill[/white]")
|
|
348
314
|
console.print(" [cyan]2.[/cyan] Protect via [bold]tweek protect openclaw[/bold]")
|
|
349
|
-
console.print(" [
|
|
315
|
+
console.print(" [white]Wraps the OpenClaw gateway with Tweek's proxy[/white]")
|
|
350
316
|
console.print(" [cyan]3.[/cyan] Skip for now")
|
|
351
|
-
console.print(" [
|
|
317
|
+
console.print(" [white]You can set up OpenClaw protection later[/white]")
|
|
352
318
|
console.print()
|
|
353
319
|
|
|
354
320
|
choice = click.prompt(
|
|
@@ -366,12 +332,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
366
332
|
proxy_override_enabled = True
|
|
367
333
|
console.print()
|
|
368
334
|
console.print("[green]✓[/green] OpenClaw proxy protection will be configured")
|
|
369
|
-
console.print(f" [
|
|
335
|
+
console.print(f" [white]Run 'tweek protect openclaw' after installation to complete setup[/white]")
|
|
370
336
|
console.print()
|
|
371
337
|
else:
|
|
372
338
|
console.print()
|
|
373
|
-
console.print("[
|
|
374
|
-
console.print("[
|
|
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]")
|
|
375
341
|
console.print()
|
|
376
342
|
|
|
377
343
|
# Check for other proxy conflicts
|
|
@@ -388,7 +354,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
388
354
|
# Proxy module not fully available, skip detection
|
|
389
355
|
pass
|
|
390
356
|
except Exception as e:
|
|
391
|
-
console.print(f"[
|
|
357
|
+
console.print(f"[white]Warning: Could not check for proxy conflicts: {e}[/white]")
|
|
392
358
|
|
|
393
359
|
# ─────────────────────────────────────────────────────────────
|
|
394
360
|
# Step 5: Install hooks into settings.json
|
|
@@ -402,7 +368,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
402
368
|
if settings_file.exists():
|
|
403
369
|
backup_path = settings_file.with_suffix(".json.tweek-backup")
|
|
404
370
|
shutil.copy(settings_file, backup_path)
|
|
405
|
-
console.print(f"[
|
|
371
|
+
console.print(f"[white]Backed up existing settings to {backup_path}[/white]")
|
|
406
372
|
|
|
407
373
|
# Create target directory
|
|
408
374
|
target.mkdir(parents=True, exist_ok=True)
|
|
@@ -477,7 +443,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
477
443
|
shutil.rmtree(skill_target)
|
|
478
444
|
shutil.copytree(skill_source, skill_target)
|
|
479
445
|
console.print(f"[green]✓[/green] Tweek skill installed to: {skill_target}")
|
|
480
|
-
console.print(f" [
|
|
446
|
+
console.print(f" [white]Claude now understands Tweek warnings and commands[/white]")
|
|
481
447
|
|
|
482
448
|
# Add whitelist entry for the skill directory in overrides
|
|
483
449
|
try:
|
|
@@ -513,12 +479,12 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
513
479
|
console.print(f"[green]✓[/green] Skill directory whitelisted in overrides")
|
|
514
480
|
|
|
515
481
|
except ImportError:
|
|
516
|
-
console.print(f"[
|
|
482
|
+
console.print(f"[white]Note: PyYAML not available — skill whitelist not added to overrides[/white]")
|
|
517
483
|
except Exception as e:
|
|
518
|
-
console.print(f"[
|
|
484
|
+
console.print(f"[white]Warning: Could not update overrides whitelist: {e}[/white]")
|
|
519
485
|
else:
|
|
520
|
-
console.print(f"[
|
|
521
|
-
console.print(f" [
|
|
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]")
|
|
522
488
|
|
|
523
489
|
# ─────────────────────────────────────────────────────────────
|
|
524
490
|
# Step 7: Security Configuration
|
|
@@ -588,7 +554,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
588
554
|
|
|
589
555
|
console.print(f"\n[green]✓[/green] Configured {len(unknown_skills)} skills")
|
|
590
556
|
else:
|
|
591
|
-
console.print("[
|
|
557
|
+
console.print("[white]All detected skills already configured[/white]")
|
|
592
558
|
|
|
593
559
|
# Apply cautious preset as base
|
|
594
560
|
cfg.apply_preset("cautious")
|
|
@@ -622,7 +588,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
622
588
|
else:
|
|
623
589
|
# Custom: ask about key tools
|
|
624
590
|
console.print("\n[bold]Configure key tools:[/bold]")
|
|
625
|
-
console.print("[
|
|
591
|
+
console.print("[white](safe/default/risky/dangerous)[/white]\n")
|
|
626
592
|
|
|
627
593
|
for tool in ["Bash", "WebFetch", "Edit"]:
|
|
628
594
|
current = cfg.get_tool_tier(tool)
|
|
@@ -641,7 +607,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
641
607
|
if not cfg.export_config("user"):
|
|
642
608
|
cfg.apply_preset("cautious")
|
|
643
609
|
console.print("\n[green]✓[/green] Applied default [bold]cautious[/bold] security preset")
|
|
644
|
-
console.print("[
|
|
610
|
+
console.print("[white]Run 'tweek config interactive' to customize[/white]")
|
|
645
611
|
install_summary["preset"] = "cautious"
|
|
646
612
|
else:
|
|
647
613
|
install_summary["preset"] = "existing"
|
|
@@ -663,7 +629,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
663
629
|
|
|
664
630
|
if env_files:
|
|
665
631
|
table = Table(title="Found .env Files")
|
|
666
|
-
table.add_column("#", style="
|
|
632
|
+
table.add_column("#", style="white")
|
|
667
633
|
table.add_column("Path")
|
|
668
634
|
table.add_column("Credentials", justify="right")
|
|
669
635
|
|
|
@@ -705,7 +671,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
705
671
|
)
|
|
706
672
|
|
|
707
673
|
# Show dry-run preview
|
|
708
|
-
console.print(f" [
|
|
674
|
+
console.print(f" [white]Preview - credentials to migrate:[/white]")
|
|
709
675
|
for key in keys:
|
|
710
676
|
console.print(f" • {key}")
|
|
711
677
|
|
|
@@ -718,9 +684,9 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
718
684
|
except Exception as e:
|
|
719
685
|
console.print(f" [red]✗[/red] Migration failed: {e}")
|
|
720
686
|
else:
|
|
721
|
-
console.print(f" [
|
|
687
|
+
console.print(f" [white]Skipped[/white]")
|
|
722
688
|
else:
|
|
723
|
-
console.print("[
|
|
689
|
+
console.print("[white]No .env files with credentials found[/white]")
|
|
724
690
|
|
|
725
691
|
# ─────────────────────────────────────────────────────────────
|
|
726
692
|
# Step 10: Linux: Prompt for firejail installation
|
|
@@ -733,8 +699,8 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
733
699
|
prompt_install_firejail(console)
|
|
734
700
|
else:
|
|
735
701
|
console.print("\n[yellow]Note:[/yellow] Sandbox (firejail) not installed.")
|
|
736
|
-
console.print(f"[
|
|
737
|
-
console.print("[
|
|
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]")
|
|
738
704
|
|
|
739
705
|
# ─────────────────────────────────────────────────────────────
|
|
740
706
|
# Step 11: Configure Tweek proxy if override was enabled
|
|
@@ -764,7 +730,7 @@ def install(install_global: bool, dev_test: bool, backup: bool, skip_env_scan: b
|
|
|
764
730
|
yaml.dump(tweek_config, f, default_flow_style=False)
|
|
765
731
|
|
|
766
732
|
console.print("\n[green]✓[/green] Proxy override configured")
|
|
767
|
-
console.print(f" [
|
|
733
|
+
console.print(f" [white]Config saved to: {proxy_config_path}[/white]")
|
|
768
734
|
console.print(" [yellow]Run 'tweek proxy start' to begin intercepting API calls[/yellow]")
|
|
769
735
|
install_summary["proxy"] = True
|
|
770
736
|
except Exception as e:
|
|
@@ -852,9 +818,9 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
|
|
|
852
818
|
console.print()
|
|
853
819
|
console.print(" [cyan]1.[/cyan] Auto-detect (recommended)")
|
|
854
820
|
if local_model_ready:
|
|
855
|
-
console.print(f" [
|
|
821
|
+
console.print(f" [white]Local model installed ({local_model_name}) — will use it first[/white]")
|
|
856
822
|
else:
|
|
857
|
-
console.print(" [
|
|
823
|
+
console.print(" [white]Uses first available: Local model > Anthropic > OpenAI > Google[/white]")
|
|
858
824
|
console.print(" [cyan]2.[/cyan] Anthropic (Claude Haiku)")
|
|
859
825
|
console.print(" [cyan]3.[/cyan] OpenAI (GPT-4o-mini)")
|
|
860
826
|
console.print(" [cyan]4.[/cyan] Google (Gemini 2.0 Flash)")
|
|
@@ -862,8 +828,8 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
|
|
|
862
828
|
console.print(" [cyan]6.[/cyan] Disable screening")
|
|
863
829
|
if not local_model_ready:
|
|
864
830
|
console.print()
|
|
865
|
-
console.print(" [
|
|
866
|
-
console.print(" [
|
|
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]")
|
|
867
833
|
console.print()
|
|
868
834
|
|
|
869
835
|
choice = click.prompt("Select", type=click.IntRange(1, 6), default=1)
|
|
@@ -883,8 +849,8 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
|
|
|
883
849
|
# Custom endpoint configuration
|
|
884
850
|
console.print()
|
|
885
851
|
console.print("[bold]Custom Endpoint Configuration[/bold]")
|
|
886
|
-
console.print("[
|
|
887
|
-
console.print("[
|
|
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]")
|
|
888
854
|
console.print()
|
|
889
855
|
|
|
890
856
|
result["provider"] = "openai"
|
|
@@ -905,7 +871,7 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
|
|
|
905
871
|
console.print()
|
|
906
872
|
elif choice == 6:
|
|
907
873
|
result["provider"] = "disabled"
|
|
908
|
-
console.print("[
|
|
874
|
+
console.print("[white]Screening disabled. Pattern matching and other layers remain active.[/white]")
|
|
909
875
|
# else: quick mode — leave as auto
|
|
910
876
|
|
|
911
877
|
# Resolve display names for summary
|
|
@@ -971,7 +937,7 @@ def _configure_llm_provider(tweek_dir: Path, interactive: bool, quick: bool) ->
|
|
|
971
937
|
else:
|
|
972
938
|
console.print(f"[green]✓[/green] LLM provider configured: {result['provider_display']}")
|
|
973
939
|
except Exception as e:
|
|
974
|
-
console.print(f"[
|
|
940
|
+
console.print(f"[white]Warning: Could not save LLM config: {e}[/white]")
|
|
975
941
|
else:
|
|
976
942
|
if result["provider_display"] and "disabled" not in (result["provider_display"] or ""):
|
|
977
943
|
console.print(f"[green]✓[/green] LLM provider: {result['provider_display']} ({result.get('model_display', 'auto')})")
|
|
@@ -1038,7 +1004,7 @@ def _validate_llm_provider(llm_config: dict) -> None:
|
|
|
1038
1004
|
expected_vars = [llm_config["api_key_env"]]
|
|
1039
1005
|
elif llm_config.get("base_url"):
|
|
1040
1006
|
# Local endpoints (Ollama etc.) don't need an API key
|
|
1041
|
-
console.print(f" [
|
|
1007
|
+
console.print(f" [white]Checking endpoint: {llm_config['base_url']}[/white]")
|
|
1042
1008
|
try:
|
|
1043
1009
|
from tweek.security.llm_reviewer import resolve_provider
|
|
1044
1010
|
test_provider = resolve_provider(
|
|
@@ -1051,10 +1017,10 @@ def _validate_llm_provider(llm_config: dict) -> None:
|
|
|
1051
1017
|
console.print(f" [green]✓[/green] Endpoint reachable")
|
|
1052
1018
|
else:
|
|
1053
1019
|
console.print(f" [yellow]⚠[/yellow] Could not verify endpoint")
|
|
1054
|
-
console.print(f" [
|
|
1020
|
+
console.print(f" [white]Tweek will try this endpoint at runtime[/white]")
|
|
1055
1021
|
except Exception:
|
|
1056
1022
|
console.print(f" [yellow]⚠[/yellow] Could not verify endpoint")
|
|
1057
|
-
console.print(f" [
|
|
1023
|
+
console.print(f" [white]Tweek will try this endpoint at runtime[/white]")
|
|
1058
1024
|
return
|
|
1059
1025
|
else:
|
|
1060
1026
|
expected_vars = env_var_map.get(provider, [])
|
|
@@ -1073,8 +1039,8 @@ def _validate_llm_provider(llm_config: dict) -> None:
|
|
|
1073
1039
|
if not found_key:
|
|
1074
1040
|
var_list = " or ".join(expected_vars)
|
|
1075
1041
|
console.print(f" [yellow]⚠[/yellow] {var_list} not set in environment")
|
|
1076
|
-
console.print(f" [
|
|
1077
|
-
console.print(f" [
|
|
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]")
|
|
1078
1044
|
|
|
1079
1045
|
# Offer fallback
|
|
1080
1046
|
console.print()
|
|
@@ -1093,7 +1059,7 @@ def _validate_llm_provider(llm_config: dict) -> None:
|
|
|
1093
1059
|
else:
|
|
1094
1060
|
llm_config["provider_display"] = "disabled (no API key found)"
|
|
1095
1061
|
llm_config["model_display"] = None
|
|
1096
|
-
console.print(f" [
|
|
1062
|
+
console.print(f" [white]No API keys found — LLM review will be disabled[/white]")
|
|
1097
1063
|
|
|
1098
1064
|
|
|
1099
1065
|
def _print_install_summary(
|
|
@@ -1133,7 +1099,7 @@ def _print_install_summary(
|
|
|
1133
1099
|
console.print(f" [green]✓[/green] Hook Python: {hook_python}")
|
|
1134
1100
|
else:
|
|
1135
1101
|
console.print(f" [yellow]⚠[/yellow] Hook Python not found: {hook_python}")
|
|
1136
|
-
console.print(f" [
|
|
1102
|
+
console.print(f" [white]Run 'tweek install' again if Python was reinstalled[/white]")
|
|
1137
1103
|
except (IndexError, KeyError):
|
|
1138
1104
|
pass
|
|
1139
1105
|
elif has_pre:
|
|
@@ -1169,14 +1135,14 @@ def _print_install_summary(
|
|
|
1169
1135
|
elif llm_display and "disabled" not in llm_display:
|
|
1170
1136
|
console.print(f" [green]✓[/green] LLM reviewer: {llm_display}")
|
|
1171
1137
|
else:
|
|
1172
|
-
console.print(f" [
|
|
1138
|
+
console.print(f" [white]○[/white] LLM reviewer: {llm_display}")
|
|
1173
1139
|
|
|
1174
1140
|
# Sandbox status
|
|
1175
1141
|
caps = get_capabilities()
|
|
1176
1142
|
if caps.sandbox_available:
|
|
1177
1143
|
console.print(f" [green]✓[/green] Sandbox: {caps.sandbox_tool}")
|
|
1178
1144
|
else:
|
|
1179
|
-
console.print(f" [
|
|
1145
|
+
console.print(f" [white]○[/white] Sandbox: not available ({caps.platform.value})")
|
|
1180
1146
|
|
|
1181
1147
|
# Summary table
|
|
1182
1148
|
console.print()
|
|
@@ -1204,34 +1170,37 @@ def _print_install_summary(
|
|
|
1204
1170
|
|
|
1205
1171
|
# Next steps
|
|
1206
1172
|
console.print()
|
|
1207
|
-
console.print("[
|
|
1208
|
-
console.print("[
|
|
1209
|
-
console.print("[
|
|
1210
|
-
console.print("[
|
|
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]")
|
|
1211
1177
|
if proxy_override_enabled:
|
|
1212
|
-
console.print("[
|
|
1178
|
+
console.print("[white] tweek proxy start — Enable API interception[/white]")
|
|
1213
1179
|
|
|
1214
1180
|
|
|
1215
1181
|
@main.command(
|
|
1216
1182
|
epilog="""\b
|
|
1217
1183
|
Examples:
|
|
1218
|
-
tweek
|
|
1219
|
-
tweek
|
|
1220
|
-
tweek
|
|
1221
|
-
tweek
|
|
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
|
|
1222
1189
|
"""
|
|
1223
1190
|
)
|
|
1224
|
-
@click.
|
|
1225
|
-
|
|
1226
|
-
@click.option("--
|
|
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,
|
|
1227
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)")
|
|
1228
1197
|
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
1229
|
-
def
|
|
1230
|
-
"""Remove Tweek
|
|
1198
|
+
def unprotect(tool: str, remove_all: bool, unprotect_global: bool, confirm: bool):
|
|
1199
|
+
"""Remove Tweek protection from an AI tool.
|
|
1231
1200
|
|
|
1232
|
-
When run without
|
|
1233
|
-
|
|
1234
|
-
Use --
|
|
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.
|
|
1235
1204
|
|
|
1236
1205
|
This command can only be run from an interactive terminal.
|
|
1237
1206
|
AI agents are blocked from running it.
|
|
@@ -1243,64 +1212,71 @@ def uninstall(uninstall_global: bool, everything: bool, confirm: bool):
|
|
|
1243
1212
|
# This is Layer 2 of protection (Layer 1 is the PreToolUse hook)
|
|
1244
1213
|
# ─────────────────────────────────────────────────────────────
|
|
1245
1214
|
if not sys.stdin.isatty():
|
|
1246
|
-
console.print("[red]ERROR: tweek
|
|
1247
|
-
console.print("[
|
|
1248
|
-
console.print("[
|
|
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]")
|
|
1249
1218
|
raise SystemExit(1)
|
|
1250
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
|
+
|
|
1251
1225
|
console.print(TWEEK_BANNER, style="cyan")
|
|
1252
1226
|
|
|
1253
1227
|
tweek_dir = Path("~/.tweek").expanduser()
|
|
1254
1228
|
global_target = Path("~/.claude").expanduser()
|
|
1255
1229
|
project_target = Path.cwd() / ".claude"
|
|
1256
1230
|
|
|
1257
|
-
if
|
|
1231
|
+
if remove_all:
|
|
1258
1232
|
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
else:
|
|
1262
|
-
# ── Interactive scope selection ──
|
|
1263
|
-
# Detect what's installed
|
|
1264
|
-
has_project = _has_tweek_at(project_target)
|
|
1265
|
-
has_global = _has_tweek_at(global_target)
|
|
1266
|
-
has_data = tweek_dir.exists() and any(tweek_dir.iterdir()) if tweek_dir.exists() else False
|
|
1267
|
-
|
|
1268
|
-
if not has_project and not has_global and not has_data:
|
|
1269
|
-
console.print("[yellow]No Tweek installation found.[/yellow]")
|
|
1270
|
-
console.print(f" Checked project: {project_target}")
|
|
1271
|
-
console.print(f" Checked global: {global_target}")
|
|
1272
|
-
console.print(f" Checked data: {tweek_dir}")
|
|
1273
|
-
_show_package_removal_hint()
|
|
1274
|
-
return
|
|
1233
|
+
_show_package_removal_hint()
|
|
1234
|
+
return
|
|
1275
1235
|
|
|
1276
|
-
|
|
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
|
|
1277
1243
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
|
1285
1262
|
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
|
1288
1268
|
|
|
1289
|
-
|
|
1290
|
-
choice = click.prompt("Select", type=click.IntRange(1, len(options)), default=len(options))
|
|
1269
|
+
_show_package_removal_hint()
|
|
1291
1270
|
|
|
1292
|
-
selected = options[choice - 1][0]
|
|
1293
|
-
console.print()
|
|
1294
1271
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
_uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
|
|
1299
|
-
elif selected == "everything":
|
|
1300
|
-
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
1272
|
+
@main.command()
|
|
1273
|
+
def status():
|
|
1274
|
+
"""Show Tweek protection status dashboard.
|
|
1301
1275
|
|
|
1302
|
-
|
|
1303
|
-
|
|
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()
|
|
1304
1280
|
|
|
1305
1281
|
|
|
1306
1282
|
# ─────────────────────────────────────────────────────────────
|
|
@@ -1360,9 +1336,9 @@ def _show_package_removal_hint():
|
|
|
1360
1336
|
console.print("[bold yellow]The tweek CLI binary is still installed on your system.[/bold yellow]")
|
|
1361
1337
|
|
|
1362
1338
|
if len(pkg_cmds) > 1:
|
|
1363
|
-
console.print(f"[
|
|
1339
|
+
console.print(f"[white]Found {len(pkg_cmds)} installations:[/white]")
|
|
1364
1340
|
for cmd in pkg_cmds:
|
|
1365
|
-
console.print(f" [
|
|
1341
|
+
console.print(f" [white]• {cmd}[/white]")
|
|
1366
1342
|
|
|
1367
1343
|
console.print()
|
|
1368
1344
|
label = " + ".join(f"[bold]{cmd}[/bold]" for cmd in pkg_cmds)
|
|
@@ -1383,10 +1359,10 @@ def _show_package_removal_hint():
|
|
|
1383
1359
|
console.print(f"[green]✓[/green] Removed ({pkg_cmd})")
|
|
1384
1360
|
else:
|
|
1385
1361
|
console.print(f"[red]✗[/red] Failed: {result.stderr.strip()}")
|
|
1386
|
-
console.print(f" [
|
|
1362
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
1387
1363
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
1388
1364
|
console.print(f"[red]✗[/red] Could not run: {e}")
|
|
1389
|
-
console.print(f" [
|
|
1365
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
1390
1366
|
|
|
1391
1367
|
|
|
1392
1368
|
def _has_tweek_at(target: Path) -> bool:
|
|
@@ -1620,18 +1596,18 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1620
1596
|
console.print()
|
|
1621
1597
|
console.print("[bold]The following will be removed:[/bold]")
|
|
1622
1598
|
if has_hooks:
|
|
1623
|
-
console.print(" [
|
|
1599
|
+
console.print(" [white]•[/white] PreToolUse and PostToolUse hooks from settings.json")
|
|
1624
1600
|
if has_skills:
|
|
1625
|
-
console.print(" [
|
|
1601
|
+
console.print(" [white]•[/white] Tweek skill directory (skills/tweek/)")
|
|
1626
1602
|
if has_backup:
|
|
1627
|
-
console.print(" [
|
|
1628
|
-
console.print(" [
|
|
1603
|
+
console.print(" [white]•[/white] Backup file (settings.json.tweek-backup)")
|
|
1604
|
+
console.print(" [white]•[/white] Project whitelist entries from overrides")
|
|
1629
1605
|
console.print()
|
|
1630
1606
|
|
|
1631
1607
|
if not confirm:
|
|
1632
1608
|
console.print(f"[yellow]Remove Tweek from this {scope_label}?[/yellow] ", end="")
|
|
1633
1609
|
if not click.confirm(""):
|
|
1634
|
-
console.print("[
|
|
1610
|
+
console.print("[white]Cancelled[/white]")
|
|
1635
1611
|
return
|
|
1636
1612
|
|
|
1637
1613
|
console.print()
|
|
@@ -1647,27 +1623,27 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1647
1623
|
if _remove_skill_directory(target):
|
|
1648
1624
|
console.print(f" [green]✓[/green] Removed Tweek skill directory (skills/tweek/)")
|
|
1649
1625
|
else:
|
|
1650
|
-
console.print(f" [
|
|
1626
|
+
console.print(f" [white]-[/white] Skipped: Tweek skill directory not found")
|
|
1651
1627
|
|
|
1652
1628
|
# 3. Remove backup file
|
|
1653
1629
|
if _remove_backup_file(target):
|
|
1654
1630
|
console.print(f" [green]✓[/green] Removed backup file (settings.json.tweek-backup)")
|
|
1655
1631
|
else:
|
|
1656
|
-
console.print(f" [
|
|
1632
|
+
console.print(f" [white]-[/white] Skipped: no backup file found")
|
|
1657
1633
|
|
|
1658
1634
|
# 4. Remove whitelist entries
|
|
1659
1635
|
wl_count = _remove_whitelist_entries(target, tweek_dir)
|
|
1660
1636
|
if wl_count > 0:
|
|
1661
1637
|
console.print(f" [green]✓[/green] Removed {wl_count} whitelist entry(s) from overrides")
|
|
1662
1638
|
else:
|
|
1663
|
-
console.print(f" [
|
|
1639
|
+
console.print(f" [white]-[/white] Skipped: no whitelist entries found for this {scope_label}")
|
|
1664
1640
|
|
|
1665
1641
|
console.print()
|
|
1666
1642
|
console.print(f"[green]Uninstall complete.[/green] Tweek is no longer active for this {scope_label}.")
|
|
1667
1643
|
if scope_label == "project":
|
|
1668
|
-
console.print("[
|
|
1644
|
+
console.print("[white]Global installation (~/.claude/) was not affected.[/white]")
|
|
1669
1645
|
else:
|
|
1670
|
-
console.print("[
|
|
1646
|
+
console.print("[white]Project installations were not affected.[/white]")
|
|
1671
1647
|
|
|
1672
1648
|
# Offer to remove data directory
|
|
1673
1649
|
if tweek_dir.exists() and not confirm:
|
|
@@ -1680,7 +1656,7 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1680
1656
|
|
|
1681
1657
|
console.print()
|
|
1682
1658
|
console.print("[yellow]Also remove Tweek data directory (~/.tweek/)?[/yellow]")
|
|
1683
|
-
console.print("[
|
|
1659
|
+
console.print("[white]This contains config, patterns, security logs, and overrides.[/white]")
|
|
1684
1660
|
if other_has_tweek:
|
|
1685
1661
|
console.print(f"[bold red]Warning:[/bold red] Tweek is still installed at {other_label} scope ({other_target}).")
|
|
1686
1662
|
console.print(f" Removing ~/.tweek/ will affect that installation (no config, patterns, or logs).")
|
|
@@ -1695,9 +1671,9 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1695
1671
|
for item in data_removed:
|
|
1696
1672
|
console.print(f" [green]✓[/green] Removed {item}")
|
|
1697
1673
|
if not data_removed:
|
|
1698
|
-
console.print(f" [
|
|
1674
|
+
console.print(f" [white]-[/white] No data to remove")
|
|
1699
1675
|
elif tweek_dir.exists():
|
|
1700
|
-
console.print("[
|
|
1676
|
+
console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
|
|
1701
1677
|
|
|
1702
1678
|
|
|
1703
1679
|
def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
|
|
@@ -1705,28 +1681,28 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1705
1681
|
import json
|
|
1706
1682
|
|
|
1707
1683
|
console.print("[bold yellow]FULL REMOVAL[/bold yellow] — This will remove ALL Tweek data:\n")
|
|
1708
|
-
console.print(" [
|
|
1709
|
-
console.print(" [
|
|
1710
|
-
console.print(" [
|
|
1711
|
-
console.print(" [
|
|
1712
|
-
console.print(" [
|
|
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/)")
|
|
1713
1689
|
|
|
1714
1690
|
# Show what exists in ~/.tweek/
|
|
1715
1691
|
if tweek_dir.exists():
|
|
1716
1692
|
for item in sorted(tweek_dir.iterdir()):
|
|
1717
1693
|
if item.is_dir():
|
|
1718
|
-
console.print(f" [
|
|
1694
|
+
console.print(f" [white]├── {item.name}/ [/white]")
|
|
1719
1695
|
else:
|
|
1720
|
-
console.print(f" [
|
|
1696
|
+
console.print(f" [white]├── {item.name}[/white]")
|
|
1721
1697
|
|
|
1722
|
-
console.print(" [
|
|
1698
|
+
console.print(" [white]•[/white] MCP integrations (Claude Desktop, ChatGPT)")
|
|
1723
1699
|
console.print()
|
|
1724
1700
|
|
|
1725
1701
|
if not confirm:
|
|
1726
1702
|
console.print("[bold red]Type 'yes' to confirm full removal[/bold red]: ", end="")
|
|
1727
1703
|
response = input()
|
|
1728
1704
|
if response.strip().lower() != "yes":
|
|
1729
|
-
console.print("[
|
|
1705
|
+
console.print("[white]Cancelled[/white]")
|
|
1730
1706
|
return
|
|
1731
1707
|
|
|
1732
1708
|
console.print()
|
|
@@ -1737,17 +1713,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1737
1713
|
for hook_type in removed_hooks:
|
|
1738
1714
|
console.print(f" [green]✓[/green] Removed {hook_type} hook from project settings.json")
|
|
1739
1715
|
if not removed_hooks:
|
|
1740
|
-
console.print(f" [
|
|
1716
|
+
console.print(f" [white]-[/white] Skipped: no project hooks found")
|
|
1741
1717
|
|
|
1742
1718
|
if _remove_skill_directory(project_target):
|
|
1743
1719
|
console.print(f" [green]✓[/green] Removed Tweek skill from project")
|
|
1744
1720
|
else:
|
|
1745
|
-
console.print(f" [
|
|
1721
|
+
console.print(f" [white]-[/white] Skipped: no project skill directory")
|
|
1746
1722
|
|
|
1747
1723
|
if _remove_backup_file(project_target):
|
|
1748
1724
|
console.print(f" [green]✓[/green] Removed project backup file")
|
|
1749
1725
|
else:
|
|
1750
|
-
console.print(f" [
|
|
1726
|
+
console.print(f" [white]-[/white] Skipped: no project backup file")
|
|
1751
1727
|
|
|
1752
1728
|
console.print()
|
|
1753
1729
|
|
|
@@ -1757,17 +1733,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1757
1733
|
for hook_type in removed_hooks:
|
|
1758
1734
|
console.print(f" [green]✓[/green] Removed {hook_type} hook from global settings.json")
|
|
1759
1735
|
if not removed_hooks:
|
|
1760
|
-
console.print(f" [
|
|
1736
|
+
console.print(f" [white]-[/white] Skipped: no global hooks found")
|
|
1761
1737
|
|
|
1762
1738
|
if _remove_skill_directory(global_target):
|
|
1763
1739
|
console.print(f" [green]✓[/green] Removed Tweek skill from global installation")
|
|
1764
1740
|
else:
|
|
1765
|
-
console.print(f" [
|
|
1741
|
+
console.print(f" [white]-[/white] Skipped: no global skill directory")
|
|
1766
1742
|
|
|
1767
1743
|
if _remove_backup_file(global_target):
|
|
1768
1744
|
console.print(f" [green]✓[/green] Removed global backup file")
|
|
1769
1745
|
else:
|
|
1770
|
-
console.print(f" [
|
|
1746
|
+
console.print(f" [white]-[/white] Skipped: no global backup file")
|
|
1771
1747
|
|
|
1772
1748
|
console.print()
|
|
1773
1749
|
|
|
@@ -1777,7 +1753,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1777
1753
|
for item in data_removed:
|
|
1778
1754
|
console.print(f" [green]✓[/green] Removed {item}")
|
|
1779
1755
|
if not data_removed:
|
|
1780
|
-
console.print(f" [
|
|
1756
|
+
console.print(f" [white]-[/white] Skipped: no data directory found")
|
|
1781
1757
|
|
|
1782
1758
|
console.print()
|
|
1783
1759
|
|
|
@@ -1787,7 +1763,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1787
1763
|
for client in mcp_removed:
|
|
1788
1764
|
console.print(f" [green]✓[/green] Removed {client} MCP integration")
|
|
1789
1765
|
if not mcp_removed:
|
|
1790
|
-
console.print(f" [
|
|
1766
|
+
console.print(f" [white]-[/white] Skipped: no MCP integrations found")
|
|
1791
1767
|
|
|
1792
1768
|
console.print()
|
|
1793
1769
|
console.print("[green]All Tweek data has been removed.[/green]")
|
|
@@ -1866,8 +1842,8 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1866
1842
|
]
|
|
1867
1843
|
|
|
1868
1844
|
if not whitelist:
|
|
1869
|
-
console.print("[
|
|
1870
|
-
console.print("[
|
|
1845
|
+
console.print("[white]No trusted paths configured.[/white]")
|
|
1846
|
+
console.print("[white]Use 'tweek trust' to trust the current project.[/white]")
|
|
1871
1847
|
return
|
|
1872
1848
|
|
|
1873
1849
|
if trusted_entries:
|
|
@@ -1876,16 +1852,16 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1876
1852
|
entry_reason = entry.get("reason", "")
|
|
1877
1853
|
console.print(f" [green]✓[/green] {entry['path']}")
|
|
1878
1854
|
if entry_reason:
|
|
1879
|
-
console.print(f" [
|
|
1855
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1880
1856
|
|
|
1881
1857
|
if tool_scoped:
|
|
1882
1858
|
console.print("\n[bold]Tool-scoped whitelist entries:[/bold]\n")
|
|
1883
1859
|
for entry in tool_scoped:
|
|
1884
1860
|
tools = ", ".join(entry.get("tools", []))
|
|
1885
1861
|
entry_reason = entry.get("reason", "")
|
|
1886
|
-
console.print(f" [cyan]○[/cyan] {entry['path']} [
|
|
1862
|
+
console.print(f" [cyan]○[/cyan] {entry['path']} [white]({tools})[/white]")
|
|
1887
1863
|
if entry_reason:
|
|
1888
|
-
console.print(f" [
|
|
1864
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1889
1865
|
|
|
1890
1866
|
if other_entries:
|
|
1891
1867
|
console.print("\n[bold]Other whitelist entries:[/bold]\n")
|
|
@@ -1896,9 +1872,9 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1896
1872
|
console.print(f" [cyan]○[/cyan] Command: {entry['command_prefix']}")
|
|
1897
1873
|
entry_reason = entry.get("reason", "")
|
|
1898
1874
|
if entry_reason:
|
|
1899
|
-
console.print(f" [
|
|
1875
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1900
1876
|
|
|
1901
|
-
console.print(f"\n[
|
|
1877
|
+
console.print(f"\n[white]Config: {overrides_path}[/white]")
|
|
1902
1878
|
return
|
|
1903
1879
|
|
|
1904
1880
|
# Resolve path to absolute
|
|
@@ -1915,7 +1891,7 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1915
1891
|
|
|
1916
1892
|
if already_trusted:
|
|
1917
1893
|
console.print(f"[green]✓[/green] Already trusted: {resolved}")
|
|
1918
|
-
console.print("[
|
|
1894
|
+
console.print("[white]Use 'tweek untrust' to remove.[/white]")
|
|
1919
1895
|
return
|
|
1920
1896
|
|
|
1921
1897
|
# Add whitelist entry (no tools restriction = all tools exempt)
|
|
@@ -1933,8 +1909,8 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1933
1909
|
return
|
|
1934
1910
|
|
|
1935
1911
|
console.print(f"[green]✓[/green] Trusted: {resolved}")
|
|
1936
|
-
console.print(f" [
|
|
1937
|
-
console.print(f" [
|
|
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]")
|
|
1938
1914
|
|
|
1939
1915
|
|
|
1940
1916
|
@main.command(
|
|
@@ -1982,7 +1958,7 @@ def untrust(path: str):
|
|
|
1982
1958
|
|
|
1983
1959
|
if len(whitelist) == original_len:
|
|
1984
1960
|
console.print(f"[yellow]This path is not currently trusted:[/yellow] {resolved}")
|
|
1985
|
-
console.print("[
|
|
1961
|
+
console.print("[white]Use 'tweek trust --list' to see all trusted paths.[/white]")
|
|
1986
1962
|
return
|
|
1987
1963
|
|
|
1988
1964
|
overrides["whitelist"] = whitelist
|
|
@@ -1998,7 +1974,7 @@ def untrust(path: str):
|
|
|
1998
1974
|
return
|
|
1999
1975
|
|
|
2000
1976
|
console.print(f"[green]✓[/green] Removed trust: {resolved}")
|
|
2001
|
-
console.print(f" [
|
|
1977
|
+
console.print(f" [white]Tweek will now screen tool calls for files in this directory.[/white]")
|
|
2002
1978
|
|
|
2003
1979
|
|
|
2004
1980
|
@main.command(
|
|
@@ -2029,7 +2005,7 @@ def update(check: bool):
|
|
|
2029
2005
|
# First time: clone the repo
|
|
2030
2006
|
if check:
|
|
2031
2007
|
console.print("[yellow]Patterns not installed.[/yellow]")
|
|
2032
|
-
console.print(f"[
|
|
2008
|
+
console.print(f"[white]Run 'tweek update' to install from {patterns_repo}[/white]")
|
|
2033
2009
|
return
|
|
2034
2010
|
|
|
2035
2011
|
console.print(f"[cyan]Installing patterns from {patterns_repo}...[/cyan]")
|
|
@@ -2051,15 +2027,15 @@ def update(check: bool):
|
|
|
2051
2027
|
data = yaml.safe_load(f)
|
|
2052
2028
|
count = data.get("pattern_count", len(data.get("patterns", [])))
|
|
2053
2029
|
free_max = data.get("free_tier_max", 23)
|
|
2054
|
-
console.print(f"[
|
|
2030
|
+
console.print(f"[white]Installed {count} patterns ({free_max} free, {count - free_max} pro)[/white]")
|
|
2055
2031
|
|
|
2056
2032
|
except subprocess.CalledProcessError as e:
|
|
2057
2033
|
console.print(f"[red]✗[/red] Failed to clone patterns: {e.stderr}")
|
|
2058
2034
|
return
|
|
2059
2035
|
except FileNotFoundError:
|
|
2060
2036
|
console.print("[red]\u2717[/red] git not found.")
|
|
2061
|
-
console.print(" [
|
|
2062
|
-
console.print(" [
|
|
2037
|
+
console.print(" [white]Hint: Install git from https://git-scm.com/downloads[/white]")
|
|
2038
|
+
console.print(" [white]On macOS: xcode-select --install[/white]")
|
|
2063
2039
|
return
|
|
2064
2040
|
|
|
2065
2041
|
else:
|
|
@@ -2080,7 +2056,7 @@ def update(check: bool):
|
|
|
2080
2056
|
)
|
|
2081
2057
|
if "behind" in result2.stdout:
|
|
2082
2058
|
console.print("[yellow]Updates available.[/yellow]")
|
|
2083
|
-
console.print("[
|
|
2059
|
+
console.print("[white]Run 'tweek update' to install[/white]")
|
|
2084
2060
|
else:
|
|
2085
2061
|
console.print("[green]✓[/green] Patterns are up to date")
|
|
2086
2062
|
except Exception as e:
|
|
@@ -2104,11 +2080,11 @@ def update(check: bool):
|
|
|
2104
2080
|
|
|
2105
2081
|
# Show what changed
|
|
2106
2082
|
if result.stdout.strip():
|
|
2107
|
-
console.print(f"[
|
|
2083
|
+
console.print(f"[white]{result.stdout.strip()}[/white]")
|
|
2108
2084
|
|
|
2109
2085
|
except subprocess.CalledProcessError as e:
|
|
2110
2086
|
console.print(f"[red]✗[/red] Failed to update patterns: {e.stderr}")
|
|
2111
|
-
console.print("[
|
|
2087
|
+
console.print("[white]Try: rm -rf ~/.tweek/patterns && tweek update[/white]")
|
|
2112
2088
|
return
|
|
2113
2089
|
|
|
2114
2090
|
# Show current version info
|
|
@@ -2126,7 +2102,7 @@ def update(check: bool):
|
|
|
2126
2102
|
console.print(f"[cyan]Total patterns:[/cyan] {count} (all included free)")
|
|
2127
2103
|
|
|
2128
2104
|
console.print(f"[cyan]All features:[/cyan] LLM review, session analysis, rate limiting, sandbox (open source)")
|
|
2129
|
-
console.print(f"[
|
|
2105
|
+
console.print(f"[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
|
|
2130
2106
|
|
|
2131
2107
|
except Exception:
|
|
2132
2108
|
pass
|
|
@@ -2159,22 +2135,6 @@ def doctor(verbose: bool, json_out: bool):
|
|
|
2159
2135
|
print_doctor_results(checks)
|
|
2160
2136
|
|
|
2161
2137
|
|
|
2162
|
-
# `tweek status` — alias for `tweek doctor`
|
|
2163
|
-
@main.command("status")
|
|
2164
|
-
@click.option("--verbose", "-v", is_flag=True, help="Show detailed check information")
|
|
2165
|
-
@click.option("--json-output", "--json", "json_out", is_flag=True, help="Output results as JSON")
|
|
2166
|
-
def status(verbose: bool, json_out: bool):
|
|
2167
|
-
"""Show Tweek protection status (alias for 'tweek doctor')."""
|
|
2168
|
-
from tweek.diagnostics import run_health_checks
|
|
2169
|
-
from tweek.cli_helpers import print_doctor_results, print_doctor_json
|
|
2170
|
-
|
|
2171
|
-
checks = run_health_checks(verbose=verbose)
|
|
2172
|
-
|
|
2173
|
-
if json_out:
|
|
2174
|
-
print_doctor_json(checks)
|
|
2175
|
-
else:
|
|
2176
|
-
print_doctor_results(checks)
|
|
2177
|
-
|
|
2178
2138
|
|
|
2179
2139
|
@main.command("upgrade")
|
|
2180
2140
|
def upgrade():
|
|
@@ -2340,8 +2300,8 @@ def audit(path, translate, llm_review, json_out):
|
|
|
2340
2300
|
skills = scan_installed_skills()
|
|
2341
2301
|
|
|
2342
2302
|
if not skills:
|
|
2343
|
-
console.print("[
|
|
2344
|
-
console.print("[
|
|
2303
|
+
console.print("[white]No installed skills found.[/white]")
|
|
2304
|
+
console.print("[white]Specify a file path to audit: tweek audit <path>[/white]")
|
|
2345
2305
|
return
|
|
2346
2306
|
|
|
2347
2307
|
console.print(f"Found {len(skills)} skill(s)")
|
|
@@ -2390,7 +2350,7 @@ def _print_audit_result(result):
|
|
|
2390
2350
|
risk_icons = {"safe": "[green]SAFE[/green]", "suspicious": "[yellow]SUSPICIOUS[/yellow]", "dangerous": "[red]DANGEROUS[/red]"}
|
|
2391
2351
|
|
|
2392
2352
|
console.print(f" [bold]{result.skill_name}[/bold] — {risk_icons.get(result.risk_level, result.risk_level)}")
|
|
2393
|
-
console.print(f" [
|
|
2353
|
+
console.print(f" [white]{result.skill_path}[/white]")
|
|
2394
2354
|
|
|
2395
2355
|
if result.error:
|
|
2396
2356
|
console.print(f" [red]Error: {result.error}[/red]")
|
|
@@ -2405,12 +2365,12 @@ def _print_audit_result(result):
|
|
|
2405
2365
|
|
|
2406
2366
|
if result.findings:
|
|
2407
2367
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
2408
|
-
table.add_column("Severity", style="
|
|
2368
|
+
table.add_column("Severity", style="white")
|
|
2409
2369
|
table.add_column("Pattern")
|
|
2410
2370
|
table.add_column("Description")
|
|
2411
|
-
table.add_column("Match", style="
|
|
2371
|
+
table.add_column("Match", style="white")
|
|
2412
2372
|
|
|
2413
|
-
severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "
|
|
2373
|
+
severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "white"}
|
|
2414
2374
|
|
|
2415
2375
|
for finding in result.findings:
|
|
2416
2376
|
table.add_row(
|
|
@@ -2509,7 +2469,7 @@ def quickstart():
|
|
|
2509
2469
|
# Step 2: Security preset
|
|
2510
2470
|
console.print("[bold cyan]Step 2/4: Security Preset[/bold cyan]")
|
|
2511
2471
|
console.print(" [cyan]1.[/cyan] paranoid \u2014 Block everything suspicious, prompt on risky")
|
|
2512
|
-
console.print(" [cyan]2.[/cyan] cautious \u2014 Block dangerous, prompt on risky [
|
|
2472
|
+
console.print(" [cyan]2.[/cyan] cautious \u2014 Block dangerous, prompt on risky [white](recommended)[/white]")
|
|
2513
2473
|
console.print(" [cyan]3.[/cyan] trusted \u2014 Allow most operations, block only dangerous")
|
|
2514
2474
|
console.print()
|
|
2515
2475
|
|
|
@@ -2548,17 +2508,16 @@ def quickstart():
|
|
|
2548
2508
|
if setup_mcp:
|
|
2549
2509
|
try:
|
|
2550
2510
|
import mcp # noqa: F401
|
|
2551
|
-
console.print("[
|
|
2552
|
-
console.print("[
|
|
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]")
|
|
2553
2513
|
except ImportError:
|
|
2554
2514
|
print_warning("MCP package not installed. Install with: pip install tweek[mcp]")
|
|
2555
2515
|
else:
|
|
2556
|
-
console.print("[
|
|
2516
|
+
console.print("[white]Skipped.[/white]")
|
|
2557
2517
|
|
|
2558
2518
|
console.print()
|
|
2559
2519
|
console.print("[bold green]Setup complete![/bold green]")
|
|
2560
2520
|
console.print(" Run [cyan]tweek doctor[/cyan] to verify your installation")
|
|
2561
|
-
console.print(" Run [cyan]tweek status[/cyan] to see protection status")
|
|
2562
2521
|
|
|
2563
2522
|
|
|
2564
2523
|
def _quickstart_install_hooks(scope: str) -> None:
|
|
@@ -2626,21 +2585,31 @@ def _quickstart_install_hooks(scope: str) -> None:
|
|
|
2626
2585
|
# =============================================================================
|
|
2627
2586
|
|
|
2628
2587
|
@main.group(
|
|
2588
|
+
invoke_without_command=True,
|
|
2629
2589
|
epilog="""\b
|
|
2630
2590
|
Examples:
|
|
2631
|
-
tweek protect
|
|
2632
|
-
tweek protect
|
|
2633
|
-
tweek protect
|
|
2634
|
-
tweek protect
|
|
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
|
|
2635
2598
|
"""
|
|
2636
2599
|
)
|
|
2637
|
-
|
|
2638
|
-
|
|
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.
|
|
2639
2604
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2605
|
+
When run without a subcommand, launches an interactive wizard
|
|
2606
|
+
that auto-detects installed AI tools and offers to protect them.
|
|
2642
2607
|
"""
|
|
2643
|
-
|
|
2608
|
+
if status:
|
|
2609
|
+
_show_protection_status()
|
|
2610
|
+
return
|
|
2611
|
+
if ctx.invoked_subcommand is None:
|
|
2612
|
+
_run_protect_wizard()
|
|
2644
2613
|
|
|
2645
2614
|
|
|
2646
2615
|
@protect.command(
|
|
@@ -2688,11 +2657,11 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2688
2657
|
console.print()
|
|
2689
2658
|
console.print("[red]OpenClaw not detected on this system.[/red]")
|
|
2690
2659
|
console.print()
|
|
2691
|
-
console.print("[
|
|
2660
|
+
console.print("[white]Install OpenClaw first:[/white]")
|
|
2692
2661
|
console.print(" npm install -g openclaw")
|
|
2693
2662
|
console.print()
|
|
2694
|
-
console.print("[
|
|
2695
|
-
console.print("[
|
|
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]")
|
|
2696
2665
|
console.print(" tweek protect openclaw --port 18789")
|
|
2697
2666
|
return
|
|
2698
2667
|
|
|
@@ -2709,7 +2678,7 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2709
2678
|
elif openclaw["process_running"]:
|
|
2710
2679
|
console.print(" [yellow](process running, gateway inactive)[/yellow]")
|
|
2711
2680
|
else:
|
|
2712
|
-
console.print(" [
|
|
2681
|
+
console.print(" [white](not running)[/white]")
|
|
2713
2682
|
|
|
2714
2683
|
if openclaw["config_path"]:
|
|
2715
2684
|
console.print(f" Config: {openclaw['config_path']}")
|
|
@@ -2733,7 +2702,7 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2733
2702
|
if anthropic_key:
|
|
2734
2703
|
console.print(" LLM Review: [green]active[/green] (ANTHROPIC_API_KEY found)")
|
|
2735
2704
|
else:
|
|
2736
|
-
console.print(" LLM Review: [
|
|
2705
|
+
console.print(" LLM Review: [white]available (set ANTHROPIC_API_KEY for semantic analysis)[/white]")
|
|
2737
2706
|
|
|
2738
2707
|
# Show warnings
|
|
2739
2708
|
for warning in result.warnings:
|
|
@@ -2743,53 +2712,366 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2743
2712
|
|
|
2744
2713
|
if not openclaw["gateway_active"]:
|
|
2745
2714
|
console.print("[yellow]Note: OpenClaw gateway is not currently running.[/yellow]")
|
|
2746
|
-
console.print("[
|
|
2715
|
+
console.print("[white]Protection will activate when OpenClaw starts.[/white]")
|
|
2747
2716
|
console.print()
|
|
2748
2717
|
|
|
2749
2718
|
console.print("[green]Protection configured.[/green] Screening all OpenClaw tool calls.")
|
|
2750
2719
|
console.print()
|
|
2751
|
-
console.print("[
|
|
2752
|
-
console.print("[
|
|
2753
|
-
console.print("[
|
|
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]")
|
|
2754
2723
|
|
|
2755
2724
|
|
|
2756
2725
|
@protect.command(
|
|
2757
|
-
"claude",
|
|
2726
|
+
"claude-code",
|
|
2758
2727
|
epilog="""\b
|
|
2759
2728
|
Examples:
|
|
2760
|
-
tweek protect claude
|
|
2761
|
-
tweek protect claude --global
|
|
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
|
|
2762
2733
|
"""
|
|
2763
2734
|
)
|
|
2764
2735
|
@click.option("--global", "install_global", is_flag=True, default=False,
|
|
2765
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")
|
|
2766
2745
|
@click.option("--preset", type=click.Choice(["paranoid", "cautious", "trusted"]),
|
|
2767
|
-
|
|
2768
|
-
@click.
|
|
2769
|
-
|
|
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):
|
|
2770
2758
|
"""Install Tweek hooks for Claude Code.
|
|
2771
2759
|
|
|
2772
|
-
|
|
2773
|
-
|
|
2760
|
+
Installs PreToolUse and PostToolUse hooks to screen all
|
|
2761
|
+
Claude Code tool calls through Tweek's security pipeline.
|
|
2774
2762
|
"""
|
|
2775
|
-
|
|
2776
|
-
# (use main.commands lookup to avoid name shadowing by mcp install)
|
|
2777
|
-
install_cmd = main.commands['install']
|
|
2778
|
-
ctx.invoke(
|
|
2779
|
-
install_cmd,
|
|
2763
|
+
_install_claude_code_hooks(
|
|
2780
2764
|
install_global=install_global,
|
|
2781
|
-
dev_test=
|
|
2782
|
-
backup=
|
|
2783
|
-
skip_env_scan=
|
|
2784
|
-
interactive=
|
|
2765
|
+
dev_test=dev_test,
|
|
2766
|
+
backup=backup,
|
|
2767
|
+
skip_env_scan=skip_env_scan,
|
|
2768
|
+
interactive=interactive,
|
|
2785
2769
|
preset=preset,
|
|
2786
|
-
ai_defaults=
|
|
2787
|
-
with_sandbox=
|
|
2788
|
-
force_proxy=
|
|
2789
|
-
skip_proxy_check=
|
|
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,
|
|
2790
2775
|
)
|
|
2791
2776
|
|
|
2792
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
|
+
|
|
2793
3075
|
# =============================================================================
|
|
2794
3076
|
# CONFIG COMMANDS
|
|
2795
3077
|
# =============================================================================
|
|
@@ -2874,7 +3156,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2874
3156
|
}
|
|
2875
3157
|
|
|
2876
3158
|
source_styles = {
|
|
2877
|
-
"default": "
|
|
3159
|
+
"default": "white",
|
|
2878
3160
|
"user": "cyan",
|
|
2879
3161
|
"project": "magenta",
|
|
2880
3162
|
}
|
|
@@ -2883,7 +3165,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2883
3165
|
table = Table(title="Tool Security Tiers")
|
|
2884
3166
|
table.add_column("Tool", style="bold")
|
|
2885
3167
|
table.add_column("Tier")
|
|
2886
|
-
table.add_column("Source", style="
|
|
3168
|
+
table.add_column("Source", style="white")
|
|
2887
3169
|
table.add_column("Description")
|
|
2888
3170
|
|
|
2889
3171
|
for tool in cfg.list_tools():
|
|
@@ -2903,7 +3185,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2903
3185
|
table = Table(title="Skill Security Tiers")
|
|
2904
3186
|
table.add_column("Skill", style="bold")
|
|
2905
3187
|
table.add_column("Tier")
|
|
2906
|
-
table.add_column("Source", style="
|
|
3188
|
+
table.add_column("Source", style="white")
|
|
2907
3189
|
table.add_column("Description")
|
|
2908
3190
|
|
|
2909
3191
|
for skill in cfg.list_skills():
|
|
@@ -2918,8 +3200,8 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2918
3200
|
|
|
2919
3201
|
console.print(table)
|
|
2920
3202
|
|
|
2921
|
-
console.print("\n[
|
|
2922
|
-
console.print("[
|
|
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]")
|
|
2923
3205
|
|
|
2924
3206
|
|
|
2925
3207
|
@config.command("set",
|
|
@@ -2982,11 +3264,11 @@ def config_preset(preset_name: str, scope: str):
|
|
|
2982
3264
|
console.print(f"[green]✓[/green] Applied [bold]{preset_name}[/bold] preset ({scope} config)")
|
|
2983
3265
|
|
|
2984
3266
|
if preset_name == "paranoid":
|
|
2985
|
-
console.print("[
|
|
3267
|
+
console.print("[white]All tools require screening, Bash commands always sandboxed[/white]")
|
|
2986
3268
|
elif preset_name == "cautious":
|
|
2987
|
-
console.print("[
|
|
3269
|
+
console.print("[white]Balanced: read-only tools safe, Bash dangerous[/white]")
|
|
2988
3270
|
elif preset_name == "trusted":
|
|
2989
|
-
console.print("[
|
|
3271
|
+
console.print("[white]Minimal prompts: only high-risk patterns trigger alerts[/white]")
|
|
2990
3272
|
|
|
2991
3273
|
|
|
2992
3274
|
@config.command("reset",
|
|
@@ -3011,7 +3293,7 @@ def config_reset(skill: str, tool: str, reset_all: bool, scope: str, confirm: bo
|
|
|
3011
3293
|
|
|
3012
3294
|
if reset_all:
|
|
3013
3295
|
if not confirm and not click.confirm(f"Reset ALL {scope} configuration?"):
|
|
3014
|
-
console.print("[
|
|
3296
|
+
console.print("[white]Cancelled[/white]")
|
|
3015
3297
|
return
|
|
3016
3298
|
cfg.reset_all(scope=scope)
|
|
3017
3299
|
console.print(f"[green]✓[/green] Reset all {scope} configuration to defaults")
|
|
@@ -3069,7 +3351,7 @@ def config_validate(scope: str, json_out: bool):
|
|
|
3069
3351
|
console.print()
|
|
3070
3352
|
console.print("[bold]Configuration Validation[/bold]")
|
|
3071
3353
|
console.print("\u2500" * 40)
|
|
3072
|
-
console.print(f"[
|
|
3354
|
+
console.print(f"[white]Scope: {scope}[/white]")
|
|
3073
3355
|
console.print()
|
|
3074
3356
|
|
|
3075
3357
|
if not issues:
|
|
@@ -3085,11 +3367,11 @@ def config_validate(scope: str, json_out: bool):
|
|
|
3085
3367
|
level_styles = {
|
|
3086
3368
|
"error": "[red]ERROR[/red]",
|
|
3087
3369
|
"warning": "[yellow]WARN[/yellow] ",
|
|
3088
|
-
"info": "[
|
|
3370
|
+
"info": "[white]INFO[/white] ",
|
|
3089
3371
|
}
|
|
3090
3372
|
|
|
3091
3373
|
for issue in issues:
|
|
3092
|
-
style = level_styles.get(issue.level, "[
|
|
3374
|
+
style = level_styles.get(issue.level, "[white]???[/white] ")
|
|
3093
3375
|
msg = f" {style} {issue.key} \u2192 {issue.message}"
|
|
3094
3376
|
if issue.suggestion:
|
|
3095
3377
|
msg += f" {issue.suggestion}"
|
|
@@ -3201,7 +3483,7 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3201
3483
|
console.print()
|
|
3202
3484
|
console.print(" [yellow]Status:[/yellow] Disabled (no provider available)")
|
|
3203
3485
|
console.print()
|
|
3204
|
-
console.print(" [
|
|
3486
|
+
console.print(" [white]To enable, set one of:[/white]")
|
|
3205
3487
|
console.print(" ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY")
|
|
3206
3488
|
console.print(" Or install Ollama: [cyan]https://ollama.ai[/cyan]")
|
|
3207
3489
|
console.print()
|
|
@@ -3236,8 +3518,8 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3236
3518
|
for m in server.all_models:
|
|
3237
3519
|
console.print(f" - {m}")
|
|
3238
3520
|
else:
|
|
3239
|
-
console.print(" [
|
|
3240
|
-
console.print(" [
|
|
3521
|
+
console.print(" [white]No local LLM server detected[/white]")
|
|
3522
|
+
console.print(" [white]Checked: Ollama (localhost:11434), LM Studio (localhost:1234)[/white]")
|
|
3241
3523
|
except Exception as e:
|
|
3242
3524
|
console.print(f" [yellow]Detection error: {e}[/yellow]")
|
|
3243
3525
|
|
|
@@ -3275,8 +3557,8 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3275
3557
|
console.print(f" [green]PASSED[/green] ({score:.0%})")
|
|
3276
3558
|
else:
|
|
3277
3559
|
console.print(f" [red]FAILED[/red] ({score:.0%}, minimum: 60%)")
|
|
3278
|
-
console.print(" [
|
|
3279
|
-
console.print(" [
|
|
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]")
|
|
3280
3562
|
except Exception as e:
|
|
3281
3563
|
console.print(f" [red]Validation error: {e}[/red]")
|
|
3282
3564
|
|
|
@@ -3306,8 +3588,8 @@ def vault_store(skill: str, key: str, value: Optional[str]):
|
|
|
3306
3588
|
|
|
3307
3589
|
if not VAULT_AVAILABLE:
|
|
3308
3590
|
console.print("[red]\u2717[/red] Vault not available.")
|
|
3309
|
-
console.print(" [
|
|
3310
|
-
console.print(" [
|
|
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]")
|
|
3311
3593
|
return
|
|
3312
3594
|
|
|
3313
3595
|
caps = get_capabilities()
|
|
@@ -3323,13 +3605,13 @@ def vault_store(skill: str, key: str, value: Optional[str]):
|
|
|
3323
3605
|
vault_instance = get_vault()
|
|
3324
3606
|
if vault_instance.store(skill, key, value):
|
|
3325
3607
|
console.print(f"[green]\u2713[/green] Stored {key} for skill '{skill}'")
|
|
3326
|
-
console.print(f"[
|
|
3608
|
+
console.print(f"[white]Backend: {caps.vault_backend}[/white]")
|
|
3327
3609
|
else:
|
|
3328
3610
|
console.print(f"[red]\u2717[/red] Failed to store credential")
|
|
3329
|
-
console.print(" [
|
|
3611
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
3330
3612
|
except Exception as e:
|
|
3331
3613
|
console.print(f"[red]\u2717[/red] Failed to store credential: {e}")
|
|
3332
|
-
console.print(" [
|
|
3614
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
3333
3615
|
|
|
3334
3616
|
|
|
3335
3617
|
@vault.command("get",
|
|
@@ -3347,7 +3629,7 @@ def vault_get(skill: str, key: str):
|
|
|
3347
3629
|
|
|
3348
3630
|
if not VAULT_AVAILABLE:
|
|
3349
3631
|
console.print("[red]\u2717[/red] Vault not available.")
|
|
3350
|
-
console.print(" [
|
|
3632
|
+
console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
|
|
3351
3633
|
return
|
|
3352
3634
|
|
|
3353
3635
|
vault_instance = get_vault()
|
|
@@ -3361,7 +3643,7 @@ def vault_get(skill: str, key: str):
|
|
|
3361
3643
|
console.print(value)
|
|
3362
3644
|
else:
|
|
3363
3645
|
console.print(f"[red]\u2717[/red] Credential not found: {key} for skill '{skill}'")
|
|
3364
|
-
console.print(" [
|
|
3646
|
+
console.print(" [white]Hint: Store it with: tweek vault store {skill} {key} <value>[/white]".format(skill=skill, key=key))
|
|
3365
3647
|
|
|
3366
3648
|
|
|
3367
3649
|
@vault.command("migrate-env",
|
|
@@ -3401,7 +3683,7 @@ def vault_migrate_env(dry_run: bool, env_file: str, skill: str):
|
|
|
3401
3683
|
successful = sum(1 for _, s in results if s)
|
|
3402
3684
|
console.print(f"\n[green]✓[/green] {'Would migrate' if dry_run else 'Migrated'} {successful} credentials to skill '{skill}'")
|
|
3403
3685
|
else:
|
|
3404
|
-
console.print("[
|
|
3686
|
+
console.print("[white]No credentials found to migrate[/white]")
|
|
3405
3687
|
|
|
3406
3688
|
except Exception as e:
|
|
3407
3689
|
console.print(f"[red]✗[/red] Migration failed: {e}")
|
|
@@ -3468,16 +3750,16 @@ def license_status():
|
|
|
3468
3750
|
console.print(f"[bold]License Tier:[/bold] [{tier_color}]{lic.tier.value.upper()}[/{tier_color}]")
|
|
3469
3751
|
|
|
3470
3752
|
if info:
|
|
3471
|
-
console.print(f"[
|
|
3753
|
+
console.print(f"[white]Licensed to: {info.email}[/white]")
|
|
3472
3754
|
if info.expires_at:
|
|
3473
3755
|
from datetime import datetime
|
|
3474
3756
|
exp_date = datetime.fromtimestamp(info.expires_at).strftime("%Y-%m-%d")
|
|
3475
3757
|
if info.is_expired:
|
|
3476
3758
|
console.print(f"[red]Expired: {exp_date}[/red]")
|
|
3477
3759
|
else:
|
|
3478
|
-
console.print(f"[
|
|
3760
|
+
console.print(f"[white]Expires: {exp_date}[/white]")
|
|
3479
3761
|
else:
|
|
3480
|
-
console.print("[
|
|
3762
|
+
console.print("[white]Expires: Never[/white]")
|
|
3481
3763
|
console.print()
|
|
3482
3764
|
|
|
3483
3765
|
# Features table
|
|
@@ -3494,7 +3776,7 @@ def license_status():
|
|
|
3494
3776
|
|
|
3495
3777
|
for feature, required_tier in feature_tiers.items():
|
|
3496
3778
|
has_it = lic.has_feature(feature)
|
|
3497
|
-
status = "[green]✓[/green]" if has_it else "[
|
|
3779
|
+
status = "[green]✓[/green]" if has_it else "[white]○[/white]"
|
|
3498
3780
|
tier_display = required_tier.value.upper()
|
|
3499
3781
|
if required_tier == Tier.PRO:
|
|
3500
3782
|
tier_display = f"[cyan]{tier_display}[/cyan]"
|
|
@@ -3506,7 +3788,7 @@ def license_status():
|
|
|
3506
3788
|
if lic.tier == Tier.FREE:
|
|
3507
3789
|
console.print()
|
|
3508
3790
|
console.print("[green]All security features are included free and open source.[/green]")
|
|
3509
|
-
console.print("[
|
|
3791
|
+
console.print("[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
|
|
3510
3792
|
|
|
3511
3793
|
|
|
3512
3794
|
@license.command("activate",
|
|
@@ -3526,7 +3808,7 @@ def license_activate(license_key: str):
|
|
|
3526
3808
|
if success:
|
|
3527
3809
|
console.print(f"[green]✓[/green] {message}")
|
|
3528
3810
|
console.print()
|
|
3529
|
-
console.print("[
|
|
3811
|
+
console.print("[white]Run 'tweek license status' to see available features[/white]")
|
|
3530
3812
|
else:
|
|
3531
3813
|
console.print(f"[red]✗[/red] {message}")
|
|
3532
3814
|
|
|
@@ -3546,7 +3828,7 @@ def license_deactivate(confirm: bool):
|
|
|
3546
3828
|
if not confirm:
|
|
3547
3829
|
console.print("[yellow]Deactivate license and revert to FREE tier?[/yellow] ", end="")
|
|
3548
3830
|
if not click.confirm(""):
|
|
3549
|
-
console.print("[
|
|
3831
|
+
console.print("[white]Cancelled[/white]")
|
|
3550
3832
|
return
|
|
3551
3833
|
|
|
3552
3834
|
lic = get_license()
|
|
@@ -3624,7 +3906,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3624
3906
|
table.add_column("Severity")
|
|
3625
3907
|
table.add_column("Count", justify="right")
|
|
3626
3908
|
|
|
3627
|
-
severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "
|
|
3909
|
+
severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "white"}
|
|
3628
3910
|
for pattern in stat_data['top_patterns']:
|
|
3629
3911
|
sev = pattern['severity'] or "unknown"
|
|
3630
3912
|
style = severity_styles.get(sev, "white")
|
|
@@ -3661,7 +3943,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3661
3943
|
et = EventType(event_type)
|
|
3662
3944
|
except ValueError:
|
|
3663
3945
|
console.print(f"[red]Unknown event type: {event_type}[/red]")
|
|
3664
|
-
console.print(f"[
|
|
3946
|
+
console.print(f"[white]Valid types: {', '.join(e.value for e in EventType)}[/white]")
|
|
3665
3947
|
return
|
|
3666
3948
|
|
|
3667
3949
|
events = logger.get_recent_events(limit=limit, event_type=et, tool_name=tool)
|
|
@@ -3672,7 +3954,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3672
3954
|
return
|
|
3673
3955
|
|
|
3674
3956
|
table = Table(title=title)
|
|
3675
|
-
table.add_column("Time", style="
|
|
3957
|
+
table.add_column("Time", style="white")
|
|
3676
3958
|
table.add_column("Type", style="cyan")
|
|
3677
3959
|
table.add_column("Tool", style="green")
|
|
3678
3960
|
table.add_column("Tier")
|
|
@@ -3713,7 +3995,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3713
3995
|
)
|
|
3714
3996
|
|
|
3715
3997
|
console.print(table)
|
|
3716
|
-
console.print(f"\n[
|
|
3998
|
+
console.print(f"\n[white]Showing {len(events)} events. Use --limit to see more.[/white]")
|
|
3717
3999
|
|
|
3718
4000
|
|
|
3719
4001
|
@logs.command("export",
|
|
@@ -3764,7 +4046,7 @@ def logs_clear(days: int, confirm: bool):
|
|
|
3764
4046
|
|
|
3765
4047
|
console.print(f"[yellow]{msg}[/yellow] ", end="")
|
|
3766
4048
|
if not click.confirm(""):
|
|
3767
|
-
console.print("[
|
|
4049
|
+
console.print("[white]Cancelled[/white]")
|
|
3768
4050
|
return
|
|
3769
4051
|
|
|
3770
4052
|
logger = get_logger()
|
|
@@ -3776,7 +4058,7 @@ def logs_clear(days: int, confirm: bool):
|
|
|
3776
4058
|
else:
|
|
3777
4059
|
console.print(f"[green]Cleared {deleted} event(s)[/green]")
|
|
3778
4060
|
else:
|
|
3779
|
-
console.print("[
|
|
4061
|
+
console.print("[white]No events to clear[/white]")
|
|
3780
4062
|
|
|
3781
4063
|
|
|
3782
4064
|
@logs.command("bundle",
|
|
@@ -3814,11 +4096,11 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
|
|
|
3814
4096
|
size = item.get("size")
|
|
3815
4097
|
size_str = f" ({size:,} bytes)" if size else ""
|
|
3816
4098
|
if "not found" in status:
|
|
3817
|
-
console.print(f" [
|
|
4099
|
+
console.print(f" [white] SKIP {name} ({status})[/white]")
|
|
3818
4100
|
else:
|
|
3819
4101
|
console.print(f" [green] ADD {name}{size_str}[/green]")
|
|
3820
4102
|
console.print()
|
|
3821
|
-
console.print("[
|
|
4103
|
+
console.print("[white]No files will be collected in dry-run mode.[/white]")
|
|
3822
4104
|
return
|
|
3823
4105
|
|
|
3824
4106
|
# Determine output path
|
|
@@ -3836,9 +4118,9 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
|
|
|
3836
4118
|
result = collector.create_bundle(output_path)
|
|
3837
4119
|
size = result.stat().st_size
|
|
3838
4120
|
console.print(f"\n[green]Bundle created: {result}[/green]")
|
|
3839
|
-
console.print(f"[
|
|
4121
|
+
console.print(f"[white]Size: {size:,} bytes[/white]")
|
|
3840
4122
|
if not no_redact:
|
|
3841
|
-
console.print("[
|
|
4123
|
+
console.print("[white]Sensitive data has been redacted.[/white]")
|
|
3842
4124
|
console.print(f"\n[bold]Send this file to Tweek support for analysis.[/bold]")
|
|
3843
4125
|
except Exception as e:
|
|
3844
4126
|
console.print(f"[red]Failed to create bundle: {e}[/red]")
|
|
@@ -3884,8 +4166,8 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
|
|
|
3884
4166
|
|
|
3885
4167
|
if not PROXY_AVAILABLE:
|
|
3886
4168
|
console.print("[red]\u2717[/red] Proxy dependencies not installed.")
|
|
3887
|
-
console.print(" [
|
|
3888
|
-
console.print(" [
|
|
4169
|
+
console.print(" [white]Hint: Install with: pip install tweek[proxy][/white]")
|
|
4170
|
+
console.print(" [white]This adds mitmproxy for HTTP(S) interception.[/white]")
|
|
3889
4171
|
return
|
|
3890
4172
|
|
|
3891
4173
|
from tweek.proxy.server import start_proxy
|
|
@@ -3906,7 +4188,7 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
|
|
|
3906
4188
|
console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
|
|
3907
4189
|
console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
|
|
3908
4190
|
console.print()
|
|
3909
|
-
console.print("[
|
|
4191
|
+
console.print("[white]Or use 'tweek proxy wrap' to create a wrapper script[/white]")
|
|
3910
4192
|
else:
|
|
3911
4193
|
console.print(f"[red]✗[/red] {message}")
|
|
3912
4194
|
|
|
@@ -3951,7 +4233,7 @@ def proxy_trust():
|
|
|
3951
4233
|
|
|
3952
4234
|
if not PROXY_AVAILABLE:
|
|
3953
4235
|
console.print("[red]✗[/red] Proxy dependencies not installed.")
|
|
3954
|
-
console.print("[
|
|
4236
|
+
console.print("[white]Run: pip install tweek\\[proxy][/white]")
|
|
3955
4237
|
return
|
|
3956
4238
|
|
|
3957
4239
|
from tweek.proxy.server import install_ca_certificate, get_proxy_info
|
|
@@ -3963,11 +4245,11 @@ def proxy_trust():
|
|
|
3963
4245
|
console.print("This will install a local CA certificate to enable HTTPS interception.")
|
|
3964
4246
|
console.print("The certificate is generated on YOUR machine and never transmitted.")
|
|
3965
4247
|
console.print()
|
|
3966
|
-
console.print(f"[
|
|
4248
|
+
console.print(f"[white]Certificate location: {info['ca_cert']}[/white]")
|
|
3967
4249
|
console.print()
|
|
3968
4250
|
|
|
3969
4251
|
if not click.confirm("Install certificate? (requires admin password)"):
|
|
3970
|
-
console.print("[
|
|
4252
|
+
console.print("[white]Cancelled[/white]")
|
|
3971
4253
|
return
|
|
3972
4254
|
|
|
3973
4255
|
success, message = install_ca_certificate()
|
|
@@ -4019,7 +4301,7 @@ def proxy_config(set_enabled, set_disabled, port):
|
|
|
4019
4301
|
yaml.dump(config, f, default_flow_style=False)
|
|
4020
4302
|
|
|
4021
4303
|
console.print(f"[green]✓[/green] Proxy mode enabled (port {port})")
|
|
4022
|
-
console.print("[
|
|
4304
|
+
console.print("[white]Run 'tweek proxy start' to start the proxy[/white]")
|
|
4023
4305
|
|
|
4024
4306
|
elif set_disabled:
|
|
4025
4307
|
if "proxy" in config:
|
|
@@ -4061,10 +4343,10 @@ def proxy_wrap(app_name: str, command: str, output: str, port: int):
|
|
|
4061
4343
|
console.print(f" chmod +x {output_path}")
|
|
4062
4344
|
console.print(f" ./{output_path.name}")
|
|
4063
4345
|
console.print()
|
|
4064
|
-
console.print("[
|
|
4065
|
-
console.print("[
|
|
4066
|
-
console.print("[
|
|
4067
|
-
console.print(f"[
|
|
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]")
|
|
4068
4350
|
|
|
4069
4351
|
|
|
4070
4352
|
@proxy.command("setup",
|
|
@@ -4139,9 +4421,9 @@ def proxy_setup():
|
|
|
4139
4421
|
print_warning("Certificate module not available. Run: tweek proxy trust")
|
|
4140
4422
|
except Exception as e:
|
|
4141
4423
|
print_warning(f"Could not set up certificate: {e}")
|
|
4142
|
-
console.print(" [
|
|
4424
|
+
console.print(" [white]You can do this later with: tweek proxy trust[/white]")
|
|
4143
4425
|
else:
|
|
4144
|
-
console.print(" [
|
|
4426
|
+
console.print(" [white]Skipped. Run 'tweek proxy trust' later.[/white]")
|
|
4145
4427
|
console.print()
|
|
4146
4428
|
|
|
4147
4429
|
# Step 3: Shell environment
|
|
@@ -4165,13 +4447,13 @@ def proxy_setup():
|
|
|
4165
4447
|
f.write(f"export HTTP_PROXY=http://127.0.0.1:{port}\n")
|
|
4166
4448
|
f.write(f"export HTTPS_PROXY=http://127.0.0.1:{port}\n")
|
|
4167
4449
|
print_success(f"Added to {shell_rc}")
|
|
4168
|
-
console.print(f" [
|
|
4450
|
+
console.print(f" [white]Restart your shell or run: source {shell_rc}[/white]")
|
|
4169
4451
|
except Exception as e:
|
|
4170
4452
|
print_warning(f"Could not write to {shell_rc}: {e}")
|
|
4171
4453
|
else:
|
|
4172
|
-
console.print(" [
|
|
4454
|
+
console.print(" [white]Skipped. Set HTTP_PROXY and HTTPS_PROXY manually.[/white]")
|
|
4173
4455
|
else:
|
|
4174
|
-
console.print(" [
|
|
4456
|
+
console.print(" [white]Could not detect shell config file.[/white]")
|
|
4175
4457
|
console.print(f" Add these to your shell profile:")
|
|
4176
4458
|
console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
|
|
4177
4459
|
console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
|
|
@@ -4265,7 +4547,7 @@ def plugins_list(category: str, show_all: bool):
|
|
|
4265
4547
|
license_style = "green" if license_tier == LicenseTier.FREE else "cyan"
|
|
4266
4548
|
|
|
4267
4549
|
source_str = info.source.value if hasattr(info, 'source') else "builtin"
|
|
4268
|
-
source_style = "blue" if source_str == "git" else "
|
|
4550
|
+
source_style = "blue" if source_str == "git" else "white"
|
|
4269
4551
|
|
|
4270
4552
|
table.add_row(
|
|
4271
4553
|
info.name,
|
|
@@ -4279,6 +4561,18 @@ def plugins_list(category: str, show_all: bool):
|
|
|
4279
4561
|
console.print(table)
|
|
4280
4562
|
console.print()
|
|
4281
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
|
+
|
|
4282
4576
|
except ImportError as e:
|
|
4283
4577
|
console.print(f"[red]Plugin system not available: {e}[/red]")
|
|
4284
4578
|
|
|
@@ -4335,7 +4629,7 @@ def plugins_info(plugin_name: str, category: str):
|
|
|
4335
4629
|
plugin_cfg = cfg.get_plugin_config(found_cat, plugin_name)
|
|
4336
4630
|
|
|
4337
4631
|
console.print(f"\n[bold]{found_info.name}[/bold] ({found_cat})")
|
|
4338
|
-
console.print(f"[
|
|
4632
|
+
console.print(f"[white]{found_info.metadata.description}[/white]")
|
|
4339
4633
|
console.print()
|
|
4340
4634
|
|
|
4341
4635
|
table = Table(show_header=False)
|
|
@@ -4507,7 +4801,7 @@ def plugins_scan(content: str, direction: str, plugin: str):
|
|
|
4507
4801
|
|
|
4508
4802
|
if not plugins_to_use:
|
|
4509
4803
|
console.print("[yellow]No compliance plugins enabled.[/yellow]")
|
|
4510
|
-
console.print("[
|
|
4804
|
+
console.print("[white]Enable plugins with: tweek plugins enable <name> -c compliance[/white]")
|
|
4511
4805
|
return
|
|
4512
4806
|
|
|
4513
4807
|
for p in plugins_to_use:
|
|
@@ -4521,12 +4815,12 @@ def plugins_scan(content: str, direction: str, plugin: str):
|
|
|
4521
4815
|
"critical": "red bold",
|
|
4522
4816
|
"high": "red",
|
|
4523
4817
|
"medium": "yellow",
|
|
4524
|
-
"low": "
|
|
4818
|
+
"low": "white",
|
|
4525
4819
|
}
|
|
4526
4820
|
style = severity_styles.get(finding.severity.value, "white")
|
|
4527
4821
|
|
|
4528
4822
|
console.print(f" [{style}]{finding.severity.value.upper()}[/{style}] {finding.pattern_name}")
|
|
4529
|
-
console.print(f" [
|
|
4823
|
+
console.print(f" [white]Matched: {finding.matched_text[:60]}{'...' if len(finding.matched_text) > 60 else ''}[/white]")
|
|
4530
4824
|
if finding.description:
|
|
4531
4825
|
console.print(f" {finding.description}")
|
|
4532
4826
|
|
|
@@ -4600,11 +4894,11 @@ def plugins_install(name: str, version: str, from_lockfile: bool, no_verify: boo
|
|
|
4600
4894
|
console.print(f"[green]\u2713[/green] {msg}")
|
|
4601
4895
|
else:
|
|
4602
4896
|
console.print(f"[red]\u2717[/red] {msg}")
|
|
4603
|
-
console.print(f" [
|
|
4897
|
+
console.print(f" [white]Hint: Check network connectivity or try: tweek plugins registry --refresh[/white]")
|
|
4604
4898
|
|
|
4605
4899
|
except Exception as e:
|
|
4606
4900
|
console.print(f"[red]Error: {e}[/red]")
|
|
4607
|
-
console.print(f" [
|
|
4901
|
+
console.print(f" [white]Hint: Check network connectivity and try again[/white]")
|
|
4608
4902
|
|
|
4609
4903
|
|
|
4610
4904
|
@plugins.command("update",
|
|
@@ -4972,87 +5266,6 @@ def serve():
|
|
|
4972
5266
|
console.print(f"[red]MCP server error: {e}[/red]")
|
|
4973
5267
|
|
|
4974
5268
|
|
|
4975
|
-
@mcp.command(
|
|
4976
|
-
epilog="""\b
|
|
4977
|
-
Examples:
|
|
4978
|
-
tweek mcp install claude-desktop Configure Claude Desktop integration
|
|
4979
|
-
tweek mcp install chatgpt Set up ChatGPT Desktop integration
|
|
4980
|
-
tweek mcp install gemini Configure Gemini CLI integration
|
|
4981
|
-
"""
|
|
4982
|
-
)
|
|
4983
|
-
@click.argument("client", type=click.Choice(["claude-desktop", "chatgpt", "gemini"]))
|
|
4984
|
-
def install(client):
|
|
4985
|
-
"""Install Tweek as MCP server for a desktop client.
|
|
4986
|
-
|
|
4987
|
-
Supported clients:
|
|
4988
|
-
claude-desktop - Auto-configures Claude Desktop
|
|
4989
|
-
chatgpt - Provides Developer Mode setup instructions
|
|
4990
|
-
gemini - Auto-configures Gemini CLI settings
|
|
4991
|
-
"""
|
|
4992
|
-
try:
|
|
4993
|
-
from tweek.mcp.clients import get_client
|
|
4994
|
-
|
|
4995
|
-
handler = get_client(client)
|
|
4996
|
-
result = handler.install()
|
|
4997
|
-
|
|
4998
|
-
if result.get("success"):
|
|
4999
|
-
console.print(f"[green]✅ {result.get('message', 'Installed successfully')}[/green]")
|
|
5000
|
-
|
|
5001
|
-
if result.get("config_path"):
|
|
5002
|
-
console.print(f" Config: {result['config_path']}")
|
|
5003
|
-
|
|
5004
|
-
if result.get("backup"):
|
|
5005
|
-
console.print(f" Backup: {result['backup']}")
|
|
5006
|
-
|
|
5007
|
-
# Show instructions for manual setup clients
|
|
5008
|
-
if result.get("instructions"):
|
|
5009
|
-
console.print()
|
|
5010
|
-
for line in result["instructions"]:
|
|
5011
|
-
console.print(f" {line}")
|
|
5012
|
-
else:
|
|
5013
|
-
console.print(f"[red]❌ {result.get('error', 'Installation failed')}[/red]")
|
|
5014
|
-
|
|
5015
|
-
except Exception as e:
|
|
5016
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
@mcp.command(
|
|
5020
|
-
epilog="""\b
|
|
5021
|
-
Examples:
|
|
5022
|
-
tweek mcp uninstall claude-desktop Remove from Claude Desktop
|
|
5023
|
-
tweek mcp uninstall chatgpt Remove from ChatGPT Desktop
|
|
5024
|
-
tweek mcp uninstall gemini Remove from Gemini CLI
|
|
5025
|
-
"""
|
|
5026
|
-
)
|
|
5027
|
-
@click.argument("client", type=click.Choice(["claude-desktop", "chatgpt", "gemini"]))
|
|
5028
|
-
def uninstall(client):
|
|
5029
|
-
"""Remove Tweek MCP server from a desktop client.
|
|
5030
|
-
|
|
5031
|
-
Supported clients: claude-desktop, chatgpt, gemini
|
|
5032
|
-
"""
|
|
5033
|
-
try:
|
|
5034
|
-
from tweek.mcp.clients import get_client
|
|
5035
|
-
|
|
5036
|
-
handler = get_client(client)
|
|
5037
|
-
result = handler.uninstall()
|
|
5038
|
-
|
|
5039
|
-
if result.get("success"):
|
|
5040
|
-
console.print(f"[green]✅ {result.get('message', 'Uninstalled successfully')}[/green]")
|
|
5041
|
-
|
|
5042
|
-
if result.get("backup"):
|
|
5043
|
-
console.print(f" Backup: {result['backup']}")
|
|
5044
|
-
|
|
5045
|
-
if result.get("instructions"):
|
|
5046
|
-
console.print()
|
|
5047
|
-
for line in result["instructions"]:
|
|
5048
|
-
console.print(f" {line}")
|
|
5049
|
-
else:
|
|
5050
|
-
console.print(f"[red]❌ {result.get('error', 'Uninstallation failed')}[/red]")
|
|
5051
|
-
|
|
5052
|
-
except Exception as e:
|
|
5053
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
5269
|
# =============================================================================
|
|
5057
5270
|
# MCP PROXY COMMANDS
|
|
5058
5271
|
# =============================================================================
|
|
@@ -5208,13 +5421,13 @@ def chamber_list():
|
|
|
5208
5421
|
items = chamber.list_chamber()
|
|
5209
5422
|
|
|
5210
5423
|
if not items:
|
|
5211
|
-
console.print("[
|
|
5424
|
+
console.print("[white]Chamber is empty.[/white]")
|
|
5212
5425
|
return
|
|
5213
5426
|
|
|
5214
5427
|
table = Table(title="Isolation Chamber")
|
|
5215
5428
|
table.add_column("Name", style="cyan")
|
|
5216
5429
|
table.add_column("Has SKILL.md", style="green")
|
|
5217
|
-
table.add_column("Path", style="
|
|
5430
|
+
table.add_column("Path", style="white")
|
|
5218
5431
|
|
|
5219
5432
|
for item in items:
|
|
5220
5433
|
has_md = "Yes" if item["has_skill_md"] else "[red]No[/red]"
|
|
@@ -5309,7 +5522,7 @@ def jail_list():
|
|
|
5309
5522
|
items = chamber.list_jail()
|
|
5310
5523
|
|
|
5311
5524
|
if not items:
|
|
5312
|
-
console.print("[
|
|
5525
|
+
console.print("[white]Jail is empty.[/white]")
|
|
5313
5526
|
return
|
|
5314
5527
|
|
|
5315
5528
|
table = Table(title="Skill Jail")
|
|
@@ -5381,7 +5594,7 @@ def skills_report(name: str):
|
|
|
5381
5594
|
report_data = chamber.get_report(name)
|
|
5382
5595
|
|
|
5383
5596
|
if not report_data:
|
|
5384
|
-
console.print(f"[
|
|
5597
|
+
console.print(f"[white]No report found for '{name}'.[/white]")
|
|
5385
5598
|
return
|
|
5386
5599
|
|
|
5387
5600
|
console.print(Panel(
|
|
@@ -5503,7 +5716,7 @@ def sandbox_status():
|
|
|
5503
5716
|
else:
|
|
5504
5717
|
console.print(f"[bold]Project:[/bold] {project_dir}")
|
|
5505
5718
|
console.print(f"[bold]Layer:[/bold] 0-1 (no project isolation)")
|
|
5506
|
-
console.print("[
|
|
5719
|
+
console.print("[white]Run 'tweek sandbox init' to enable project isolation.[/white]")
|
|
5507
5720
|
|
|
5508
5721
|
|
|
5509
5722
|
@sandbox.command("init")
|
|
@@ -5577,7 +5790,7 @@ def sandbox_list():
|
|
|
5577
5790
|
projects = registry.list_projects()
|
|
5578
5791
|
|
|
5579
5792
|
if not projects:
|
|
5580
|
-
console.print("[
|
|
5793
|
+
console.print("[white]No projects registered. Run 'tweek sandbox init' in a project.[/white]")
|
|
5581
5794
|
return
|
|
5582
5795
|
|
|
5583
5796
|
table = Table(title="Registered Projects")
|
|
@@ -5649,12 +5862,12 @@ def sandbox_logs(show_global: bool, limit: int):
|
|
|
5649
5862
|
|
|
5650
5863
|
events = logger.get_recent_events(limit=limit)
|
|
5651
5864
|
if not events:
|
|
5652
|
-
console.print("[
|
|
5865
|
+
console.print("[white]No events found.[/white]")
|
|
5653
5866
|
return
|
|
5654
5867
|
|
|
5655
5868
|
from rich.table import Table
|
|
5656
5869
|
table = Table()
|
|
5657
|
-
table.add_column("Time", style="
|
|
5870
|
+
table.add_column("Time", style="white")
|
|
5658
5871
|
table.add_column("Type")
|
|
5659
5872
|
table.add_column("Tool")
|
|
5660
5873
|
table.add_column("Decision", style="green")
|
|
@@ -5734,7 +5947,7 @@ def sandbox_verify():
|
|
|
5734
5947
|
checks_passed += 1
|
|
5735
5948
|
else:
|
|
5736
5949
|
console.print(" Sandbox initialized: [red]NO[/red]")
|
|
5737
|
-
console.print(" [
|
|
5950
|
+
console.print(" [white]Run 'tweek sandbox init' to enable.[/white]")
|
|
5738
5951
|
|
|
5739
5952
|
# Check 3: Layer
|
|
5740
5953
|
checks_total += 1
|
|
@@ -5755,7 +5968,7 @@ def sandbox_verify():
|
|
|
5755
5968
|
elif sandbox:
|
|
5756
5969
|
console.print(" Project security.db: [yellow]NOT FOUND[/yellow]")
|
|
5757
5970
|
else:
|
|
5758
|
-
console.print(" Project security.db: [
|
|
5971
|
+
console.print(" Project security.db: [white]N/A (sandbox inactive)[/white]")
|
|
5759
5972
|
|
|
5760
5973
|
# Check 5: .gitignore
|
|
5761
5974
|
checks_total += 1
|
|
@@ -5785,7 +5998,7 @@ def docker_init():
|
|
|
5785
5998
|
bridge = DockerBridge()
|
|
5786
5999
|
if not bridge.is_docker_available():
|
|
5787
6000
|
console.print("[red]Docker is not installed or not running.[/red]")
|
|
5788
|
-
console.print("[
|
|
6001
|
+
console.print("[white]Install Docker Desktop from https://www.docker.com/products/docker-desktop/[/white]")
|
|
5789
6002
|
raise SystemExit(1)
|
|
5790
6003
|
|
|
5791
6004
|
from tweek.sandbox.project import _detect_project_dir
|
|
@@ -5796,7 +6009,7 @@ def docker_init():
|
|
|
5796
6009
|
|
|
5797
6010
|
compose_path = bridge.init(project_dir)
|
|
5798
6011
|
console.print(f"[green]Docker Sandbox config generated: {compose_path}[/green]")
|
|
5799
|
-
console.print("[
|
|
6012
|
+
console.print("[white]Run 'tweek sandbox docker run' to start the container.[/white]")
|
|
5800
6013
|
|
|
5801
6014
|
|
|
5802
6015
|
@sandbox_docker.command("run")
|
|
@@ -5833,7 +6046,7 @@ def docker_status():
|
|
|
5833
6046
|
compose = project_dir / ".tweek" / "docker-compose.yaml"
|
|
5834
6047
|
console.print(f"[bold]Docker config:[/bold] {'exists' if compose.exists() else 'not generated'}")
|
|
5835
6048
|
else:
|
|
5836
|
-
console.print("[
|
|
6049
|
+
console.print("[white]Not in a project directory.[/white]")
|
|
5837
6050
|
|
|
5838
6051
|
|
|
5839
6052
|
# =========================================================================
|
|
@@ -5894,7 +6107,7 @@ def override_create(pattern: str, mode: str, duration_minutes: Optional[int], re
|
|
|
5894
6107
|
if reason:
|
|
5895
6108
|
console.print(f" Reason: {reason}")
|
|
5896
6109
|
console.print()
|
|
5897
|
-
console.print("[
|
|
6110
|
+
console.print("[white]Next time this pattern triggers, you'll see an 'ask' prompt instead of a hard block.[/white]")
|
|
5898
6111
|
|
|
5899
6112
|
|
|
5900
6113
|
@override_group.command("list")
|
|
@@ -5907,7 +6120,7 @@ def override_list():
|
|
|
5907
6120
|
active_patterns = {o["pattern"] for o in active}
|
|
5908
6121
|
|
|
5909
6122
|
if not all_overrides:
|
|
5910
|
-
console.print("[
|
|
6123
|
+
console.print("[white]No break-glass overrides found.[/white]")
|
|
5911
6124
|
return
|
|
5912
6125
|
|
|
5913
6126
|
table = Table(title="Break-Glass Overrides")
|
|
@@ -5921,9 +6134,9 @@ def override_list():
|
|
|
5921
6134
|
if o["pattern"] in active_patterns and not o.get("used"):
|
|
5922
6135
|
status = "[green]active[/green]"
|
|
5923
6136
|
elif o.get("used"):
|
|
5924
|
-
status = "[
|
|
6137
|
+
status = "[white]consumed[/white]"
|
|
5925
6138
|
else:
|
|
5926
|
-
status = "[
|
|
6139
|
+
status = "[white]expired[/white]"
|
|
5927
6140
|
|
|
5928
6141
|
table.add_row(
|
|
5929
6142
|
o["pattern"],
|
|
@@ -6012,7 +6225,7 @@ def feedback_stats(above_threshold: bool):
|
|
|
6012
6225
|
|
|
6013
6226
|
stats = get_stats()
|
|
6014
6227
|
if not stats:
|
|
6015
|
-
console.print("[
|
|
6228
|
+
console.print("[white]No feedback data recorded yet.[/white]")
|
|
6016
6229
|
return
|
|
6017
6230
|
|
|
6018
6231
|
table = Table(title="Pattern FP Statistics")
|
|
@@ -6053,7 +6266,7 @@ def feedback_reset(pattern_name: str):
|
|
|
6053
6266
|
if result.get("was_demoted"):
|
|
6054
6267
|
console.print(f" Restored severity: {result.get('original_severity')}")
|
|
6055
6268
|
else:
|
|
6056
|
-
console.print(f"[
|
|
6269
|
+
console.print(f"[white]No feedback data found for '{pattern_name}'.[/white]")
|
|
6057
6270
|
|
|
6058
6271
|
|
|
6059
6272
|
# =========================================================================
|
|
@@ -6094,7 +6307,7 @@ def memory_status():
|
|
|
6094
6307
|
if last_decay:
|
|
6095
6308
|
console.print(f" Last decay: {last_decay}")
|
|
6096
6309
|
else:
|
|
6097
|
-
console.print(" Last decay: [
|
|
6310
|
+
console.print(" Last decay: [white]never[/white]")
|
|
6098
6311
|
|
|
6099
6312
|
db_size = stats.get("db_size_bytes", 0)
|
|
6100
6313
|
if db_size > 1024 * 1024:
|
|
@@ -6118,7 +6331,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
|
|
|
6118
6331
|
patterns = store.get_pattern_stats(min_decisions=min_decisions, sort_by=sort_by)
|
|
6119
6332
|
|
|
6120
6333
|
if not patterns:
|
|
6121
|
-
console.print("[
|
|
6334
|
+
console.print("[white]No pattern decision data recorded yet.[/white]")
|
|
6122
6335
|
return
|
|
6123
6336
|
|
|
6124
6337
|
table = Table(title="Pattern Decision History")
|
|
@@ -6135,7 +6348,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
|
|
|
6135
6348
|
ratio_style = "green" if ratio >= 0.9 else ("yellow" if ratio >= 0.5 else "red")
|
|
6136
6349
|
table.add_row(
|
|
6137
6350
|
p.get("pattern_name", "?"),
|
|
6138
|
-
p.get("path_prefix") or "[
|
|
6351
|
+
p.get("path_prefix") or "[white]-[/white]",
|
|
6139
6352
|
str(p.get("total_decisions", 0)),
|
|
6140
6353
|
f"{p.get('weighted_approvals', 0):.1f}",
|
|
6141
6354
|
f"{p.get('weighted_denials', 0):.1f}",
|
|
@@ -6156,7 +6369,7 @@ def memory_sources(suspicious: bool):
|
|
|
6156
6369
|
sources = store.get_all_sources(suspicious_only=suspicious)
|
|
6157
6370
|
|
|
6158
6371
|
if not sources:
|
|
6159
|
-
console.print("[
|
|
6372
|
+
console.print("[white]No source trust data recorded yet.[/white]")
|
|
6160
6373
|
return
|
|
6161
6374
|
|
|
6162
6375
|
table = Table(title="Source Trust Scores")
|
|
@@ -6191,7 +6404,7 @@ def memory_suggestions(show_all: bool):
|
|
|
6191
6404
|
suggestions = store.get_whitelist_suggestions(pending_only=not show_all)
|
|
6192
6405
|
|
|
6193
6406
|
if not suggestions:
|
|
6194
|
-
console.print("[
|
|
6407
|
+
console.print("[white]No whitelist suggestions available.[/white]")
|
|
6195
6408
|
return
|
|
6196
6409
|
|
|
6197
6410
|
table = Table(title="Learned Whitelist Suggestions")
|
|
@@ -6209,8 +6422,8 @@ def memory_suggestions(show_all: bool):
|
|
|
6209
6422
|
table.add_row(
|
|
6210
6423
|
str(s.id),
|
|
6211
6424
|
s.pattern_name,
|
|
6212
|
-
s.tool_name or "[
|
|
6213
|
-
s.path_prefix or "[
|
|
6425
|
+
s.tool_name or "[white]-[/white]",
|
|
6426
|
+
s.path_prefix or "[white]-[/white]",
|
|
6214
6427
|
str(s.approval_count),
|
|
6215
6428
|
str(s.denial_count),
|
|
6216
6429
|
f"{s.confidence:.0%}",
|
|
@@ -6229,7 +6442,7 @@ def memory_accept(suggestion_id: int):
|
|
|
6229
6442
|
store = get_memory_store()
|
|
6230
6443
|
if store.review_whitelist_suggestion(suggestion_id, accepted=True):
|
|
6231
6444
|
console.print(f"[bold green]Accepted[/bold green] suggestion #{suggestion_id}")
|
|
6232
|
-
console.print(" [
|
|
6445
|
+
console.print(" [white]Note: To apply to overrides.yaml, manually add the whitelist rule.[/white]")
|
|
6233
6446
|
else:
|
|
6234
6447
|
console.print(f"[red]Suggestion #{suggestion_id} not found.[/red]")
|
|
6235
6448
|
|
|
@@ -6260,7 +6473,7 @@ def memory_baseline(project_hash: Optional[str]):
|
|
|
6260
6473
|
baselines = store.get_workflow_baseline(project_hash)
|
|
6261
6474
|
|
|
6262
6475
|
if not baselines:
|
|
6263
|
-
console.print("[
|
|
6476
|
+
console.print("[white]No workflow baseline data for this project.[/white]")
|
|
6264
6477
|
return
|
|
6265
6478
|
|
|
6266
6479
|
table = Table(title=f"Workflow Baseline (project: {project_hash[:8]}...)")
|
|
@@ -6276,7 +6489,7 @@ def memory_baseline(project_hash: Optional[str]):
|
|
|
6276
6489
|
pct_style = "green" if denial_pct < 0.1 else ("yellow" if denial_pct < 0.3 else "red")
|
|
6277
6490
|
table.add_row(
|
|
6278
6491
|
b.tool_name,
|
|
6279
|
-
str(b.hour_of_day) if b.hour_of_day is not None else "[
|
|
6492
|
+
str(b.hour_of_day) if b.hour_of_day is not None else "[white]-[/white]",
|
|
6280
6493
|
str(b.invocation_count),
|
|
6281
6494
|
str(b.denied_count),
|
|
6282
6495
|
f"[{pct_style}]{denial_pct:.0%}[/{pct_style}]",
|
|
@@ -6295,7 +6508,7 @@ def memory_audit(limit: int):
|
|
|
6295
6508
|
entries = store.get_audit_log(limit=limit)
|
|
6296
6509
|
|
|
6297
6510
|
if not entries:
|
|
6298
|
-
console.print("[
|
|
6511
|
+
console.print("[white]No audit entries.[/white]")
|
|
6299
6512
|
return
|
|
6300
6513
|
|
|
6301
6514
|
table = Table(title=f"Memory Audit Log (last {limit})")
|
|
@@ -6337,7 +6550,7 @@ def memory_clear(table_name: Optional[str], confirm: bool):
|
|
|
6337
6550
|
if not confirm:
|
|
6338
6551
|
target = table_name or "ALL"
|
|
6339
6552
|
if not click.confirm(f"Clear {target} memory data? This cannot be undone"):
|
|
6340
|
-
console.print("[
|
|
6553
|
+
console.print("[white]Cancelled.[/white]")
|
|
6341
6554
|
return
|
|
6342
6555
|
|
|
6343
6556
|
store = get_memory_store()
|