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
tweek/diagnostics.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek Diagnostics Engine
|
|
4
|
+
|
|
5
|
+
Health check system for verifying Tweek installation, configuration,
|
|
6
|
+
and runtime dependencies. Used by `tweek doctor` and the status banner.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CheckStatus(Enum):
|
|
19
|
+
"""Health check result status."""
|
|
20
|
+
OK = "ok"
|
|
21
|
+
WARNING = "warning"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
SKIPPED = "skipped"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class HealthCheck:
|
|
28
|
+
"""Result of a single health check."""
|
|
29
|
+
name: str # Machine name: "hooks_installed"
|
|
30
|
+
label: str # Human label: "Hook Installation"
|
|
31
|
+
status: CheckStatus
|
|
32
|
+
message: str # Description: "Global hooks installed at ~/.claude/"
|
|
33
|
+
fix_hint: str = "" # Recovery: "Run: tweek install --scope global"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_health_checks(verbose: bool = False) -> List[HealthCheck]:
|
|
37
|
+
"""
|
|
38
|
+
Run all health checks and return results.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
verbose: If True, include extra detail in messages.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of HealthCheck results in check order.
|
|
45
|
+
"""
|
|
46
|
+
checks = [
|
|
47
|
+
_check_hooks_installed,
|
|
48
|
+
_check_config_valid,
|
|
49
|
+
_check_patterns_loaded,
|
|
50
|
+
_check_security_db,
|
|
51
|
+
_check_vault_available,
|
|
52
|
+
_check_sandbox_available,
|
|
53
|
+
_check_license_status,
|
|
54
|
+
_check_mcp_available,
|
|
55
|
+
_check_proxy_config,
|
|
56
|
+
_check_plugin_integrity,
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
results = []
|
|
60
|
+
for check_fn in checks:
|
|
61
|
+
try:
|
|
62
|
+
result = check_fn(verbose)
|
|
63
|
+
results.append(result)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
results.append(HealthCheck(
|
|
66
|
+
name=check_fn.__name__.replace("_check_", ""),
|
|
67
|
+
label=check_fn.__name__.replace("_check_", "").replace("_", " ").title(),
|
|
68
|
+
status=CheckStatus.ERROR,
|
|
69
|
+
message=f"Check failed: {e}",
|
|
70
|
+
fix_hint="This may indicate a corrupted installation. Try: pip install --force-reinstall tweek",
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
# Log health check results
|
|
74
|
+
try:
|
|
75
|
+
from tweek.logging.security_log import get_logger, SecurityEvent, EventType
|
|
76
|
+
|
|
77
|
+
checks_passed = sum(1 for c in results if c.status == CheckStatus.OK)
|
|
78
|
+
checks_failed = sum(1 for c in results if c.status == CheckStatus.ERROR)
|
|
79
|
+
checks_warning = sum(1 for c in results if c.status == CheckStatus.WARNING)
|
|
80
|
+
|
|
81
|
+
if checks_failed > 0:
|
|
82
|
+
overall_status = "error"
|
|
83
|
+
elif checks_warning > 0:
|
|
84
|
+
overall_status = "warning"
|
|
85
|
+
else:
|
|
86
|
+
overall_status = "ok"
|
|
87
|
+
|
|
88
|
+
get_logger().log(SecurityEvent(
|
|
89
|
+
event_type=EventType.HEALTH_CHECK,
|
|
90
|
+
tool_name="diagnostics",
|
|
91
|
+
decision="allow",
|
|
92
|
+
metadata={
|
|
93
|
+
"overall_status": overall_status,
|
|
94
|
+
"checks_passed": checks_passed,
|
|
95
|
+
"checks_failed": checks_failed,
|
|
96
|
+
"checks_warning": checks_warning,
|
|
97
|
+
},
|
|
98
|
+
source="diagnostics",
|
|
99
|
+
))
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return results
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_health_verdict(checks: List[HealthCheck]) -> Tuple[str, str]:
|
|
107
|
+
"""
|
|
108
|
+
Compute an overall health verdict from check results.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Tuple of (verdict_text, color) for Rich display.
|
|
112
|
+
"""
|
|
113
|
+
ok_count = sum(1 for c in checks if c.status == CheckStatus.OK)
|
|
114
|
+
warn_count = sum(1 for c in checks if c.status == CheckStatus.WARNING)
|
|
115
|
+
error_count = sum(1 for c in checks if c.status == CheckStatus.ERROR)
|
|
116
|
+
skip_count = sum(1 for c in checks if c.status == CheckStatus.SKIPPED)
|
|
117
|
+
total = len(checks)
|
|
118
|
+
|
|
119
|
+
if error_count == 0 and warn_count == 0:
|
|
120
|
+
return f"All systems operational ({ok_count}/{total - skip_count} OK)", "green"
|
|
121
|
+
elif error_count == 0:
|
|
122
|
+
return (
|
|
123
|
+
f"Mostly healthy ({ok_count} OK, {warn_count} warning{'s' if warn_count != 1 else ''})",
|
|
124
|
+
"yellow",
|
|
125
|
+
)
|
|
126
|
+
elif error_count <= 2:
|
|
127
|
+
return (
|
|
128
|
+
f"Issues detected ({ok_count} OK, {error_count} error{'s' if error_count != 1 else ''}, "
|
|
129
|
+
f"{warn_count} warning{'s' if warn_count != 1 else ''})",
|
|
130
|
+
"red",
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
return f"Multiple issues ({error_count} errors, {warn_count} warnings)", "red"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ==================== Individual Health Checks ====================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_hooks_installed(verbose: bool = False) -> HealthCheck:
|
|
140
|
+
"""Check if Tweek hooks are installed in Claude Code settings."""
|
|
141
|
+
global_claude = Path("~/.claude").expanduser()
|
|
142
|
+
project_claude = Path.cwd() / ".claude"
|
|
143
|
+
|
|
144
|
+
global_installed = _has_tweek_hooks(global_claude / "settings.json")
|
|
145
|
+
project_installed = _has_tweek_hooks(project_claude / "settings.json")
|
|
146
|
+
|
|
147
|
+
if global_installed and project_installed:
|
|
148
|
+
return HealthCheck(
|
|
149
|
+
name="hooks_installed",
|
|
150
|
+
label="Hook Installation",
|
|
151
|
+
status=CheckStatus.OK,
|
|
152
|
+
message="Installed globally (~/.claude) and in project (./.claude)",
|
|
153
|
+
)
|
|
154
|
+
elif global_installed:
|
|
155
|
+
msg = "Installed globally (~/.claude)"
|
|
156
|
+
if verbose:
|
|
157
|
+
msg += " — project-level hooks not configured"
|
|
158
|
+
return HealthCheck(
|
|
159
|
+
name="hooks_installed",
|
|
160
|
+
label="Hook Installation",
|
|
161
|
+
status=CheckStatus.OK,
|
|
162
|
+
message=msg,
|
|
163
|
+
)
|
|
164
|
+
elif project_installed:
|
|
165
|
+
return HealthCheck(
|
|
166
|
+
name="hooks_installed",
|
|
167
|
+
label="Hook Installation",
|
|
168
|
+
status=CheckStatus.WARNING,
|
|
169
|
+
message="Installed in project only (./.claude)",
|
|
170
|
+
fix_hint="Run: tweek install --scope global (to protect all projects)",
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
return HealthCheck(
|
|
174
|
+
name="hooks_installed",
|
|
175
|
+
label="Hook Installation",
|
|
176
|
+
status=CheckStatus.ERROR,
|
|
177
|
+
message="No hooks installed",
|
|
178
|
+
fix_hint="Run: tweek install",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_tweek_hooks(settings_path: Path) -> bool:
|
|
183
|
+
"""Check if a settings.json file contains Tweek hooks."""
|
|
184
|
+
if not settings_path.exists():
|
|
185
|
+
return False
|
|
186
|
+
try:
|
|
187
|
+
with open(settings_path) as f:
|
|
188
|
+
settings = json.load(f)
|
|
189
|
+
hooks = settings.get("hooks", {})
|
|
190
|
+
for hook_type in ["PreToolUse", "PostToolUse"]:
|
|
191
|
+
for hook_config in hooks.get(hook_type, []):
|
|
192
|
+
for hook in hook_config.get("hooks", []):
|
|
193
|
+
if "tweek" in hook.get("command", "").lower():
|
|
194
|
+
return True
|
|
195
|
+
except (json.JSONDecodeError, IOError, KeyError):
|
|
196
|
+
pass
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _check_config_valid(verbose: bool = False) -> HealthCheck:
|
|
201
|
+
"""Check that configuration files parse correctly."""
|
|
202
|
+
from tweek.config import ConfigManager
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
config = ConfigManager()
|
|
206
|
+
tools = config.list_tools()
|
|
207
|
+
skills = config.list_skills()
|
|
208
|
+
|
|
209
|
+
# Check for validation issues if the method exists
|
|
210
|
+
issues = []
|
|
211
|
+
if hasattr(config, "validate_config"):
|
|
212
|
+
issues = config.validate_config()
|
|
213
|
+
errors = [i for i in issues if i.level == "error"]
|
|
214
|
+
warnings = [i for i in issues if i.level == "warning"]
|
|
215
|
+
if errors:
|
|
216
|
+
return HealthCheck(
|
|
217
|
+
name="config_valid",
|
|
218
|
+
label="Configuration",
|
|
219
|
+
status=CheckStatus.ERROR,
|
|
220
|
+
message=f"{len(errors)} config error{'s' if len(errors) != 1 else ''}",
|
|
221
|
+
fix_hint="Run: tweek config validate (for details)",
|
|
222
|
+
)
|
|
223
|
+
if warnings:
|
|
224
|
+
return HealthCheck(
|
|
225
|
+
name="config_valid",
|
|
226
|
+
label="Configuration",
|
|
227
|
+
status=CheckStatus.WARNING,
|
|
228
|
+
message=f"Config valid with {len(warnings)} warning{'s' if len(warnings) != 1 else ''} "
|
|
229
|
+
f"({len(tools)} tools, {len(skills)} skills)",
|
|
230
|
+
fix_hint="Run: tweek config validate (for details)",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return HealthCheck(
|
|
234
|
+
name="config_valid",
|
|
235
|
+
label="Configuration",
|
|
236
|
+
status=CheckStatus.OK,
|
|
237
|
+
message=f"Config valid ({len(tools)} tools, {len(skills)} skills)",
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
return HealthCheck(
|
|
241
|
+
name="config_valid",
|
|
242
|
+
label="Configuration",
|
|
243
|
+
status=CheckStatus.ERROR,
|
|
244
|
+
message=f"Failed to load config: {e}",
|
|
245
|
+
fix_hint="Run: tweek config validate (to see errors)",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_patterns_loaded(verbose: bool = False) -> HealthCheck:
|
|
250
|
+
"""Check that attack patterns are loaded and accessible."""
|
|
251
|
+
user_patterns = Path("~/.tweek/patterns/patterns.yaml").expanduser()
|
|
252
|
+
bundled_patterns = Path(__file__).parent / "config" / "patterns.yaml"
|
|
253
|
+
|
|
254
|
+
patterns_file = None
|
|
255
|
+
source = None
|
|
256
|
+
|
|
257
|
+
if user_patterns.exists():
|
|
258
|
+
patterns_file = user_patterns
|
|
259
|
+
source = "~/.tweek/patterns"
|
|
260
|
+
elif bundled_patterns.exists():
|
|
261
|
+
patterns_file = bundled_patterns
|
|
262
|
+
source = "bundled"
|
|
263
|
+
|
|
264
|
+
if patterns_file is None:
|
|
265
|
+
return HealthCheck(
|
|
266
|
+
name="patterns_loaded",
|
|
267
|
+
label="Attack Patterns",
|
|
268
|
+
status=CheckStatus.ERROR,
|
|
269
|
+
message="No patterns file found",
|
|
270
|
+
fix_hint="Run: tweek update (to download patterns)",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
import yaml
|
|
275
|
+
with open(patterns_file) as f:
|
|
276
|
+
pdata = yaml.safe_load(f) or {}
|
|
277
|
+
|
|
278
|
+
count = pdata.get("pattern_count", len(pdata.get("patterns", [])))
|
|
279
|
+
if count == 0:
|
|
280
|
+
return HealthCheck(
|
|
281
|
+
name="patterns_loaded",
|
|
282
|
+
label="Attack Patterns",
|
|
283
|
+
status=CheckStatus.WARNING,
|
|
284
|
+
message=f"Patterns file found ({source}) but contains 0 patterns",
|
|
285
|
+
fix_hint="Run: tweek update --force (to re-download patterns)",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return HealthCheck(
|
|
289
|
+
name="patterns_loaded",
|
|
290
|
+
label="Attack Patterns",
|
|
291
|
+
status=CheckStatus.OK,
|
|
292
|
+
message=f"{count:,} patterns loaded ({source})",
|
|
293
|
+
)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
return HealthCheck(
|
|
296
|
+
name="patterns_loaded",
|
|
297
|
+
label="Attack Patterns",
|
|
298
|
+
status=CheckStatus.ERROR,
|
|
299
|
+
message=f"Failed to parse patterns: {e}",
|
|
300
|
+
fix_hint="Run: tweek update --force (to re-download patterns)",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _check_security_db(verbose: bool = False) -> HealthCheck:
|
|
305
|
+
"""Check that the security database exists and is accessible."""
|
|
306
|
+
db_path = Path("~/.tweek/security.db").expanduser()
|
|
307
|
+
|
|
308
|
+
if not db_path.exists():
|
|
309
|
+
return HealthCheck(
|
|
310
|
+
name="security_db",
|
|
311
|
+
label="Security Database",
|
|
312
|
+
status=CheckStatus.OK,
|
|
313
|
+
message="Not yet created (will be created on first event)",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
size_bytes = db_path.stat().st_size
|
|
318
|
+
size_mb = size_bytes / (1024 * 1024)
|
|
319
|
+
|
|
320
|
+
# Test that we can open it
|
|
321
|
+
conn = sqlite3.connect(str(db_path))
|
|
322
|
+
conn.execute("SELECT 1")
|
|
323
|
+
conn.close()
|
|
324
|
+
|
|
325
|
+
if size_mb > 100:
|
|
326
|
+
return HealthCheck(
|
|
327
|
+
name="security_db",
|
|
328
|
+
label="Security Database",
|
|
329
|
+
status=CheckStatus.WARNING,
|
|
330
|
+
message=f"DB is {size_mb:.0f}MB — consider cleanup",
|
|
331
|
+
fix_hint="Run: tweek logs clear --older-than 30d",
|
|
332
|
+
)
|
|
333
|
+
elif size_mb > 10:
|
|
334
|
+
return HealthCheck(
|
|
335
|
+
name="security_db",
|
|
336
|
+
label="Security Database",
|
|
337
|
+
status=CheckStatus.OK,
|
|
338
|
+
message=f"Active ({size_mb:.1f}MB)",
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
return HealthCheck(
|
|
342
|
+
name="security_db",
|
|
343
|
+
label="Security Database",
|
|
344
|
+
status=CheckStatus.OK,
|
|
345
|
+
message=f"Active ({size_mb:.1f}MB)" if size_mb >= 0.1 else "Active (< 100KB)",
|
|
346
|
+
)
|
|
347
|
+
except sqlite3.Error as e:
|
|
348
|
+
return HealthCheck(
|
|
349
|
+
name="security_db",
|
|
350
|
+
label="Security Database",
|
|
351
|
+
status=CheckStatus.ERROR,
|
|
352
|
+
message=f"Cannot open database: {e}",
|
|
353
|
+
fix_hint="Check file permissions on ~/.tweek/security.db",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _check_vault_available(verbose: bool = False) -> HealthCheck:
|
|
358
|
+
"""Check credential vault availability."""
|
|
359
|
+
try:
|
|
360
|
+
from tweek.platform import get_capabilities
|
|
361
|
+
caps = get_capabilities()
|
|
362
|
+
|
|
363
|
+
if caps.vault_available:
|
|
364
|
+
return HealthCheck(
|
|
365
|
+
name="vault_available",
|
|
366
|
+
label="Credential Vault",
|
|
367
|
+
status=CheckStatus.OK,
|
|
368
|
+
message=f"{caps.vault_backend} available",
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
return HealthCheck(
|
|
372
|
+
name="vault_available",
|
|
373
|
+
label="Credential Vault",
|
|
374
|
+
status=CheckStatus.WARNING,
|
|
375
|
+
message="No vault backend available",
|
|
376
|
+
fix_hint="Vault enables secure credential storage. "
|
|
377
|
+
"Install system keyring support for your platform.",
|
|
378
|
+
)
|
|
379
|
+
except ImportError:
|
|
380
|
+
return HealthCheck(
|
|
381
|
+
name="vault_available",
|
|
382
|
+
label="Credential Vault",
|
|
383
|
+
status=CheckStatus.WARNING,
|
|
384
|
+
message="Platform module not available",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _check_sandbox_available(verbose: bool = False) -> HealthCheck:
|
|
389
|
+
"""Check sandbox availability (platform-dependent)."""
|
|
390
|
+
try:
|
|
391
|
+
from tweek.sandbox import get_sandbox_status
|
|
392
|
+
from tweek.platform import IS_LINUX
|
|
393
|
+
|
|
394
|
+
status = get_sandbox_status()
|
|
395
|
+
|
|
396
|
+
if status.get("available"):
|
|
397
|
+
return HealthCheck(
|
|
398
|
+
name="sandbox_available",
|
|
399
|
+
label="Sandbox",
|
|
400
|
+
status=CheckStatus.OK,
|
|
401
|
+
message=f"{status.get('tool', 'Available')} available",
|
|
402
|
+
)
|
|
403
|
+
elif IS_LINUX:
|
|
404
|
+
return HealthCheck(
|
|
405
|
+
name="sandbox_available",
|
|
406
|
+
label="Sandbox",
|
|
407
|
+
status=CheckStatus.WARNING,
|
|
408
|
+
message="firejail not installed",
|
|
409
|
+
fix_hint="Install firejail: sudo apt install firejail",
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
return HealthCheck(
|
|
413
|
+
name="sandbox_available",
|
|
414
|
+
label="Sandbox",
|
|
415
|
+
status=CheckStatus.SKIPPED,
|
|
416
|
+
message="Not available on this platform",
|
|
417
|
+
)
|
|
418
|
+
except ImportError:
|
|
419
|
+
return HealthCheck(
|
|
420
|
+
name="sandbox_available",
|
|
421
|
+
label="Sandbox",
|
|
422
|
+
status=CheckStatus.SKIPPED,
|
|
423
|
+
message="Sandbox module not available",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _check_license_status(verbose: bool = False) -> HealthCheck:
|
|
428
|
+
"""Check license status."""
|
|
429
|
+
try:
|
|
430
|
+
from tweek.licensing import get_license, Tier
|
|
431
|
+
|
|
432
|
+
lic = get_license()
|
|
433
|
+
|
|
434
|
+
if lic.tier == Tier.FREE:
|
|
435
|
+
return HealthCheck(
|
|
436
|
+
name="license_status",
|
|
437
|
+
label="License",
|
|
438
|
+
status=CheckStatus.OK,
|
|
439
|
+
message="Open source (all features included)",
|
|
440
|
+
)
|
|
441
|
+
elif lic.info and lic.info.is_expired:
|
|
442
|
+
return HealthCheck(
|
|
443
|
+
name="license_status",
|
|
444
|
+
label="License",
|
|
445
|
+
status=CheckStatus.WARNING,
|
|
446
|
+
message=f"{lic.tier.value.upper()} license expired",
|
|
447
|
+
fix_hint="Pro and Enterprise tiers coming soon: gettweek.com",
|
|
448
|
+
)
|
|
449
|
+
else:
|
|
450
|
+
email = lic.info.email if lic.info else "unknown"
|
|
451
|
+
return HealthCheck(
|
|
452
|
+
name="license_status",
|
|
453
|
+
label="License",
|
|
454
|
+
status=CheckStatus.OK,
|
|
455
|
+
message=f"{lic.tier.value.upper()} license ({email})",
|
|
456
|
+
)
|
|
457
|
+
except Exception as e:
|
|
458
|
+
return HealthCheck(
|
|
459
|
+
name="license_status",
|
|
460
|
+
label="License",
|
|
461
|
+
status=CheckStatus.WARNING,
|
|
462
|
+
message=f"Cannot check license: {e}",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _check_mcp_available(verbose: bool = False) -> HealthCheck:
|
|
467
|
+
"""Check if MCP dependencies are available."""
|
|
468
|
+
try:
|
|
469
|
+
import mcp # noqa: F401
|
|
470
|
+
return HealthCheck(
|
|
471
|
+
name="mcp_available",
|
|
472
|
+
label="MCP Server",
|
|
473
|
+
status=CheckStatus.OK,
|
|
474
|
+
message="MCP package installed",
|
|
475
|
+
)
|
|
476
|
+
except ImportError:
|
|
477
|
+
return HealthCheck(
|
|
478
|
+
name="mcp_available",
|
|
479
|
+
label="MCP Server",
|
|
480
|
+
status=CheckStatus.SKIPPED,
|
|
481
|
+
message="MCP package not installed (optional)",
|
|
482
|
+
fix_hint="Install with: pip install tweek[mcp]",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _check_proxy_config(verbose: bool = False) -> HealthCheck:
|
|
487
|
+
"""Check proxy configuration if present."""
|
|
488
|
+
try:
|
|
489
|
+
from tweek.config import ConfigManager
|
|
490
|
+
config = ConfigManager()
|
|
491
|
+
full_config = config.get_full_config()
|
|
492
|
+
|
|
493
|
+
proxy_config = full_config.get("proxy", {})
|
|
494
|
+
mcp_config = full_config.get("mcp", {})
|
|
495
|
+
|
|
496
|
+
if not proxy_config and not mcp_config:
|
|
497
|
+
return HealthCheck(
|
|
498
|
+
name="proxy_config",
|
|
499
|
+
label="Proxy Config",
|
|
500
|
+
status=CheckStatus.SKIPPED,
|
|
501
|
+
message="No proxy or MCP proxy configured",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
issues = []
|
|
505
|
+
|
|
506
|
+
# Check MCP proxy upstreams
|
|
507
|
+
mcp_proxy = mcp_config.get("proxy", {})
|
|
508
|
+
upstreams = mcp_proxy.get("upstreams", {})
|
|
509
|
+
if upstreams:
|
|
510
|
+
for name, upstream in upstreams.items():
|
|
511
|
+
if not upstream.get("command"):
|
|
512
|
+
issues.append(f"MCP upstream '{name}' missing 'command'")
|
|
513
|
+
|
|
514
|
+
if issues:
|
|
515
|
+
return HealthCheck(
|
|
516
|
+
name="proxy_config",
|
|
517
|
+
label="Proxy Config",
|
|
518
|
+
status=CheckStatus.WARNING,
|
|
519
|
+
message="; ".join(issues),
|
|
520
|
+
fix_hint="Check proxy configuration in ~/.tweek/config.yaml",
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
parts = []
|
|
524
|
+
if proxy_config:
|
|
525
|
+
parts.append("HTTP proxy configured")
|
|
526
|
+
if upstreams:
|
|
527
|
+
parts.append(f"MCP proxy: {len(upstreams)} upstream{'s' if len(upstreams) != 1 else ''}")
|
|
528
|
+
|
|
529
|
+
return HealthCheck(
|
|
530
|
+
name="proxy_config",
|
|
531
|
+
label="Proxy Config",
|
|
532
|
+
status=CheckStatus.OK,
|
|
533
|
+
message=", ".join(parts) if parts else "Configured",
|
|
534
|
+
)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
return HealthCheck(
|
|
537
|
+
name="proxy_config",
|
|
538
|
+
label="Proxy Config",
|
|
539
|
+
status=CheckStatus.WARNING,
|
|
540
|
+
message=f"Cannot check proxy config: {e}",
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _check_plugin_integrity(verbose: bool = False) -> HealthCheck:
|
|
545
|
+
"""Check installed plugin integrity."""
|
|
546
|
+
try:
|
|
547
|
+
from tweek.plugins import get_registry
|
|
548
|
+
|
|
549
|
+
registry = get_registry()
|
|
550
|
+
stats = registry.get_stats()
|
|
551
|
+
total = stats.get("total", 0)
|
|
552
|
+
enabled = stats.get("enabled", 0)
|
|
553
|
+
|
|
554
|
+
if total == 0:
|
|
555
|
+
return HealthCheck(
|
|
556
|
+
name="plugin_integrity",
|
|
557
|
+
label="Plugin Integrity",
|
|
558
|
+
status=CheckStatus.OK,
|
|
559
|
+
message="No plugins installed",
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Check for plugins with load errors
|
|
563
|
+
all_plugins = registry.list_plugins()
|
|
564
|
+
errors = [p for p in all_plugins if p.load_error]
|
|
565
|
+
|
|
566
|
+
if errors:
|
|
567
|
+
error_names = ", ".join(p.name for p in errors[:3])
|
|
568
|
+
suffix = f" (+{len(errors) - 3} more)" if len(errors) > 3 else ""
|
|
569
|
+
return HealthCheck(
|
|
570
|
+
name="plugin_integrity",
|
|
571
|
+
label="Plugin Integrity",
|
|
572
|
+
status=CheckStatus.WARNING,
|
|
573
|
+
message=f"{len(errors)} plugin{'s' if len(errors) != 1 else ''} with errors: {error_names}{suffix}",
|
|
574
|
+
fix_hint="Run: tweek plugins verify (for details)",
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return HealthCheck(
|
|
578
|
+
name="plugin_integrity",
|
|
579
|
+
label="Plugin Integrity",
|
|
580
|
+
status=CheckStatus.OK,
|
|
581
|
+
message=f"{enabled}/{total} plugins verified",
|
|
582
|
+
)
|
|
583
|
+
except Exception as e:
|
|
584
|
+
return HealthCheck(
|
|
585
|
+
name="plugin_integrity",
|
|
586
|
+
label="Plugin Integrity",
|
|
587
|
+
status=CheckStatus.WARNING,
|
|
588
|
+
message=f"Cannot check plugins: {e}",
|
|
589
|
+
)
|
tweek/hooks/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tweek hooks module for Claude Code integration."""
|