stravinsky 0.2.52__py3-none-any.whl → 0.4.18__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.

Potentially problematic release.


This version of stravinsky might be problematic. Click here for more details.

Files changed (58) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/token_store.py +113 -11
  3. mcp_bridge/cli/__init__.py +6 -0
  4. mcp_bridge/cli/install_hooks.py +1265 -0
  5. mcp_bridge/cli/session_report.py +585 -0
  6. mcp_bridge/config/MANIFEST_SCHEMA.md +305 -0
  7. mcp_bridge/config/README.md +276 -0
  8. mcp_bridge/config/hook_config.py +249 -0
  9. mcp_bridge/config/hooks_manifest.json +138 -0
  10. mcp_bridge/config/rate_limits.py +222 -0
  11. mcp_bridge/config/skills_manifest.json +128 -0
  12. mcp_bridge/hooks/HOOKS_SETTINGS.json +175 -0
  13. mcp_bridge/hooks/README.md +215 -0
  14. mcp_bridge/hooks/__init__.py +119 -60
  15. mcp_bridge/hooks/edit_recovery.py +42 -37
  16. mcp_bridge/hooks/git_noninteractive.py +89 -0
  17. mcp_bridge/hooks/keyword_detector.py +30 -0
  18. mcp_bridge/hooks/manager.py +8 -0
  19. mcp_bridge/hooks/notification_hook.py +103 -0
  20. mcp_bridge/hooks/parallel_execution.py +111 -0
  21. mcp_bridge/hooks/pre_compact.py +82 -183
  22. mcp_bridge/hooks/rules_injector.py +507 -0
  23. mcp_bridge/hooks/session_notifier.py +125 -0
  24. mcp_bridge/{native_hooks → hooks}/stravinsky_mode.py +51 -16
  25. mcp_bridge/hooks/subagent_stop.py +98 -0
  26. mcp_bridge/hooks/task_validator.py +73 -0
  27. mcp_bridge/hooks/tmux_manager.py +141 -0
  28. mcp_bridge/hooks/todo_continuation.py +90 -0
  29. mcp_bridge/hooks/todo_delegation.py +88 -0
  30. mcp_bridge/hooks/tool_messaging.py +267 -0
  31. mcp_bridge/hooks/truncator.py +21 -17
  32. mcp_bridge/notifications.py +151 -0
  33. mcp_bridge/prompts/multimodal.py +24 -3
  34. mcp_bridge/server.py +214 -49
  35. mcp_bridge/server_tools.py +445 -0
  36. mcp_bridge/tools/__init__.py +22 -18
  37. mcp_bridge/tools/agent_manager.py +220 -32
  38. mcp_bridge/tools/code_search.py +97 -11
  39. mcp_bridge/tools/lsp/__init__.py +7 -0
  40. mcp_bridge/tools/lsp/manager.py +448 -0
  41. mcp_bridge/tools/lsp/tools.py +637 -150
  42. mcp_bridge/tools/model_invoke.py +208 -106
  43. mcp_bridge/tools/query_classifier.py +323 -0
  44. mcp_bridge/tools/semantic_search.py +3042 -0
  45. mcp_bridge/tools/templates.py +32 -18
  46. mcp_bridge/update_manager.py +589 -0
  47. mcp_bridge/update_manager_pypi.py +299 -0
  48. stravinsky-0.4.18.dist-info/METADATA +468 -0
  49. stravinsky-0.4.18.dist-info/RECORD +88 -0
  50. stravinsky-0.4.18.dist-info/entry_points.txt +5 -0
  51. mcp_bridge/native_hooks/edit_recovery.py +0 -46
  52. mcp_bridge/native_hooks/todo_delegation.py +0 -54
  53. mcp_bridge/native_hooks/truncator.py +0 -23
  54. stravinsky-0.2.52.dist-info/METADATA +0 -204
  55. stravinsky-0.2.52.dist-info/RECORD +0 -63
  56. stravinsky-0.2.52.dist-info/entry_points.txt +0 -3
  57. /mcp_bridge/{native_hooks → hooks}/context.py +0 -0
  58. {stravinsky-0.2.52.dist-info → stravinsky-0.4.18.dist-info}/WHEEL +0 -0
mcp_bridge/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.52"
1
+ __version__ = "0.4.18"
@@ -8,10 +8,12 @@ Stores OAuth tokens securely using the OS keyring:
8
8
  """
9
9
 
10
10
  import json
11
+ import os
11
12
  import time
13
+ from pathlib import Path
12
14
  from typing import TypedDict
13
15
 
14
- import keyring
16
+ from cryptography.fernet import Fernet
15
17
 
16
18
 
17
19
  class TokenData(TypedDict, total=False):
@@ -26,13 +28,15 @@ class TokenData(TypedDict, total=False):
26
28
 
27
29
  class TokenStore:
28
30
  """
29
- Secure storage for OAuth tokens using system keyring.
31
+ Secure storage for OAuth tokens using system keyring with encrypted file fallback.
30
32
 
31
33
  Each provider (gemini, openai) stores its tokens separately.
32
34
  Tokens are serialized as JSON for storage.
35
+ Falls back to encrypted file storage if keyring fails.
33
36
  """
34
37
 
35
38
  SERVICE_NAME = "stravinsky"
39
+ FALLBACK_DIR = Path.home() / ".stravinsky" / "tokens"
36
40
 
37
41
  def __init__(self, service_name: str | None = None):
38
42
  """Initialize the token store.
@@ -41,6 +45,63 @@ class TokenStore:
41
45
  service_name: Override the default service name for testing.
42
46
  """
43
47
  self.service_name = service_name or self.SERVICE_NAME
48
+ self._init_fallback_storage()
49
+
50
+ def _init_fallback_storage(self) -> None:
51
+ """Initialize encrypted file storage fallback directory."""
52
+ self.FALLBACK_DIR.mkdir(parents=True, exist_ok=True)
53
+
54
+ def _get_fallback_path(self, provider: str) -> Path:
55
+ """Get the path for encrypted fallback storage."""
56
+ return self.FALLBACK_DIR / f"{provider}.enc"
57
+
58
+ def _get_or_create_key(self) -> bytes:
59
+ """Get or create encryption key for fallback storage."""
60
+ key_file = self.FALLBACK_DIR / ".key"
61
+ if key_file.exists():
62
+ return key_file.read_bytes()
63
+ # Create new key and save it
64
+ key = Fernet.generate_key()
65
+ key_file.write_bytes(key)
66
+ key_file.chmod(0o600) # Read/write for owner only
67
+ return key
68
+
69
+ def _save_encrypted(self, provider: str, data: str) -> None:
70
+ """Save data to encrypted file."""
71
+ try:
72
+ key = self._get_or_create_key()
73
+ cipher = Fernet(key)
74
+ encrypted = cipher.encrypt(data.encode())
75
+ path = self._get_fallback_path(provider)
76
+ path.write_bytes(encrypted)
77
+ path.chmod(0o600)
78
+ except Exception as e:
79
+ raise RuntimeError(f"Failed to save encrypted token: {e}")
80
+
81
+ def _load_encrypted(self, provider: str) -> str | None:
82
+ """Load data from encrypted file."""
83
+ try:
84
+ path = self._get_fallback_path(provider)
85
+ if not path.exists():
86
+ return None
87
+ key = self._get_or_create_key()
88
+ cipher = Fernet(key)
89
+ encrypted = path.read_bytes()
90
+ decrypted = cipher.decrypt(encrypted)
91
+ return decrypted.decode()
92
+ except Exception:
93
+ return None
94
+
95
+ def _delete_encrypted(self, provider: str) -> bool:
96
+ """Delete encrypted token file."""
97
+ try:
98
+ path = self._get_fallback_path(provider)
99
+ if path.exists():
100
+ path.unlink()
101
+ return True
102
+ return False
103
+ except Exception:
104
+ return False
44
105
 
45
106
  def _key(self, provider: str) -> str:
46
107
  """Generate the keyring key for a provider."""
@@ -56,13 +117,24 @@ class TokenStore:
56
117
  Returns:
57
118
  TokenData if found and valid, None otherwise.
58
119
  """
120
+ # Try keyring first (import inside try to catch backend initialization errors)
59
121
  try:
122
+ import keyring
60
123
  data = keyring.get_password(self.service_name, self._key(provider))
61
- if not data:
62
- return None
63
- return json.loads(data)
64
- except (json.JSONDecodeError, keyring.errors.KeyringError):
65
- return None
124
+ if data:
125
+ return json.loads(data)
126
+ except Exception:
127
+ pass # Fall back to encrypted file (catches KeyringError, import errors, etc.)
128
+
129
+ # Fall back to encrypted file storage
130
+ try:
131
+ data = self._load_encrypted(provider)
132
+ if data:
133
+ return json.loads(data)
134
+ except json.JSONDecodeError:
135
+ pass
136
+
137
+ return None
66
138
 
67
139
  def set_token(
68
140
  self,
@@ -77,6 +149,7 @@ class TokenStore:
77
149
  Store a token for a provider.
78
150
 
79
151
  Can be called with a TokenData dict or individual parameters.
152
+ Falls back to encrypted file storage if keyring fails.
80
153
 
81
154
  Args:
82
155
  provider: The provider name (e.g., 'gemini', 'openai')
@@ -96,7 +169,27 @@ class TokenStore:
96
169
  if expires_at:
97
170
  token_data["expires_at"] = expires_at
98
171
  data = json.dumps(token_data)
99
- keyring.set_password(self.service_name, self._key(provider), data)
172
+
173
+ # Try keyring first, but always also write to encrypted storage
174
+ # Import inside try to catch backend initialization errors (e.g., "No recommended backend")
175
+ keyring_failed = False
176
+ try:
177
+ import keyring
178
+ keyring.set_password(self.service_name, self._key(provider), data)
179
+ except Exception:
180
+ # Keyring failed - fall back to encrypted file
181
+ keyring_failed = True
182
+
183
+ # Always write to encrypted file storage as fallback
184
+ try:
185
+ self._save_encrypted(provider, data)
186
+ except Exception as e:
187
+ # Only fail if both backends failed
188
+ if keyring_failed:
189
+ raise RuntimeError(
190
+ f"Failed to save token to both keyring and encrypted storage: {e}"
191
+ )
192
+
100
193
 
101
194
  def delete_token(self, provider: str) -> bool:
102
195
  """
@@ -108,11 +201,20 @@ class TokenStore:
108
201
  Returns:
109
202
  True if deleted, False if not found.
110
203
  """
204
+ # Try keyring first (import inside try to catch backend initialization errors)
205
+ deleted_from_keyring = False
111
206
  try:
207
+ import keyring
112
208
  keyring.delete_password(self.service_name, self._key(provider))
113
- return True
114
- except keyring.errors.PasswordDeleteError:
115
- return False
209
+ deleted_from_keyring = True
210
+ except Exception:
211
+ # Keyring failed (no backend, delete error, etc.) - continue to encrypted file
212
+ pass
213
+
214
+ # Also delete from encrypted file storage
215
+ deleted_from_file = self._delete_encrypted(provider)
216
+
217
+ return deleted_from_keyring or deleted_from_file
116
218
 
117
219
  def has_valid_token(self, provider: str) -> bool:
118
220
  """
@@ -0,0 +1,6 @@
1
+ """CLI tools for Stravinsky."""
2
+
3
+ from .session_report import main as session_report_main
4
+ from .install_hooks import main as install_hooks_main
5
+
6
+ __all__ = ["session_report_main", "install_hooks_main"]