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.
- 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 +170 -74
- 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.1.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/cli_logs.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek CLI — Logs command group.
|
|
4
|
+
|
|
5
|
+
Extracted from cli.py to keep the main CLI module manageable.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from tweek.cli_helpers import console, TWEEK_BANNER
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================
|
|
21
|
+
# LOGS COMMANDS
|
|
22
|
+
# ============================================================
|
|
23
|
+
|
|
24
|
+
@click.group()
|
|
25
|
+
def logs():
|
|
26
|
+
"""View and manage security logs."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@logs.command("show",
|
|
31
|
+
epilog="""\b
|
|
32
|
+
Examples:
|
|
33
|
+
tweek logs show Show last 20 security events
|
|
34
|
+
tweek logs show -n 50 Show last 50 events
|
|
35
|
+
tweek logs show --type block Filter by event type
|
|
36
|
+
tweek logs show --blocked Show only blocked/flagged events
|
|
37
|
+
tweek logs show --stats Show security statistics summary
|
|
38
|
+
tweek logs show --stats --days 30 Statistics for the last 30 days
|
|
39
|
+
"""
|
|
40
|
+
)
|
|
41
|
+
@click.option("--limit", "-n", default=20, help="Number of events to show")
|
|
42
|
+
@click.option("--type", "-t", "event_type", help="Filter by event type")
|
|
43
|
+
@click.option("--tool", help="Filter by tool name")
|
|
44
|
+
@click.option("--blocked", is_flag=True, help="Show only blocked/flagged events")
|
|
45
|
+
@click.option("--stats", is_flag=True, help="Show security statistics instead of events")
|
|
46
|
+
@click.option("--days", "-d", default=7, help="Number of days to analyze (with --stats)")
|
|
47
|
+
def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool, days: int):
|
|
48
|
+
"""Show recent security events."""
|
|
49
|
+
from tweek.logging.security_log import get_logger
|
|
50
|
+
|
|
51
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
52
|
+
|
|
53
|
+
logger = get_logger()
|
|
54
|
+
|
|
55
|
+
# Handle stats mode
|
|
56
|
+
if stats:
|
|
57
|
+
stat_data = logger.get_stats(days=days)
|
|
58
|
+
|
|
59
|
+
console.print(Panel.fit(
|
|
60
|
+
f"[cyan]Period:[/cyan] Last {days} days\n"
|
|
61
|
+
f"[cyan]Total Events:[/cyan] {stat_data['total_events']}",
|
|
62
|
+
title="Security Statistics"
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
# Decisions breakdown
|
|
66
|
+
if stat_data['by_decision']:
|
|
67
|
+
table = Table(title="Decisions")
|
|
68
|
+
table.add_column("Decision", style="cyan")
|
|
69
|
+
table.add_column("Count", justify="right")
|
|
70
|
+
|
|
71
|
+
decision_styles = {"allow": "green", "block": "red", "ask": "yellow", "deny": "red"}
|
|
72
|
+
for decision, count in stat_data['by_decision'].items():
|
|
73
|
+
style = decision_styles.get(decision, "white")
|
|
74
|
+
table.add_row(f"[{style}]{decision}[/{style}]", str(count))
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
console.print()
|
|
78
|
+
|
|
79
|
+
# Top triggered patterns
|
|
80
|
+
if stat_data['top_patterns']:
|
|
81
|
+
table = Table(title="Top Triggered Patterns")
|
|
82
|
+
table.add_column("Pattern", style="cyan")
|
|
83
|
+
table.add_column("Severity")
|
|
84
|
+
table.add_column("Count", justify="right")
|
|
85
|
+
|
|
86
|
+
severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "white"}
|
|
87
|
+
for pattern in stat_data['top_patterns']:
|
|
88
|
+
sev = pattern['severity'] or "unknown"
|
|
89
|
+
style = severity_styles.get(sev, "white")
|
|
90
|
+
table.add_row(
|
|
91
|
+
pattern['name'] or "unknown",
|
|
92
|
+
f"[{style}]{sev}[/{style}]",
|
|
93
|
+
str(pattern['count'])
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
console.print(table)
|
|
97
|
+
console.print()
|
|
98
|
+
|
|
99
|
+
# By tool
|
|
100
|
+
if stat_data['by_tool']:
|
|
101
|
+
table = Table(title="Events by Tool")
|
|
102
|
+
table.add_column("Tool", style="green")
|
|
103
|
+
table.add_column("Count", justify="right")
|
|
104
|
+
|
|
105
|
+
for tool_name, count in stat_data['by_tool'].items():
|
|
106
|
+
table.add_row(tool_name, str(count))
|
|
107
|
+
|
|
108
|
+
console.print(table)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
from tweek.logging.security_log import EventType
|
|
112
|
+
|
|
113
|
+
if blocked:
|
|
114
|
+
events = logger.get_blocked_commands(limit=limit)
|
|
115
|
+
title = "Recent Blocked/Flagged Commands"
|
|
116
|
+
else:
|
|
117
|
+
et = None
|
|
118
|
+
if event_type:
|
|
119
|
+
try:
|
|
120
|
+
et = EventType(event_type)
|
|
121
|
+
except ValueError:
|
|
122
|
+
console.print(f"[red]Unknown event type: {event_type}[/red]")
|
|
123
|
+
console.print(f"[white]Valid types: {', '.join(e.value for e in EventType)}[/white]")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
events = logger.get_recent_events(limit=limit, event_type=et, tool_name=tool)
|
|
127
|
+
title = "Recent Security Events"
|
|
128
|
+
|
|
129
|
+
if not events:
|
|
130
|
+
console.print("[yellow]No events found[/yellow]")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
table = Table(title=title)
|
|
134
|
+
table.add_column("Time", style="white")
|
|
135
|
+
table.add_column("Type", style="cyan")
|
|
136
|
+
table.add_column("Tool", style="green")
|
|
137
|
+
table.add_column("Tier")
|
|
138
|
+
table.add_column("Decision")
|
|
139
|
+
table.add_column("Pattern/Reason", max_width=30)
|
|
140
|
+
|
|
141
|
+
decision_styles = {
|
|
142
|
+
"allow": "green",
|
|
143
|
+
"block": "red",
|
|
144
|
+
"ask": "yellow",
|
|
145
|
+
"deny": "red",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for event in events:
|
|
149
|
+
timestamp = event.get("timestamp", "")
|
|
150
|
+
if timestamp:
|
|
151
|
+
# Format timestamp nicely
|
|
152
|
+
try:
|
|
153
|
+
dt = datetime.fromisoformat(timestamp)
|
|
154
|
+
timestamp = dt.strftime("%m/%d %H:%M:%S")
|
|
155
|
+
except (ValueError, TypeError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
decision = event.get("decision", "")
|
|
159
|
+
decision_style = decision_styles.get(decision, "white")
|
|
160
|
+
|
|
161
|
+
reason = event.get("pattern_name") or event.get("decision_reason", "")
|
|
162
|
+
if len(str(reason)) > 30:
|
|
163
|
+
reason = str(reason)[:27] + "..."
|
|
164
|
+
|
|
165
|
+
table.add_row(
|
|
166
|
+
timestamp,
|
|
167
|
+
event.get("event_type", ""),
|
|
168
|
+
event.get("tool_name", ""),
|
|
169
|
+
event.get("tier", ""),
|
|
170
|
+
f"[{decision_style}]{decision}[/{decision_style}]" if decision else "",
|
|
171
|
+
str(reason)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
console.print(table)
|
|
175
|
+
console.print(f"\n[white]Showing {len(events)} events. Use --limit to see more.[/white]")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@logs.command("export",
|
|
179
|
+
epilog="""\b
|
|
180
|
+
Examples:
|
|
181
|
+
tweek logs export Export all logs to tweek_security_log.csv
|
|
182
|
+
tweek logs export --days 7 Export only the last 7 days
|
|
183
|
+
tweek logs export -o audit.csv Export to a custom file path
|
|
184
|
+
tweek logs export --days 30 -o monthly.csv Last 30 days to custom file
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
@click.option("--days", "-d", type=int, help="Limit to last N days")
|
|
188
|
+
@click.option("--output", "-o", default="tweek_security_log.csv", help="Output file path")
|
|
189
|
+
def logs_export(days: int, output: str):
|
|
190
|
+
"""Export security logs to CSV."""
|
|
191
|
+
from tweek.logging.security_log import get_logger
|
|
192
|
+
|
|
193
|
+
logger = get_logger()
|
|
194
|
+
output_path = Path(output)
|
|
195
|
+
|
|
196
|
+
count = logger.export_csv(output_path, days=days)
|
|
197
|
+
|
|
198
|
+
if count > 0:
|
|
199
|
+
console.print(f"[green]\u2713[/green] Exported {count} events to {output_path}")
|
|
200
|
+
else:
|
|
201
|
+
console.print("[yellow]No events to export[/yellow]")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@logs.command("clear",
|
|
205
|
+
epilog="""\b
|
|
206
|
+
Examples:
|
|
207
|
+
tweek logs clear Clear all security logs (with prompt)
|
|
208
|
+
tweek logs clear --days 30 Clear logs older than 30 days
|
|
209
|
+
tweek logs clear --confirm Clear all logs without confirmation
|
|
210
|
+
"""
|
|
211
|
+
)
|
|
212
|
+
@click.option("--days", "-d", type=int, help="Clear events older than N days")
|
|
213
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
214
|
+
def logs_clear(days: int, confirm: bool):
|
|
215
|
+
"""Clear security logs."""
|
|
216
|
+
from tweek.logging.security_log import get_logger
|
|
217
|
+
|
|
218
|
+
if not confirm:
|
|
219
|
+
if days:
|
|
220
|
+
msg = f"Clear all events older than {days} days?"
|
|
221
|
+
else:
|
|
222
|
+
msg = "Clear ALL security logs?"
|
|
223
|
+
|
|
224
|
+
console.print(f"[yellow]{msg}[/yellow] ", end="")
|
|
225
|
+
if not click.confirm(""):
|
|
226
|
+
console.print("[white]Cancelled[/white]")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
logger = get_logger()
|
|
230
|
+
deleted = logger.delete_events(days=days)
|
|
231
|
+
|
|
232
|
+
if deleted > 0:
|
|
233
|
+
if days:
|
|
234
|
+
console.print(f"[green]Cleared {deleted} event(s) older than {days} days[/green]")
|
|
235
|
+
else:
|
|
236
|
+
console.print(f"[green]Cleared {deleted} event(s)[/green]")
|
|
237
|
+
else:
|
|
238
|
+
console.print("[white]No events to clear[/white]")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@logs.command("bundle",
|
|
242
|
+
epilog="""\b
|
|
243
|
+
Examples:
|
|
244
|
+
tweek logs bundle Create diagnostic bundle
|
|
245
|
+
tweek logs bundle -o /tmp/diag.zip Specify output path
|
|
246
|
+
tweek logs bundle --days 7 Only last 7 days of events
|
|
247
|
+
tweek logs bundle --dry-run Show what would be collected
|
|
248
|
+
"""
|
|
249
|
+
)
|
|
250
|
+
@click.option("--output", "-o", type=click.Path(), help="Output zip file path")
|
|
251
|
+
@click.option("--days", "-d", type=int, help="Only include events from last N days")
|
|
252
|
+
@click.option("--no-redact", is_flag=True, help="Skip redaction (for internal debugging)")
|
|
253
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be collected")
|
|
254
|
+
def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
|
|
255
|
+
"""Create a diagnostic bundle for support.
|
|
256
|
+
|
|
257
|
+
Collects security logs, configs (redacted), system info, and
|
|
258
|
+
doctor output into a zip file suitable for sending to Tweek support.
|
|
259
|
+
|
|
260
|
+
Sensitive data (API keys, passwords, tokens) is automatically
|
|
261
|
+
redacted before inclusion.
|
|
262
|
+
"""
|
|
263
|
+
from tweek.logging.bundle import BundleCollector
|
|
264
|
+
|
|
265
|
+
collector = BundleCollector(redact=not no_redact, days=days)
|
|
266
|
+
|
|
267
|
+
if dry_run:
|
|
268
|
+
report = collector.get_dry_run_report()
|
|
269
|
+
console.print("[bold]Diagnostic Bundle - Dry Run[/bold]\n")
|
|
270
|
+
for item in report:
|
|
271
|
+
status = item.get("status", "unknown")
|
|
272
|
+
name = item.get("file", "?")
|
|
273
|
+
size = item.get("size")
|
|
274
|
+
size_str = f" ({size:,} bytes)" if size else ""
|
|
275
|
+
if "not found" in status:
|
|
276
|
+
console.print(f" [white] SKIP {name} ({status})[/white]")
|
|
277
|
+
else:
|
|
278
|
+
console.print(f" [green] ADD {name}{size_str}[/green]")
|
|
279
|
+
console.print()
|
|
280
|
+
console.print("[white]No files will be collected in dry-run mode.[/white]")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# Determine output path
|
|
284
|
+
if not output:
|
|
285
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
286
|
+
output = f"tweek_diagnostic_bundle_{ts}.zip"
|
|
287
|
+
|
|
288
|
+
output_path = Path(output)
|
|
289
|
+
|
|
290
|
+
console.print("[bold]Creating diagnostic bundle...[/bold]")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
result = collector.create_bundle(output_path)
|
|
294
|
+
size = result.stat().st_size
|
|
295
|
+
console.print(f"\n[green]Bundle created: {result}[/green]")
|
|
296
|
+
console.print(f"[white]Size: {size:,} bytes[/white]")
|
|
297
|
+
if not no_redact:
|
|
298
|
+
console.print("[white]Sensitive data has been redacted.[/white]")
|
|
299
|
+
console.print(f"\n[bold]Send this file to Tweek support for analysis.[/bold]")
|
|
300
|
+
except Exception as e:
|
|
301
|
+
console.print(f"[red]Failed to create bundle: {e}[/red]")
|
tweek/cli_mcp.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""MCP Proxy CLI commands extracted from tweek.cli.
|
|
2
|
+
|
|
3
|
+
Provides the ``mcp`` Click group with subcommands: proxy, approve, decide.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from tweek.cli_helpers import console
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# MCP GATEWAY COMMANDS
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def mcp():
|
|
19
|
+
"""MCP Security Proxy for desktop LLM applications.
|
|
20
|
+
|
|
21
|
+
Provides security-screened MCP proxy with built-in vault and status tools.
|
|
22
|
+
Supports Claude Desktop, ChatGPT Desktop, and Gemini CLI.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@mcp.command("proxy",
|
|
27
|
+
epilog="""\b
|
|
28
|
+
Examples:
|
|
29
|
+
tweek mcp proxy Start MCP proxy on stdio transport
|
|
30
|
+
"""
|
|
31
|
+
)
|
|
32
|
+
def mcp_proxy():
|
|
33
|
+
"""Start MCP proxy server (stdio transport).
|
|
34
|
+
|
|
35
|
+
Connects to upstream MCP servers configured in config.yaml,
|
|
36
|
+
screens all tool calls through Tweek's security pipeline,
|
|
37
|
+
and queues flagged operations for human approval.
|
|
38
|
+
|
|
39
|
+
Configure upstreams in ~/.tweek/config.yaml:
|
|
40
|
+
mcp:
|
|
41
|
+
proxy:
|
|
42
|
+
upstreams:
|
|
43
|
+
filesystem:
|
|
44
|
+
command: "npx"
|
|
45
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
|
|
46
|
+
|
|
47
|
+
Example Claude Desktop config:
|
|
48
|
+
{"mcpServers": {"tweek-proxy": {"command": "tweek", "args": ["mcp", "proxy"]}}}
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from tweek.mcp.proxy import run_proxy, MCP_AVAILABLE
|
|
52
|
+
|
|
53
|
+
if not MCP_AVAILABLE:
|
|
54
|
+
console.print("[red]MCP SDK not installed.[/red]")
|
|
55
|
+
console.print("Install with: pip install 'tweek[mcp]' or pip install mcp")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Load config
|
|
59
|
+
try:
|
|
60
|
+
from tweek.config.manager import ConfigManager
|
|
61
|
+
cfg = ConfigManager()
|
|
62
|
+
config = cfg.get_full_config()
|
|
63
|
+
except Exception:
|
|
64
|
+
config = {}
|
|
65
|
+
|
|
66
|
+
asyncio.run(run_proxy(config=config))
|
|
67
|
+
|
|
68
|
+
except KeyboardInterrupt:
|
|
69
|
+
pass
|
|
70
|
+
except Exception as e:
|
|
71
|
+
console.print(f"[red]MCP proxy error: {e}[/red]")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.command("approve",
|
|
75
|
+
epilog="""\b
|
|
76
|
+
Examples:
|
|
77
|
+
tweek mcp approve Start approval daemon (interactive)
|
|
78
|
+
tweek mcp approve --list List pending requests and exit
|
|
79
|
+
tweek mcp approve -p 5 Poll every 5 seconds
|
|
80
|
+
"""
|
|
81
|
+
)
|
|
82
|
+
@click.option("--poll-interval", "-p", default=2.0, type=float,
|
|
83
|
+
help="Seconds between polls for new requests")
|
|
84
|
+
@click.option("--list", "list_pending", is_flag=True, help="List pending requests and exit")
|
|
85
|
+
def mcp_approve(poll_interval, list_pending):
|
|
86
|
+
"""Start the approval daemon for MCP proxy requests.
|
|
87
|
+
|
|
88
|
+
Shows pending requests and allows approve/deny decisions.
|
|
89
|
+
Press Ctrl+C to exit.
|
|
90
|
+
|
|
91
|
+
Run this in a separate terminal while 'tweek mcp proxy' is serving.
|
|
92
|
+
Use --list to show pending requests without starting the daemon.
|
|
93
|
+
"""
|
|
94
|
+
if list_pending:
|
|
95
|
+
try:
|
|
96
|
+
from tweek.mcp.approval import ApprovalQueue
|
|
97
|
+
from tweek.mcp.approval_cli import display_pending
|
|
98
|
+
queue = ApprovalQueue()
|
|
99
|
+
display_pending(queue)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
from tweek.mcp.approval import ApprovalQueue
|
|
106
|
+
from tweek.mcp.approval_cli import run_approval_daemon
|
|
107
|
+
|
|
108
|
+
queue = ApprovalQueue()
|
|
109
|
+
run_approval_daemon(queue, poll_interval=poll_interval)
|
|
110
|
+
|
|
111
|
+
except KeyboardInterrupt:
|
|
112
|
+
pass
|
|
113
|
+
except Exception as e:
|
|
114
|
+
console.print(f"[red]Approval daemon error: {e}[/red]")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp.command("decide",
|
|
118
|
+
epilog="""\b
|
|
119
|
+
Examples:
|
|
120
|
+
tweek mcp decide abc12345 approve Approve a request
|
|
121
|
+
tweek mcp decide abc12345 deny Deny a request
|
|
122
|
+
tweek mcp decide abc12345 deny -n "Not authorized" Deny with notes
|
|
123
|
+
"""
|
|
124
|
+
)
|
|
125
|
+
@click.argument("request_id")
|
|
126
|
+
@click.argument("decision", type=click.Choice(["approve", "deny"]))
|
|
127
|
+
@click.option("--notes", "-n", help="Decision notes")
|
|
128
|
+
def mcp_decide(request_id, decision, notes):
|
|
129
|
+
"""Approve or deny a specific approval request.
|
|
130
|
+
|
|
131
|
+
REQUEST_ID can be the full UUID or the first 8 characters.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
from tweek.mcp.approval import ApprovalQueue
|
|
135
|
+
from tweek.mcp.approval_cli import decide_request
|
|
136
|
+
|
|
137
|
+
queue = ApprovalQueue()
|
|
138
|
+
success = decide_request(queue, request_id, decision, notes=notes)
|
|
139
|
+
|
|
140
|
+
if success:
|
|
141
|
+
verb = "Approved" if decision == "approve" else "Denied"
|
|
142
|
+
style = "green" if decision == "approve" else "red"
|
|
143
|
+
console.print(f"[{style}]{verb} request {request_id}[/{style}]")
|
|
144
|
+
else:
|
|
145
|
+
console.print(f"[yellow]Could not {decision} request {request_id}[/yellow]")
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
console.print(f"[red]Error: {e}[/red]")
|