tweek 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6559
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/__init__.py +8 -0
  21. tweek/config/allowed_dirs.yaml +16 -17
  22. tweek/config/families.yaml +4 -1
  23. tweek/config/manager.py +49 -0
  24. tweek/config/models.py +307 -0
  25. tweek/config/patterns.yaml +29 -5
  26. tweek/config/templates/config.yaml.template +212 -0
  27. tweek/config/templates/env.template +45 -0
  28. tweek/config/templates/overrides.yaml.template +121 -0
  29. tweek/config/templates/tweek.yaml.template +20 -0
  30. tweek/config/templates.py +136 -0
  31. tweek/config/tiers.yaml +5 -4
  32. tweek/diagnostics.py +112 -32
  33. tweek/hooks/overrides.py +4 -0
  34. tweek/hooks/post_tool_use.py +46 -1
  35. tweek/hooks/pre_tool_use.py +149 -49
  36. tweek/integrations/openclaw.py +84 -0
  37. tweek/licensing.py +1 -1
  38. tweek/mcp/__init__.py +7 -9
  39. tweek/mcp/clients/chatgpt.py +2 -2
  40. tweek/mcp/clients/claude_desktop.py +2 -2
  41. tweek/mcp/clients/gemini.py +2 -2
  42. tweek/mcp/proxy.py +165 -1
  43. tweek/memory/provenance.py +438 -0
  44. tweek/memory/queries.py +2 -0
  45. tweek/memory/safety.py +23 -4
  46. tweek/memory/schemas.py +1 -0
  47. tweek/memory/store.py +101 -71
  48. tweek/plugins/screening/heuristic_scorer.py +1 -1
  49. tweek/security/integrity.py +77 -0
  50. tweek/security/llm_reviewer.py +162 -68
  51. tweek/security/local_reviewer.py +44 -2
  52. tweek/security/model_registry.py +73 -7
  53. tweek/skill_template/overrides-reference.md +1 -1
  54. tweek/skills/context.py +221 -0
  55. tweek/skills/scanner.py +2 -2
  56. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
  57. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
  58. tweek/mcp/server.py +0 -320
  59. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
  60. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
  61. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
  63. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_protect.py ADDED
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek CLI Protect / Unprotect Commands
4
+
5
+ Per-tool protection lifecycle:
6
+ tweek protect Interactive wizard
7
+ tweek protect claude-code Install Claude Code hooks
8
+ tweek protect openclaw One-command OpenClaw protection
9
+ tweek protect claude-desktop Configure Claude Desktop MCP
10
+ tweek protect chatgpt Configure ChatGPT Desktop MCP
11
+ tweek protect gemini Configure Gemini CLI MCP
12
+ tweek unprotect [tool] Remove protection from a tool
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import click
17
+ import json
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from tweek.cli_helpers import (
23
+ console,
24
+ TWEEK_BANNER,
25
+ _has_tweek_hooks,
26
+ _has_tweek_at,
27
+ _detect_all_tools,
28
+ )
29
+ from tweek.cli_install import _install_claude_code_hooks
30
+
31
+
32
+ # =============================================================================
33
+ # PROTECT GROUP
34
+ # =============================================================================
35
+
36
+ @click.group(
37
+ invoke_without_command=True,
38
+ epilog="""\b
39
+ Examples:
40
+ tweek protect Interactive wizard — detect & protect all tools
41
+ tweek protect --status Show protection status for all tools
42
+ tweek protect claude-code Install Claude Code hooks
43
+ tweek protect openclaw One-command OpenClaw protection
44
+ tweek protect claude-desktop Configure Claude Desktop integration
45
+ tweek protect chatgpt Set up ChatGPT Desktop integration
46
+ tweek protect gemini Configure Gemini CLI integration
47
+ """
48
+ )
49
+ @click.option("--status", is_flag=True, help="Show protection status for all tools")
50
+ @click.pass_context
51
+ def protect(ctx, status):
52
+ """Set up Tweek protection for AI tools.
53
+
54
+ When run without a subcommand, launches an interactive wizard
55
+ that auto-detects installed AI tools and offers to protect them.
56
+ """
57
+ if status:
58
+ _show_protection_status()
59
+ return
60
+ if ctx.invoked_subcommand is None:
61
+ _run_protect_wizard()
62
+
63
+
64
+ @protect.command(
65
+ "openclaw",
66
+ epilog="""\b
67
+ Examples:
68
+ tweek protect openclaw Auto-detect and protect OpenClaw
69
+ tweek protect openclaw --paranoid Maximum security preset
70
+ tweek protect openclaw --port 9999 Custom gateway port
71
+ """
72
+ )
73
+ @click.option("--port", default=None, type=int,
74
+ help="OpenClaw gateway port (default: auto-detect)")
75
+ @click.option("--paranoid", is_flag=True,
76
+ help="Use paranoid security preset (default: cautious)")
77
+ @click.option("--preset", type=click.Choice(["paranoid", "cautious", "balanced", "trusted"]),
78
+ default=None, help="Security preset to apply")
79
+ def protect_openclaw(port, paranoid, preset):
80
+ """One-command OpenClaw protection setup.
81
+
82
+ Auto-detects OpenClaw, configures proxy wrapping,
83
+ and starts screening all tool calls through Tweek's
84
+ five-layer defense pipeline.
85
+ """
86
+ from tweek.integrations.openclaw import (
87
+ detect_openclaw_installation,
88
+ setup_openclaw_protection,
89
+ )
90
+
91
+ console.print(TWEEK_BANNER, style="cyan")
92
+
93
+ # Resolve preset
94
+ if paranoid:
95
+ effective_preset = "paranoid"
96
+ elif preset:
97
+ effective_preset = preset
98
+ else:
99
+ effective_preset = "cautious"
100
+
101
+ # Step 1: Detect OpenClaw
102
+ console.print("[cyan]Detecting OpenClaw...[/cyan]")
103
+ openclaw = detect_openclaw_installation()
104
+
105
+ if not openclaw["installed"]:
106
+ console.print()
107
+ console.print("[red]OpenClaw not detected on this system.[/red]")
108
+ console.print()
109
+ console.print("[white]Install OpenClaw first:[/white]")
110
+ console.print(" npm install -g openclaw")
111
+ console.print()
112
+ console.print("[white]Or if OpenClaw is installed in a non-standard location,[/white]")
113
+ console.print("[white]specify the gateway port manually:[/white]")
114
+ console.print(" tweek protect openclaw --port 18789")
115
+ return
116
+
117
+ # Show detection results
118
+ console.print()
119
+ console.print(" [green]OpenClaw detected[/green]")
120
+
121
+ if openclaw["version"]:
122
+ console.print(f" Version: {openclaw['version']}")
123
+
124
+ console.print(f" Gateway: port {openclaw['gateway_port']}", end="")
125
+ if openclaw["gateway_active"]:
126
+ console.print(" [green](running)[/green]")
127
+ elif openclaw["process_running"]:
128
+ console.print(" [yellow](process running, gateway inactive)[/yellow]")
129
+ else:
130
+ console.print(" [white](not running)[/white]")
131
+
132
+ if openclaw["config_path"]:
133
+ console.print(f" Config: {openclaw['config_path']}")
134
+
135
+ console.print()
136
+
137
+ # Step 2: Configure protection
138
+ console.print("[cyan]Configuring Tweek protection...[/cyan]")
139
+ result = setup_openclaw_protection(port=port, preset=effective_preset)
140
+
141
+ if not result.success:
142
+ console.print(f"\n[red]Setup failed: {result.error}[/red]")
143
+ return
144
+
145
+ # Show configuration
146
+ console.print(f" Scanner: port {result.scanner_port} -> wrapping OpenClaw gateway")
147
+ console.print(f" Preset: {result.preset} (262 patterns + rate limiting)")
148
+
149
+ # Check for API key
150
+ anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
151
+ if anthropic_key:
152
+ console.print(" LLM Review: [green]active[/green] (ANTHROPIC_API_KEY found)")
153
+ else:
154
+ console.print(" LLM Review: [white]available (set ANTHROPIC_API_KEY for semantic analysis)[/white]")
155
+
156
+ # Show warnings
157
+ for warning in result.warnings:
158
+ console.print(f"\n [yellow]Warning: {warning}[/yellow]")
159
+
160
+ console.print()
161
+
162
+ if not openclaw["gateway_active"]:
163
+ console.print("[yellow]Note: OpenClaw gateway is not currently running.[/yellow]")
164
+ console.print("[white]Protection will activate when OpenClaw starts.[/white]")
165
+ console.print()
166
+
167
+ console.print("[green]Protection configured.[/green] Screening all OpenClaw tool calls.")
168
+ console.print()
169
+ console.print("[white]Verify: tweek doctor[/white]")
170
+ console.print("[white]Logs: tweek logs show[/white]")
171
+ console.print("[white]Stop: tweek proxy stop[/white]")
172
+
173
+
174
+ @protect.command(
175
+ "claude-code",
176
+ epilog="""\b
177
+ Examples:
178
+ tweek protect claude-code Install for current project
179
+ tweek protect claude-code --global Install globally (all projects)
180
+ tweek protect claude-code --quick Zero-prompt install with defaults
181
+ tweek protect claude-code --preset paranoid Apply paranoid security preset
182
+ """
183
+ )
184
+ @click.option("--global", "install_global", is_flag=True, default=False,
185
+ help="Install globally to ~/.claude/ (protects all projects)")
186
+ @click.option("--dev-test", is_flag=True, hidden=True,
187
+ help="Install to test environment (for Tweek development only)")
188
+ @click.option("--backup/--no-backup", default=True,
189
+ help="Backup existing hooks before installation")
190
+ @click.option("--skip-env-scan", is_flag=True,
191
+ help="Skip scanning for .env files to migrate")
192
+ @click.option("--interactive", "-i", is_flag=True,
193
+ help="Interactively configure security settings")
194
+ @click.option("--preset", type=click.Choice(["paranoid", "cautious", "balanced", "trusted"]),
195
+ help="Apply a security preset (skip interactive)")
196
+ @click.option("--ai-defaults", is_flag=True,
197
+ help="Let AI suggest default settings based on detected skills")
198
+ @click.option("--with-sandbox", is_flag=True,
199
+ help="Prompt to install sandbox tool if not available (Linux only)")
200
+ @click.option("--force-proxy", is_flag=True,
201
+ help="Force Tweek proxy to override existing proxy configurations (e.g., openclaw)")
202
+ @click.option("--skip-proxy-check", is_flag=True,
203
+ help="Skip checking for existing proxy configurations")
204
+ @click.option("--quick", is_flag=True,
205
+ help="Zero-prompt install with balanced defaults (skips env scan and proxy check)")
206
+ def protect_claude_code(install_global, dev_test, backup, skip_env_scan, interactive, preset, ai_defaults, with_sandbox, force_proxy, skip_proxy_check, quick):
207
+ """Install Tweek hooks for Claude Code.
208
+
209
+ Installs PreToolUse and PostToolUse hooks to screen all
210
+ Claude Code tool calls through Tweek's security pipeline.
211
+ """
212
+ _install_claude_code_hooks(
213
+ install_global=install_global,
214
+ dev_test=dev_test,
215
+ backup=backup,
216
+ skip_env_scan=skip_env_scan,
217
+ interactive=interactive,
218
+ preset=preset,
219
+ ai_defaults=ai_defaults,
220
+ with_sandbox=with_sandbox,
221
+ force_proxy=force_proxy,
222
+ skip_proxy_check=skip_proxy_check,
223
+ quick=quick,
224
+ )
225
+
226
+
227
+ @protect.command("claude-desktop")
228
+ def protect_claude_desktop():
229
+ """Configure Tweek as MCP server for Claude Desktop."""
230
+ _protect_mcp_client("claude-desktop")
231
+
232
+
233
+ @protect.command("chatgpt")
234
+ def protect_chatgpt():
235
+ """Configure Tweek as MCP server for ChatGPT Desktop."""
236
+ _protect_mcp_client("chatgpt")
237
+
238
+
239
+ @protect.command("gemini")
240
+ def protect_gemini():
241
+ """Configure Tweek as MCP server for Gemini CLI."""
242
+ _protect_mcp_client("gemini")
243
+
244
+
245
+ def _protect_mcp_client(client_name: str):
246
+ """Shared logic for MCP client protection commands."""
247
+ try:
248
+ from tweek.mcp.clients import get_client
249
+
250
+ handler = get_client(client_name)
251
+ result = handler.install()
252
+
253
+ if result.get("success"):
254
+ console.print(f"[green]{result.get('message', 'Installed successfully')}[/green]")
255
+ if result.get("config_path"):
256
+ console.print(f" Config: {result['config_path']}")
257
+ if result.get("backup"):
258
+ console.print(f" Backup: {result['backup']}")
259
+ if result.get("instructions"):
260
+ console.print()
261
+ for line in result["instructions"]:
262
+ console.print(f" {line}")
263
+ else:
264
+ console.print(f"[red]{result.get('error', 'Installation failed')}[/red]")
265
+ except Exception as e:
266
+ console.print(f"[red]Error: {e}[/red]")
267
+
268
+
269
+ # =============================================================================
270
+ # UNPROTECT COMMAND
271
+ # =============================================================================
272
+
273
+ @click.command(
274
+ epilog="""\b
275
+ Examples:
276
+ tweek unprotect Interactive — choose what to unprotect
277
+ tweek unprotect claude-code Remove Claude Code hooks
278
+ tweek unprotect claude-code --global Remove global Claude Code hooks
279
+ tweek unprotect claude-desktop Remove from Claude Desktop
280
+ tweek unprotect openclaw Remove OpenClaw protection
281
+ """
282
+ )
283
+ @click.argument("tool", required=False, type=click.Choice(
284
+ ["claude-code", "openclaw", "claude-desktop", "chatgpt", "gemini"]))
285
+ @click.option("--global", "unprotect_global", is_flag=True, default=False,
286
+ help="Remove from ~/.claude/ (global installation)")
287
+ @click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
288
+ def unprotect(tool: str, unprotect_global: bool, confirm: bool):
289
+ """Remove Tweek protection from an AI tool.
290
+
291
+ This removes hooks and MCP configuration for a specific tool
292
+ but keeps Tweek installed on your system. Use `tweek uninstall`
293
+ to fully remove Tweek.
294
+
295
+ When run without arguments, launches an interactive wizard
296
+ that walks through each protected tool asking if you want
297
+ to remove protection.
298
+
299
+ This command can only be run from an interactive terminal.
300
+ AI agents are blocked from running it.
301
+ """
302
+ from tweek.cli_uninstall import _uninstall_scope
303
+
304
+ # ─────────────────────────────────────────────────────────────
305
+ # HUMAN-ONLY GATE: Block non-interactive execution
306
+ # This is Layer 2 of protection (Layer 1 is the PreToolUse hook)
307
+ # ─────────────────────────────────────────────────────────────
308
+ if not sys.stdin.isatty():
309
+ console.print("[red]ERROR: tweek unprotect must be run from an interactive terminal.[/red]")
310
+ console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
311
+ console.print("[white]Open a terminal and run the command directly.[/white]")
312
+ raise SystemExit(1)
313
+
314
+ # No tool: run interactive wizard
315
+ if not tool:
316
+ _run_unprotect_wizard()
317
+ return
318
+
319
+ console.print(TWEEK_BANNER, style="cyan")
320
+
321
+ tweek_dir = Path("~/.tweek").expanduser()
322
+ global_target = Path("~/.claude").expanduser()
323
+ project_target = Path.cwd() / ".claude"
324
+
325
+ if tool == "claude-code":
326
+ if unprotect_global:
327
+ _uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
328
+ else:
329
+ _uninstall_scope(project_target, tweek_dir, confirm, scope_label="project")
330
+ return
331
+
332
+ if tool in ("claude-desktop", "chatgpt", "gemini"):
333
+ try:
334
+ from tweek.mcp.clients import get_client
335
+ handler = get_client(tool)
336
+ result = handler.uninstall()
337
+ if result.get("success"):
338
+ console.print(f"[green]{result.get('message', 'Uninstalled successfully')}[/green]")
339
+ if result.get("backup"):
340
+ console.print(f" Backup: {result['backup']}")
341
+ if result.get("instructions"):
342
+ console.print()
343
+ for line in result["instructions"]:
344
+ console.print(f" {line}")
345
+ else:
346
+ console.print(f"[red]{result.get('error', 'Uninstallation failed')}[/red]")
347
+ except Exception as e:
348
+ console.print(f"[red]Error: {e}[/red]")
349
+ return
350
+
351
+ if tool == "openclaw":
352
+ from tweek.integrations.openclaw import remove_openclaw_protection
353
+ result = remove_openclaw_protection()
354
+ if result.get("success"):
355
+ console.print(f"[green]{result.get('message', 'OpenClaw protection removed')}[/green]")
356
+ for detail in result.get("details", []):
357
+ console.print(f" [green]\u2713[/green] {detail}")
358
+ else:
359
+ console.print(f"[red]{result.get('error', 'Failed to remove OpenClaw protection')}[/red]")
360
+ return
361
+
362
+
363
+ # =============================================================================
364
+ # PROTECT WIZARD & STATUS HELPERS
365
+ # =============================================================================
366
+
367
+ def _run_protect_wizard():
368
+ """Interactive wizard: detect tools and ask Y/n for each one."""
369
+ console.print(TWEEK_BANNER, style="cyan")
370
+ console.print("[bold]Tweek Protection Wizard[/bold]\n")
371
+ console.print("Scanning for AI tools...\n")
372
+
373
+ tools = _detect_all_tools()
374
+
375
+ # Show detection summary
376
+ detected = [(tid, label, prot) for tid, label, inst, prot, _ in tools if inst]
377
+ not_detected = [label for _, label, inst, _, _ in tools if not inst]
378
+
379
+ if not_detected:
380
+ for label in not_detected:
381
+ console.print(f" [white]{label:<20}[/white] [white]not found[/white]")
382
+
383
+ if not detected:
384
+ console.print("\n[yellow]No AI tools detected on this system.[/yellow]")
385
+ return
386
+
387
+ # Show already-protected tools
388
+ already_protected = [(tid, label) for tid, label, prot in detected if prot]
389
+ unprotected = [(tid, label) for tid, label, prot in detected if not prot]
390
+
391
+ for _, label in already_protected:
392
+ console.print(f" [green]{label:<20} protected[/green]")
393
+
394
+ if not unprotected:
395
+ console.print(f"\n[green]All {len(already_protected)} detected tool(s) already protected.[/green]")
396
+ console.print("Run 'tweek status' to see details.")
397
+ return
398
+
399
+ for _, label in unprotected:
400
+ console.print(f" [yellow]{label:<20} not protected[/yellow]")
401
+
402
+ # Ask for preset first (applies to all)
403
+ console.print()
404
+ console.print("[bold]Security preset:[/bold]")
405
+ console.print(" [bold]1.[/bold] cautious [white](recommended)[/white] \u2014 screen risky & dangerous tools")
406
+ console.print(" [bold]2.[/bold] paranoid \u2014 screen everything except safe tools")
407
+ console.print(" [bold]3.[/bold] trusted \u2014 only screen dangerous tools")
408
+ console.print()
409
+ preset_choice = click.prompt("Select preset", type=click.IntRange(1, 3), default=1)
410
+ preset = ["cautious", "paranoid", "trusted"][preset_choice - 1]
411
+
412
+ # Walk through each unprotected tool
413
+ console.print()
414
+ protected_count = 0
415
+ skipped_count = 0
416
+
417
+ for tool_id, label in unprotected:
418
+ protect_it = click.confirm(f" Protect {label}?", default=True)
419
+
420
+ if not protect_it:
421
+ console.print(f" [white]skipped[/white]")
422
+ skipped_count += 1
423
+ continue
424
+
425
+ try:
426
+ if tool_id == "claude-code":
427
+ _install_claude_code_hooks(
428
+ install_global=True, dev_test=False, backup=True,
429
+ skip_env_scan=True, interactive=False, preset=preset,
430
+ ai_defaults=False, with_sandbox=False, force_proxy=False,
431
+ skip_proxy_check=True, quick=True,
432
+ )
433
+ elif tool_id == "openclaw":
434
+ from tweek.integrations.openclaw import setup_openclaw_protection
435
+ result = setup_openclaw_protection(preset=preset)
436
+ if result.success:
437
+ console.print(f" [green]done[/green]")
438
+ else:
439
+ console.print(f" [red]failed: {result.error}[/red]")
440
+ continue
441
+ elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
442
+ _protect_mcp_client(tool_id)
443
+ protected_count += 1
444
+ except Exception as e:
445
+ console.print(f" [red]error: {e}[/red]")
446
+
447
+ console.print()
448
+ if protected_count:
449
+ console.print(f"[green]Protected {protected_count} tool(s).[/green]", end="")
450
+ if skipped_count:
451
+ console.print(f" [white]Skipped {skipped_count}.[/white]", end="")
452
+ console.print()
453
+ console.print("Run 'tweek status' to see the full dashboard.")
454
+
455
+
456
+ def _run_unprotect_wizard():
457
+ """Interactive wizard: detect protected tools and ask Y/n to unprotect each."""
458
+ from tweek.cli_uninstall import _uninstall_scope
459
+
460
+ console.print(TWEEK_BANNER, style="cyan")
461
+ console.print("[bold]Tweek Unprotect Wizard[/bold]\n")
462
+ console.print("Scanning for protected AI tools...\n")
463
+
464
+ tools = _detect_all_tools()
465
+ tweek_dir = Path("~/.tweek").expanduser()
466
+ global_target = Path("~/.claude").expanduser()
467
+
468
+ protected = [(tid, label) for tid, label, inst, prot, _ in tools if inst and prot]
469
+
470
+ if not protected:
471
+ console.print("[yellow]No protected tools found.[/yellow]")
472
+ return
473
+
474
+ for _, label in protected:
475
+ console.print(f" [green]{label:<20} protected[/green]")
476
+
477
+ console.print()
478
+ removed_count = 0
479
+ skipped_count = 0
480
+
481
+ for tool_id, label in protected:
482
+ remove_it = click.confirm(f" Remove protection from {label}?", default=False)
483
+
484
+ if not remove_it:
485
+ console.print(f" [white]kept[/white]")
486
+ skipped_count += 1
487
+ continue
488
+
489
+ try:
490
+ if tool_id == "claude-code":
491
+ _uninstall_scope(global_target, tweek_dir, confirm=True, scope_label="global")
492
+ elif tool_id in ("claude-desktop", "chatgpt", "gemini"):
493
+ from tweek.mcp.clients import get_client
494
+ handler = get_client(tool_id)
495
+ result = handler.uninstall()
496
+ if result.get("success"):
497
+ console.print(f" [green]{result.get('message', 'removed')}[/green]")
498
+ else:
499
+ console.print(f" [red]{result.get('error', 'failed')}[/red]")
500
+ continue
501
+ elif tool_id == "openclaw":
502
+ from tweek.integrations.openclaw import remove_openclaw_protection
503
+ result = remove_openclaw_protection()
504
+ if result.get("success"):
505
+ console.print(f" [green]{result.get('message', 'removed')}[/green]")
506
+ else:
507
+ console.print(f" [red]{result.get('error', 'failed')}[/red]")
508
+ continue
509
+ removed_count += 1
510
+ except Exception as e:
511
+ console.print(f" [red]error: {e}[/red]")
512
+
513
+ console.print()
514
+ if removed_count:
515
+ console.print(f"[green]Removed protection from {removed_count} tool(s).[/green]", end="")
516
+ if skipped_count:
517
+ console.print(f" [white]Kept {skipped_count}.[/white]", end="")
518
+ console.print()
519
+
520
+
521
+ def _show_protection_status():
522
+ """Show protection status dashboard for all AI tools."""
523
+ from rich.table import Table
524
+
525
+ console.print(TWEEK_BANNER, style="cyan")
526
+
527
+ tools = _detect_all_tools()
528
+
529
+ # Build status table
530
+ table = Table(title="Protection Status", show_lines=False)
531
+ table.add_column("Tool", style="cyan", min_width=18)
532
+ table.add_column("Installed", justify="center", min_width=10)
533
+ table.add_column("Protected", justify="center", min_width=10)
534
+ table.add_column("Details")
535
+
536
+ detected_count = 0
537
+ protected_count = 0
538
+
539
+ for tool_id, label, installed, prot, detail in tools:
540
+ if installed:
541
+ detected_count += 1
542
+ if prot:
543
+ protected_count += 1
544
+
545
+ table.add_row(
546
+ label,
547
+ "[green]yes[/green]" if installed else "[white]no[/white]",
548
+ "[green]yes[/green]" if prot else "[yellow]no[/yellow]" if installed else "[white]\u2014[/white]",
549
+ detail,
550
+ )
551
+
552
+ console.print(table)
553
+ console.print()
554
+
555
+ # Summary line
556
+ unprotected_count = detected_count - protected_count
557
+ if detected_count == 0:
558
+ console.print("[yellow]No AI tools detected.[/yellow]")
559
+ elif unprotected_count == 0:
560
+ console.print(f"[green]{protected_count}/{detected_count} detected tools protected.[/green]")
561
+ else:
562
+ console.print(f"[yellow]{protected_count}/{detected_count} detected tools protected. {unprotected_count} unprotected.[/yellow]")
563
+ console.print("[white]Run 'tweek protect' to set up protection.[/white]")
564
+ console.print()