tweek 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tweek/__init__.py +1 -1
- tweek/audit.py +2 -2
- tweek/cli.py +698 -439
- tweek/cli_helpers.py +6 -6
- tweek/cli_model.py +7 -7
- tweek/config/__init__.py +8 -0
- tweek/config/manager.py +33 -1
- tweek/config/models.py +307 -0
- tweek/config/patterns.yaml +1 -1
- tweek/diagnostics.py +59 -7
- tweek/hooks/post_tool_use.py +1 -1
- tweek/hooks/pre_tool_use.py +3 -3
- tweek/licensing.py +1 -1
- 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.1.dist-info}/METADATA +22 -15
- {tweek-0.2.1.dist-info → tweek-0.3.1.dist-info}/RECORD +25 -23
- tweek-0.3.1.dist-info/licenses/NOTICE +199 -0
- {tweek-0.2.1.dist-info → tweek-0.3.1.dist-info}/WHEEL +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.2.1.dist-info → tweek-0.3.1.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,38 @@ 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 unprotect
|
|
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
|
|
1222
1188
|
"""
|
|
1223
1189
|
)
|
|
1224
|
-
@click.
|
|
1225
|
-
|
|
1226
|
-
@click.option("--
|
|
1227
|
-
help="Remove
|
|
1190
|
+
@click.argument("tool", required=False, type=click.Choice(
|
|
1191
|
+
["claude-code", "openclaw", "claude-desktop", "chatgpt", "gemini"]))
|
|
1192
|
+
@click.option("--global", "unprotect_global", is_flag=True, default=False,
|
|
1193
|
+
help="Remove from ~/.claude/ (global installation)")
|
|
1228
1194
|
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
1229
|
-
def
|
|
1230
|
-
"""Remove Tweek
|
|
1195
|
+
def unprotect(tool: str, unprotect_global: bool, confirm: bool):
|
|
1196
|
+
"""Remove Tweek protection from an AI tool.
|
|
1231
1197
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1198
|
+
This removes hooks and MCP configuration for a specific tool
|
|
1199
|
+
but keeps Tweek installed on your system. Use `tweek uninstall`
|
|
1200
|
+
to fully remove Tweek.
|
|
1201
|
+
|
|
1202
|
+
When run without arguments, launches an interactive wizard
|
|
1203
|
+
that walks through each protected tool asking if you want
|
|
1204
|
+
to remove protection.
|
|
1235
1205
|
|
|
1236
1206
|
This command can only be run from an interactive terminal.
|
|
1237
1207
|
AI agents are blocked from running it.
|
|
@@ -1243,66 +1213,118 @@ def uninstall(uninstall_global: bool, everything: bool, confirm: bool):
|
|
|
1243
1213
|
# This is Layer 2 of protection (Layer 1 is the PreToolUse hook)
|
|
1244
1214
|
# ─────────────────────────────────────────────────────────────
|
|
1245
1215
|
if not sys.stdin.isatty():
|
|
1246
|
-
console.print("[red]ERROR: tweek
|
|
1247
|
-
console.print("[
|
|
1248
|
-
console.print("[
|
|
1216
|
+
console.print("[red]ERROR: tweek unprotect must be run from an interactive terminal.[/red]")
|
|
1217
|
+
console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
|
|
1218
|
+
console.print("[white]Open a terminal and run the command directly.[/white]")
|
|
1249
1219
|
raise SystemExit(1)
|
|
1250
1220
|
|
|
1221
|
+
# No tool: run interactive wizard
|
|
1222
|
+
if not tool:
|
|
1223
|
+
_run_unprotect_wizard()
|
|
1224
|
+
return
|
|
1225
|
+
|
|
1251
1226
|
console.print(TWEEK_BANNER, style="cyan")
|
|
1252
1227
|
|
|
1253
1228
|
tweek_dir = Path("~/.tweek").expanduser()
|
|
1254
1229
|
global_target = Path("~/.claude").expanduser()
|
|
1255
1230
|
project_target = Path.cwd() / ".claude"
|
|
1256
1231
|
|
|
1257
|
-
if
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
|
1232
|
+
if tool == "claude-code":
|
|
1233
|
+
if unprotect_global:
|
|
1234
|
+
_uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
|
|
1235
|
+
else:
|
|
1236
|
+
_uninstall_scope(project_target, tweek_dir, confirm, scope_label="project")
|
|
1237
|
+
return
|
|
1275
1238
|
|
|
1276
|
-
|
|
1239
|
+
if tool in ("claude-desktop", "chatgpt", "gemini"):
|
|
1240
|
+
try:
|
|
1241
|
+
from tweek.mcp.clients import get_client
|
|
1242
|
+
handler = get_client(tool)
|
|
1243
|
+
result = handler.uninstall()
|
|
1244
|
+
if result.get("success"):
|
|
1245
|
+
console.print(f"[green]{result.get('message', 'Uninstalled successfully')}[/green]")
|
|
1246
|
+
if result.get("backup"):
|
|
1247
|
+
console.print(f" Backup: {result['backup']}")
|
|
1248
|
+
if result.get("instructions"):
|
|
1249
|
+
console.print()
|
|
1250
|
+
for line in result["instructions"]:
|
|
1251
|
+
console.print(f" {line}")
|
|
1252
|
+
else:
|
|
1253
|
+
console.print(f"[red]{result.get('error', 'Uninstallation failed')}[/red]")
|
|
1254
|
+
except Exception as e:
|
|
1255
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
1256
|
+
return
|
|
1277
1257
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if has_project or has_global or has_data:
|
|
1284
|
-
options.append(("everything", "Everything — all hooks, skills, config, data, and MCP integrations"))
|
|
1258
|
+
if tool == "openclaw":
|
|
1259
|
+
console.print("[yellow]OpenClaw unprotect: removing Tweek proxy configuration...[/yellow]")
|
|
1260
|
+
# TODO: implement openclaw unprotect
|
|
1261
|
+
console.print("[white]Manual step: remove tweek plugin from openclaw.json[/white]")
|
|
1262
|
+
return
|
|
1285
1263
|
|
|
1286
|
-
for i, (_, label) in enumerate(options, 1):
|
|
1287
|
-
console.print(f" [bold]{i}.[/bold] {label}")
|
|
1288
1264
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1265
|
+
@main.command(
|
|
1266
|
+
epilog="""\b
|
|
1267
|
+
Examples:
|
|
1268
|
+
tweek uninstall Interactive full removal
|
|
1269
|
+
tweek uninstall --all Remove ALL Tweek data system-wide
|
|
1270
|
+
tweek uninstall --all --confirm Remove everything without prompts
|
|
1271
|
+
"""
|
|
1272
|
+
)
|
|
1273
|
+
@click.option("--all", "remove_all", is_flag=True, default=False,
|
|
1274
|
+
help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
|
|
1275
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts")
|
|
1276
|
+
def uninstall(remove_all: bool, confirm: bool):
|
|
1277
|
+
"""Fully remove Tweek from your system.
|
|
1291
1278
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1279
|
+
Removes all hooks, skills, configuration, data, and optionally
|
|
1280
|
+
the Tweek package itself. For removing protection from a single
|
|
1281
|
+
tool without uninstalling, use `tweek unprotect` instead.
|
|
1294
1282
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1283
|
+
This command can only be run from an interactive terminal.
|
|
1284
|
+
AI agents are blocked from running it.
|
|
1285
|
+
"""
|
|
1286
|
+
# ─────────────────────────────────────────────────────────────
|
|
1287
|
+
# HUMAN-ONLY GATE: Block non-interactive execution
|
|
1288
|
+
# ─────────────────────────────────────────────────────────────
|
|
1289
|
+
if not sys.stdin.isatty():
|
|
1290
|
+
console.print("[red]ERROR: tweek uninstall must be run from an interactive terminal.[/red]")
|
|
1291
|
+
console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
|
|
1292
|
+
console.print("[white]Open a terminal and run the command directly.[/white]")
|
|
1293
|
+
raise SystemExit(1)
|
|
1301
1294
|
|
|
1302
|
-
|
|
1295
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
1296
|
+
|
|
1297
|
+
tweek_dir = Path("~/.tweek").expanduser()
|
|
1298
|
+
global_target = Path("~/.claude").expanduser()
|
|
1299
|
+
project_target = Path.cwd() / ".claude"
|
|
1300
|
+
|
|
1301
|
+
if not remove_all:
|
|
1302
|
+
# Interactive: ask what to remove
|
|
1303
|
+
console.print("[bold]What would you like to remove?[/bold]")
|
|
1304
|
+
console.print()
|
|
1305
|
+
console.print(" [bold]1.[/bold] Everything (all hooks, data, config, and package)")
|
|
1306
|
+
console.print(" [bold]2.[/bold] Cancel")
|
|
1307
|
+
console.print()
|
|
1308
|
+
choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
|
|
1309
|
+
if choice == 2:
|
|
1310
|
+
console.print("[white]Cancelled[/white]")
|
|
1311
|
+
return
|
|
1312
|
+
console.print()
|
|
1313
|
+
|
|
1314
|
+
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
1303
1315
|
_show_package_removal_hint()
|
|
1304
1316
|
|
|
1305
1317
|
|
|
1318
|
+
@main.command()
|
|
1319
|
+
def status():
|
|
1320
|
+
"""Show Tweek protection status dashboard.
|
|
1321
|
+
|
|
1322
|
+
Scans for all supported AI tools and displays which are
|
|
1323
|
+
detected, which are protected by Tweek, and configuration details.
|
|
1324
|
+
"""
|
|
1325
|
+
_show_protection_status()
|
|
1326
|
+
|
|
1327
|
+
|
|
1306
1328
|
# ─────────────────────────────────────────────────────────────
|
|
1307
1329
|
# Uninstall Helpers
|
|
1308
1330
|
# ─────────────────────────────────────────────────────────────
|
|
@@ -1360,9 +1382,9 @@ def _show_package_removal_hint():
|
|
|
1360
1382
|
console.print("[bold yellow]The tweek CLI binary is still installed on your system.[/bold yellow]")
|
|
1361
1383
|
|
|
1362
1384
|
if len(pkg_cmds) > 1:
|
|
1363
|
-
console.print(f"[
|
|
1385
|
+
console.print(f"[white]Found {len(pkg_cmds)} installations:[/white]")
|
|
1364
1386
|
for cmd in pkg_cmds:
|
|
1365
|
-
console.print(f" [
|
|
1387
|
+
console.print(f" [white]• {cmd}[/white]")
|
|
1366
1388
|
|
|
1367
1389
|
console.print()
|
|
1368
1390
|
label = " + ".join(f"[bold]{cmd}[/bold]" for cmd in pkg_cmds)
|
|
@@ -1383,10 +1405,10 @@ def _show_package_removal_hint():
|
|
|
1383
1405
|
console.print(f"[green]✓[/green] Removed ({pkg_cmd})")
|
|
1384
1406
|
else:
|
|
1385
1407
|
console.print(f"[red]✗[/red] Failed: {result.stderr.strip()}")
|
|
1386
|
-
console.print(f" [
|
|
1408
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
1387
1409
|
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
1388
1410
|
console.print(f"[red]✗[/red] Could not run: {e}")
|
|
1389
|
-
console.print(f" [
|
|
1411
|
+
console.print(f" [white]Run manually: {pkg_cmd}[/white]")
|
|
1390
1412
|
|
|
1391
1413
|
|
|
1392
1414
|
def _has_tweek_at(target: Path) -> bool:
|
|
@@ -1620,18 +1642,18 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1620
1642
|
console.print()
|
|
1621
1643
|
console.print("[bold]The following will be removed:[/bold]")
|
|
1622
1644
|
if has_hooks:
|
|
1623
|
-
console.print(" [
|
|
1645
|
+
console.print(" [white]•[/white] PreToolUse and PostToolUse hooks from settings.json")
|
|
1624
1646
|
if has_skills:
|
|
1625
|
-
console.print(" [
|
|
1647
|
+
console.print(" [white]•[/white] Tweek skill directory (skills/tweek/)")
|
|
1626
1648
|
if has_backup:
|
|
1627
|
-
console.print(" [
|
|
1628
|
-
console.print(" [
|
|
1649
|
+
console.print(" [white]•[/white] Backup file (settings.json.tweek-backup)")
|
|
1650
|
+
console.print(" [white]•[/white] Project whitelist entries from overrides")
|
|
1629
1651
|
console.print()
|
|
1630
1652
|
|
|
1631
1653
|
if not confirm:
|
|
1632
1654
|
console.print(f"[yellow]Remove Tweek from this {scope_label}?[/yellow] ", end="")
|
|
1633
1655
|
if not click.confirm(""):
|
|
1634
|
-
console.print("[
|
|
1656
|
+
console.print("[white]Cancelled[/white]")
|
|
1635
1657
|
return
|
|
1636
1658
|
|
|
1637
1659
|
console.print()
|
|
@@ -1647,27 +1669,27 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1647
1669
|
if _remove_skill_directory(target):
|
|
1648
1670
|
console.print(f" [green]✓[/green] Removed Tweek skill directory (skills/tweek/)")
|
|
1649
1671
|
else:
|
|
1650
|
-
console.print(f" [
|
|
1672
|
+
console.print(f" [white]-[/white] Skipped: Tweek skill directory not found")
|
|
1651
1673
|
|
|
1652
1674
|
# 3. Remove backup file
|
|
1653
1675
|
if _remove_backup_file(target):
|
|
1654
1676
|
console.print(f" [green]✓[/green] Removed backup file (settings.json.tweek-backup)")
|
|
1655
1677
|
else:
|
|
1656
|
-
console.print(f" [
|
|
1678
|
+
console.print(f" [white]-[/white] Skipped: no backup file found")
|
|
1657
1679
|
|
|
1658
1680
|
# 4. Remove whitelist entries
|
|
1659
1681
|
wl_count = _remove_whitelist_entries(target, tweek_dir)
|
|
1660
1682
|
if wl_count > 0:
|
|
1661
1683
|
console.print(f" [green]✓[/green] Removed {wl_count} whitelist entry(s) from overrides")
|
|
1662
1684
|
else:
|
|
1663
|
-
console.print(f" [
|
|
1685
|
+
console.print(f" [white]-[/white] Skipped: no whitelist entries found for this {scope_label}")
|
|
1664
1686
|
|
|
1665
1687
|
console.print()
|
|
1666
1688
|
console.print(f"[green]Uninstall complete.[/green] Tweek is no longer active for this {scope_label}.")
|
|
1667
1689
|
if scope_label == "project":
|
|
1668
|
-
console.print("[
|
|
1690
|
+
console.print("[white]Global installation (~/.claude/) was not affected.[/white]")
|
|
1669
1691
|
else:
|
|
1670
|
-
console.print("[
|
|
1692
|
+
console.print("[white]Project installations were not affected.[/white]")
|
|
1671
1693
|
|
|
1672
1694
|
# Offer to remove data directory
|
|
1673
1695
|
if tweek_dir.exists() and not confirm:
|
|
@@ -1680,7 +1702,7 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1680
1702
|
|
|
1681
1703
|
console.print()
|
|
1682
1704
|
console.print("[yellow]Also remove Tweek data directory (~/.tweek/)?[/yellow]")
|
|
1683
|
-
console.print("[
|
|
1705
|
+
console.print("[white]This contains config, patterns, security logs, and overrides.[/white]")
|
|
1684
1706
|
if other_has_tweek:
|
|
1685
1707
|
console.print(f"[bold red]Warning:[/bold red] Tweek is still installed at {other_label} scope ({other_target}).")
|
|
1686
1708
|
console.print(f" Removing ~/.tweek/ will affect that installation (no config, patterns, or logs).")
|
|
@@ -1695,9 +1717,9 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
1695
1717
|
for item in data_removed:
|
|
1696
1718
|
console.print(f" [green]✓[/green] Removed {item}")
|
|
1697
1719
|
if not data_removed:
|
|
1698
|
-
console.print(f" [
|
|
1720
|
+
console.print(f" [white]-[/white] No data to remove")
|
|
1699
1721
|
elif tweek_dir.exists():
|
|
1700
|
-
console.print("[
|
|
1722
|
+
console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
|
|
1701
1723
|
|
|
1702
1724
|
|
|
1703
1725
|
def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
|
|
@@ -1705,28 +1727,28 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1705
1727
|
import json
|
|
1706
1728
|
|
|
1707
1729
|
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(" [
|
|
1730
|
+
console.print(" [white]•[/white] Hooks from current project (.claude/settings.json)")
|
|
1731
|
+
console.print(" [white]•[/white] Hooks from global installation (~/.claude/settings.json)")
|
|
1732
|
+
console.print(" [white]•[/white] Tweek skill directories (project + global)")
|
|
1733
|
+
console.print(" [white]•[/white] All backup files")
|
|
1734
|
+
console.print(" [white]•[/white] Tweek data directory (~/.tweek/)")
|
|
1713
1735
|
|
|
1714
1736
|
# Show what exists in ~/.tweek/
|
|
1715
1737
|
if tweek_dir.exists():
|
|
1716
1738
|
for item in sorted(tweek_dir.iterdir()):
|
|
1717
1739
|
if item.is_dir():
|
|
1718
|
-
console.print(f" [
|
|
1740
|
+
console.print(f" [white]├── {item.name}/ [/white]")
|
|
1719
1741
|
else:
|
|
1720
|
-
console.print(f" [
|
|
1742
|
+
console.print(f" [white]├── {item.name}[/white]")
|
|
1721
1743
|
|
|
1722
|
-
console.print(" [
|
|
1744
|
+
console.print(" [white]•[/white] MCP integrations (Claude Desktop, ChatGPT)")
|
|
1723
1745
|
console.print()
|
|
1724
1746
|
|
|
1725
1747
|
if not confirm:
|
|
1726
1748
|
console.print("[bold red]Type 'yes' to confirm full removal[/bold red]: ", end="")
|
|
1727
1749
|
response = input()
|
|
1728
1750
|
if response.strip().lower() != "yes":
|
|
1729
|
-
console.print("[
|
|
1751
|
+
console.print("[white]Cancelled[/white]")
|
|
1730
1752
|
return
|
|
1731
1753
|
|
|
1732
1754
|
console.print()
|
|
@@ -1737,17 +1759,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1737
1759
|
for hook_type in removed_hooks:
|
|
1738
1760
|
console.print(f" [green]✓[/green] Removed {hook_type} hook from project settings.json")
|
|
1739
1761
|
if not removed_hooks:
|
|
1740
|
-
console.print(f" [
|
|
1762
|
+
console.print(f" [white]-[/white] Skipped: no project hooks found")
|
|
1741
1763
|
|
|
1742
1764
|
if _remove_skill_directory(project_target):
|
|
1743
1765
|
console.print(f" [green]✓[/green] Removed Tweek skill from project")
|
|
1744
1766
|
else:
|
|
1745
|
-
console.print(f" [
|
|
1767
|
+
console.print(f" [white]-[/white] Skipped: no project skill directory")
|
|
1746
1768
|
|
|
1747
1769
|
if _remove_backup_file(project_target):
|
|
1748
1770
|
console.print(f" [green]✓[/green] Removed project backup file")
|
|
1749
1771
|
else:
|
|
1750
|
-
console.print(f" [
|
|
1772
|
+
console.print(f" [white]-[/white] Skipped: no project backup file")
|
|
1751
1773
|
|
|
1752
1774
|
console.print()
|
|
1753
1775
|
|
|
@@ -1757,17 +1779,17 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1757
1779
|
for hook_type in removed_hooks:
|
|
1758
1780
|
console.print(f" [green]✓[/green] Removed {hook_type} hook from global settings.json")
|
|
1759
1781
|
if not removed_hooks:
|
|
1760
|
-
console.print(f" [
|
|
1782
|
+
console.print(f" [white]-[/white] Skipped: no global hooks found")
|
|
1761
1783
|
|
|
1762
1784
|
if _remove_skill_directory(global_target):
|
|
1763
1785
|
console.print(f" [green]✓[/green] Removed Tweek skill from global installation")
|
|
1764
1786
|
else:
|
|
1765
|
-
console.print(f" [
|
|
1787
|
+
console.print(f" [white]-[/white] Skipped: no global skill directory")
|
|
1766
1788
|
|
|
1767
1789
|
if _remove_backup_file(global_target):
|
|
1768
1790
|
console.print(f" [green]✓[/green] Removed global backup file")
|
|
1769
1791
|
else:
|
|
1770
|
-
console.print(f" [
|
|
1792
|
+
console.print(f" [white]-[/white] Skipped: no global backup file")
|
|
1771
1793
|
|
|
1772
1794
|
console.print()
|
|
1773
1795
|
|
|
@@ -1777,7 +1799,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1777
1799
|
for item in data_removed:
|
|
1778
1800
|
console.print(f" [green]✓[/green] Removed {item}")
|
|
1779
1801
|
if not data_removed:
|
|
1780
|
-
console.print(f" [
|
|
1802
|
+
console.print(f" [white]-[/white] Skipped: no data directory found")
|
|
1781
1803
|
|
|
1782
1804
|
console.print()
|
|
1783
1805
|
|
|
@@ -1787,7 +1809,7 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
1787
1809
|
for client in mcp_removed:
|
|
1788
1810
|
console.print(f" [green]✓[/green] Removed {client} MCP integration")
|
|
1789
1811
|
if not mcp_removed:
|
|
1790
|
-
console.print(f" [
|
|
1812
|
+
console.print(f" [white]-[/white] Skipped: no MCP integrations found")
|
|
1791
1813
|
|
|
1792
1814
|
console.print()
|
|
1793
1815
|
console.print("[green]All Tweek data has been removed.[/green]")
|
|
@@ -1866,8 +1888,8 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1866
1888
|
]
|
|
1867
1889
|
|
|
1868
1890
|
if not whitelist:
|
|
1869
|
-
console.print("[
|
|
1870
|
-
console.print("[
|
|
1891
|
+
console.print("[white]No trusted paths configured.[/white]")
|
|
1892
|
+
console.print("[white]Use 'tweek trust' to trust the current project.[/white]")
|
|
1871
1893
|
return
|
|
1872
1894
|
|
|
1873
1895
|
if trusted_entries:
|
|
@@ -1876,16 +1898,16 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1876
1898
|
entry_reason = entry.get("reason", "")
|
|
1877
1899
|
console.print(f" [green]✓[/green] {entry['path']}")
|
|
1878
1900
|
if entry_reason:
|
|
1879
|
-
console.print(f" [
|
|
1901
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1880
1902
|
|
|
1881
1903
|
if tool_scoped:
|
|
1882
1904
|
console.print("\n[bold]Tool-scoped whitelist entries:[/bold]\n")
|
|
1883
1905
|
for entry in tool_scoped:
|
|
1884
1906
|
tools = ", ".join(entry.get("tools", []))
|
|
1885
1907
|
entry_reason = entry.get("reason", "")
|
|
1886
|
-
console.print(f" [cyan]○[/cyan] {entry['path']} [
|
|
1908
|
+
console.print(f" [cyan]○[/cyan] {entry['path']} [white]({tools})[/white]")
|
|
1887
1909
|
if entry_reason:
|
|
1888
|
-
console.print(f" [
|
|
1910
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1889
1911
|
|
|
1890
1912
|
if other_entries:
|
|
1891
1913
|
console.print("\n[bold]Other whitelist entries:[/bold]\n")
|
|
@@ -1896,9 +1918,9 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1896
1918
|
console.print(f" [cyan]○[/cyan] Command: {entry['command_prefix']}")
|
|
1897
1919
|
entry_reason = entry.get("reason", "")
|
|
1898
1920
|
if entry_reason:
|
|
1899
|
-
console.print(f" [
|
|
1921
|
+
console.print(f" [white]{entry_reason}[/white]")
|
|
1900
1922
|
|
|
1901
|
-
console.print(f"\n[
|
|
1923
|
+
console.print(f"\n[white]Config: {overrides_path}[/white]")
|
|
1902
1924
|
return
|
|
1903
1925
|
|
|
1904
1926
|
# Resolve path to absolute
|
|
@@ -1915,7 +1937,7 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1915
1937
|
|
|
1916
1938
|
if already_trusted:
|
|
1917
1939
|
console.print(f"[green]✓[/green] Already trusted: {resolved}")
|
|
1918
|
-
console.print("[
|
|
1940
|
+
console.print("[white]Use 'tweek untrust' to remove.[/white]")
|
|
1919
1941
|
return
|
|
1920
1942
|
|
|
1921
1943
|
# Add whitelist entry (no tools restriction = all tools exempt)
|
|
@@ -1933,8 +1955,8 @@ def trust(path: str, reason: str, list_trusted: bool):
|
|
|
1933
1955
|
return
|
|
1934
1956
|
|
|
1935
1957
|
console.print(f"[green]✓[/green] Trusted: {resolved}")
|
|
1936
|
-
console.print(f" [
|
|
1937
|
-
console.print(f" [
|
|
1958
|
+
console.print(f" [white]All screening is now skipped for files in this directory.[/white]")
|
|
1959
|
+
console.print(f" [white]To resume screening: tweek untrust {path}[/white]")
|
|
1938
1960
|
|
|
1939
1961
|
|
|
1940
1962
|
@main.command(
|
|
@@ -1982,7 +2004,7 @@ def untrust(path: str):
|
|
|
1982
2004
|
|
|
1983
2005
|
if len(whitelist) == original_len:
|
|
1984
2006
|
console.print(f"[yellow]This path is not currently trusted:[/yellow] {resolved}")
|
|
1985
|
-
console.print("[
|
|
2007
|
+
console.print("[white]Use 'tweek trust --list' to see all trusted paths.[/white]")
|
|
1986
2008
|
return
|
|
1987
2009
|
|
|
1988
2010
|
overrides["whitelist"] = whitelist
|
|
@@ -1998,7 +2020,7 @@ def untrust(path: str):
|
|
|
1998
2020
|
return
|
|
1999
2021
|
|
|
2000
2022
|
console.print(f"[green]✓[/green] Removed trust: {resolved}")
|
|
2001
|
-
console.print(f" [
|
|
2023
|
+
console.print(f" [white]Tweek will now screen tool calls for files in this directory.[/white]")
|
|
2002
2024
|
|
|
2003
2025
|
|
|
2004
2026
|
@main.command(
|
|
@@ -2015,7 +2037,7 @@ def update(check: bool):
|
|
|
2015
2037
|
Patterns are stored in ~/.tweek/patterns/ and can be updated
|
|
2016
2038
|
independently of the Tweek application.
|
|
2017
2039
|
|
|
2018
|
-
All
|
|
2040
|
+
All 259 patterns are included free. PRO tier adds LLM review,
|
|
2019
2041
|
session analysis, and rate limiting.
|
|
2020
2042
|
"""
|
|
2021
2043
|
import subprocess
|
|
@@ -2029,7 +2051,7 @@ def update(check: bool):
|
|
|
2029
2051
|
# First time: clone the repo
|
|
2030
2052
|
if check:
|
|
2031
2053
|
console.print("[yellow]Patterns not installed.[/yellow]")
|
|
2032
|
-
console.print(f"[
|
|
2054
|
+
console.print(f"[white]Run 'tweek update' to install from {patterns_repo}[/white]")
|
|
2033
2055
|
return
|
|
2034
2056
|
|
|
2035
2057
|
console.print(f"[cyan]Installing patterns from {patterns_repo}...[/cyan]")
|
|
@@ -2051,15 +2073,15 @@ def update(check: bool):
|
|
|
2051
2073
|
data = yaml.safe_load(f)
|
|
2052
2074
|
count = data.get("pattern_count", len(data.get("patterns", [])))
|
|
2053
2075
|
free_max = data.get("free_tier_max", 23)
|
|
2054
|
-
console.print(f"[
|
|
2076
|
+
console.print(f"[white]Installed {count} patterns ({free_max} free, {count - free_max} pro)[/white]")
|
|
2055
2077
|
|
|
2056
2078
|
except subprocess.CalledProcessError as e:
|
|
2057
2079
|
console.print(f"[red]✗[/red] Failed to clone patterns: {e.stderr}")
|
|
2058
2080
|
return
|
|
2059
2081
|
except FileNotFoundError:
|
|
2060
2082
|
console.print("[red]\u2717[/red] git not found.")
|
|
2061
|
-
console.print(" [
|
|
2062
|
-
console.print(" [
|
|
2083
|
+
console.print(" [white]Hint: Install git from https://git-scm.com/downloads[/white]")
|
|
2084
|
+
console.print(" [white]On macOS: xcode-select --install[/white]")
|
|
2063
2085
|
return
|
|
2064
2086
|
|
|
2065
2087
|
else:
|
|
@@ -2080,7 +2102,7 @@ def update(check: bool):
|
|
|
2080
2102
|
)
|
|
2081
2103
|
if "behind" in result2.stdout:
|
|
2082
2104
|
console.print("[yellow]Updates available.[/yellow]")
|
|
2083
|
-
console.print("[
|
|
2105
|
+
console.print("[white]Run 'tweek update' to install[/white]")
|
|
2084
2106
|
else:
|
|
2085
2107
|
console.print("[green]✓[/green] Patterns are up to date")
|
|
2086
2108
|
except Exception as e:
|
|
@@ -2104,11 +2126,11 @@ def update(check: bool):
|
|
|
2104
2126
|
|
|
2105
2127
|
# Show what changed
|
|
2106
2128
|
if result.stdout.strip():
|
|
2107
|
-
console.print(f"[
|
|
2129
|
+
console.print(f"[white]{result.stdout.strip()}[/white]")
|
|
2108
2130
|
|
|
2109
2131
|
except subprocess.CalledProcessError as e:
|
|
2110
2132
|
console.print(f"[red]✗[/red] Failed to update patterns: {e.stderr}")
|
|
2111
|
-
console.print("[
|
|
2133
|
+
console.print("[white]Try: rm -rf ~/.tweek/patterns && tweek update[/white]")
|
|
2112
2134
|
return
|
|
2113
2135
|
|
|
2114
2136
|
# Show current version info
|
|
@@ -2126,7 +2148,7 @@ def update(check: bool):
|
|
|
2126
2148
|
console.print(f"[cyan]Total patterns:[/cyan] {count} (all included free)")
|
|
2127
2149
|
|
|
2128
2150
|
console.print(f"[cyan]All features:[/cyan] LLM review, session analysis, rate limiting, sandbox (open source)")
|
|
2129
|
-
console.print(f"[
|
|
2151
|
+
console.print(f"[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
|
|
2130
2152
|
|
|
2131
2153
|
except Exception:
|
|
2132
2154
|
pass
|
|
@@ -2159,22 +2181,6 @@ def doctor(verbose: bool, json_out: bool):
|
|
|
2159
2181
|
print_doctor_results(checks)
|
|
2160
2182
|
|
|
2161
2183
|
|
|
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
2184
|
|
|
2179
2185
|
@main.command("upgrade")
|
|
2180
2186
|
def upgrade():
|
|
@@ -2307,7 +2313,7 @@ def audit(path, translate, llm_review, json_out):
|
|
|
2307
2313
|
credential theft, data exfiltration, and other attack patterns.
|
|
2308
2314
|
|
|
2309
2315
|
Non-English content is detected and translated to English before
|
|
2310
|
-
running all
|
|
2316
|
+
running all 259 regex patterns. LLM semantic review provides
|
|
2311
2317
|
additional analysis for obfuscated attacks.
|
|
2312
2318
|
|
|
2313
2319
|
\b
|
|
@@ -2340,8 +2346,8 @@ def audit(path, translate, llm_review, json_out):
|
|
|
2340
2346
|
skills = scan_installed_skills()
|
|
2341
2347
|
|
|
2342
2348
|
if not skills:
|
|
2343
|
-
console.print("[
|
|
2344
|
-
console.print("[
|
|
2349
|
+
console.print("[white]No installed skills found.[/white]")
|
|
2350
|
+
console.print("[white]Specify a file path to audit: tweek audit <path>[/white]")
|
|
2345
2351
|
return
|
|
2346
2352
|
|
|
2347
2353
|
console.print(f"Found {len(skills)} skill(s)")
|
|
@@ -2390,7 +2396,7 @@ def _print_audit_result(result):
|
|
|
2390
2396
|
risk_icons = {"safe": "[green]SAFE[/green]", "suspicious": "[yellow]SUSPICIOUS[/yellow]", "dangerous": "[red]DANGEROUS[/red]"}
|
|
2391
2397
|
|
|
2392
2398
|
console.print(f" [bold]{result.skill_name}[/bold] — {risk_icons.get(result.risk_level, result.risk_level)}")
|
|
2393
|
-
console.print(f" [
|
|
2399
|
+
console.print(f" [white]{result.skill_path}[/white]")
|
|
2394
2400
|
|
|
2395
2401
|
if result.error:
|
|
2396
2402
|
console.print(f" [red]Error: {result.error}[/red]")
|
|
@@ -2405,12 +2411,12 @@ def _print_audit_result(result):
|
|
|
2405
2411
|
|
|
2406
2412
|
if result.findings:
|
|
2407
2413
|
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
2408
|
-
table.add_column("Severity", style="
|
|
2414
|
+
table.add_column("Severity", style="white")
|
|
2409
2415
|
table.add_column("Pattern")
|
|
2410
2416
|
table.add_column("Description")
|
|
2411
|
-
table.add_column("Match", style="
|
|
2417
|
+
table.add_column("Match", style="white")
|
|
2412
2418
|
|
|
2413
|
-
severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "
|
|
2419
|
+
severity_styles = {"critical": "red bold", "high": "red", "medium": "yellow", "low": "white"}
|
|
2414
2420
|
|
|
2415
2421
|
for finding in result.findings:
|
|
2416
2422
|
table.add_row(
|
|
@@ -2509,7 +2515,7 @@ def quickstart():
|
|
|
2509
2515
|
# Step 2: Security preset
|
|
2510
2516
|
console.print("[bold cyan]Step 2/4: Security Preset[/bold cyan]")
|
|
2511
2517
|
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 [
|
|
2518
|
+
console.print(" [cyan]2.[/cyan] cautious \u2014 Block dangerous, prompt on risky [white](recommended)[/white]")
|
|
2513
2519
|
console.print(" [cyan]3.[/cyan] trusted \u2014 Allow most operations, block only dangerous")
|
|
2514
2520
|
console.print()
|
|
2515
2521
|
|
|
@@ -2548,17 +2554,16 @@ def quickstart():
|
|
|
2548
2554
|
if setup_mcp:
|
|
2549
2555
|
try:
|
|
2550
2556
|
import mcp # noqa: F401
|
|
2551
|
-
console.print("[
|
|
2552
|
-
console.print("[
|
|
2557
|
+
console.print("[white]MCP package available. Configure upstream servers in ~/.tweek/config.yaml[/white]")
|
|
2558
|
+
console.print("[white]Then run: tweek mcp proxy[/white]")
|
|
2553
2559
|
except ImportError:
|
|
2554
2560
|
print_warning("MCP package not installed. Install with: pip install tweek[mcp]")
|
|
2555
2561
|
else:
|
|
2556
|
-
console.print("[
|
|
2562
|
+
console.print("[white]Skipped.[/white]")
|
|
2557
2563
|
|
|
2558
2564
|
console.print()
|
|
2559
2565
|
console.print("[bold green]Setup complete![/bold green]")
|
|
2560
2566
|
console.print(" Run [cyan]tweek doctor[/cyan] to verify your installation")
|
|
2561
|
-
console.print(" Run [cyan]tweek status[/cyan] to see protection status")
|
|
2562
2567
|
|
|
2563
2568
|
|
|
2564
2569
|
def _quickstart_install_hooks(scope: str) -> None:
|
|
@@ -2626,21 +2631,31 @@ def _quickstart_install_hooks(scope: str) -> None:
|
|
|
2626
2631
|
# =============================================================================
|
|
2627
2632
|
|
|
2628
2633
|
@main.group(
|
|
2634
|
+
invoke_without_command=True,
|
|
2629
2635
|
epilog="""\b
|
|
2630
2636
|
Examples:
|
|
2631
|
-
tweek protect
|
|
2632
|
-
tweek protect
|
|
2633
|
-
tweek protect
|
|
2634
|
-
tweek protect
|
|
2637
|
+
tweek protect Interactive wizard — detect & protect all tools
|
|
2638
|
+
tweek protect --status Show protection status for all tools
|
|
2639
|
+
tweek protect claude-code Install Claude Code hooks
|
|
2640
|
+
tweek protect openclaw One-command OpenClaw protection
|
|
2641
|
+
tweek protect claude-desktop Configure Claude Desktop integration
|
|
2642
|
+
tweek protect chatgpt Set up ChatGPT Desktop integration
|
|
2643
|
+
tweek protect gemini Configure Gemini CLI integration
|
|
2635
2644
|
"""
|
|
2636
2645
|
)
|
|
2637
|
-
|
|
2638
|
-
|
|
2646
|
+
@click.option("--status", is_flag=True, help="Show protection status for all tools")
|
|
2647
|
+
@click.pass_context
|
|
2648
|
+
def protect(ctx, status):
|
|
2649
|
+
"""Set up Tweek protection for AI tools.
|
|
2639
2650
|
|
|
2640
|
-
|
|
2641
|
-
|
|
2651
|
+
When run without a subcommand, launches an interactive wizard
|
|
2652
|
+
that auto-detects installed AI tools and offers to protect them.
|
|
2642
2653
|
"""
|
|
2643
|
-
|
|
2654
|
+
if status:
|
|
2655
|
+
_show_protection_status()
|
|
2656
|
+
return
|
|
2657
|
+
if ctx.invoked_subcommand is None:
|
|
2658
|
+
_run_protect_wizard()
|
|
2644
2659
|
|
|
2645
2660
|
|
|
2646
2661
|
@protect.command(
|
|
@@ -2688,11 +2703,11 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2688
2703
|
console.print()
|
|
2689
2704
|
console.print("[red]OpenClaw not detected on this system.[/red]")
|
|
2690
2705
|
console.print()
|
|
2691
|
-
console.print("[
|
|
2706
|
+
console.print("[white]Install OpenClaw first:[/white]")
|
|
2692
2707
|
console.print(" npm install -g openclaw")
|
|
2693
2708
|
console.print()
|
|
2694
|
-
console.print("[
|
|
2695
|
-
console.print("[
|
|
2709
|
+
console.print("[white]Or if OpenClaw is installed in a non-standard location,[/white]")
|
|
2710
|
+
console.print("[white]specify the gateway port manually:[/white]")
|
|
2696
2711
|
console.print(" tweek protect openclaw --port 18789")
|
|
2697
2712
|
return
|
|
2698
2713
|
|
|
@@ -2709,7 +2724,7 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2709
2724
|
elif openclaw["process_running"]:
|
|
2710
2725
|
console.print(" [yellow](process running, gateway inactive)[/yellow]")
|
|
2711
2726
|
else:
|
|
2712
|
-
console.print(" [
|
|
2727
|
+
console.print(" [white](not running)[/white]")
|
|
2713
2728
|
|
|
2714
2729
|
if openclaw["config_path"]:
|
|
2715
2730
|
console.print(f" Config: {openclaw['config_path']}")
|
|
@@ -2726,14 +2741,14 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2726
2741
|
|
|
2727
2742
|
# Show configuration
|
|
2728
2743
|
console.print(f" Scanner: port {result.scanner_port} -> wrapping OpenClaw gateway")
|
|
2729
|
-
console.print(f" Preset: {result.preset} (
|
|
2744
|
+
console.print(f" Preset: {result.preset} (259 patterns + rate limiting)")
|
|
2730
2745
|
|
|
2731
2746
|
# Check for API key
|
|
2732
2747
|
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
2733
2748
|
if anthropic_key:
|
|
2734
2749
|
console.print(" LLM Review: [green]active[/green] (ANTHROPIC_API_KEY found)")
|
|
2735
2750
|
else:
|
|
2736
|
-
console.print(" LLM Review: [
|
|
2751
|
+
console.print(" LLM Review: [white]available (set ANTHROPIC_API_KEY for semantic analysis)[/white]")
|
|
2737
2752
|
|
|
2738
2753
|
# Show warnings
|
|
2739
2754
|
for warning in result.warnings:
|
|
@@ -2743,53 +2758,366 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2743
2758
|
|
|
2744
2759
|
if not openclaw["gateway_active"]:
|
|
2745
2760
|
console.print("[yellow]Note: OpenClaw gateway is not currently running.[/yellow]")
|
|
2746
|
-
console.print("[
|
|
2761
|
+
console.print("[white]Protection will activate when OpenClaw starts.[/white]")
|
|
2747
2762
|
console.print()
|
|
2748
2763
|
|
|
2749
2764
|
console.print("[green]Protection configured.[/green] Screening all OpenClaw tool calls.")
|
|
2750
2765
|
console.print()
|
|
2751
|
-
console.print("[
|
|
2752
|
-
console.print("[
|
|
2753
|
-
console.print("[
|
|
2766
|
+
console.print("[white]Verify: tweek doctor[/white]")
|
|
2767
|
+
console.print("[white]Logs: tweek logs show[/white]")
|
|
2768
|
+
console.print("[white]Stop: tweek proxy stop[/white]")
|
|
2754
2769
|
|
|
2755
2770
|
|
|
2756
2771
|
@protect.command(
|
|
2757
|
-
"claude",
|
|
2772
|
+
"claude-code",
|
|
2758
2773
|
epilog="""\b
|
|
2759
2774
|
Examples:
|
|
2760
|
-
tweek protect claude
|
|
2761
|
-
tweek protect claude --global
|
|
2775
|
+
tweek protect claude-code Install for current project
|
|
2776
|
+
tweek protect claude-code --global Install globally (all projects)
|
|
2777
|
+
tweek protect claude-code --quick Zero-prompt install with defaults
|
|
2778
|
+
tweek protect claude-code --preset paranoid Apply paranoid security preset
|
|
2762
2779
|
"""
|
|
2763
2780
|
)
|
|
2764
2781
|
@click.option("--global", "install_global", is_flag=True, default=False,
|
|
2765
2782
|
help="Install globally to ~/.claude/ (protects all projects)")
|
|
2783
|
+
@click.option("--dev-test", is_flag=True, hidden=True,
|
|
2784
|
+
help="Install to test environment (for Tweek development only)")
|
|
2785
|
+
@click.option("--backup/--no-backup", default=True,
|
|
2786
|
+
help="Backup existing hooks before installation")
|
|
2787
|
+
@click.option("--skip-env-scan", is_flag=True,
|
|
2788
|
+
help="Skip scanning for .env files to migrate")
|
|
2789
|
+
@click.option("--interactive", "-i", is_flag=True,
|
|
2790
|
+
help="Interactively configure security settings")
|
|
2766
2791
|
@click.option("--preset", type=click.Choice(["paranoid", "cautious", "trusted"]),
|
|
2767
|
-
|
|
2768
|
-
@click.
|
|
2769
|
-
|
|
2792
|
+
help="Apply a security preset (skip interactive)")
|
|
2793
|
+
@click.option("--ai-defaults", is_flag=True,
|
|
2794
|
+
help="Let AI suggest default settings based on detected skills")
|
|
2795
|
+
@click.option("--with-sandbox", is_flag=True,
|
|
2796
|
+
help="Prompt to install sandbox tool if not available (Linux only)")
|
|
2797
|
+
@click.option("--force-proxy", is_flag=True,
|
|
2798
|
+
help="Force Tweek proxy to override existing proxy configurations (e.g., openclaw)")
|
|
2799
|
+
@click.option("--skip-proxy-check", is_flag=True,
|
|
2800
|
+
help="Skip checking for existing proxy configurations")
|
|
2801
|
+
@click.option("--quick", is_flag=True,
|
|
2802
|
+
help="Zero-prompt install with cautious defaults (skips env scan and proxy check)")
|
|
2803
|
+
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
2804
|
"""Install Tweek hooks for Claude Code.
|
|
2771
2805
|
|
|
2772
|
-
|
|
2773
|
-
|
|
2806
|
+
Installs PreToolUse and PostToolUse hooks to screen all
|
|
2807
|
+
Claude Code tool calls through Tweek's security pipeline.
|
|
2774
2808
|
"""
|
|
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,
|
|
2809
|
+
_install_claude_code_hooks(
|
|
2780
2810
|
install_global=install_global,
|
|
2781
|
-
dev_test=
|
|
2782
|
-
backup=
|
|
2783
|
-
skip_env_scan=
|
|
2784
|
-
interactive=
|
|
2811
|
+
dev_test=dev_test,
|
|
2812
|
+
backup=backup,
|
|
2813
|
+
skip_env_scan=skip_env_scan,
|
|
2814
|
+
interactive=interactive,
|
|
2785
2815
|
preset=preset,
|
|
2786
|
-
ai_defaults=
|
|
2787
|
-
with_sandbox=
|
|
2788
|
-
force_proxy=
|
|
2789
|
-
skip_proxy_check=
|
|
2816
|
+
ai_defaults=ai_defaults,
|
|
2817
|
+
with_sandbox=with_sandbox,
|
|
2818
|
+
force_proxy=force_proxy,
|
|
2819
|
+
skip_proxy_check=skip_proxy_check,
|
|
2820
|
+
quick=quick,
|
|
2790
2821
|
)
|
|
2791
2822
|
|
|
2792
2823
|
|
|
2824
|
+
@protect.command("claude-desktop")
|
|
2825
|
+
def protect_claude_desktop():
|
|
2826
|
+
"""Configure Tweek as MCP server for Claude Desktop."""
|
|
2827
|
+
_protect_mcp_client("claude-desktop")
|
|
2828
|
+
|
|
2829
|
+
|
|
2830
|
+
@protect.command("chatgpt")
|
|
2831
|
+
def protect_chatgpt():
|
|
2832
|
+
"""Configure Tweek as MCP server for ChatGPT Desktop."""
|
|
2833
|
+
_protect_mcp_client("chatgpt")
|
|
2834
|
+
|
|
2835
|
+
|
|
2836
|
+
@protect.command("gemini")
|
|
2837
|
+
def protect_gemini():
|
|
2838
|
+
"""Configure Tweek as MCP server for Gemini CLI."""
|
|
2839
|
+
_protect_mcp_client("gemini")
|
|
2840
|
+
|
|
2841
|
+
|
|
2842
|
+
def _protect_mcp_client(client_name: str):
|
|
2843
|
+
"""Shared logic for MCP client protection commands."""
|
|
2844
|
+
try:
|
|
2845
|
+
from tweek.mcp.clients import get_client
|
|
2846
|
+
|
|
2847
|
+
handler = get_client(client_name)
|
|
2848
|
+
result = handler.install()
|
|
2849
|
+
|
|
2850
|
+
if result.get("success"):
|
|
2851
|
+
console.print(f"[green]{result.get('message', 'Installed successfully')}[/green]")
|
|
2852
|
+
if result.get("config_path"):
|
|
2853
|
+
console.print(f" Config: {result['config_path']}")
|
|
2854
|
+
if result.get("backup"):
|
|
2855
|
+
console.print(f" Backup: {result['backup']}")
|
|
2856
|
+
if result.get("instructions"):
|
|
2857
|
+
console.print()
|
|
2858
|
+
for line in result["instructions"]:
|
|
2859
|
+
console.print(f" {line}")
|
|
2860
|
+
else:
|
|
2861
|
+
console.print(f"[red]{result.get('error', 'Installation failed')}[/red]")
|
|
2862
|
+
except Exception as e:
|
|
2863
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
2864
|
+
|
|
2865
|
+
|
|
2866
|
+
# =============================================================================
|
|
2867
|
+
# PROTECT WIZARD & STATUS HELPERS
|
|
2868
|
+
# =============================================================================
|
|
2869
|
+
|
|
2870
|
+
|
|
2871
|
+
def _detect_all_tools():
|
|
2872
|
+
"""Detect all supported AI tools and their protection status.
|
|
2873
|
+
|
|
2874
|
+
Returns list of (tool_id, label, installed, protected, detail) tuples.
|
|
2875
|
+
"""
|
|
2876
|
+
import shutil
|
|
2877
|
+
import json
|
|
2878
|
+
|
|
2879
|
+
tools = []
|
|
2880
|
+
|
|
2881
|
+
# Claude Code
|
|
2882
|
+
claude_installed = shutil.which("claude") is not None
|
|
2883
|
+
claude_protected = _has_tweek_at(Path("~/.claude").expanduser()) if claude_installed else False
|
|
2884
|
+
tools.append((
|
|
2885
|
+
"claude-code", "Claude Code", claude_installed, claude_protected,
|
|
2886
|
+
"Hooks in ~/.claude/settings.json" if claude_protected else "",
|
|
2887
|
+
))
|
|
2888
|
+
|
|
2889
|
+
# OpenClaw
|
|
2890
|
+
oc_installed = False
|
|
2891
|
+
oc_protected = False
|
|
2892
|
+
oc_detail = ""
|
|
2893
|
+
try:
|
|
2894
|
+
from tweek.integrations.openclaw import detect_openclaw_installation
|
|
2895
|
+
openclaw = detect_openclaw_installation()
|
|
2896
|
+
oc_installed = openclaw.get("installed", False)
|
|
2897
|
+
if oc_installed:
|
|
2898
|
+
oc_protected = openclaw.get("tweek_configured", False)
|
|
2899
|
+
oc_detail = f"Gateway port {openclaw.get('gateway_port', '?')}"
|
|
2900
|
+
except Exception:
|
|
2901
|
+
pass
|
|
2902
|
+
tools.append(("openclaw", "OpenClaw", oc_installed, oc_protected, oc_detail))
|
|
2903
|
+
|
|
2904
|
+
# MCP clients
|
|
2905
|
+
mcp_configs = [
|
|
2906
|
+
("claude-desktop", "Claude Desktop",
|
|
2907
|
+
Path("~/Library/Application Support/Claude/claude_desktop_config.json").expanduser()),
|
|
2908
|
+
("chatgpt", "ChatGPT Desktop",
|
|
2909
|
+
Path("~/Library/Application Support/com.openai.chat/developer_settings.json").expanduser()),
|
|
2910
|
+
("gemini", "Gemini CLI",
|
|
2911
|
+
Path("~/.gemini/settings.json").expanduser()),
|
|
2912
|
+
]
|
|
2913
|
+
for tool_id, label, config_path in mcp_configs:
|
|
2914
|
+
installed = config_path.exists()
|
|
2915
|
+
protected = False
|
|
2916
|
+
if installed:
|
|
2917
|
+
try:
|
|
2918
|
+
with open(config_path) as f:
|
|
2919
|
+
data = json.load(f)
|
|
2920
|
+
mcp_servers = data.get("mcpServers", {})
|
|
2921
|
+
protected = "tweek-security" in mcp_servers or "tweek" in mcp_servers
|
|
2922
|
+
except Exception:
|
|
2923
|
+
pass
|
|
2924
|
+
detail = str(config_path) if protected else ""
|
|
2925
|
+
tools.append((tool_id, label, installed, protected, detail))
|
|
2926
|
+
|
|
2927
|
+
return tools
|
|
2928
|
+
|
|
2929
|
+
|
|
2930
|
+
def _run_protect_wizard():
|
|
2931
|
+
"""Interactive wizard: detect tools and ask Y/n for each one."""
|
|
2932
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
2933
|
+
console.print("[bold]Tweek Protection Wizard[/bold]\n")
|
|
2934
|
+
console.print("Scanning for AI tools...\n")
|
|
2935
|
+
|
|
2936
|
+
tools = _detect_all_tools()
|
|
2937
|
+
|
|
2938
|
+
# Show detection summary
|
|
2939
|
+
detected = [(tid, label, prot) for tid, label, inst, prot, _ in tools if inst]
|
|
2940
|
+
not_detected = [label for _, label, inst, _, _ in tools if not inst]
|
|
2941
|
+
|
|
2942
|
+
if not_detected:
|
|
2943
|
+
for label in not_detected:
|
|
2944
|
+
console.print(f" [white]{label:<20}[/white] [white]not found[/white]")
|
|
2945
|
+
|
|
2946
|
+
if not detected:
|
|
2947
|
+
console.print("\n[yellow]No AI tools detected on this system.[/yellow]")
|
|
2948
|
+
return
|
|
2949
|
+
|
|
2950
|
+
# Show already-protected tools
|
|
2951
|
+
already_protected = [(tid, label) for tid, label, prot in detected if prot]
|
|
2952
|
+
unprotected = [(tid, label) for tid, label, prot in detected if not prot]
|
|
2953
|
+
|
|
2954
|
+
for _, label in already_protected:
|
|
2955
|
+
console.print(f" [green]{label:<20} protected[/green]")
|
|
2956
|
+
|
|
2957
|
+
if not unprotected:
|
|
2958
|
+
console.print(f"\n[green]All {len(already_protected)} detected tool(s) already protected.[/green]")
|
|
2959
|
+
console.print("Run 'tweek status' to see details.")
|
|
2960
|
+
return
|
|
2961
|
+
|
|
2962
|
+
for _, label in unprotected:
|
|
2963
|
+
console.print(f" [yellow]{label:<20} not protected[/yellow]")
|
|
2964
|
+
|
|
2965
|
+
# Ask for preset first (applies to all)
|
|
2966
|
+
console.print()
|
|
2967
|
+
console.print("[bold]Security preset:[/bold]")
|
|
2968
|
+
console.print(" [bold]1.[/bold] cautious [white](recommended)[/white] — screen risky & dangerous tools")
|
|
2969
|
+
console.print(" [bold]2.[/bold] paranoid — screen everything except safe tools")
|
|
2970
|
+
console.print(" [bold]3.[/bold] trusted — only screen dangerous tools")
|
|
2971
|
+
console.print()
|
|
2972
|
+
preset_choice = click.prompt("Select preset", type=click.IntRange(1, 3), default=1)
|
|
2973
|
+
preset = ["cautious", "paranoid", "trusted"][preset_choice - 1]
|
|
2974
|
+
|
|
2975
|
+
# Walk through each unprotected tool
|
|
2976
|
+
console.print()
|
|
2977
|
+
protected_count = 0
|
|
2978
|
+
skipped_count = 0
|
|
2979
|
+
|
|
2980
|
+
for tool_id, label in unprotected:
|
|
2981
|
+
protect_it = click.confirm(f" Protect {label}?", default=True)
|
|
2982
|
+
|
|
2983
|
+
if not protect_it:
|
|
2984
|
+
console.print(f" [white]skipped[/white]")
|
|
2985
|
+
skipped_count += 1
|
|
2986
|
+
continue
|
|
2987
|
+
|
|
2988
|
+
try:
|
|
2989
|
+
if tool_id == "claude-code":
|
|
2990
|
+
_install_claude_code_hooks(
|
|
2991
|
+
install_global=True, dev_test=False, backup=True,
|
|
2992
|
+
skip_env_scan=True, interactive=False, preset=preset,
|
|
2993
|
+
ai_defaults=False, with_sandbox=False, force_proxy=False,
|
|
2994
|
+
skip_proxy_check=True, quick=True,
|
|
2995
|
+
)
|
|
2996
|
+
elif tool_id == "openclaw":
|
|
2997
|
+
from tweek.integrations.openclaw import setup_openclaw_protection
|
|
2998
|
+
result = setup_openclaw_protection(preset=preset)
|
|
2999
|
+
if result.success:
|
|
3000
|
+
console.print(f" [green]done[/green]")
|
|
3001
|
+
else:
|
|
3002
|
+
console.print(f" [red]failed: {result.error}[/red]")
|
|
3003
|
+
continue
|
|
3004
|
+
elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
|
|
3005
|
+
_protect_mcp_client(tool_id)
|
|
3006
|
+
protected_count += 1
|
|
3007
|
+
except Exception as e:
|
|
3008
|
+
console.print(f" [red]error: {e}[/red]")
|
|
3009
|
+
|
|
3010
|
+
console.print()
|
|
3011
|
+
if protected_count:
|
|
3012
|
+
console.print(f"[green]Protected {protected_count} tool(s).[/green]", end="")
|
|
3013
|
+
if skipped_count:
|
|
3014
|
+
console.print(f" [white]Skipped {skipped_count}.[/white]", end="")
|
|
3015
|
+
console.print()
|
|
3016
|
+
console.print("Run 'tweek status' to see the full dashboard.")
|
|
3017
|
+
|
|
3018
|
+
|
|
3019
|
+
def _run_unprotect_wizard():
|
|
3020
|
+
"""Interactive wizard: detect protected tools and ask Y/n to unprotect each."""
|
|
3021
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
3022
|
+
console.print("[bold]Tweek Unprotect Wizard[/bold]\n")
|
|
3023
|
+
console.print("Scanning for protected AI tools...\n")
|
|
3024
|
+
|
|
3025
|
+
tools = _detect_all_tools()
|
|
3026
|
+
tweek_dir = Path("~/.tweek").expanduser()
|
|
3027
|
+
global_target = Path("~/.claude").expanduser()
|
|
3028
|
+
project_target = Path.cwd() / ".claude"
|
|
3029
|
+
|
|
3030
|
+
protected = [(tid, label) for tid, label, inst, prot, _ in tools if inst and prot]
|
|
3031
|
+
|
|
3032
|
+
if not protected:
|
|
3033
|
+
console.print("[yellow]No protected tools found.[/yellow]")
|
|
3034
|
+
return
|
|
3035
|
+
|
|
3036
|
+
for _, label in protected:
|
|
3037
|
+
console.print(f" [green]{label:<20} protected[/green]")
|
|
3038
|
+
|
|
3039
|
+
console.print()
|
|
3040
|
+
removed_count = 0
|
|
3041
|
+
skipped_count = 0
|
|
3042
|
+
|
|
3043
|
+
for tool_id, label in protected:
|
|
3044
|
+
remove_it = click.confirm(f" Remove protection from {label}?", default=False)
|
|
3045
|
+
|
|
3046
|
+
if not remove_it:
|
|
3047
|
+
console.print(f" [white]kept[/white]")
|
|
3048
|
+
skipped_count += 1
|
|
3049
|
+
continue
|
|
3050
|
+
|
|
3051
|
+
try:
|
|
3052
|
+
if tool_id == "claude-code":
|
|
3053
|
+
_uninstall_scope(global_target, tweek_dir, confirm=True, scope_label="global")
|
|
3054
|
+
elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
|
|
3055
|
+
from tweek.mcp.clients import get_client
|
|
3056
|
+
handler = get_client(tool_id)
|
|
3057
|
+
result = handler.uninstall()
|
|
3058
|
+
if result.get("success"):
|
|
3059
|
+
console.print(f" [green]{result.get('message', 'removed')}[/green]")
|
|
3060
|
+
else:
|
|
3061
|
+
console.print(f" [red]{result.get('error', 'failed')}[/red]")
|
|
3062
|
+
continue
|
|
3063
|
+
elif tool_id == "openclaw":
|
|
3064
|
+
console.print(" [white]Manual step: remove tweek plugin from openclaw.json[/white]")
|
|
3065
|
+
removed_count += 1
|
|
3066
|
+
except Exception as e:
|
|
3067
|
+
console.print(f" [red]error: {e}[/red]")
|
|
3068
|
+
|
|
3069
|
+
console.print()
|
|
3070
|
+
if removed_count:
|
|
3071
|
+
console.print(f"[green]Removed protection from {removed_count} tool(s).[/green]", end="")
|
|
3072
|
+
if skipped_count:
|
|
3073
|
+
console.print(f" [white]Kept {skipped_count}.[/white]", end="")
|
|
3074
|
+
console.print()
|
|
3075
|
+
|
|
3076
|
+
|
|
3077
|
+
def _show_protection_status():
|
|
3078
|
+
"""Show protection status dashboard for all AI tools."""
|
|
3079
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
3080
|
+
|
|
3081
|
+
tools = _detect_all_tools()
|
|
3082
|
+
|
|
3083
|
+
# Build status table
|
|
3084
|
+
table = Table(title="Protection Status", show_lines=False)
|
|
3085
|
+
table.add_column("Tool", style="cyan", min_width=18)
|
|
3086
|
+
table.add_column("Installed", justify="center", min_width=10)
|
|
3087
|
+
table.add_column("Protected", justify="center", min_width=10)
|
|
3088
|
+
table.add_column("Details")
|
|
3089
|
+
|
|
3090
|
+
detected_count = 0
|
|
3091
|
+
protected_count = 0
|
|
3092
|
+
|
|
3093
|
+
for tool_id, label, installed, protected, detail in tools:
|
|
3094
|
+
if installed:
|
|
3095
|
+
detected_count += 1
|
|
3096
|
+
if protected:
|
|
3097
|
+
protected_count += 1
|
|
3098
|
+
|
|
3099
|
+
table.add_row(
|
|
3100
|
+
label,
|
|
3101
|
+
"[green]yes[/green]" if installed else "[white]no[/white]",
|
|
3102
|
+
"[green]yes[/green]" if protected else "[yellow]no[/yellow]" if installed else "[white]—[/white]",
|
|
3103
|
+
detail,
|
|
3104
|
+
)
|
|
3105
|
+
|
|
3106
|
+
console.print(table)
|
|
3107
|
+
console.print()
|
|
3108
|
+
|
|
3109
|
+
# Summary line
|
|
3110
|
+
unprotected_count = detected_count - protected_count
|
|
3111
|
+
if detected_count == 0:
|
|
3112
|
+
console.print("[yellow]No AI tools detected.[/yellow]")
|
|
3113
|
+
elif unprotected_count == 0:
|
|
3114
|
+
console.print(f"[green]{protected_count}/{detected_count} detected tools protected.[/green]")
|
|
3115
|
+
else:
|
|
3116
|
+
console.print(f"[yellow]{protected_count}/{detected_count} detected tools protected. {unprotected_count} unprotected.[/yellow]")
|
|
3117
|
+
console.print("[white]Run 'tweek protect' to set up protection.[/white]")
|
|
3118
|
+
console.print()
|
|
3119
|
+
|
|
3120
|
+
|
|
2793
3121
|
# =============================================================================
|
|
2794
3122
|
# CONFIG COMMANDS
|
|
2795
3123
|
# =============================================================================
|
|
@@ -2874,7 +3202,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2874
3202
|
}
|
|
2875
3203
|
|
|
2876
3204
|
source_styles = {
|
|
2877
|
-
"default": "
|
|
3205
|
+
"default": "white",
|
|
2878
3206
|
"user": "cyan",
|
|
2879
3207
|
"project": "magenta",
|
|
2880
3208
|
}
|
|
@@ -2883,7 +3211,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2883
3211
|
table = Table(title="Tool Security Tiers")
|
|
2884
3212
|
table.add_column("Tool", style="bold")
|
|
2885
3213
|
table.add_column("Tier")
|
|
2886
|
-
table.add_column("Source", style="
|
|
3214
|
+
table.add_column("Source", style="white")
|
|
2887
3215
|
table.add_column("Description")
|
|
2888
3216
|
|
|
2889
3217
|
for tool in cfg.list_tools():
|
|
@@ -2903,7 +3231,7 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2903
3231
|
table = Table(title="Skill Security Tiers")
|
|
2904
3232
|
table.add_column("Skill", style="bold")
|
|
2905
3233
|
table.add_column("Tier")
|
|
2906
|
-
table.add_column("Source", style="
|
|
3234
|
+
table.add_column("Source", style="white")
|
|
2907
3235
|
table.add_column("Description")
|
|
2908
3236
|
|
|
2909
3237
|
for skill in cfg.list_skills():
|
|
@@ -2918,8 +3246,8 @@ def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
|
2918
3246
|
|
|
2919
3247
|
console.print(table)
|
|
2920
3248
|
|
|
2921
|
-
console.print("\n[
|
|
2922
|
-
console.print("[
|
|
3249
|
+
console.print("\n[white]Tiers: safe (no checks) → default (regex) → risky (+LLM) → dangerous (+sandbox)[/white]")
|
|
3250
|
+
console.print("[white]Sources: default (built-in), user (~/.tweek/config.yaml), project (.tweek/config.yaml)[/white]")
|
|
2923
3251
|
|
|
2924
3252
|
|
|
2925
3253
|
@config.command("set",
|
|
@@ -2982,11 +3310,11 @@ def config_preset(preset_name: str, scope: str):
|
|
|
2982
3310
|
console.print(f"[green]✓[/green] Applied [bold]{preset_name}[/bold] preset ({scope} config)")
|
|
2983
3311
|
|
|
2984
3312
|
if preset_name == "paranoid":
|
|
2985
|
-
console.print("[
|
|
3313
|
+
console.print("[white]All tools require screening, Bash commands always sandboxed[/white]")
|
|
2986
3314
|
elif preset_name == "cautious":
|
|
2987
|
-
console.print("[
|
|
3315
|
+
console.print("[white]Balanced: read-only tools safe, Bash dangerous[/white]")
|
|
2988
3316
|
elif preset_name == "trusted":
|
|
2989
|
-
console.print("[
|
|
3317
|
+
console.print("[white]Minimal prompts: only high-risk patterns trigger alerts[/white]")
|
|
2990
3318
|
|
|
2991
3319
|
|
|
2992
3320
|
@config.command("reset",
|
|
@@ -3011,7 +3339,7 @@ def config_reset(skill: str, tool: str, reset_all: bool, scope: str, confirm: bo
|
|
|
3011
3339
|
|
|
3012
3340
|
if reset_all:
|
|
3013
3341
|
if not confirm and not click.confirm(f"Reset ALL {scope} configuration?"):
|
|
3014
|
-
console.print("[
|
|
3342
|
+
console.print("[white]Cancelled[/white]")
|
|
3015
3343
|
return
|
|
3016
3344
|
cfg.reset_all(scope=scope)
|
|
3017
3345
|
console.print(f"[green]✓[/green] Reset all {scope} configuration to defaults")
|
|
@@ -3069,7 +3397,7 @@ def config_validate(scope: str, json_out: bool):
|
|
|
3069
3397
|
console.print()
|
|
3070
3398
|
console.print("[bold]Configuration Validation[/bold]")
|
|
3071
3399
|
console.print("\u2500" * 40)
|
|
3072
|
-
console.print(f"[
|
|
3400
|
+
console.print(f"[white]Scope: {scope}[/white]")
|
|
3073
3401
|
console.print()
|
|
3074
3402
|
|
|
3075
3403
|
if not issues:
|
|
@@ -3085,11 +3413,11 @@ def config_validate(scope: str, json_out: bool):
|
|
|
3085
3413
|
level_styles = {
|
|
3086
3414
|
"error": "[red]ERROR[/red]",
|
|
3087
3415
|
"warning": "[yellow]WARN[/yellow] ",
|
|
3088
|
-
"info": "[
|
|
3416
|
+
"info": "[white]INFO[/white] ",
|
|
3089
3417
|
}
|
|
3090
3418
|
|
|
3091
3419
|
for issue in issues:
|
|
3092
|
-
style = level_styles.get(issue.level, "[
|
|
3420
|
+
style = level_styles.get(issue.level, "[white]???[/white] ")
|
|
3093
3421
|
msg = f" {style} {issue.key} \u2192 {issue.message}"
|
|
3094
3422
|
if issue.suggestion:
|
|
3095
3423
|
msg += f" {issue.suggestion}"
|
|
@@ -3201,7 +3529,7 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3201
3529
|
console.print()
|
|
3202
3530
|
console.print(" [yellow]Status:[/yellow] Disabled (no provider available)")
|
|
3203
3531
|
console.print()
|
|
3204
|
-
console.print(" [
|
|
3532
|
+
console.print(" [white]To enable, set one of:[/white]")
|
|
3205
3533
|
console.print(" ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY")
|
|
3206
3534
|
console.print(" Or install Ollama: [cyan]https://ollama.ai[/cyan]")
|
|
3207
3535
|
console.print()
|
|
@@ -3236,8 +3564,8 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3236
3564
|
for m in server.all_models:
|
|
3237
3565
|
console.print(f" - {m}")
|
|
3238
3566
|
else:
|
|
3239
|
-
console.print(" [
|
|
3240
|
-
console.print(" [
|
|
3567
|
+
console.print(" [white]No local LLM server detected[/white]")
|
|
3568
|
+
console.print(" [white]Checked: Ollama (localhost:11434), LM Studio (localhost:1234)[/white]")
|
|
3241
3569
|
except Exception as e:
|
|
3242
3570
|
console.print(f" [yellow]Detection error: {e}[/yellow]")
|
|
3243
3571
|
|
|
@@ -3275,8 +3603,8 @@ def config_llm(verbose: bool, validate: bool):
|
|
|
3275
3603
|
console.print(f" [green]PASSED[/green] ({score:.0%})")
|
|
3276
3604
|
else:
|
|
3277
3605
|
console.print(f" [red]FAILED[/red] ({score:.0%}, minimum: 60%)")
|
|
3278
|
-
console.print(" [
|
|
3279
|
-
console.print(" [
|
|
3606
|
+
console.print(" [white]This model may not reliably classify security threats.[/white]")
|
|
3607
|
+
console.print(" [white]Try a larger model: ollama pull qwen2.5:7b-instruct[/white]")
|
|
3280
3608
|
except Exception as e:
|
|
3281
3609
|
console.print(f" [red]Validation error: {e}[/red]")
|
|
3282
3610
|
|
|
@@ -3306,8 +3634,8 @@ def vault_store(skill: str, key: str, value: Optional[str]):
|
|
|
3306
3634
|
|
|
3307
3635
|
if not VAULT_AVAILABLE:
|
|
3308
3636
|
console.print("[red]\u2717[/red] Vault not available.")
|
|
3309
|
-
console.print(" [
|
|
3310
|
-
console.print(" [
|
|
3637
|
+
console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
|
|
3638
|
+
console.print(" [white]On macOS, keyring uses Keychain. On Linux, install gnome-keyring or kwallet.[/white]")
|
|
3311
3639
|
return
|
|
3312
3640
|
|
|
3313
3641
|
caps = get_capabilities()
|
|
@@ -3323,13 +3651,13 @@ def vault_store(skill: str, key: str, value: Optional[str]):
|
|
|
3323
3651
|
vault_instance = get_vault()
|
|
3324
3652
|
if vault_instance.store(skill, key, value):
|
|
3325
3653
|
console.print(f"[green]\u2713[/green] Stored {key} for skill '{skill}'")
|
|
3326
|
-
console.print(f"[
|
|
3654
|
+
console.print(f"[white]Backend: {caps.vault_backend}[/white]")
|
|
3327
3655
|
else:
|
|
3328
3656
|
console.print(f"[red]\u2717[/red] Failed to store credential")
|
|
3329
|
-
console.print(" [
|
|
3657
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
3330
3658
|
except Exception as e:
|
|
3331
3659
|
console.print(f"[red]\u2717[/red] Failed to store credential: {e}")
|
|
3332
|
-
console.print(" [
|
|
3660
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
3333
3661
|
|
|
3334
3662
|
|
|
3335
3663
|
@vault.command("get",
|
|
@@ -3347,7 +3675,7 @@ def vault_get(skill: str, key: str):
|
|
|
3347
3675
|
|
|
3348
3676
|
if not VAULT_AVAILABLE:
|
|
3349
3677
|
console.print("[red]\u2717[/red] Vault not available.")
|
|
3350
|
-
console.print(" [
|
|
3678
|
+
console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
|
|
3351
3679
|
return
|
|
3352
3680
|
|
|
3353
3681
|
vault_instance = get_vault()
|
|
@@ -3361,7 +3689,7 @@ def vault_get(skill: str, key: str):
|
|
|
3361
3689
|
console.print(value)
|
|
3362
3690
|
else:
|
|
3363
3691
|
console.print(f"[red]\u2717[/red] Credential not found: {key} for skill '{skill}'")
|
|
3364
|
-
console.print(" [
|
|
3692
|
+
console.print(" [white]Hint: Store it with: tweek vault store {skill} {key} <value>[/white]".format(skill=skill, key=key))
|
|
3365
3693
|
|
|
3366
3694
|
|
|
3367
3695
|
@vault.command("migrate-env",
|
|
@@ -3401,7 +3729,7 @@ def vault_migrate_env(dry_run: bool, env_file: str, skill: str):
|
|
|
3401
3729
|
successful = sum(1 for _, s in results if s)
|
|
3402
3730
|
console.print(f"\n[green]✓[/green] {'Would migrate' if dry_run else 'Migrated'} {successful} credentials to skill '{skill}'")
|
|
3403
3731
|
else:
|
|
3404
|
-
console.print("[
|
|
3732
|
+
console.print("[white]No credentials found to migrate[/white]")
|
|
3405
3733
|
|
|
3406
3734
|
except Exception as e:
|
|
3407
3735
|
console.print(f"[red]✗[/red] Migration failed: {e}")
|
|
@@ -3468,16 +3796,16 @@ def license_status():
|
|
|
3468
3796
|
console.print(f"[bold]License Tier:[/bold] [{tier_color}]{lic.tier.value.upper()}[/{tier_color}]")
|
|
3469
3797
|
|
|
3470
3798
|
if info:
|
|
3471
|
-
console.print(f"[
|
|
3799
|
+
console.print(f"[white]Licensed to: {info.email}[/white]")
|
|
3472
3800
|
if info.expires_at:
|
|
3473
3801
|
from datetime import datetime
|
|
3474
3802
|
exp_date = datetime.fromtimestamp(info.expires_at).strftime("%Y-%m-%d")
|
|
3475
3803
|
if info.is_expired:
|
|
3476
3804
|
console.print(f"[red]Expired: {exp_date}[/red]")
|
|
3477
3805
|
else:
|
|
3478
|
-
console.print(f"[
|
|
3806
|
+
console.print(f"[white]Expires: {exp_date}[/white]")
|
|
3479
3807
|
else:
|
|
3480
|
-
console.print("[
|
|
3808
|
+
console.print("[white]Expires: Never[/white]")
|
|
3481
3809
|
console.print()
|
|
3482
3810
|
|
|
3483
3811
|
# Features table
|
|
@@ -3494,7 +3822,7 @@ def license_status():
|
|
|
3494
3822
|
|
|
3495
3823
|
for feature, required_tier in feature_tiers.items():
|
|
3496
3824
|
has_it = lic.has_feature(feature)
|
|
3497
|
-
status = "[green]✓[/green]" if has_it else "[
|
|
3825
|
+
status = "[green]✓[/green]" if has_it else "[white]○[/white]"
|
|
3498
3826
|
tier_display = required_tier.value.upper()
|
|
3499
3827
|
if required_tier == Tier.PRO:
|
|
3500
3828
|
tier_display = f"[cyan]{tier_display}[/cyan]"
|
|
@@ -3506,7 +3834,7 @@ def license_status():
|
|
|
3506
3834
|
if lic.tier == Tier.FREE:
|
|
3507
3835
|
console.print()
|
|
3508
3836
|
console.print("[green]All security features are included free and open source.[/green]")
|
|
3509
|
-
console.print("[
|
|
3837
|
+
console.print("[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
|
|
3510
3838
|
|
|
3511
3839
|
|
|
3512
3840
|
@license.command("activate",
|
|
@@ -3526,7 +3854,7 @@ def license_activate(license_key: str):
|
|
|
3526
3854
|
if success:
|
|
3527
3855
|
console.print(f"[green]✓[/green] {message}")
|
|
3528
3856
|
console.print()
|
|
3529
|
-
console.print("[
|
|
3857
|
+
console.print("[white]Run 'tweek license status' to see available features[/white]")
|
|
3530
3858
|
else:
|
|
3531
3859
|
console.print(f"[red]✗[/red] {message}")
|
|
3532
3860
|
|
|
@@ -3546,7 +3874,7 @@ def license_deactivate(confirm: bool):
|
|
|
3546
3874
|
if not confirm:
|
|
3547
3875
|
console.print("[yellow]Deactivate license and revert to FREE tier?[/yellow] ", end="")
|
|
3548
3876
|
if not click.confirm(""):
|
|
3549
|
-
console.print("[
|
|
3877
|
+
console.print("[white]Cancelled[/white]")
|
|
3550
3878
|
return
|
|
3551
3879
|
|
|
3552
3880
|
lic = get_license()
|
|
@@ -3624,7 +3952,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3624
3952
|
table.add_column("Severity")
|
|
3625
3953
|
table.add_column("Count", justify="right")
|
|
3626
3954
|
|
|
3627
|
-
severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "
|
|
3955
|
+
severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "white"}
|
|
3628
3956
|
for pattern in stat_data['top_patterns']:
|
|
3629
3957
|
sev = pattern['severity'] or "unknown"
|
|
3630
3958
|
style = severity_styles.get(sev, "white")
|
|
@@ -3661,7 +3989,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3661
3989
|
et = EventType(event_type)
|
|
3662
3990
|
except ValueError:
|
|
3663
3991
|
console.print(f"[red]Unknown event type: {event_type}[/red]")
|
|
3664
|
-
console.print(f"[
|
|
3992
|
+
console.print(f"[white]Valid types: {', '.join(e.value for e in EventType)}[/white]")
|
|
3665
3993
|
return
|
|
3666
3994
|
|
|
3667
3995
|
events = logger.get_recent_events(limit=limit, event_type=et, tool_name=tool)
|
|
@@ -3672,7 +4000,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3672
4000
|
return
|
|
3673
4001
|
|
|
3674
4002
|
table = Table(title=title)
|
|
3675
|
-
table.add_column("Time", style="
|
|
4003
|
+
table.add_column("Time", style="white")
|
|
3676
4004
|
table.add_column("Type", style="cyan")
|
|
3677
4005
|
table.add_column("Tool", style="green")
|
|
3678
4006
|
table.add_column("Tier")
|
|
@@ -3713,7 +4041,7 @@ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool
|
|
|
3713
4041
|
)
|
|
3714
4042
|
|
|
3715
4043
|
console.print(table)
|
|
3716
|
-
console.print(f"\n[
|
|
4044
|
+
console.print(f"\n[white]Showing {len(events)} events. Use --limit to see more.[/white]")
|
|
3717
4045
|
|
|
3718
4046
|
|
|
3719
4047
|
@logs.command("export",
|
|
@@ -3764,7 +4092,7 @@ def logs_clear(days: int, confirm: bool):
|
|
|
3764
4092
|
|
|
3765
4093
|
console.print(f"[yellow]{msg}[/yellow] ", end="")
|
|
3766
4094
|
if not click.confirm(""):
|
|
3767
|
-
console.print("[
|
|
4095
|
+
console.print("[white]Cancelled[/white]")
|
|
3768
4096
|
return
|
|
3769
4097
|
|
|
3770
4098
|
logger = get_logger()
|
|
@@ -3776,7 +4104,7 @@ def logs_clear(days: int, confirm: bool):
|
|
|
3776
4104
|
else:
|
|
3777
4105
|
console.print(f"[green]Cleared {deleted} event(s)[/green]")
|
|
3778
4106
|
else:
|
|
3779
|
-
console.print("[
|
|
4107
|
+
console.print("[white]No events to clear[/white]")
|
|
3780
4108
|
|
|
3781
4109
|
|
|
3782
4110
|
@logs.command("bundle",
|
|
@@ -3814,11 +4142,11 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
|
|
|
3814
4142
|
size = item.get("size")
|
|
3815
4143
|
size_str = f" ({size:,} bytes)" if size else ""
|
|
3816
4144
|
if "not found" in status:
|
|
3817
|
-
console.print(f" [
|
|
4145
|
+
console.print(f" [white] SKIP {name} ({status})[/white]")
|
|
3818
4146
|
else:
|
|
3819
4147
|
console.print(f" [green] ADD {name}{size_str}[/green]")
|
|
3820
4148
|
console.print()
|
|
3821
|
-
console.print("[
|
|
4149
|
+
console.print("[white]No files will be collected in dry-run mode.[/white]")
|
|
3822
4150
|
return
|
|
3823
4151
|
|
|
3824
4152
|
# Determine output path
|
|
@@ -3836,9 +4164,9 @@ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
|
|
|
3836
4164
|
result = collector.create_bundle(output_path)
|
|
3837
4165
|
size = result.stat().st_size
|
|
3838
4166
|
console.print(f"\n[green]Bundle created: {result}[/green]")
|
|
3839
|
-
console.print(f"[
|
|
4167
|
+
console.print(f"[white]Size: {size:,} bytes[/white]")
|
|
3840
4168
|
if not no_redact:
|
|
3841
|
-
console.print("[
|
|
4169
|
+
console.print("[white]Sensitive data has been redacted.[/white]")
|
|
3842
4170
|
console.print(f"\n[bold]Send this file to Tweek support for analysis.[/bold]")
|
|
3843
4171
|
except Exception as e:
|
|
3844
4172
|
console.print(f"[red]Failed to create bundle: {e}[/red]")
|
|
@@ -3884,8 +4212,8 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
|
|
|
3884
4212
|
|
|
3885
4213
|
if not PROXY_AVAILABLE:
|
|
3886
4214
|
console.print("[red]\u2717[/red] Proxy dependencies not installed.")
|
|
3887
|
-
console.print(" [
|
|
3888
|
-
console.print(" [
|
|
4215
|
+
console.print(" [white]Hint: Install with: pip install tweek[proxy][/white]")
|
|
4216
|
+
console.print(" [white]This adds mitmproxy for HTTP(S) interception.[/white]")
|
|
3889
4217
|
return
|
|
3890
4218
|
|
|
3891
4219
|
from tweek.proxy.server import start_proxy
|
|
@@ -3906,7 +4234,7 @@ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
|
|
|
3906
4234
|
console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
|
|
3907
4235
|
console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
|
|
3908
4236
|
console.print()
|
|
3909
|
-
console.print("[
|
|
4237
|
+
console.print("[white]Or use 'tweek proxy wrap' to create a wrapper script[/white]")
|
|
3910
4238
|
else:
|
|
3911
4239
|
console.print(f"[red]✗[/red] {message}")
|
|
3912
4240
|
|
|
@@ -3951,7 +4279,7 @@ def proxy_trust():
|
|
|
3951
4279
|
|
|
3952
4280
|
if not PROXY_AVAILABLE:
|
|
3953
4281
|
console.print("[red]✗[/red] Proxy dependencies not installed.")
|
|
3954
|
-
console.print("[
|
|
4282
|
+
console.print("[white]Run: pip install tweek\\[proxy][/white]")
|
|
3955
4283
|
return
|
|
3956
4284
|
|
|
3957
4285
|
from tweek.proxy.server import install_ca_certificate, get_proxy_info
|
|
@@ -3963,11 +4291,11 @@ def proxy_trust():
|
|
|
3963
4291
|
console.print("This will install a local CA certificate to enable HTTPS interception.")
|
|
3964
4292
|
console.print("The certificate is generated on YOUR machine and never transmitted.")
|
|
3965
4293
|
console.print()
|
|
3966
|
-
console.print(f"[
|
|
4294
|
+
console.print(f"[white]Certificate location: {info['ca_cert']}[/white]")
|
|
3967
4295
|
console.print()
|
|
3968
4296
|
|
|
3969
4297
|
if not click.confirm("Install certificate? (requires admin password)"):
|
|
3970
|
-
console.print("[
|
|
4298
|
+
console.print("[white]Cancelled[/white]")
|
|
3971
4299
|
return
|
|
3972
4300
|
|
|
3973
4301
|
success, message = install_ca_certificate()
|
|
@@ -4019,7 +4347,7 @@ def proxy_config(set_enabled, set_disabled, port):
|
|
|
4019
4347
|
yaml.dump(config, f, default_flow_style=False)
|
|
4020
4348
|
|
|
4021
4349
|
console.print(f"[green]✓[/green] Proxy mode enabled (port {port})")
|
|
4022
|
-
console.print("[
|
|
4350
|
+
console.print("[white]Run 'tweek proxy start' to start the proxy[/white]")
|
|
4023
4351
|
|
|
4024
4352
|
elif set_disabled:
|
|
4025
4353
|
if "proxy" in config:
|
|
@@ -4061,10 +4389,10 @@ def proxy_wrap(app_name: str, command: str, output: str, port: int):
|
|
|
4061
4389
|
console.print(f" chmod +x {output_path}")
|
|
4062
4390
|
console.print(f" ./{output_path.name}")
|
|
4063
4391
|
console.print()
|
|
4064
|
-
console.print("[
|
|
4065
|
-
console.print("[
|
|
4066
|
-
console.print("[
|
|
4067
|
-
console.print(f"[
|
|
4392
|
+
console.print("[white]The script will:[/white]")
|
|
4393
|
+
console.print("[white] 1. Start Tweek proxy if not running[/white]")
|
|
4394
|
+
console.print("[white] 2. Set proxy environment variables[/white]")
|
|
4395
|
+
console.print(f"[white] 3. Run: {command}[/white]")
|
|
4068
4396
|
|
|
4069
4397
|
|
|
4070
4398
|
@proxy.command("setup",
|
|
@@ -4139,9 +4467,9 @@ def proxy_setup():
|
|
|
4139
4467
|
print_warning("Certificate module not available. Run: tweek proxy trust")
|
|
4140
4468
|
except Exception as e:
|
|
4141
4469
|
print_warning(f"Could not set up certificate: {e}")
|
|
4142
|
-
console.print(" [
|
|
4470
|
+
console.print(" [white]You can do this later with: tweek proxy trust[/white]")
|
|
4143
4471
|
else:
|
|
4144
|
-
console.print(" [
|
|
4472
|
+
console.print(" [white]Skipped. Run 'tweek proxy trust' later.[/white]")
|
|
4145
4473
|
console.print()
|
|
4146
4474
|
|
|
4147
4475
|
# Step 3: Shell environment
|
|
@@ -4165,13 +4493,13 @@ def proxy_setup():
|
|
|
4165
4493
|
f.write(f"export HTTP_PROXY=http://127.0.0.1:{port}\n")
|
|
4166
4494
|
f.write(f"export HTTPS_PROXY=http://127.0.0.1:{port}\n")
|
|
4167
4495
|
print_success(f"Added to {shell_rc}")
|
|
4168
|
-
console.print(f" [
|
|
4496
|
+
console.print(f" [white]Restart your shell or run: source {shell_rc}[/white]")
|
|
4169
4497
|
except Exception as e:
|
|
4170
4498
|
print_warning(f"Could not write to {shell_rc}: {e}")
|
|
4171
4499
|
else:
|
|
4172
|
-
console.print(" [
|
|
4500
|
+
console.print(" [white]Skipped. Set HTTP_PROXY and HTTPS_PROXY manually.[/white]")
|
|
4173
4501
|
else:
|
|
4174
|
-
console.print(" [
|
|
4502
|
+
console.print(" [white]Could not detect shell config file.[/white]")
|
|
4175
4503
|
console.print(f" Add these to your shell profile:")
|
|
4176
4504
|
console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
|
|
4177
4505
|
console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
|
|
@@ -4265,7 +4593,7 @@ def plugins_list(category: str, show_all: bool):
|
|
|
4265
4593
|
license_style = "green" if license_tier == LicenseTier.FREE else "cyan"
|
|
4266
4594
|
|
|
4267
4595
|
source_str = info.source.value if hasattr(info, 'source') else "builtin"
|
|
4268
|
-
source_style = "blue" if source_str == "git" else "
|
|
4596
|
+
source_style = "blue" if source_str == "git" else "white"
|
|
4269
4597
|
|
|
4270
4598
|
table.add_row(
|
|
4271
4599
|
info.name,
|
|
@@ -4279,6 +4607,18 @@ def plugins_list(category: str, show_all: bool):
|
|
|
4279
4607
|
console.print(table)
|
|
4280
4608
|
console.print()
|
|
4281
4609
|
|
|
4610
|
+
# Summary line across all categories
|
|
4611
|
+
total_count = 0
|
|
4612
|
+
enabled_count = 0
|
|
4613
|
+
for cat in list(PluginCategory):
|
|
4614
|
+
for info in registry.list_plugins(cat):
|
|
4615
|
+
total_count += 1
|
|
4616
|
+
if info.enabled:
|
|
4617
|
+
enabled_count += 1
|
|
4618
|
+
disabled_count = total_count - enabled_count
|
|
4619
|
+
console.print(f"Plugins: {total_count} registered, {enabled_count} enabled, {disabled_count} disabled")
|
|
4620
|
+
console.print()
|
|
4621
|
+
|
|
4282
4622
|
except ImportError as e:
|
|
4283
4623
|
console.print(f"[red]Plugin system not available: {e}[/red]")
|
|
4284
4624
|
|
|
@@ -4335,7 +4675,7 @@ def plugins_info(plugin_name: str, category: str):
|
|
|
4335
4675
|
plugin_cfg = cfg.get_plugin_config(found_cat, plugin_name)
|
|
4336
4676
|
|
|
4337
4677
|
console.print(f"\n[bold]{found_info.name}[/bold] ({found_cat})")
|
|
4338
|
-
console.print(f"[
|
|
4678
|
+
console.print(f"[white]{found_info.metadata.description}[/white]")
|
|
4339
4679
|
console.print()
|
|
4340
4680
|
|
|
4341
4681
|
table = Table(show_header=False)
|
|
@@ -4507,7 +4847,7 @@ def plugins_scan(content: str, direction: str, plugin: str):
|
|
|
4507
4847
|
|
|
4508
4848
|
if not plugins_to_use:
|
|
4509
4849
|
console.print("[yellow]No compliance plugins enabled.[/yellow]")
|
|
4510
|
-
console.print("[
|
|
4850
|
+
console.print("[white]Enable plugins with: tweek plugins enable <name> -c compliance[/white]")
|
|
4511
4851
|
return
|
|
4512
4852
|
|
|
4513
4853
|
for p in plugins_to_use:
|
|
@@ -4521,12 +4861,12 @@ def plugins_scan(content: str, direction: str, plugin: str):
|
|
|
4521
4861
|
"critical": "red bold",
|
|
4522
4862
|
"high": "red",
|
|
4523
4863
|
"medium": "yellow",
|
|
4524
|
-
"low": "
|
|
4864
|
+
"low": "white",
|
|
4525
4865
|
}
|
|
4526
4866
|
style = severity_styles.get(finding.severity.value, "white")
|
|
4527
4867
|
|
|
4528
4868
|
console.print(f" [{style}]{finding.severity.value.upper()}[/{style}] {finding.pattern_name}")
|
|
4529
|
-
console.print(f" [
|
|
4869
|
+
console.print(f" [white]Matched: {finding.matched_text[:60]}{'...' if len(finding.matched_text) > 60 else ''}[/white]")
|
|
4530
4870
|
if finding.description:
|
|
4531
4871
|
console.print(f" {finding.description}")
|
|
4532
4872
|
|
|
@@ -4600,11 +4940,11 @@ def plugins_install(name: str, version: str, from_lockfile: bool, no_verify: boo
|
|
|
4600
4940
|
console.print(f"[green]\u2713[/green] {msg}")
|
|
4601
4941
|
else:
|
|
4602
4942
|
console.print(f"[red]\u2717[/red] {msg}")
|
|
4603
|
-
console.print(f" [
|
|
4943
|
+
console.print(f" [white]Hint: Check network connectivity or try: tweek plugins registry --refresh[/white]")
|
|
4604
4944
|
|
|
4605
4945
|
except Exception as e:
|
|
4606
4946
|
console.print(f"[red]Error: {e}[/red]")
|
|
4607
|
-
console.print(f" [
|
|
4947
|
+
console.print(f" [white]Hint: Check network connectivity and try again[/white]")
|
|
4608
4948
|
|
|
4609
4949
|
|
|
4610
4950
|
@plugins.command("update",
|
|
@@ -4972,87 +5312,6 @@ def serve():
|
|
|
4972
5312
|
console.print(f"[red]MCP server error: {e}[/red]")
|
|
4973
5313
|
|
|
4974
5314
|
|
|
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
5315
|
# =============================================================================
|
|
5057
5316
|
# MCP PROXY COMMANDS
|
|
5058
5317
|
# =============================================================================
|
|
@@ -5208,13 +5467,13 @@ def chamber_list():
|
|
|
5208
5467
|
items = chamber.list_chamber()
|
|
5209
5468
|
|
|
5210
5469
|
if not items:
|
|
5211
|
-
console.print("[
|
|
5470
|
+
console.print("[white]Chamber is empty.[/white]")
|
|
5212
5471
|
return
|
|
5213
5472
|
|
|
5214
5473
|
table = Table(title="Isolation Chamber")
|
|
5215
5474
|
table.add_column("Name", style="cyan")
|
|
5216
5475
|
table.add_column("Has SKILL.md", style="green")
|
|
5217
|
-
table.add_column("Path", style="
|
|
5476
|
+
table.add_column("Path", style="white")
|
|
5218
5477
|
|
|
5219
5478
|
for item in items:
|
|
5220
5479
|
has_md = "Yes" if item["has_skill_md"] else "[red]No[/red]"
|
|
@@ -5309,7 +5568,7 @@ def jail_list():
|
|
|
5309
5568
|
items = chamber.list_jail()
|
|
5310
5569
|
|
|
5311
5570
|
if not items:
|
|
5312
|
-
console.print("[
|
|
5571
|
+
console.print("[white]Jail is empty.[/white]")
|
|
5313
5572
|
return
|
|
5314
5573
|
|
|
5315
5574
|
table = Table(title="Skill Jail")
|
|
@@ -5381,7 +5640,7 @@ def skills_report(name: str):
|
|
|
5381
5640
|
report_data = chamber.get_report(name)
|
|
5382
5641
|
|
|
5383
5642
|
if not report_data:
|
|
5384
|
-
console.print(f"[
|
|
5643
|
+
console.print(f"[white]No report found for '{name}'.[/white]")
|
|
5385
5644
|
return
|
|
5386
5645
|
|
|
5387
5646
|
console.print(Panel(
|
|
@@ -5503,7 +5762,7 @@ def sandbox_status():
|
|
|
5503
5762
|
else:
|
|
5504
5763
|
console.print(f"[bold]Project:[/bold] {project_dir}")
|
|
5505
5764
|
console.print(f"[bold]Layer:[/bold] 0-1 (no project isolation)")
|
|
5506
|
-
console.print("[
|
|
5765
|
+
console.print("[white]Run 'tweek sandbox init' to enable project isolation.[/white]")
|
|
5507
5766
|
|
|
5508
5767
|
|
|
5509
5768
|
@sandbox.command("init")
|
|
@@ -5577,7 +5836,7 @@ def sandbox_list():
|
|
|
5577
5836
|
projects = registry.list_projects()
|
|
5578
5837
|
|
|
5579
5838
|
if not projects:
|
|
5580
|
-
console.print("[
|
|
5839
|
+
console.print("[white]No projects registered. Run 'tweek sandbox init' in a project.[/white]")
|
|
5581
5840
|
return
|
|
5582
5841
|
|
|
5583
5842
|
table = Table(title="Registered Projects")
|
|
@@ -5649,12 +5908,12 @@ def sandbox_logs(show_global: bool, limit: int):
|
|
|
5649
5908
|
|
|
5650
5909
|
events = logger.get_recent_events(limit=limit)
|
|
5651
5910
|
if not events:
|
|
5652
|
-
console.print("[
|
|
5911
|
+
console.print("[white]No events found.[/white]")
|
|
5653
5912
|
return
|
|
5654
5913
|
|
|
5655
5914
|
from rich.table import Table
|
|
5656
5915
|
table = Table()
|
|
5657
|
-
table.add_column("Time", style="
|
|
5916
|
+
table.add_column("Time", style="white")
|
|
5658
5917
|
table.add_column("Type")
|
|
5659
5918
|
table.add_column("Tool")
|
|
5660
5919
|
table.add_column("Decision", style="green")
|
|
@@ -5734,7 +5993,7 @@ def sandbox_verify():
|
|
|
5734
5993
|
checks_passed += 1
|
|
5735
5994
|
else:
|
|
5736
5995
|
console.print(" Sandbox initialized: [red]NO[/red]")
|
|
5737
|
-
console.print(" [
|
|
5996
|
+
console.print(" [white]Run 'tweek sandbox init' to enable.[/white]")
|
|
5738
5997
|
|
|
5739
5998
|
# Check 3: Layer
|
|
5740
5999
|
checks_total += 1
|
|
@@ -5755,7 +6014,7 @@ def sandbox_verify():
|
|
|
5755
6014
|
elif sandbox:
|
|
5756
6015
|
console.print(" Project security.db: [yellow]NOT FOUND[/yellow]")
|
|
5757
6016
|
else:
|
|
5758
|
-
console.print(" Project security.db: [
|
|
6017
|
+
console.print(" Project security.db: [white]N/A (sandbox inactive)[/white]")
|
|
5759
6018
|
|
|
5760
6019
|
# Check 5: .gitignore
|
|
5761
6020
|
checks_total += 1
|
|
@@ -5785,7 +6044,7 @@ def docker_init():
|
|
|
5785
6044
|
bridge = DockerBridge()
|
|
5786
6045
|
if not bridge.is_docker_available():
|
|
5787
6046
|
console.print("[red]Docker is not installed or not running.[/red]")
|
|
5788
|
-
console.print("[
|
|
6047
|
+
console.print("[white]Install Docker Desktop from https://www.docker.com/products/docker-desktop/[/white]")
|
|
5789
6048
|
raise SystemExit(1)
|
|
5790
6049
|
|
|
5791
6050
|
from tweek.sandbox.project import _detect_project_dir
|
|
@@ -5796,7 +6055,7 @@ def docker_init():
|
|
|
5796
6055
|
|
|
5797
6056
|
compose_path = bridge.init(project_dir)
|
|
5798
6057
|
console.print(f"[green]Docker Sandbox config generated: {compose_path}[/green]")
|
|
5799
|
-
console.print("[
|
|
6058
|
+
console.print("[white]Run 'tweek sandbox docker run' to start the container.[/white]")
|
|
5800
6059
|
|
|
5801
6060
|
|
|
5802
6061
|
@sandbox_docker.command("run")
|
|
@@ -5833,7 +6092,7 @@ def docker_status():
|
|
|
5833
6092
|
compose = project_dir / ".tweek" / "docker-compose.yaml"
|
|
5834
6093
|
console.print(f"[bold]Docker config:[/bold] {'exists' if compose.exists() else 'not generated'}")
|
|
5835
6094
|
else:
|
|
5836
|
-
console.print("[
|
|
6095
|
+
console.print("[white]Not in a project directory.[/white]")
|
|
5837
6096
|
|
|
5838
6097
|
|
|
5839
6098
|
# =========================================================================
|
|
@@ -5894,7 +6153,7 @@ def override_create(pattern: str, mode: str, duration_minutes: Optional[int], re
|
|
|
5894
6153
|
if reason:
|
|
5895
6154
|
console.print(f" Reason: {reason}")
|
|
5896
6155
|
console.print()
|
|
5897
|
-
console.print("[
|
|
6156
|
+
console.print("[white]Next time this pattern triggers, you'll see an 'ask' prompt instead of a hard block.[/white]")
|
|
5898
6157
|
|
|
5899
6158
|
|
|
5900
6159
|
@override_group.command("list")
|
|
@@ -5907,7 +6166,7 @@ def override_list():
|
|
|
5907
6166
|
active_patterns = {o["pattern"] for o in active}
|
|
5908
6167
|
|
|
5909
6168
|
if not all_overrides:
|
|
5910
|
-
console.print("[
|
|
6169
|
+
console.print("[white]No break-glass overrides found.[/white]")
|
|
5911
6170
|
return
|
|
5912
6171
|
|
|
5913
6172
|
table = Table(title="Break-Glass Overrides")
|
|
@@ -5921,9 +6180,9 @@ def override_list():
|
|
|
5921
6180
|
if o["pattern"] in active_patterns and not o.get("used"):
|
|
5922
6181
|
status = "[green]active[/green]"
|
|
5923
6182
|
elif o.get("used"):
|
|
5924
|
-
status = "[
|
|
6183
|
+
status = "[white]consumed[/white]"
|
|
5925
6184
|
else:
|
|
5926
|
-
status = "[
|
|
6185
|
+
status = "[white]expired[/white]"
|
|
5927
6186
|
|
|
5928
6187
|
table.add_row(
|
|
5929
6188
|
o["pattern"],
|
|
@@ -6012,7 +6271,7 @@ def feedback_stats(above_threshold: bool):
|
|
|
6012
6271
|
|
|
6013
6272
|
stats = get_stats()
|
|
6014
6273
|
if not stats:
|
|
6015
|
-
console.print("[
|
|
6274
|
+
console.print("[white]No feedback data recorded yet.[/white]")
|
|
6016
6275
|
return
|
|
6017
6276
|
|
|
6018
6277
|
table = Table(title="Pattern FP Statistics")
|
|
@@ -6053,7 +6312,7 @@ def feedback_reset(pattern_name: str):
|
|
|
6053
6312
|
if result.get("was_demoted"):
|
|
6054
6313
|
console.print(f" Restored severity: {result.get('original_severity')}")
|
|
6055
6314
|
else:
|
|
6056
|
-
console.print(f"[
|
|
6315
|
+
console.print(f"[white]No feedback data found for '{pattern_name}'.[/white]")
|
|
6057
6316
|
|
|
6058
6317
|
|
|
6059
6318
|
# =========================================================================
|
|
@@ -6094,7 +6353,7 @@ def memory_status():
|
|
|
6094
6353
|
if last_decay:
|
|
6095
6354
|
console.print(f" Last decay: {last_decay}")
|
|
6096
6355
|
else:
|
|
6097
|
-
console.print(" Last decay: [
|
|
6356
|
+
console.print(" Last decay: [white]never[/white]")
|
|
6098
6357
|
|
|
6099
6358
|
db_size = stats.get("db_size_bytes", 0)
|
|
6100
6359
|
if db_size > 1024 * 1024:
|
|
@@ -6118,7 +6377,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
|
|
|
6118
6377
|
patterns = store.get_pattern_stats(min_decisions=min_decisions, sort_by=sort_by)
|
|
6119
6378
|
|
|
6120
6379
|
if not patterns:
|
|
6121
|
-
console.print("[
|
|
6380
|
+
console.print("[white]No pattern decision data recorded yet.[/white]")
|
|
6122
6381
|
return
|
|
6123
6382
|
|
|
6124
6383
|
table = Table(title="Pattern Decision History")
|
|
@@ -6135,7 +6394,7 @@ def memory_patterns(min_decisions: int, sort_by: str):
|
|
|
6135
6394
|
ratio_style = "green" if ratio >= 0.9 else ("yellow" if ratio >= 0.5 else "red")
|
|
6136
6395
|
table.add_row(
|
|
6137
6396
|
p.get("pattern_name", "?"),
|
|
6138
|
-
p.get("path_prefix") or "[
|
|
6397
|
+
p.get("path_prefix") or "[white]-[/white]",
|
|
6139
6398
|
str(p.get("total_decisions", 0)),
|
|
6140
6399
|
f"{p.get('weighted_approvals', 0):.1f}",
|
|
6141
6400
|
f"{p.get('weighted_denials', 0):.1f}",
|
|
@@ -6156,7 +6415,7 @@ def memory_sources(suspicious: bool):
|
|
|
6156
6415
|
sources = store.get_all_sources(suspicious_only=suspicious)
|
|
6157
6416
|
|
|
6158
6417
|
if not sources:
|
|
6159
|
-
console.print("[
|
|
6418
|
+
console.print("[white]No source trust data recorded yet.[/white]")
|
|
6160
6419
|
return
|
|
6161
6420
|
|
|
6162
6421
|
table = Table(title="Source Trust Scores")
|
|
@@ -6191,7 +6450,7 @@ def memory_suggestions(show_all: bool):
|
|
|
6191
6450
|
suggestions = store.get_whitelist_suggestions(pending_only=not show_all)
|
|
6192
6451
|
|
|
6193
6452
|
if not suggestions:
|
|
6194
|
-
console.print("[
|
|
6453
|
+
console.print("[white]No whitelist suggestions available.[/white]")
|
|
6195
6454
|
return
|
|
6196
6455
|
|
|
6197
6456
|
table = Table(title="Learned Whitelist Suggestions")
|
|
@@ -6209,8 +6468,8 @@ def memory_suggestions(show_all: bool):
|
|
|
6209
6468
|
table.add_row(
|
|
6210
6469
|
str(s.id),
|
|
6211
6470
|
s.pattern_name,
|
|
6212
|
-
s.tool_name or "[
|
|
6213
|
-
s.path_prefix or "[
|
|
6471
|
+
s.tool_name or "[white]-[/white]",
|
|
6472
|
+
s.path_prefix or "[white]-[/white]",
|
|
6214
6473
|
str(s.approval_count),
|
|
6215
6474
|
str(s.denial_count),
|
|
6216
6475
|
f"{s.confidence:.0%}",
|
|
@@ -6229,7 +6488,7 @@ def memory_accept(suggestion_id: int):
|
|
|
6229
6488
|
store = get_memory_store()
|
|
6230
6489
|
if store.review_whitelist_suggestion(suggestion_id, accepted=True):
|
|
6231
6490
|
console.print(f"[bold green]Accepted[/bold green] suggestion #{suggestion_id}")
|
|
6232
|
-
console.print(" [
|
|
6491
|
+
console.print(" [white]Note: To apply to overrides.yaml, manually add the whitelist rule.[/white]")
|
|
6233
6492
|
else:
|
|
6234
6493
|
console.print(f"[red]Suggestion #{suggestion_id} not found.[/red]")
|
|
6235
6494
|
|
|
@@ -6260,7 +6519,7 @@ def memory_baseline(project_hash: Optional[str]):
|
|
|
6260
6519
|
baselines = store.get_workflow_baseline(project_hash)
|
|
6261
6520
|
|
|
6262
6521
|
if not baselines:
|
|
6263
|
-
console.print("[
|
|
6522
|
+
console.print("[white]No workflow baseline data for this project.[/white]")
|
|
6264
6523
|
return
|
|
6265
6524
|
|
|
6266
6525
|
table = Table(title=f"Workflow Baseline (project: {project_hash[:8]}...)")
|
|
@@ -6276,7 +6535,7 @@ def memory_baseline(project_hash: Optional[str]):
|
|
|
6276
6535
|
pct_style = "green" if denial_pct < 0.1 else ("yellow" if denial_pct < 0.3 else "red")
|
|
6277
6536
|
table.add_row(
|
|
6278
6537
|
b.tool_name,
|
|
6279
|
-
str(b.hour_of_day) if b.hour_of_day is not None else "[
|
|
6538
|
+
str(b.hour_of_day) if b.hour_of_day is not None else "[white]-[/white]",
|
|
6280
6539
|
str(b.invocation_count),
|
|
6281
6540
|
str(b.denied_count),
|
|
6282
6541
|
f"[{pct_style}]{denial_pct:.0%}[/{pct_style}]",
|
|
@@ -6295,7 +6554,7 @@ def memory_audit(limit: int):
|
|
|
6295
6554
|
entries = store.get_audit_log(limit=limit)
|
|
6296
6555
|
|
|
6297
6556
|
if not entries:
|
|
6298
|
-
console.print("[
|
|
6557
|
+
console.print("[white]No audit entries.[/white]")
|
|
6299
6558
|
return
|
|
6300
6559
|
|
|
6301
6560
|
table = Table(title=f"Memory Audit Log (last {limit})")
|
|
@@ -6337,7 +6596,7 @@ def memory_clear(table_name: Optional[str], confirm: bool):
|
|
|
6337
6596
|
if not confirm:
|
|
6338
6597
|
target = table_name or "ALL"
|
|
6339
6598
|
if not click.confirm(f"Clear {target} memory data? This cannot be undone"):
|
|
6340
|
-
console.print("[
|
|
6599
|
+
console.print("[white]Cancelled.[/white]")
|
|
6341
6600
|
return
|
|
6342
6601
|
|
|
6343
6602
|
store = get_memory_store()
|