tweek 0.1.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 +16 -0
- tweek/cli.py +3390 -0
- tweek/cli_helpers.py +193 -0
- tweek/config/__init__.py +13 -0
- tweek/config/allowed_dirs.yaml +23 -0
- tweek/config/manager.py +1064 -0
- tweek/config/patterns.yaml +751 -0
- tweek/config/tiers.yaml +129 -0
- tweek/diagnostics.py +589 -0
- tweek/hooks/__init__.py +1 -0
- tweek/hooks/pre_tool_use.py +861 -0
- tweek/integrations/__init__.py +3 -0
- tweek/integrations/moltbot.py +243 -0
- tweek/licensing.py +398 -0
- tweek/logging/__init__.py +9 -0
- tweek/logging/bundle.py +350 -0
- tweek/logging/json_logger.py +150 -0
- tweek/logging/security_log.py +745 -0
- tweek/mcp/__init__.py +24 -0
- tweek/mcp/approval.py +456 -0
- tweek/mcp/approval_cli.py +356 -0
- tweek/mcp/clients/__init__.py +37 -0
- tweek/mcp/clients/chatgpt.py +112 -0
- tweek/mcp/clients/claude_desktop.py +203 -0
- tweek/mcp/clients/gemini.py +178 -0
- tweek/mcp/proxy.py +667 -0
- tweek/mcp/screening.py +175 -0
- tweek/mcp/server.py +317 -0
- tweek/platform/__init__.py +131 -0
- tweek/plugins/__init__.py +835 -0
- tweek/plugins/base.py +1080 -0
- tweek/plugins/compliance/__init__.py +30 -0
- tweek/plugins/compliance/gdpr.py +333 -0
- tweek/plugins/compliance/gov.py +324 -0
- tweek/plugins/compliance/hipaa.py +285 -0
- tweek/plugins/compliance/legal.py +322 -0
- tweek/plugins/compliance/pci.py +361 -0
- tweek/plugins/compliance/soc2.py +275 -0
- tweek/plugins/detectors/__init__.py +30 -0
- tweek/plugins/detectors/continue_dev.py +206 -0
- tweek/plugins/detectors/copilot.py +254 -0
- tweek/plugins/detectors/cursor.py +192 -0
- tweek/plugins/detectors/moltbot.py +205 -0
- tweek/plugins/detectors/windsurf.py +214 -0
- tweek/plugins/git_discovery.py +395 -0
- tweek/plugins/git_installer.py +491 -0
- tweek/plugins/git_lockfile.py +338 -0
- tweek/plugins/git_registry.py +503 -0
- tweek/plugins/git_security.py +482 -0
- tweek/plugins/providers/__init__.py +30 -0
- tweek/plugins/providers/anthropic.py +181 -0
- tweek/plugins/providers/azure_openai.py +289 -0
- tweek/plugins/providers/bedrock.py +248 -0
- tweek/plugins/providers/google.py +197 -0
- tweek/plugins/providers/openai.py +230 -0
- tweek/plugins/scope.py +130 -0
- tweek/plugins/screening/__init__.py +26 -0
- tweek/plugins/screening/llm_reviewer.py +149 -0
- tweek/plugins/screening/pattern_matcher.py +273 -0
- tweek/plugins/screening/rate_limiter.py +174 -0
- tweek/plugins/screening/session_analyzer.py +159 -0
- tweek/proxy/__init__.py +302 -0
- tweek/proxy/addon.py +223 -0
- tweek/proxy/interceptor.py +313 -0
- tweek/proxy/server.py +315 -0
- tweek/sandbox/__init__.py +71 -0
- tweek/sandbox/executor.py +382 -0
- tweek/sandbox/linux.py +278 -0
- tweek/sandbox/profile_generator.py +323 -0
- tweek/screening/__init__.py +13 -0
- tweek/screening/context.py +81 -0
- tweek/security/__init__.py +22 -0
- tweek/security/llm_reviewer.py +348 -0
- tweek/security/rate_limiter.py +682 -0
- tweek/security/secret_scanner.py +506 -0
- tweek/security/session_analyzer.py +600 -0
- tweek/vault/__init__.py +40 -0
- tweek/vault/cross_platform.py +251 -0
- tweek/vault/keychain.py +288 -0
- tweek-0.1.0.dist-info/METADATA +335 -0
- tweek-0.1.0.dist-info/RECORD +85 -0
- tweek-0.1.0.dist-info/WHEEL +5 -0
- tweek-0.1.0.dist-info/entry_points.txt +25 -0
- tweek-0.1.0.dist-info/licenses/LICENSE +190 -0
- tweek-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek MCP Approval CLI Daemon
|
|
4
|
+
|
|
5
|
+
Interactive terminal interface for reviewing and deciding on
|
|
6
|
+
MCP proxy approval requests. Polls the approval queue and
|
|
7
|
+
displays pending requests for human review.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
tweek mcp approve # Start daemon (2s poll)
|
|
11
|
+
tweek mcp approve --poll-interval 5 # Slower polling
|
|
12
|
+
tweek mcp pending # One-shot listing
|
|
13
|
+
tweek mcp decide <ID> approve # One-shot decision
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from tweek.mcp.approval import ApprovalQueue, ApprovalRequest, ApprovalStatus
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def display_pending(queue: ApprovalQueue) -> int:
|
|
27
|
+
"""
|
|
28
|
+
Display all pending approval requests as a rich table.
|
|
29
|
+
|
|
30
|
+
Returns the number of pending requests displayed.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
from rich.console import Console
|
|
34
|
+
from rich.table import Table
|
|
35
|
+
except ImportError:
|
|
36
|
+
return _display_pending_plain(queue)
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
pending = queue.get_pending()
|
|
40
|
+
|
|
41
|
+
if not pending:
|
|
42
|
+
console.print("[dim]No pending approval requests.[/dim]")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
table = Table(title="Pending Approval Requests", show_lines=True)
|
|
46
|
+
table.add_column("ID", style="cyan", width=10)
|
|
47
|
+
table.add_column("Server", style="blue")
|
|
48
|
+
table.add_column("Tool", style="yellow")
|
|
49
|
+
table.add_column("Risk", width=10)
|
|
50
|
+
table.add_column("Reason")
|
|
51
|
+
table.add_column("Time Left", width=10)
|
|
52
|
+
|
|
53
|
+
for req in pending:
|
|
54
|
+
risk_style = _risk_style(req.risk_level)
|
|
55
|
+
remaining = req.time_remaining
|
|
56
|
+
time_str = f"{int(remaining)}s" if remaining > 0 else "[red]EXPIRED[/red]"
|
|
57
|
+
|
|
58
|
+
table.add_row(
|
|
59
|
+
req.short_id,
|
|
60
|
+
req.upstream_server,
|
|
61
|
+
req.tool_name,
|
|
62
|
+
f"[{risk_style}]{req.risk_level}[/{risk_style}]",
|
|
63
|
+
req.screening_reason[:60],
|
|
64
|
+
time_str,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
console.print(table)
|
|
68
|
+
return len(pending)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def display_request_detail(request: ApprovalRequest):
|
|
72
|
+
"""Display detailed information about a single request."""
|
|
73
|
+
try:
|
|
74
|
+
from rich.console import Console
|
|
75
|
+
from rich.panel import Panel
|
|
76
|
+
from rich.text import Text
|
|
77
|
+
except ImportError:
|
|
78
|
+
_display_request_plain(request)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
console = Console()
|
|
82
|
+
risk_style = _risk_style(request.risk_level)
|
|
83
|
+
|
|
84
|
+
lines = [
|
|
85
|
+
f"[bold]Request:[/bold] {request.short_id}",
|
|
86
|
+
f"[bold]Server:[/bold] {request.upstream_server}",
|
|
87
|
+
f"[bold]Tool:[/bold] {request.tool_name}",
|
|
88
|
+
f"[bold]Risk:[/bold] [{risk_style}]{request.risk_level}[/{risk_style}]",
|
|
89
|
+
f"[bold]Reason:[/bold] {request.screening_reason}",
|
|
90
|
+
"",
|
|
91
|
+
"[bold]Arguments:[/bold]",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Display arguments (already redacted from queue storage)
|
|
95
|
+
args = request.arguments
|
|
96
|
+
for key, value in args.items():
|
|
97
|
+
display_val = str(value)
|
|
98
|
+
if len(display_val) > 120:
|
|
99
|
+
display_val = display_val[:120] + "..."
|
|
100
|
+
lines.append(f" {key}: {display_val}")
|
|
101
|
+
|
|
102
|
+
# Display findings
|
|
103
|
+
findings = request.screening_findings
|
|
104
|
+
if findings:
|
|
105
|
+
lines.append("")
|
|
106
|
+
lines.append("[bold]Findings:[/bold]")
|
|
107
|
+
for f in findings[:5]:
|
|
108
|
+
name = f.get("name", f.get("pattern", "unknown"))
|
|
109
|
+
severity = f.get("severity", "")
|
|
110
|
+
lines.append(f" - {name} ({severity})")
|
|
111
|
+
|
|
112
|
+
remaining = request.time_remaining
|
|
113
|
+
if remaining > 0:
|
|
114
|
+
lines.append("")
|
|
115
|
+
lines.append(f"[dim]Auto-deny in {int(remaining)}s[/dim]")
|
|
116
|
+
|
|
117
|
+
panel = Panel(
|
|
118
|
+
"\n".join(lines),
|
|
119
|
+
title=f"Approval Request {request.short_id}",
|
|
120
|
+
border_style=risk_style,
|
|
121
|
+
)
|
|
122
|
+
console.print(panel)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_approval_daemon(
|
|
126
|
+
queue: ApprovalQueue,
|
|
127
|
+
poll_interval: float = 2.0,
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Run the interactive approval daemon.
|
|
131
|
+
|
|
132
|
+
Polls for pending requests, displays them, and prompts for decisions.
|
|
133
|
+
Runs until Ctrl+C.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
from rich.console import Console
|
|
137
|
+
except ImportError:
|
|
138
|
+
print("Error: rich library required for approval daemon", file=sys.stderr)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
console = Console()
|
|
142
|
+
console.print("[bold]Tweek MCP Approval Daemon[/bold]")
|
|
143
|
+
console.print(f"Polling every {poll_interval}s. Press Ctrl+C to exit.\n")
|
|
144
|
+
|
|
145
|
+
seen_ids = set()
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
while True:
|
|
149
|
+
pending = queue.get_pending()
|
|
150
|
+
|
|
151
|
+
# Show new requests
|
|
152
|
+
new_requests = [r for r in pending if r.id not in seen_ids]
|
|
153
|
+
|
|
154
|
+
for req in new_requests:
|
|
155
|
+
seen_ids.add(req.id)
|
|
156
|
+
console.print()
|
|
157
|
+
display_request_detail(req)
|
|
158
|
+
console.print()
|
|
159
|
+
|
|
160
|
+
# Prompt for decision
|
|
161
|
+
decision = _prompt_decision(console, req)
|
|
162
|
+
if decision == "quit":
|
|
163
|
+
console.print("[dim]Exiting approval daemon.[/dim]")
|
|
164
|
+
return
|
|
165
|
+
elif decision == "skip":
|
|
166
|
+
continue
|
|
167
|
+
elif decision == "approve":
|
|
168
|
+
success = queue.decide(
|
|
169
|
+
req.id, ApprovalStatus.APPROVED, decided_by="cli"
|
|
170
|
+
)
|
|
171
|
+
if success:
|
|
172
|
+
console.print(f"[green]Approved {req.short_id}[/green]")
|
|
173
|
+
_log_decision(req, "approved")
|
|
174
|
+
else:
|
|
175
|
+
console.print(
|
|
176
|
+
f"[yellow]Could not approve {req.short_id} "
|
|
177
|
+
f"(may have expired)[/yellow]"
|
|
178
|
+
)
|
|
179
|
+
elif decision == "deny":
|
|
180
|
+
notes = _prompt_notes(console)
|
|
181
|
+
success = queue.decide(
|
|
182
|
+
req.id,
|
|
183
|
+
ApprovalStatus.DENIED,
|
|
184
|
+
decided_by="cli",
|
|
185
|
+
notes=notes,
|
|
186
|
+
)
|
|
187
|
+
if success:
|
|
188
|
+
console.print(f"[red]Denied {req.short_id}[/red]")
|
|
189
|
+
_log_decision(req, "denied")
|
|
190
|
+
else:
|
|
191
|
+
console.print(
|
|
192
|
+
f"[yellow]Could not deny {req.short_id} "
|
|
193
|
+
f"(may have expired)[/yellow]"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not new_requests and pending:
|
|
197
|
+
# Still have pending but already seen them
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
time.sleep(poll_interval)
|
|
201
|
+
|
|
202
|
+
except KeyboardInterrupt:
|
|
203
|
+
console.print("\n[dim]Approval daemon stopped.[/dim]")
|
|
204
|
+
|
|
205
|
+
# Print summary
|
|
206
|
+
stats = queue.get_stats()
|
|
207
|
+
console.print("\n[bold]Session Summary:[/bold]")
|
|
208
|
+
for status, count in stats.items():
|
|
209
|
+
console.print(f" {status}: {count}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def decide_request(
|
|
213
|
+
queue: ApprovalQueue,
|
|
214
|
+
request_id: str,
|
|
215
|
+
decision: str,
|
|
216
|
+
notes: Optional[str] = None,
|
|
217
|
+
) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Make a one-shot decision on a specific request.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
queue: The approval queue
|
|
223
|
+
request_id: Full or short ID
|
|
224
|
+
decision: "approve" or "deny"
|
|
225
|
+
notes: Optional decision notes
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if decision was recorded successfully
|
|
229
|
+
"""
|
|
230
|
+
request = queue.get_request(request_id)
|
|
231
|
+
if request is None:
|
|
232
|
+
print(f"Request '{request_id}' not found.", file=sys.stderr)
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
if request.status != ApprovalStatus.PENDING:
|
|
236
|
+
print(
|
|
237
|
+
f"Request '{request_id}' is not pending (status: {request.status.value}).",
|
|
238
|
+
file=sys.stderr,
|
|
239
|
+
)
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
status = ApprovalStatus.APPROVED if decision == "approve" else ApprovalStatus.DENIED
|
|
243
|
+
success = queue.decide(request.id, status, decided_by="cli", notes=notes)
|
|
244
|
+
|
|
245
|
+
if success:
|
|
246
|
+
_log_decision(request, decision)
|
|
247
|
+
|
|
248
|
+
return success
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _prompt_decision(console, request: ApprovalRequest) -> str:
|
|
252
|
+
"""Prompt user for a decision. Returns 'approve', 'deny', 'skip', or 'quit'."""
|
|
253
|
+
while True:
|
|
254
|
+
try:
|
|
255
|
+
console.print(
|
|
256
|
+
"[bold][A][/bold]pprove "
|
|
257
|
+
"[bold][D][/bold]eny "
|
|
258
|
+
"[bold][S][/bold]kip "
|
|
259
|
+
"[bold][Q][/bold]uit"
|
|
260
|
+
)
|
|
261
|
+
choice = input(f"Decision for {request.short_id}: ").strip().lower()
|
|
262
|
+
except EOFError:
|
|
263
|
+
return "quit"
|
|
264
|
+
|
|
265
|
+
if choice in ("a", "approve"):
|
|
266
|
+
return "approve"
|
|
267
|
+
elif choice in ("d", "deny"):
|
|
268
|
+
return "deny"
|
|
269
|
+
elif choice in ("s", "skip"):
|
|
270
|
+
return "skip"
|
|
271
|
+
elif choice in ("q", "quit"):
|
|
272
|
+
return "quit"
|
|
273
|
+
else:
|
|
274
|
+
console.print("[yellow]Invalid choice. Use A/D/S/Q.[/yellow]")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _prompt_notes(console) -> Optional[str]:
|
|
278
|
+
"""Prompt for optional denial notes."""
|
|
279
|
+
try:
|
|
280
|
+
notes = input("Notes (optional, press Enter to skip): ").strip()
|
|
281
|
+
return notes if notes else None
|
|
282
|
+
except EOFError:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _risk_style(risk_level: str) -> str:
|
|
287
|
+
"""Return a rich style string for a risk level."""
|
|
288
|
+
styles = {
|
|
289
|
+
"safe": "green",
|
|
290
|
+
"default": "blue",
|
|
291
|
+
"risky": "yellow",
|
|
292
|
+
"dangerous": "red bold",
|
|
293
|
+
}
|
|
294
|
+
return styles.get(risk_level, "white")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _log_decision(request: ApprovalRequest, decision: str):
|
|
298
|
+
"""Log an approval decision to SecurityLogger."""
|
|
299
|
+
try:
|
|
300
|
+
from tweek.logging.security_log import (
|
|
301
|
+
SecurityLogger,
|
|
302
|
+
SecurityEvent,
|
|
303
|
+
EventType,
|
|
304
|
+
get_logger,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
evt = EventType.USER_APPROVED if decision == "approved" else EventType.USER_DENIED
|
|
308
|
+
|
|
309
|
+
sec_logger = get_logger()
|
|
310
|
+
sec_logger.log(SecurityEvent(
|
|
311
|
+
event_type=evt,
|
|
312
|
+
tool_name=request.tool_name,
|
|
313
|
+
decision=decision,
|
|
314
|
+
decision_reason=request.screening_reason,
|
|
315
|
+
user_response=decision,
|
|
316
|
+
metadata={
|
|
317
|
+
"source": "mcp_proxy_approval_cli",
|
|
318
|
+
"upstream_server": request.upstream_server,
|
|
319
|
+
"request_id": request.id,
|
|
320
|
+
"risk_level": request.risk_level,
|
|
321
|
+
},
|
|
322
|
+
))
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.debug(f"Failed to log approval decision: {e}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _display_pending_plain(queue: ApprovalQueue) -> int:
|
|
328
|
+
"""Fallback plain-text display when rich is unavailable."""
|
|
329
|
+
pending = queue.get_pending()
|
|
330
|
+
if not pending:
|
|
331
|
+
print("No pending approval requests.")
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
print(f"\nPending Approval Requests ({len(pending)}):")
|
|
335
|
+
print("-" * 70)
|
|
336
|
+
for req in pending:
|
|
337
|
+
remaining = req.time_remaining
|
|
338
|
+
time_str = f"{int(remaining)}s" if remaining > 0 else "EXPIRED"
|
|
339
|
+
print(
|
|
340
|
+
f" [{req.short_id}] {req.upstream_server}/{req.tool_name} "
|
|
341
|
+
f"({req.risk_level}) - {time_str} remaining"
|
|
342
|
+
)
|
|
343
|
+
print(f" Reason: {req.screening_reason[:60]}")
|
|
344
|
+
print()
|
|
345
|
+
return len(pending)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _display_request_plain(request: ApprovalRequest):
|
|
349
|
+
"""Fallback plain-text display for a single request."""
|
|
350
|
+
print(f"\nRequest: {request.short_id}")
|
|
351
|
+
print(f" Server: {request.upstream_server}")
|
|
352
|
+
print(f" Tool: {request.tool_name}")
|
|
353
|
+
print(f" Risk: {request.risk_level}")
|
|
354
|
+
print(f" Reason: {request.screening_reason}")
|
|
355
|
+
remaining = request.time_remaining
|
|
356
|
+
print(f" Time: {int(remaining)}s remaining")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Client Auto-Configuration
|
|
4
|
+
|
|
5
|
+
Handles installing and uninstalling Tweek as an MCP server
|
|
6
|
+
for various desktop LLM clients.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from tweek.mcp.clients.claude_desktop import ClaudeDesktopClient
|
|
10
|
+
from tweek.mcp.clients.chatgpt import ChatGPTClient
|
|
11
|
+
from tweek.mcp.clients.gemini import GeminiClient
|
|
12
|
+
|
|
13
|
+
SUPPORTED_CLIENTS = {
|
|
14
|
+
"claude-desktop": ClaudeDesktopClient,
|
|
15
|
+
"chatgpt": ChatGPTClient,
|
|
16
|
+
"gemini": GeminiClient,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_client(name: str):
|
|
21
|
+
"""Get a client configuration handler by name."""
|
|
22
|
+
client_class = SUPPORTED_CLIENTS.get(name)
|
|
23
|
+
if client_class is None:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Unknown client: {name}. "
|
|
26
|
+
f"Supported: {', '.join(SUPPORTED_CLIENTS.keys())}"
|
|
27
|
+
)
|
|
28
|
+
return client_class()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ClaudeDesktopClient",
|
|
33
|
+
"ChatGPTClient",
|
|
34
|
+
"GeminiClient",
|
|
35
|
+
"SUPPORTED_CLIENTS",
|
|
36
|
+
"get_client",
|
|
37
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ChatGPT Desktop MCP Client Configuration
|
|
4
|
+
|
|
5
|
+
ChatGPT Desktop supports MCP via Developer Mode.
|
|
6
|
+
Unlike Claude Desktop, ChatGPT requires manual Developer Mode activation,
|
|
7
|
+
so we provide guided instructions plus the config commands.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChatGPTClient:
|
|
18
|
+
"""Manage Tweek MCP integration with ChatGPT Desktop."""
|
|
19
|
+
|
|
20
|
+
CLIENT_NAME = "chatgpt"
|
|
21
|
+
DISPLAY_NAME = "ChatGPT Desktop"
|
|
22
|
+
|
|
23
|
+
def _get_tweek_command(self) -> str:
|
|
24
|
+
"""Get the path to the tweek executable."""
|
|
25
|
+
tweek_path = shutil.which("tweek")
|
|
26
|
+
if tweek_path:
|
|
27
|
+
return tweek_path
|
|
28
|
+
return sys.executable
|
|
29
|
+
|
|
30
|
+
def _get_tweek_args(self) -> list:
|
|
31
|
+
"""Get the arguments for the tweek MCP server."""
|
|
32
|
+
tweek_path = shutil.which("tweek")
|
|
33
|
+
if tweek_path:
|
|
34
|
+
return ["mcp", "serve"]
|
|
35
|
+
return ["-m", "tweek.mcp", "serve"]
|
|
36
|
+
|
|
37
|
+
def install(self) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Provide instructions for ChatGPT Desktop MCP setup.
|
|
40
|
+
|
|
41
|
+
ChatGPT requires manual Developer Mode activation.
|
|
42
|
+
"""
|
|
43
|
+
command = self._get_tweek_command()
|
|
44
|
+
args = self._get_tweek_args()
|
|
45
|
+
|
|
46
|
+
instructions = [
|
|
47
|
+
"ChatGPT Desktop MCP Setup:",
|
|
48
|
+
"",
|
|
49
|
+
"1. Open ChatGPT Desktop",
|
|
50
|
+
"2. Go to Settings -> Developer -> Advanced",
|
|
51
|
+
"3. Enable Developer Mode",
|
|
52
|
+
"4. Go to Connectors settings",
|
|
53
|
+
"5. Click 'Add MCP Server'",
|
|
54
|
+
"6. Configure with:",
|
|
55
|
+
f" - Name: Tweek Security",
|
|
56
|
+
f" - Command: {command}",
|
|
57
|
+
f" - Args: {' '.join(args)}",
|
|
58
|
+
"",
|
|
59
|
+
"After adding, restart ChatGPT Desktop to activate.",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"success": True,
|
|
64
|
+
"client": self.DISPLAY_NAME,
|
|
65
|
+
"manual_setup_required": True,
|
|
66
|
+
"command": command,
|
|
67
|
+
"args": args,
|
|
68
|
+
"instructions": instructions,
|
|
69
|
+
"message": "ChatGPT requires manual Developer Mode activation. See instructions.",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def uninstall(self) -> Dict[str, Any]:
|
|
73
|
+
"""Provide instructions for removing Tweek from ChatGPT Desktop."""
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"client": self.DISPLAY_NAME,
|
|
77
|
+
"manual_removal_required": True,
|
|
78
|
+
"instructions": [
|
|
79
|
+
"To remove Tweek from ChatGPT Desktop:",
|
|
80
|
+
"",
|
|
81
|
+
"1. Open ChatGPT Desktop",
|
|
82
|
+
"2. Go to Settings -> Developer -> Connectors",
|
|
83
|
+
"3. Find 'Tweek Security' and remove it",
|
|
84
|
+
"4. Restart ChatGPT Desktop",
|
|
85
|
+
],
|
|
86
|
+
"message": "Follow instructions to remove Tweek from ChatGPT Desktop.",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def status(self) -> Dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Check ChatGPT Desktop MCP status.
|
|
92
|
+
|
|
93
|
+
Since ChatGPT doesn't use a file-based config, we can only
|
|
94
|
+
check if the app is installed.
|
|
95
|
+
"""
|
|
96
|
+
# Check if ChatGPT Desktop is installed
|
|
97
|
+
chatgpt_installed = False
|
|
98
|
+
|
|
99
|
+
if sys.platform == "darwin":
|
|
100
|
+
chatgpt_path = Path("/Applications/ChatGPT.app")
|
|
101
|
+
chatgpt_installed = chatgpt_path.exists()
|
|
102
|
+
elif sys.platform == "win32":
|
|
103
|
+
# Windows store app or standard install
|
|
104
|
+
chatgpt_installed = shutil.which("chatgpt") is not None
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"installed": None, # Can't determine MCP status
|
|
108
|
+
"client": self.DISPLAY_NAME,
|
|
109
|
+
"app_installed": chatgpt_installed,
|
|
110
|
+
"note": "Cannot detect MCP configuration status for ChatGPT Desktop. "
|
|
111
|
+
"Check Developer Mode -> Connectors manually.",
|
|
112
|
+
}
|