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.
Files changed (85) hide show
  1. tweek/__init__.py +16 -0
  2. tweek/cli.py +3390 -0
  3. tweek/cli_helpers.py +193 -0
  4. tweek/config/__init__.py +13 -0
  5. tweek/config/allowed_dirs.yaml +23 -0
  6. tweek/config/manager.py +1064 -0
  7. tweek/config/patterns.yaml +751 -0
  8. tweek/config/tiers.yaml +129 -0
  9. tweek/diagnostics.py +589 -0
  10. tweek/hooks/__init__.py +1 -0
  11. tweek/hooks/pre_tool_use.py +861 -0
  12. tweek/integrations/__init__.py +3 -0
  13. tweek/integrations/moltbot.py +243 -0
  14. tweek/licensing.py +398 -0
  15. tweek/logging/__init__.py +9 -0
  16. tweek/logging/bundle.py +350 -0
  17. tweek/logging/json_logger.py +150 -0
  18. tweek/logging/security_log.py +745 -0
  19. tweek/mcp/__init__.py +24 -0
  20. tweek/mcp/approval.py +456 -0
  21. tweek/mcp/approval_cli.py +356 -0
  22. tweek/mcp/clients/__init__.py +37 -0
  23. tweek/mcp/clients/chatgpt.py +112 -0
  24. tweek/mcp/clients/claude_desktop.py +203 -0
  25. tweek/mcp/clients/gemini.py +178 -0
  26. tweek/mcp/proxy.py +667 -0
  27. tweek/mcp/screening.py +175 -0
  28. tweek/mcp/server.py +317 -0
  29. tweek/platform/__init__.py +131 -0
  30. tweek/plugins/__init__.py +835 -0
  31. tweek/plugins/base.py +1080 -0
  32. tweek/plugins/compliance/__init__.py +30 -0
  33. tweek/plugins/compliance/gdpr.py +333 -0
  34. tweek/plugins/compliance/gov.py +324 -0
  35. tweek/plugins/compliance/hipaa.py +285 -0
  36. tweek/plugins/compliance/legal.py +322 -0
  37. tweek/plugins/compliance/pci.py +361 -0
  38. tweek/plugins/compliance/soc2.py +275 -0
  39. tweek/plugins/detectors/__init__.py +30 -0
  40. tweek/plugins/detectors/continue_dev.py +206 -0
  41. tweek/plugins/detectors/copilot.py +254 -0
  42. tweek/plugins/detectors/cursor.py +192 -0
  43. tweek/plugins/detectors/moltbot.py +205 -0
  44. tweek/plugins/detectors/windsurf.py +214 -0
  45. tweek/plugins/git_discovery.py +395 -0
  46. tweek/plugins/git_installer.py +491 -0
  47. tweek/plugins/git_lockfile.py +338 -0
  48. tweek/plugins/git_registry.py +503 -0
  49. tweek/plugins/git_security.py +482 -0
  50. tweek/plugins/providers/__init__.py +30 -0
  51. tweek/plugins/providers/anthropic.py +181 -0
  52. tweek/plugins/providers/azure_openai.py +289 -0
  53. tweek/plugins/providers/bedrock.py +248 -0
  54. tweek/plugins/providers/google.py +197 -0
  55. tweek/plugins/providers/openai.py +230 -0
  56. tweek/plugins/scope.py +130 -0
  57. tweek/plugins/screening/__init__.py +26 -0
  58. tweek/plugins/screening/llm_reviewer.py +149 -0
  59. tweek/plugins/screening/pattern_matcher.py +273 -0
  60. tweek/plugins/screening/rate_limiter.py +174 -0
  61. tweek/plugins/screening/session_analyzer.py +159 -0
  62. tweek/proxy/__init__.py +302 -0
  63. tweek/proxy/addon.py +223 -0
  64. tweek/proxy/interceptor.py +313 -0
  65. tweek/proxy/server.py +315 -0
  66. tweek/sandbox/__init__.py +71 -0
  67. tweek/sandbox/executor.py +382 -0
  68. tweek/sandbox/linux.py +278 -0
  69. tweek/sandbox/profile_generator.py +323 -0
  70. tweek/screening/__init__.py +13 -0
  71. tweek/screening/context.py +81 -0
  72. tweek/security/__init__.py +22 -0
  73. tweek/security/llm_reviewer.py +348 -0
  74. tweek/security/rate_limiter.py +682 -0
  75. tweek/security/secret_scanner.py +506 -0
  76. tweek/security/session_analyzer.py +600 -0
  77. tweek/vault/__init__.py +40 -0
  78. tweek/vault/cross_platform.py +251 -0
  79. tweek/vault/keychain.py +288 -0
  80. tweek-0.1.0.dist-info/METADATA +335 -0
  81. tweek-0.1.0.dist-info/RECORD +85 -0
  82. tweek-0.1.0.dist-info/WHEEL +5 -0
  83. tweek-0.1.0.dist-info/entry_points.txt +25 -0
  84. tweek-0.1.0.dist-info/licenses/LICENSE +190 -0
  85. tweek-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Plugin Lockfile Management
4
+
5
+ Manages version pinning for installed plugins via lockfiles:
6
+ - User lockfile: ~/.tweek/plugins.lock.json
7
+ - Project lockfile: .tweek/plugins.lock.json (takes precedence)
8
+
9
+ Lockfiles pin exact versions + commit SHAs + checksums for
10
+ reproducible installations across team environments.
11
+
12
+ Format:
13
+ {
14
+ "schema_version": 1,
15
+ "generated_at": "2026-01-29T00:00:00Z",
16
+ "generated_by": "tweek 0.1.0",
17
+ "plugins": {
18
+ "tweek-plugin-cursor-detector": {
19
+ "version": "1.2.0",
20
+ "git_ref": "v1.2.0",
21
+ "commit_sha": "abc123...",
22
+ "checksums": {
23
+ "plugin.py": "sha256:..."
24
+ }
25
+ }
26
+ }
27
+ }
28
+ """
29
+
30
+ import json
31
+ import logging
32
+ import subprocess
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+ from typing import Any, Dict, List, Optional, Tuple
36
+
37
+ from tweek.plugins.git_registry import PLUGINS_DIR, TWEEK_HOME
38
+ from tweek.plugins.git_security import generate_checksums
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # Lockfile locations
43
+ USER_LOCKFILE = TWEEK_HOME / "plugins.lock.json"
44
+ PROJECT_LOCKFILE = Path(".tweek") / "plugins.lock.json"
45
+
46
+ # Current lockfile schema version
47
+ LOCKFILE_SCHEMA_VERSION = 1
48
+
49
+
50
+ class LockfileError(Exception):
51
+ """Raised when lockfile operations fail."""
52
+ pass
53
+
54
+
55
+ class PluginLock:
56
+ """Represents a single plugin's locked state."""
57
+
58
+ def __init__(self, data: dict):
59
+ self._data = data
60
+
61
+ @property
62
+ def version(self) -> str:
63
+ return self._data.get("version", "")
64
+
65
+ @property
66
+ def git_ref(self) -> str:
67
+ return self._data.get("git_ref", "")
68
+
69
+ @property
70
+ def commit_sha(self) -> str:
71
+ return self._data.get("commit_sha", "")
72
+
73
+ @property
74
+ def checksums(self) -> Dict[str, str]:
75
+ return self._data.get("checksums", {})
76
+
77
+ def to_dict(self) -> dict:
78
+ return dict(self._data)
79
+
80
+
81
+ class PluginLockfile:
82
+ """
83
+ Manages plugin version lockfiles.
84
+
85
+ Supports two lockfile locations:
86
+ - User lockfile: ~/.tweek/plugins.lock.json
87
+ - Project lockfile: .tweek/plugins.lock.json (takes precedence)
88
+
89
+ When a project lockfile exists, it overrides the user lockfile.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ user_lockfile: Optional[Path] = None,
95
+ project_lockfile: Optional[Path] = None,
96
+ plugins_dir: Optional[Path] = None,
97
+ ):
98
+ self._user_lockfile = user_lockfile or USER_LOCKFILE
99
+ self._project_lockfile = project_lockfile or PROJECT_LOCKFILE
100
+ self._plugins_dir = plugins_dir or PLUGINS_DIR
101
+ self._locks: Optional[Dict[str, PluginLock]] = None
102
+
103
+ @property
104
+ def active_lockfile(self) -> Optional[Path]:
105
+ """Get the active lockfile path (project takes precedence)."""
106
+ if self._project_lockfile.exists():
107
+ return self._project_lockfile
108
+ if self._user_lockfile.exists():
109
+ return self._user_lockfile
110
+ return None
111
+
112
+ @property
113
+ def has_lockfile(self) -> bool:
114
+ """Check if any lockfile exists."""
115
+ return self.active_lockfile is not None
116
+
117
+ def load(self) -> Dict[str, PluginLock]:
118
+ """
119
+ Load the active lockfile.
120
+
121
+ Returns:
122
+ Dict mapping plugin name to PluginLock
123
+ """
124
+ lockfile = self.active_lockfile
125
+ if lockfile is None:
126
+ return {}
127
+
128
+ try:
129
+ with open(lockfile) as f:
130
+ data = json.load(f)
131
+ except (json.JSONDecodeError, IOError) as e:
132
+ raise LockfileError(f"Failed to read lockfile {lockfile}: {e}")
133
+
134
+ schema = data.get("schema_version", 0)
135
+ if schema != LOCKFILE_SCHEMA_VERSION:
136
+ raise LockfileError(
137
+ f"Unsupported lockfile schema version {schema}. "
138
+ f"Expected {LOCKFILE_SCHEMA_VERSION}. "
139
+ f"Regenerate with 'tweek plugins lock'."
140
+ )
141
+
142
+ plugins_data = data.get("plugins", {})
143
+ self._locks = {
144
+ name: PluginLock(lock_data)
145
+ for name, lock_data in plugins_data.items()
146
+ }
147
+ return self._locks
148
+
149
+ def get_lock(self, name: str) -> Optional[PluginLock]:
150
+ """
151
+ Get the lock for a specific plugin.
152
+
153
+ Args:
154
+ name: Plugin name
155
+
156
+ Returns:
157
+ PluginLock if locked, None otherwise
158
+ """
159
+ if self._locks is None:
160
+ self.load()
161
+ return self._locks.get(name) if self._locks else None
162
+
163
+ def is_locked(self, name: str) -> bool:
164
+ """Check if a plugin version is locked."""
165
+ return self.get_lock(name) is not None
166
+
167
+ def generate(
168
+ self,
169
+ target: str = "user",
170
+ specific_plugins: Optional[Dict[str, str]] = None,
171
+ ) -> Path:
172
+ """
173
+ Generate a lockfile from currently installed plugins.
174
+
175
+ Args:
176
+ target: "user" for ~/.tweek/ or "project" for .tweek/
177
+ specific_plugins: Optional dict of {name: version} to lock.
178
+ If None, locks all installed plugins.
179
+
180
+ Returns:
181
+ Path to the generated lockfile
182
+ """
183
+ lockfile_path = self._user_lockfile if target == "user" else self._project_lockfile
184
+
185
+ # Build plugin locks
186
+ plugins_lock = {}
187
+
188
+ if specific_plugins:
189
+ # Lock specific plugins
190
+ for name, version in specific_plugins.items():
191
+ lock_data = self._build_lock_entry(name, version)
192
+ if lock_data:
193
+ plugins_lock[name] = lock_data
194
+ else:
195
+ # Lock all installed plugins
196
+ if self._plugins_dir.exists():
197
+ for plugin_dir in sorted(self._plugins_dir.iterdir()):
198
+ if not plugin_dir.is_dir():
199
+ continue
200
+
201
+ manifest_path = plugin_dir / "tweek_plugin.json"
202
+ if not manifest_path.exists():
203
+ continue
204
+
205
+ try:
206
+ with open(manifest_path) as f:
207
+ manifest = json.load(f)
208
+ name = manifest.get("name", plugin_dir.name)
209
+ version = manifest.get("version", "unknown")
210
+ lock_data = self._build_lock_entry(name, version)
211
+ if lock_data:
212
+ plugins_lock[name] = lock_data
213
+ except (json.JSONDecodeError, IOError) as e:
214
+ logger.warning(f"Skipping {plugin_dir.name}: {e}")
215
+
216
+ # Build lockfile
217
+ lockfile_data = {
218
+ "schema_version": LOCKFILE_SCHEMA_VERSION,
219
+ "generated_at": datetime.now(timezone.utc).isoformat(),
220
+ "generated_by": self._get_tweek_version(),
221
+ "plugins": plugins_lock,
222
+ }
223
+
224
+ # Write lockfile
225
+ lockfile_path.parent.mkdir(parents=True, exist_ok=True)
226
+ with open(lockfile_path, "w") as f:
227
+ json.dump(lockfile_data, f, indent=2, sort_keys=True)
228
+
229
+ logger.info(f"Generated lockfile with {len(plugins_lock)} plugin(s): {lockfile_path}")
230
+ return lockfile_path
231
+
232
+ def check_compliance(self) -> Tuple[bool, List[str]]:
233
+ """
234
+ Check if installed plugins match the lockfile.
235
+
236
+ Returns:
237
+ (all_compliant, list_of_violations)
238
+ """
239
+ if not self.has_lockfile:
240
+ return True, []
241
+
242
+ violations = []
243
+ locks = self.load()
244
+
245
+ for name, lock in locks.items():
246
+ plugin_dir = self._plugins_dir / name
247
+ if not plugin_dir.exists():
248
+ violations.append(f"Locked plugin '{name}' is not installed")
249
+ continue
250
+
251
+ # Check version
252
+ manifest_path = plugin_dir / "tweek_plugin.json"
253
+ if not manifest_path.exists():
254
+ violations.append(f"Plugin '{name}' has no manifest")
255
+ continue
256
+
257
+ try:
258
+ with open(manifest_path) as f:
259
+ manifest = json.load(f)
260
+ except (json.JSONDecodeError, IOError):
261
+ violations.append(f"Plugin '{name}' has invalid manifest")
262
+ continue
263
+
264
+ current_version = manifest.get("version", "")
265
+ if current_version != lock.version:
266
+ violations.append(
267
+ f"Plugin '{name}' version mismatch: "
268
+ f"installed={current_version}, locked={lock.version}"
269
+ )
270
+
271
+ # Check commit SHA if available
272
+ if lock.commit_sha:
273
+ current_sha = self._get_git_commit(plugin_dir)
274
+ if current_sha and current_sha != lock.commit_sha:
275
+ violations.append(
276
+ f"Plugin '{name}' commit mismatch: "
277
+ f"installed={current_sha[:12]}, locked={lock.commit_sha[:12]}"
278
+ )
279
+
280
+ # Check checksums
281
+ if lock.checksums:
282
+ current_checksums = generate_checksums(plugin_dir)
283
+ for filename, expected in lock.checksums.items():
284
+ actual = current_checksums.get(filename, "")
285
+ if actual and actual != expected:
286
+ violations.append(
287
+ f"Plugin '{name}' file '{filename}' has been modified"
288
+ )
289
+
290
+ return len(violations) == 0, violations
291
+
292
+ def _build_lock_entry(self, name: str, version: str) -> Optional[dict]:
293
+ """Build a lock entry for a plugin."""
294
+ plugin_dir = self._plugins_dir / name
295
+ if not plugin_dir.exists():
296
+ return None
297
+
298
+ entry = {
299
+ "version": version,
300
+ "git_ref": f"v{version}",
301
+ }
302
+
303
+ # Get commit SHA
304
+ commit = self._get_git_commit(plugin_dir)
305
+ if commit:
306
+ entry["commit_sha"] = commit
307
+
308
+ # Generate checksums
309
+ checksums = generate_checksums(plugin_dir)
310
+ if checksums:
311
+ entry["checksums"] = checksums
312
+
313
+ return entry
314
+
315
+ def _get_git_commit(self, plugin_dir: Path) -> Optional[str]:
316
+ """Get the current git commit SHA."""
317
+ try:
318
+ result = subprocess.run(
319
+ ["git", "rev-parse", "HEAD"],
320
+ capture_output=True,
321
+ text=True,
322
+ timeout=5,
323
+ cwd=str(plugin_dir),
324
+ )
325
+ if result.returncode == 0:
326
+ return result.stdout.strip()
327
+ except (subprocess.TimeoutExpired, FileNotFoundError):
328
+ pass
329
+ return None
330
+
331
+ @staticmethod
332
+ def _get_tweek_version() -> str:
333
+ """Get the current Tweek version string."""
334
+ try:
335
+ from tweek import __version__
336
+ return f"tweek {__version__}"
337
+ except ImportError:
338
+ return "tweek unknown"