tweek 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tweek/__init__.py +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6559
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/__init__.py +8 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +49 -0
- tweek/config/models.py +307 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +162 -68
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
- tweek/mcp/server.py +0 -320
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_config.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek CLI Config Management
|
|
4
|
+
|
|
5
|
+
Commands for configuring Tweek security policies:
|
|
6
|
+
tweek config list List all tools and skills with security tiers
|
|
7
|
+
tweek config set Set security tier for a skill or tool
|
|
8
|
+
tweek config preset Apply a configuration preset
|
|
9
|
+
tweek config reset Reset configuration to defaults
|
|
10
|
+
tweek config validate Validate configuration for errors
|
|
11
|
+
tweek config diff Show what would change if a preset were applied
|
|
12
|
+
tweek config llm Show LLM review configuration and provider status
|
|
13
|
+
tweek config edit Open config files in your editor
|
|
14
|
+
tweek config show-defaults View bundled default configuration
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from tweek.cli_helpers import console, TWEEK_BANNER
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
def config():
|
|
26
|
+
"""Configure Tweek security policies."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@config.command("list",
|
|
31
|
+
epilog="""\b
|
|
32
|
+
Examples:
|
|
33
|
+
tweek config list List all tools and skills
|
|
34
|
+
tweek config list --tools Show only tool security tiers
|
|
35
|
+
tweek config list --skills Show only skill security tiers
|
|
36
|
+
tweek config list --summary Show tier counts and overrides summary
|
|
37
|
+
"""
|
|
38
|
+
)
|
|
39
|
+
@click.option("--tools", "show_tools", is_flag=True, help="Show tools only")
|
|
40
|
+
@click.option("--skills", "show_skills", is_flag=True, help="Show skills only")
|
|
41
|
+
@click.option("--summary", is_flag=True, help="Show configuration summary instead of full list")
|
|
42
|
+
def config_list(show_tools: bool, show_skills: bool, summary: bool):
|
|
43
|
+
"""List all tools and skills with their security tiers."""
|
|
44
|
+
from tweek.config.manager import ConfigManager
|
|
45
|
+
|
|
46
|
+
cfg = ConfigManager()
|
|
47
|
+
|
|
48
|
+
# Handle summary mode
|
|
49
|
+
if summary:
|
|
50
|
+
# Count by tier
|
|
51
|
+
tool_tiers = {}
|
|
52
|
+
for tool in cfg.list_tools():
|
|
53
|
+
tier = tool.tier.value
|
|
54
|
+
tool_tiers[tier] = tool_tiers.get(tier, 0) + 1
|
|
55
|
+
|
|
56
|
+
skill_tiers = {}
|
|
57
|
+
for skill in cfg.list_skills():
|
|
58
|
+
tier = skill.tier.value
|
|
59
|
+
skill_tiers[tier] = skill_tiers.get(tier, 0) + 1
|
|
60
|
+
|
|
61
|
+
# User overrides
|
|
62
|
+
user_config = cfg.export_config("user")
|
|
63
|
+
user_tools = user_config.get("tools", {})
|
|
64
|
+
user_skills = user_config.get("skills", {})
|
|
65
|
+
|
|
66
|
+
summary_text = f"[cyan]Default Tier:[/cyan] {cfg.get_default_tier().value}\n\n"
|
|
67
|
+
|
|
68
|
+
summary_text += "[cyan]Tools by Tier:[/cyan]\n"
|
|
69
|
+
for tier in ["safe", "default", "risky", "dangerous"]:
|
|
70
|
+
count = tool_tiers.get(tier, 0)
|
|
71
|
+
if count:
|
|
72
|
+
summary_text += f" {tier}: {count}\n"
|
|
73
|
+
|
|
74
|
+
summary_text += "\n[cyan]Skills by Tier:[/cyan]\n"
|
|
75
|
+
for tier in ["safe", "default", "risky", "dangerous"]:
|
|
76
|
+
count = skill_tiers.get(tier, 0)
|
|
77
|
+
if count:
|
|
78
|
+
summary_text += f" {tier}: {count}\n"
|
|
79
|
+
|
|
80
|
+
if user_tools or user_skills:
|
|
81
|
+
summary_text += "\n[cyan]User Overrides:[/cyan]\n"
|
|
82
|
+
for tool_name, tier in user_tools.items():
|
|
83
|
+
summary_text += f" {tool_name}: {tier}\n"
|
|
84
|
+
for skill_name, tier in user_skills.items():
|
|
85
|
+
summary_text += f" {skill_name}: {tier}\n"
|
|
86
|
+
else:
|
|
87
|
+
summary_text += "\n[cyan]User Overrides:[/cyan] (none)"
|
|
88
|
+
|
|
89
|
+
console.print(Panel.fit(summary_text, title="Tweek Configuration"))
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Default to showing both if neither specified
|
|
93
|
+
if not show_tools and not show_skills:
|
|
94
|
+
show_tools = show_skills = True
|
|
95
|
+
|
|
96
|
+
tier_styles = {
|
|
97
|
+
"safe": "green",
|
|
98
|
+
"default": "blue",
|
|
99
|
+
"risky": "yellow",
|
|
100
|
+
"dangerous": "red",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
source_styles = {
|
|
104
|
+
"default": "white",
|
|
105
|
+
"user": "cyan",
|
|
106
|
+
"project": "magenta",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if show_tools:
|
|
110
|
+
table = Table(title="Tool Security Tiers")
|
|
111
|
+
table.add_column("Tool", style="bold")
|
|
112
|
+
table.add_column("Tier")
|
|
113
|
+
table.add_column("Source", style="white")
|
|
114
|
+
table.add_column("Description")
|
|
115
|
+
|
|
116
|
+
for tool in cfg.list_tools():
|
|
117
|
+
tier_style = tier_styles.get(tool.tier.value, "white")
|
|
118
|
+
source_style = source_styles.get(tool.source, "white")
|
|
119
|
+
table.add_row(
|
|
120
|
+
tool.name,
|
|
121
|
+
f"[{tier_style}]{tool.tier.value}[/{tier_style}]",
|
|
122
|
+
f"[{source_style}]{tool.source}[/{source_style}]",
|
|
123
|
+
tool.description or ""
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
console.print(table)
|
|
127
|
+
console.print()
|
|
128
|
+
|
|
129
|
+
if show_skills:
|
|
130
|
+
table = Table(title="Skill Security Tiers")
|
|
131
|
+
table.add_column("Skill", style="bold")
|
|
132
|
+
table.add_column("Tier")
|
|
133
|
+
table.add_column("Source", style="white")
|
|
134
|
+
table.add_column("Description")
|
|
135
|
+
|
|
136
|
+
for skill in cfg.list_skills():
|
|
137
|
+
tier_style = tier_styles.get(skill.tier.value, "white")
|
|
138
|
+
source_style = source_styles.get(skill.source, "white")
|
|
139
|
+
table.add_row(
|
|
140
|
+
skill.name,
|
|
141
|
+
f"[{tier_style}]{skill.tier.value}[/{tier_style}]",
|
|
142
|
+
f"[{source_style}]{skill.source}[/{source_style}]",
|
|
143
|
+
skill.description or ""
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
console.print(table)
|
|
147
|
+
|
|
148
|
+
console.print("\n[white]Tiers: safe (no checks) \u2192 default (regex) \u2192 risky (+LLM) \u2192 dangerous (+sandbox)[/white]")
|
|
149
|
+
console.print("[white]Sources: default (built-in), user (~/.tweek/config.yaml), project (.tweek/config.yaml)[/white]")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@config.command("set",
|
|
153
|
+
epilog="""\b
|
|
154
|
+
Examples:
|
|
155
|
+
tweek config set --tool Bash --tier dangerous Mark Bash as dangerous
|
|
156
|
+
tweek config set --skill web-fetch --tier risky Set skill to risky tier
|
|
157
|
+
tweek config set --tier cautious Set default tier for all
|
|
158
|
+
tweek config set --tool Edit --tier safe --scope project Project-level override
|
|
159
|
+
"""
|
|
160
|
+
)
|
|
161
|
+
@click.option("--skill", help="Skill name to configure")
|
|
162
|
+
@click.option("--tool", help="Tool name to configure")
|
|
163
|
+
@click.option("--tier", type=click.Choice(["safe", "default", "risky", "dangerous"]), required=True,
|
|
164
|
+
help="Security tier to set")
|
|
165
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user",
|
|
166
|
+
help="Config scope (user=global, project=this directory)")
|
|
167
|
+
def config_set(skill: str, tool: str, tier: str, scope: str):
|
|
168
|
+
"""Set security tier for a skill or tool."""
|
|
169
|
+
from tweek.config.manager import ConfigManager, SecurityTier
|
|
170
|
+
|
|
171
|
+
cfg = ConfigManager()
|
|
172
|
+
tier_enum = SecurityTier.from_string(tier)
|
|
173
|
+
|
|
174
|
+
if skill:
|
|
175
|
+
cfg.set_skill_tier(skill, tier_enum, scope=scope)
|
|
176
|
+
console.print(f"[green]\u2713[/green] Set skill '{skill}' to [bold]{tier}[/bold] tier ({scope} config)")
|
|
177
|
+
elif tool:
|
|
178
|
+
cfg.set_tool_tier(tool, tier_enum, scope=scope)
|
|
179
|
+
console.print(f"[green]\u2713[/green] Set tool '{tool}' to [bold]{tier}[/bold] tier ({scope} config)")
|
|
180
|
+
else:
|
|
181
|
+
cfg.set_default_tier(tier_enum, scope=scope)
|
|
182
|
+
console.print(f"[green]\u2713[/green] Set default tier to [bold]{tier}[/bold] ({scope} config)")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@config.command("preset",
|
|
186
|
+
epilog="""\b
|
|
187
|
+
Examples:
|
|
188
|
+
tweek config preset paranoid Maximum security, prompt for everything
|
|
189
|
+
tweek config preset cautious Balanced security (recommended)
|
|
190
|
+
tweek config preset trusted Minimal prompts, trust AI decisions
|
|
191
|
+
tweek config preset paranoid --scope project Apply preset to project only
|
|
192
|
+
"""
|
|
193
|
+
)
|
|
194
|
+
@click.argument("preset_name", type=click.Choice(["paranoid", "cautious", "balanced", "trusted"]))
|
|
195
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user")
|
|
196
|
+
def config_preset(preset_name: str, scope: str):
|
|
197
|
+
"""Apply a configuration preset.
|
|
198
|
+
|
|
199
|
+
Presets:
|
|
200
|
+
paranoid - Maximum security, prompt for everything
|
|
201
|
+
cautious - Balanced security (recommended)
|
|
202
|
+
trusted - Minimal prompts, trust AI decisions
|
|
203
|
+
"""
|
|
204
|
+
from tweek.config.manager import ConfigManager
|
|
205
|
+
|
|
206
|
+
cfg = ConfigManager()
|
|
207
|
+
cfg.apply_preset(preset_name, scope=scope)
|
|
208
|
+
|
|
209
|
+
console.print(f"[green]\u2713[/green] Applied [bold]{preset_name}[/bold] preset ({scope} config)")
|
|
210
|
+
|
|
211
|
+
if preset_name == "paranoid":
|
|
212
|
+
console.print("[white]All tools require screening, Bash commands always sandboxed[/white]")
|
|
213
|
+
elif preset_name == "cautious":
|
|
214
|
+
console.print("[white]Balanced: read-only tools safe, Bash dangerous[/white]")
|
|
215
|
+
elif preset_name == "trusted":
|
|
216
|
+
console.print("[white]Minimal prompts: only high-risk patterns trigger alerts[/white]")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@config.command("reset",
|
|
220
|
+
epilog="""\b
|
|
221
|
+
Examples:
|
|
222
|
+
tweek config reset --tool Bash Reset Bash to default tier
|
|
223
|
+
tweek config reset --skill web-fetch Reset a skill to default tier
|
|
224
|
+
tweek config reset --all Reset all user configuration
|
|
225
|
+
tweek config reset --all --confirm Reset all without confirmation prompt
|
|
226
|
+
"""
|
|
227
|
+
)
|
|
228
|
+
@click.option("--skill", help="Reset specific skill to default")
|
|
229
|
+
@click.option("--tool", help="Reset specific tool to default")
|
|
230
|
+
@click.option("--all", "reset_all", is_flag=True, help="Reset all user configuration")
|
|
231
|
+
@click.option("--scope", type=click.Choice(["user", "project"]), default="user")
|
|
232
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
233
|
+
def config_reset(skill: str, tool: str, reset_all: bool, scope: str, confirm: bool):
|
|
234
|
+
"""Reset configuration to defaults."""
|
|
235
|
+
from tweek.config.manager import ConfigManager
|
|
236
|
+
|
|
237
|
+
cfg = ConfigManager()
|
|
238
|
+
|
|
239
|
+
if reset_all:
|
|
240
|
+
if not confirm and not click.confirm(f"Reset ALL {scope} configuration?"):
|
|
241
|
+
console.print("[white]Cancelled[/white]")
|
|
242
|
+
return
|
|
243
|
+
cfg.reset_all(scope=scope)
|
|
244
|
+
console.print(f"[green]\u2713[/green] Reset all {scope} configuration to defaults")
|
|
245
|
+
elif skill:
|
|
246
|
+
if cfg.reset_skill(skill, scope=scope):
|
|
247
|
+
console.print(f"[green]\u2713[/green] Reset skill '{skill}' to default")
|
|
248
|
+
else:
|
|
249
|
+
console.print(f"[yellow]![/yellow] Skill '{skill}' has no {scope} override")
|
|
250
|
+
elif tool:
|
|
251
|
+
if cfg.reset_tool(tool, scope=scope):
|
|
252
|
+
console.print(f"[green]\u2713[/green] Reset tool '{tool}' to default")
|
|
253
|
+
else:
|
|
254
|
+
console.print(f"[yellow]![/yellow] Tool '{tool}' has no {scope} override")
|
|
255
|
+
else:
|
|
256
|
+
console.print("[red]Specify --skill, --tool, or --all[/red]")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@config.command("validate",
|
|
260
|
+
epilog="""\b
|
|
261
|
+
Examples:
|
|
262
|
+
tweek config validate Validate merged configuration
|
|
263
|
+
tweek config validate --scope user Validate only user-level config
|
|
264
|
+
tweek config validate --scope project Validate only project-level config
|
|
265
|
+
tweek config validate --json Output validation results as JSON
|
|
266
|
+
"""
|
|
267
|
+
)
|
|
268
|
+
@click.option("--scope", type=click.Choice(["user", "project", "merged"]), default="merged",
|
|
269
|
+
help="Which config scope to validate")
|
|
270
|
+
@click.option("--json-output", "--json", "json_out", is_flag=True, help="Output as JSON")
|
|
271
|
+
def config_validate(scope: str, json_out: bool):
|
|
272
|
+
"""Validate configuration for errors and typos.
|
|
273
|
+
|
|
274
|
+
Checks for unknown keys, invalid tier values, unknown tool/skill names,
|
|
275
|
+
and suggests corrections for typos.
|
|
276
|
+
"""
|
|
277
|
+
from tweek.config.manager import ConfigManager
|
|
278
|
+
|
|
279
|
+
cfg = ConfigManager()
|
|
280
|
+
issues = cfg.validate_config(scope=scope)
|
|
281
|
+
|
|
282
|
+
if json_out:
|
|
283
|
+
import json as json_mod
|
|
284
|
+
output = [
|
|
285
|
+
{
|
|
286
|
+
"level": i.level,
|
|
287
|
+
"key": i.key,
|
|
288
|
+
"message": i.message,
|
|
289
|
+
"suggestion": i.suggestion,
|
|
290
|
+
}
|
|
291
|
+
for i in issues
|
|
292
|
+
]
|
|
293
|
+
console.print_json(json_mod.dumps(output, indent=2))
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
console.print()
|
|
297
|
+
console.print("[bold]Configuration Validation[/bold]")
|
|
298
|
+
console.print("\u2500" * 40)
|
|
299
|
+
console.print(f"[white]Scope: {scope}[/white]")
|
|
300
|
+
console.print()
|
|
301
|
+
|
|
302
|
+
if not issues:
|
|
303
|
+
tools = cfg.list_tools()
|
|
304
|
+
skills = cfg.list_skills()
|
|
305
|
+
console.print(f" [green]OK[/green] Configuration valid ({len(tools)} tools, {len(skills)} skills)")
|
|
306
|
+
console.print()
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
errors = [i for i in issues if i.level == "error"]
|
|
310
|
+
warnings = [i for i in issues if i.level == "warning"]
|
|
311
|
+
|
|
312
|
+
level_styles = {
|
|
313
|
+
"error": "[red]ERROR[/red]",
|
|
314
|
+
"warning": "[yellow]WARN[/yellow] ",
|
|
315
|
+
"info": "[white]INFO[/white] ",
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for issue in issues:
|
|
319
|
+
style = level_styles.get(issue.level, "[white]???[/white] ")
|
|
320
|
+
msg = f" {style} {issue.key} \u2192 {issue.message}"
|
|
321
|
+
if issue.suggestion:
|
|
322
|
+
msg += f" {issue.suggestion}"
|
|
323
|
+
console.print(msg)
|
|
324
|
+
|
|
325
|
+
console.print()
|
|
326
|
+
parts = []
|
|
327
|
+
if errors:
|
|
328
|
+
parts.append(f"{len(errors)} error{'s' if len(errors) != 1 else ''}")
|
|
329
|
+
if warnings:
|
|
330
|
+
parts.append(f"{len(warnings)} warning{'s' if len(warnings) != 1 else ''}")
|
|
331
|
+
console.print(f" Result: {', '.join(parts)}")
|
|
332
|
+
console.print()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@config.command("diff",
|
|
336
|
+
epilog="""\b
|
|
337
|
+
Examples:
|
|
338
|
+
tweek config diff paranoid Show changes if paranoid preset applied
|
|
339
|
+
tweek config diff cautious Show changes if cautious preset applied
|
|
340
|
+
tweek config diff trusted Show changes if trusted preset applied
|
|
341
|
+
"""
|
|
342
|
+
)
|
|
343
|
+
@click.argument("preset_name", type=click.Choice(["paranoid", "cautious", "balanced", "trusted"]))
|
|
344
|
+
def config_diff(preset_name: str):
|
|
345
|
+
"""Show what would change if a preset were applied.
|
|
346
|
+
|
|
347
|
+
Compare your current configuration against a preset to see
|
|
348
|
+
exactly which settings would be modified.
|
|
349
|
+
"""
|
|
350
|
+
from tweek.config.manager import ConfigManager
|
|
351
|
+
|
|
352
|
+
cfg = ConfigManager()
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
changes = cfg.diff_preset(preset_name)
|
|
356
|
+
except ValueError as e:
|
|
357
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
console.print()
|
|
361
|
+
console.print(f"[bold]Changes if '{preset_name}' preset is applied:[/bold]")
|
|
362
|
+
console.print("\u2500" * 50)
|
|
363
|
+
|
|
364
|
+
if not changes:
|
|
365
|
+
console.print()
|
|
366
|
+
console.print(" [green]No changes[/green] \u2014 your config already matches this preset.")
|
|
367
|
+
console.print()
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
table = Table(show_header=True, show_edge=False, pad_edge=False)
|
|
371
|
+
table.add_column("Setting", style="cyan", min_width=25)
|
|
372
|
+
table.add_column("Current", min_width=12)
|
|
373
|
+
table.add_column("", min_width=3)
|
|
374
|
+
table.add_column("New", min_width=12)
|
|
375
|
+
|
|
376
|
+
tier_colors = {"safe": "green", "default": "white", "risky": "yellow", "dangerous": "red"}
|
|
377
|
+
|
|
378
|
+
for change in changes:
|
|
379
|
+
cur_color = tier_colors.get(str(change.current_value), "white")
|
|
380
|
+
new_color = tier_colors.get(str(change.new_value), "white")
|
|
381
|
+
table.add_row(
|
|
382
|
+
change.key,
|
|
383
|
+
f"[{cur_color}]{change.current_value}[/{cur_color}]",
|
|
384
|
+
"\u2192",
|
|
385
|
+
f"[{new_color}]{change.new_value}[/{new_color}]",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
console.print()
|
|
389
|
+
console.print(table)
|
|
390
|
+
console.print()
|
|
391
|
+
console.print(f" {len(changes)} change{'s' if len(changes) != 1 else ''} would be made. "
|
|
392
|
+
f"Apply with: [cyan]tweek config preset {preset_name}[/cyan]")
|
|
393
|
+
console.print()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@config.command("llm",
|
|
397
|
+
epilog="""\b
|
|
398
|
+
Examples:
|
|
399
|
+
tweek config llm Show current LLM provider status
|
|
400
|
+
tweek config llm --verbose Show detailed provider information
|
|
401
|
+
tweek config llm --validate Re-run local model validation suite
|
|
402
|
+
"""
|
|
403
|
+
)
|
|
404
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed provider info")
|
|
405
|
+
@click.option("--validate", is_flag=True, help="Re-run local model validation suite")
|
|
406
|
+
def config_llm(verbose: bool, validate: bool):
|
|
407
|
+
"""Show LLM review configuration and provider status.
|
|
408
|
+
|
|
409
|
+
Displays the current LLM review provider, model, and availability.
|
|
410
|
+
With --verbose, shows local server detection and fallback chain details.
|
|
411
|
+
With --validate, re-runs the validation suite against local models.
|
|
412
|
+
"""
|
|
413
|
+
from tweek.security.llm_reviewer import (
|
|
414
|
+
get_llm_reviewer,
|
|
415
|
+
_detect_local_server,
|
|
416
|
+
_validate_local_model,
|
|
417
|
+
FallbackReviewProvider,
|
|
418
|
+
LOCAL_MODEL_PREFERENCES,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
console.print()
|
|
422
|
+
console.print("[bold]LLM Review Configuration[/bold]")
|
|
423
|
+
console.print("\u2500" * 45)
|
|
424
|
+
|
|
425
|
+
reviewer = get_llm_reviewer()
|
|
426
|
+
|
|
427
|
+
if not reviewer.enabled:
|
|
428
|
+
console.print()
|
|
429
|
+
console.print(" [yellow]Status:[/yellow] Disabled (no provider available)")
|
|
430
|
+
console.print()
|
|
431
|
+
console.print(" [white]To enable, set one of:[/white]")
|
|
432
|
+
console.print(" ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY")
|
|
433
|
+
console.print(" Or install Ollama: [cyan]https://ollama.ai[/cyan]")
|
|
434
|
+
console.print()
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
console.print()
|
|
438
|
+
console.print(f" [green]Status:[/green] Enabled")
|
|
439
|
+
console.print(f" [cyan]Provider:[/cyan] {reviewer.provider_name}")
|
|
440
|
+
console.print(f" [cyan]Model:[/cyan] {reviewer.model}")
|
|
441
|
+
|
|
442
|
+
# Check for fallback chain
|
|
443
|
+
provider = reviewer._provider_instance
|
|
444
|
+
if isinstance(provider, FallbackReviewProvider):
|
|
445
|
+
console.print(f" [cyan]Chain:[/cyan] {provider.provider_count} providers in fallback chain")
|
|
446
|
+
if provider.active_provider:
|
|
447
|
+
console.print(f" [cyan]Active:[/cyan] {provider.active_provider.name}")
|
|
448
|
+
|
|
449
|
+
# Local server detection
|
|
450
|
+
if verbose:
|
|
451
|
+
console.print()
|
|
452
|
+
console.print("[bold]Local LLM Servers[/bold]")
|
|
453
|
+
console.print("\u2500" * 45)
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
server = _detect_local_server()
|
|
457
|
+
if server:
|
|
458
|
+
console.print(f" [green]Detected:[/green] {server.server_type}")
|
|
459
|
+
console.print(f" [cyan]URL:[/cyan] {server.base_url}")
|
|
460
|
+
console.print(f" [cyan]Model:[/cyan] {server.model}")
|
|
461
|
+
console.print(f" [cyan]Available:[/cyan] {len(server.all_models)} model{'s' if len(server.all_models) != 1 else ''}")
|
|
462
|
+
if len(server.all_models) <= 10:
|
|
463
|
+
for m in server.all_models:
|
|
464
|
+
console.print(f" - {m}")
|
|
465
|
+
else:
|
|
466
|
+
console.print(" [white]No local LLM server detected[/white]")
|
|
467
|
+
console.print(" [white]Checked: Ollama (localhost:11434), LM Studio (localhost:1234)[/white]")
|
|
468
|
+
except Exception as e:
|
|
469
|
+
console.print(f" [yellow]Detection error: {e}[/yellow]")
|
|
470
|
+
|
|
471
|
+
console.print()
|
|
472
|
+
console.print("[bold]Recommended Local Models[/bold]")
|
|
473
|
+
console.print("\u2500" * 45)
|
|
474
|
+
for i, model_name in enumerate(LOCAL_MODEL_PREFERENCES[:5], 1):
|
|
475
|
+
console.print(f" {i}. {model_name}")
|
|
476
|
+
|
|
477
|
+
# Validation mode
|
|
478
|
+
if validate:
|
|
479
|
+
console.print()
|
|
480
|
+
console.print("[bold]Model Validation[/bold]")
|
|
481
|
+
console.print("\u2500" * 45)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
server = _detect_local_server()
|
|
485
|
+
if not server:
|
|
486
|
+
console.print(" [yellow]No local server detected. Nothing to validate.[/yellow]")
|
|
487
|
+
console.print()
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
from tweek.security.llm_reviewer import OpenAIReviewProvider
|
|
491
|
+
local_prov = OpenAIReviewProvider(
|
|
492
|
+
model=server.model,
|
|
493
|
+
api_key="not-needed",
|
|
494
|
+
timeout=10.0,
|
|
495
|
+
base_url=server.base_url,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
console.print(f" Validating [cyan]{server.model}[/cyan] on {server.server_type}...")
|
|
499
|
+
passed, score = _validate_local_model(local_prov, server.model)
|
|
500
|
+
|
|
501
|
+
if passed:
|
|
502
|
+
console.print(f" [green]PASSED[/green] ({score:.0%})")
|
|
503
|
+
else:
|
|
504
|
+
console.print(f" [red]FAILED[/red] ({score:.0%}, minimum: 60%)")
|
|
505
|
+
console.print(" [white]This model may not reliably classify security threats.[/white]")
|
|
506
|
+
console.print(" [white]Try a larger model: ollama pull qwen2.5:7b-instruct[/white]")
|
|
507
|
+
except Exception as e:
|
|
508
|
+
console.print(f" [red]Validation error: {e}[/red]")
|
|
509
|
+
|
|
510
|
+
console.print()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@config.command("edit",
|
|
514
|
+
epilog="""\b
|
|
515
|
+
Examples:
|
|
516
|
+
tweek config edit Open interactive file selector
|
|
517
|
+
tweek config edit config Open security settings directly
|
|
518
|
+
tweek config edit env Open API keys file
|
|
519
|
+
tweek config edit overrides Open security overrides
|
|
520
|
+
tweek config edit hooks Open hook control file
|
|
521
|
+
tweek config edit --create Create missing files from templates first
|
|
522
|
+
"""
|
|
523
|
+
)
|
|
524
|
+
@click.argument("file_id", required=False, default=None)
|
|
525
|
+
@click.option("--create", "create_missing", is_flag=True,
|
|
526
|
+
help="Create missing config files from templates")
|
|
527
|
+
def config_edit(file_id: str, create_missing: bool):
|
|
528
|
+
"""Open Tweek configuration files in your editor.
|
|
529
|
+
|
|
530
|
+
Lists all config files with descriptions and status, then opens
|
|
531
|
+
the selected file in $VISUAL, $EDITOR, or a platform default.
|
|
532
|
+
"""
|
|
533
|
+
import os
|
|
534
|
+
import shutil
|
|
535
|
+
import subprocess
|
|
536
|
+
from pathlib import Path
|
|
537
|
+
from tweek.config.templates import CONFIG_FILES, deploy_template, resolve_target_path
|
|
538
|
+
|
|
539
|
+
# Determine editor
|
|
540
|
+
editor = os.environ.get("VISUAL") or os.environ.get("EDITOR")
|
|
541
|
+
if not editor:
|
|
542
|
+
for candidate in ["nano", "vim", "vi"]:
|
|
543
|
+
if shutil.which(candidate):
|
|
544
|
+
editor = candidate
|
|
545
|
+
break
|
|
546
|
+
if not editor:
|
|
547
|
+
console.print("[red]No editor found. Set $EDITOR or $VISUAL.[/red]")
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
# Build file list with resolved paths and existence status
|
|
551
|
+
entries = []
|
|
552
|
+
for entry in CONFIG_FILES:
|
|
553
|
+
target = resolve_target_path(entry)
|
|
554
|
+
entries.append({**entry, "resolved_path": target, "exists": target.exists()})
|
|
555
|
+
|
|
556
|
+
# Direct access by ID
|
|
557
|
+
if file_id:
|
|
558
|
+
valid_ids = [e["id"] for e in entries]
|
|
559
|
+
if file_id not in valid_ids:
|
|
560
|
+
console.print(f"[red]Unknown file: {file_id}[/red]")
|
|
561
|
+
console.print(f"[white]Valid options: {', '.join(valid_ids)}[/white]")
|
|
562
|
+
return
|
|
563
|
+
selected = next(e for e in entries if e["id"] == file_id)
|
|
564
|
+
_open_config_file(selected, editor, create_missing)
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
# Interactive: show file list
|
|
568
|
+
console.print()
|
|
569
|
+
console.print("[bold]Tweek Configuration Files[/bold]")
|
|
570
|
+
console.print("\u2500" * 70)
|
|
571
|
+
|
|
572
|
+
for i, entry in enumerate(entries, 1):
|
|
573
|
+
if not entry["editable"]:
|
|
574
|
+
status = "[dim](read-only)[/dim]"
|
|
575
|
+
elif entry["exists"]:
|
|
576
|
+
status = "[green]\u2713 exists[/green]"
|
|
577
|
+
else:
|
|
578
|
+
status = "[yellow]\u2717 missing[/yellow]"
|
|
579
|
+
|
|
580
|
+
path_display = str(entry["resolved_path"]).replace(str(Path.home()), "~")
|
|
581
|
+
console.print(f" [cyan]{i}.[/cyan] {entry['name']:<22s} {path_display}")
|
|
582
|
+
console.print(f" {status} [dim]{entry['description']}[/dim]")
|
|
583
|
+
|
|
584
|
+
console.print()
|
|
585
|
+
|
|
586
|
+
choice = click.prompt(
|
|
587
|
+
f"Select file (1-{len(entries)})",
|
|
588
|
+
type=click.IntRange(1, len(entries)),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
selected = entries[choice - 1]
|
|
592
|
+
_open_config_file(selected, editor, create_missing)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _open_config_file(entry: dict, editor: str, create_missing: bool):
|
|
596
|
+
"""Open a single config file in the user's editor."""
|
|
597
|
+
import os
|
|
598
|
+
import subprocess
|
|
599
|
+
from tweek.config.templates import deploy_template
|
|
600
|
+
|
|
601
|
+
target = entry["resolved_path"]
|
|
602
|
+
|
|
603
|
+
if not entry["editable"]:
|
|
604
|
+
pager = os.environ.get("PAGER", "less")
|
|
605
|
+
console.print(f"[white]Opening read-only reference: {target}[/white]")
|
|
606
|
+
subprocess.run([pager, str(target)])
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
if not target.exists():
|
|
610
|
+
if create_missing or click.confirm(
|
|
611
|
+
f" {entry['name']} does not exist. Create from template?", default=True
|
|
612
|
+
):
|
|
613
|
+
if entry.get("template"):
|
|
614
|
+
deploy_template(entry["template"], target)
|
|
615
|
+
console.print(f"[green]\u2713[/green] Created {target} from template")
|
|
616
|
+
else:
|
|
617
|
+
console.print(f"[yellow]No template available for {entry['name']}[/yellow]")
|
|
618
|
+
return
|
|
619
|
+
else:
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
console.print(f"[white]Opening: {target}[/white]")
|
|
623
|
+
subprocess.run([editor, str(target)])
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@config.command("show-defaults")
|
|
627
|
+
def config_show_defaults():
|
|
628
|
+
"""Display the bundled default configuration.
|
|
629
|
+
|
|
630
|
+
Shows all available options with their default values from tiers.yaml.
|
|
631
|
+
This is read-only — to override, edit ~/.tweek/config.yaml.
|
|
632
|
+
"""
|
|
633
|
+
import os
|
|
634
|
+
import subprocess
|
|
635
|
+
from pathlib import Path
|
|
636
|
+
|
|
637
|
+
defaults_path = Path(__file__).resolve().parent / "config" / "tiers.yaml"
|
|
638
|
+
if not defaults_path.exists():
|
|
639
|
+
console.print("[red]Default configuration not found[/red]")
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
pager = os.environ.get("PAGER", "less")
|
|
643
|
+
subprocess.run([pager, str(defaults_path)])
|