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