tweek 0.3.1__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 -6605
- 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/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +17 -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.1.dist-info → tweek-0.4.0.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.0.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")
|