tweek 0.3.1__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +170 -74
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
  61. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/cli_memory.py ADDED
@@ -0,0 +1,343 @@
1
+ """CLI commands for agentic memory management.
2
+
3
+ Extracted from cli.py to keep the main CLI module manageable.
4
+ Groups:
5
+ memory_group -- status, patterns, sources, suggestions, accept,
6
+ reject, baseline, audit, clear, export, decay
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import click
14
+ from rich.table import Table
15
+
16
+ from tweek.cli_helpers import console
17
+
18
+ # =========================================================================
19
+ # Memory commands
20
+ # =========================================================================
21
+
22
+
23
+ @click.group("memory")
24
+ def memory_group():
25
+ """Agentic memory management.
26
+
27
+ View and manage Tweek's learned security decisions, source trust scores,
28
+ workflow baselines, and whitelist suggestions.
29
+ """
30
+ pass
31
+
32
+
33
+ @memory_group.command("status")
34
+ def memory_status():
35
+ """Show overall memory statistics."""
36
+ from tweek.memory.store import get_memory_store
37
+
38
+ store = get_memory_store()
39
+ stats = store.get_stats()
40
+
41
+ table = Table(title="Tweek Memory Status")
42
+ table.add_column("Table", style="bold")
43
+ table.add_column("Entries", justify="right")
44
+
45
+ for table_name in ("pattern_decisions", "source_trust", "workflow_baselines",
46
+ "learned_whitelists", "memory_audit"):
47
+ table.add_row(table_name, str(stats.get(table_name, 0)))
48
+
49
+ console.print(table)
50
+ console.print()
51
+
52
+ last_decay = stats.get("last_decay")
53
+ if last_decay:
54
+ console.print(f" Last decay: {last_decay}")
55
+ else:
56
+ console.print(" Last decay: [white]never[/white]")
57
+
58
+ db_size = stats.get("db_size_bytes", 0)
59
+ if db_size > 1024 * 1024:
60
+ console.print(f" DB size: {db_size / (1024*1024):.1f} MB")
61
+ elif db_size > 1024:
62
+ console.print(f" DB size: {db_size / 1024:.1f} KB")
63
+ else:
64
+ console.print(f" DB size: {db_size} bytes")
65
+
66
+
67
+ @memory_group.command("patterns")
68
+ @click.option("--min-decisions", default=0, type=int, help="Minimum decisions to show")
69
+ @click.option("--sort", "sort_by", default="count",
70
+ type=click.Choice(["count", "approval", "name"]),
71
+ help="Sort order")
72
+ def memory_patterns(min_decisions: int, sort_by: str):
73
+ """Show per-pattern confidence adjustments."""
74
+ from tweek.memory.store import get_memory_store
75
+
76
+ store = get_memory_store()
77
+ patterns = store.get_pattern_stats(min_decisions=min_decisions, sort_by=sort_by)
78
+
79
+ if not patterns:
80
+ console.print("[white]No pattern decision data recorded yet.[/white]")
81
+ return
82
+
83
+ table = Table(title="Pattern Decision History")
84
+ table.add_column("Pattern", style="bold")
85
+ table.add_column("Path Prefix", max_width=30)
86
+ table.add_column("Decisions", justify="right")
87
+ table.add_column("Approvals", justify="right")
88
+ table.add_column("Denials", justify="right")
89
+ table.add_column("Approval %", justify="right")
90
+ table.add_column("Last", max_width=19)
91
+
92
+ for p in patterns:
93
+ ratio = p.get("approval_ratio", 0)
94
+ ratio_style = "green" if ratio >= 0.9 else ("yellow" if ratio >= 0.5 else "red")
95
+ table.add_row(
96
+ p.get("pattern_name", "?"),
97
+ p.get("path_prefix") or "[white]-[/white]",
98
+ str(p.get("total_decisions", 0)),
99
+ f"{p.get('weighted_approvals', 0):.1f}",
100
+ f"{p.get('weighted_denials', 0):.1f}",
101
+ f"[{ratio_style}]{ratio:.0%}[/{ratio_style}]",
102
+ (p.get("last_decision") or "")[:19],
103
+ )
104
+
105
+ console.print(table)
106
+
107
+
108
+ @memory_group.command("sources")
109
+ @click.option("--suspicious", is_flag=True, help="Show only sources with trust < 0.5")
110
+ def memory_sources(suspicious: bool):
111
+ """Show source trustworthiness scores."""
112
+ from tweek.memory.store import get_memory_store
113
+
114
+ store = get_memory_store()
115
+ sources = store.get_all_sources(suspicious_only=suspicious)
116
+
117
+ if not sources:
118
+ console.print("[white]No source trust data recorded yet.[/white]")
119
+ return
120
+
121
+ table = Table(title="Source Trust Scores")
122
+ table.add_column("Type", style="bold")
123
+ table.add_column("Source", max_width=60)
124
+ table.add_column("Scans", justify="right")
125
+ table.add_column("Injections", justify="right")
126
+ table.add_column("Trust", justify="right")
127
+ table.add_column("Last Injection", max_width=19)
128
+
129
+ for s in sources:
130
+ trust_style = "green" if s.trust_score >= 0.8 else ("yellow" if s.trust_score >= 0.5 else "red")
131
+ table.add_row(
132
+ s.source_type,
133
+ s.source_key[:60],
134
+ str(s.total_scans),
135
+ str(s.injection_detections),
136
+ f"[{trust_style}]{s.trust_score:.2f}[/{trust_style}]",
137
+ (s.last_injection or "")[:19],
138
+ )
139
+
140
+ console.print(table)
141
+
142
+
143
+ @memory_group.command("suggestions")
144
+ @click.option("--all", "show_all", is_flag=True, help="Show all suggestions (including reviewed)")
145
+ def memory_suggestions(show_all: bool):
146
+ """Show learned whitelist suggestions pending review."""
147
+ from tweek.memory.store import get_memory_store
148
+
149
+ store = get_memory_store()
150
+ suggestions = store.get_whitelist_suggestions(pending_only=not show_all)
151
+
152
+ if not suggestions:
153
+ console.print("[white]No whitelist suggestions available.[/white]")
154
+ return
155
+
156
+ table = Table(title="Learned Whitelist Suggestions")
157
+ table.add_column("ID", justify="right")
158
+ table.add_column("Pattern", style="bold")
159
+ table.add_column("Tool")
160
+ table.add_column("Path Prefix", max_width=30)
161
+ table.add_column("Approvals", justify="right")
162
+ table.add_column("Denials", justify="right")
163
+ table.add_column("Confidence", justify="right")
164
+ table.add_column("Status")
165
+
166
+ for s in suggestions:
167
+ status = {0: "[yellow]pending[/yellow]", 1: "[green]accepted[/green]", -1: "[red]rejected[/red]"}
168
+ table.add_row(
169
+ str(s.id),
170
+ s.pattern_name,
171
+ s.tool_name or "[white]-[/white]",
172
+ s.path_prefix or "[white]-[/white]",
173
+ str(s.approval_count),
174
+ str(s.denial_count),
175
+ f"{s.confidence:.0%}",
176
+ status.get(s.human_reviewed, "?"),
177
+ )
178
+
179
+ console.print(table)
180
+
181
+
182
+ @memory_group.command("accept")
183
+ @click.argument("suggestion_id", type=int)
184
+ def memory_accept(suggestion_id: int):
185
+ """Accept a whitelist suggestion."""
186
+ from tweek.memory.store import get_memory_store
187
+
188
+ store = get_memory_store()
189
+ if store.review_whitelist_suggestion(suggestion_id, accepted=True):
190
+ console.print(f"[bold green]Accepted[/bold green] suggestion #{suggestion_id}")
191
+ console.print(" [white]Note: To apply to overrides.yaml, manually add the whitelist rule.[/white]")
192
+ else:
193
+ console.print(f"[red]Suggestion #{suggestion_id} not found.[/red]")
194
+
195
+
196
+ @memory_group.command("reject")
197
+ @click.argument("suggestion_id", type=int)
198
+ def memory_reject(suggestion_id: int):
199
+ """Reject a whitelist suggestion."""
200
+ from tweek.memory.store import get_memory_store
201
+
202
+ store = get_memory_store()
203
+ if store.review_whitelist_suggestion(suggestion_id, accepted=False):
204
+ console.print(f"[bold]Rejected[/bold] suggestion #{suggestion_id}")
205
+ else:
206
+ console.print(f"[red]Suggestion #{suggestion_id} not found.[/red]")
207
+
208
+
209
+ @memory_group.command("baseline")
210
+ @click.option("--project-hash", default=None, help="Override project hash (default: auto-detect from cwd)")
211
+ def memory_baseline(project_hash: Optional[str]):
212
+ """Show workflow baseline for current project."""
213
+ from tweek.memory.store import get_memory_store, hash_project
214
+
215
+ if not project_hash:
216
+ project_hash = hash_project(str(Path.cwd()))
217
+
218
+ store = get_memory_store()
219
+ baselines = store.get_workflow_baseline(project_hash)
220
+
221
+ if not baselines:
222
+ console.print("[white]No workflow baseline data for this project.[/white]")
223
+ return
224
+
225
+ table = Table(title=f"Workflow Baseline (project: {project_hash[:8]}...)")
226
+ table.add_column("Tool", style="bold")
227
+ table.add_column("Hour", justify="right")
228
+ table.add_column("Invocations", justify="right")
229
+ table.add_column("Denied", justify="right")
230
+ table.add_column("Denial %", justify="right")
231
+
232
+ for b in baselines:
233
+ total = b.invocation_count or 1
234
+ denial_pct = b.denied_count / total
235
+ pct_style = "green" if denial_pct < 0.1 else ("yellow" if denial_pct < 0.3 else "red")
236
+ table.add_row(
237
+ b.tool_name,
238
+ str(b.hour_of_day) if b.hour_of_day is not None else "[white]-[/white]",
239
+ str(b.invocation_count),
240
+ str(b.denied_count),
241
+ f"[{pct_style}]{denial_pct:.0%}[/{pct_style}]",
242
+ )
243
+
244
+ console.print(table)
245
+
246
+
247
+ @memory_group.command("audit")
248
+ @click.option("--limit", default=50, type=int, help="Number of entries to show")
249
+ def memory_audit(limit: int):
250
+ """Show memory operation audit log."""
251
+ from tweek.memory.store import get_memory_store
252
+
253
+ store = get_memory_store()
254
+ entries = store.get_audit_log(limit=limit)
255
+
256
+ if not entries:
257
+ console.print("[white]No audit entries.[/white]")
258
+ return
259
+
260
+ table = Table(title=f"Memory Audit Log (last {limit})")
261
+ table.add_column("Timestamp", max_width=19)
262
+ table.add_column("Op", style="bold")
263
+ table.add_column("Table")
264
+ table.add_column("Key", max_width=30)
265
+ table.add_column("Result", max_width=50)
266
+
267
+ for e in entries:
268
+ table.add_row(
269
+ (e.get("timestamp") or "")[:19],
270
+ e.get("operation", "?"),
271
+ e.get("table_name", "?"),
272
+ (e.get("key_info") or "")[:30],
273
+ (e.get("result") or "")[:50],
274
+ )
275
+
276
+ console.print(table)
277
+
278
+
279
+ @memory_group.command("clear")
280
+ @click.option("--table", "table_name", default=None,
281
+ type=click.Choice(["patterns", "sources", "baselines", "whitelists", "audit", "all"]),
282
+ help="Table to clear (default: all)")
283
+ @click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
284
+ def memory_clear(table_name: Optional[str], confirm: bool):
285
+ """Clear memory data."""
286
+ from tweek.memory.store import get_memory_store
287
+
288
+ table_map = {
289
+ "patterns": "pattern_decisions",
290
+ "sources": "source_trust",
291
+ "baselines": "workflow_baselines",
292
+ "whitelists": "learned_whitelists",
293
+ "audit": "memory_audit",
294
+ }
295
+
296
+ if not confirm:
297
+ target = table_name or "ALL"
298
+ if not click.confirm(f"Clear {target} memory data? This cannot be undone"):
299
+ console.print("[white]Cancelled.[/white]")
300
+ return
301
+
302
+ store = get_memory_store()
303
+
304
+ if table_name and table_name != "all":
305
+ actual_name = table_map.get(table_name, table_name)
306
+ count = store.clear_table(actual_name)
307
+ console.print(f"Cleared {count} entries from {actual_name}")
308
+ else:
309
+ results = store.clear_all()
310
+ for tbl, count in results.items():
311
+ console.print(f" {tbl}: {count} entries cleared")
312
+ console.print("[bold]All memory data cleared.[/bold]")
313
+
314
+
315
+ @memory_group.command("export")
316
+ @click.option("--output", "-o", default=None, help="Output file path (default: stdout)")
317
+ def memory_export(output: Optional[str]):
318
+ """Export all memory data to JSON."""
319
+ from tweek.memory.store import get_memory_store
320
+
321
+ store = get_memory_store()
322
+ data = store.export_all()
323
+
324
+ json_str = json.dumps(data, indent=2, default=str)
325
+
326
+ if output:
327
+ Path(output).write_text(json_str)
328
+ console.print(f"Exported memory to {output}")
329
+ else:
330
+ click.echo(json_str)
331
+
332
+
333
+ @memory_group.command("decay")
334
+ def memory_decay():
335
+ """Manually trigger time decay on all memory entries."""
336
+ from tweek.memory.store import get_memory_store
337
+
338
+ store = get_memory_store()
339
+ results = store.apply_decay()
340
+
341
+ console.print("[bold]Decay applied:[/bold]")
342
+ for table, count in results.items():
343
+ console.print(f" {table}: {count} entries updated")