@voria/cli 0.0.2

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 (67) hide show
  1. package/README.md +439 -0
  2. package/bin/voria +730 -0
  3. package/docs/ARCHITECTURE.md +419 -0
  4. package/docs/CHANGELOG.md +189 -0
  5. package/docs/CONTRIBUTING.md +447 -0
  6. package/docs/DESIGN_DECISIONS.md +380 -0
  7. package/docs/DEVELOPMENT.md +535 -0
  8. package/docs/EXAMPLES.md +434 -0
  9. package/docs/INSTALL.md +335 -0
  10. package/docs/IPC_PROTOCOL.md +310 -0
  11. package/docs/LLM_INTEGRATION.md +416 -0
  12. package/docs/MODULES.md +470 -0
  13. package/docs/PERFORMANCE.md +346 -0
  14. package/docs/PLUGINS.md +432 -0
  15. package/docs/QUICKSTART.md +184 -0
  16. package/docs/README.md +133 -0
  17. package/docs/ROADMAP.md +346 -0
  18. package/docs/SECURITY.md +334 -0
  19. package/docs/TROUBLESHOOTING.md +565 -0
  20. package/docs/USER_GUIDE.md +700 -0
  21. package/package.json +63 -0
  22. package/python/voria/__init__.py +8 -0
  23. package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
  24. package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
  25. package/python/voria/core/__init__.py +1 -0
  26. package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
  27. package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
  28. package/python/voria/core/agent/__init__.py +9 -0
  29. package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
  30. package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
  31. package/python/voria/core/agent/loop.py +343 -0
  32. package/python/voria/core/executor/__init__.py +19 -0
  33. package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
  34. package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
  35. package/python/voria/core/executor/executor.py +431 -0
  36. package/python/voria/core/github/__init__.py +33 -0
  37. package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
  38. package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
  39. package/python/voria/core/github/client.py +438 -0
  40. package/python/voria/core/llm/__init__.py +55 -0
  41. package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
  42. package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
  43. package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
  44. package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
  45. package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
  46. package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
  47. package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
  48. package/python/voria/core/llm/base.py +152 -0
  49. package/python/voria/core/llm/claude_provider.py +188 -0
  50. package/python/voria/core/llm/gemini_provider.py +148 -0
  51. package/python/voria/core/llm/modal_provider.py +228 -0
  52. package/python/voria/core/llm/model_discovery.py +289 -0
  53. package/python/voria/core/llm/openai_provider.py +146 -0
  54. package/python/voria/core/patcher/__init__.py +9 -0
  55. package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
  57. package/python/voria/core/patcher/patcher.py +375 -0
  58. package/python/voria/core/planner/__init__.py +1 -0
  59. package/python/voria/core/setup.py +201 -0
  60. package/python/voria/core/token_manager/__init__.py +29 -0
  61. package/python/voria/core/token_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/python/voria/core/token_manager/__pycache__/manager.cpython-312.pyc +0 -0
  63. package/python/voria/core/token_manager/manager.py +241 -0
  64. package/python/voria/engine.py +1185 -0
  65. package/python/voria/plugins/__init__.py +1 -0
  66. package/python/voria/plugins/python/__init__.py +1 -0
  67. package/python/voria/plugins/typescript/__init__.py +1 -0
@@ -0,0 +1,375 @@
1
+ """
2
+ Code Patcher Module - Apply and rollback code patches
3
+
4
+ Supports unified diff format patches, with ability to:
5
+ - Parse unified diffs
6
+ - Apply patches to files
7
+ - Create backups for rollback
8
+ - Handle merge conflicts gracefully
9
+ """
10
+
11
+ import os
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Dict, List, Tuple, Optional
15
+ from dataclasses import dataclass
16
+ import logging
17
+ import shutil
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class PatchHunk:
24
+ """A single hunk from a unified diff"""
25
+
26
+ old_file: str
27
+ new_file: str
28
+ old_start: int
29
+ old_count: int
30
+ new_start: int
31
+ new_count: int
32
+ lines: List[str] # Patch lines (with +/- prefix)
33
+
34
+
35
+ class UnifiedDiffParser:
36
+ """Parse unified diff format"""
37
+
38
+ @staticmethod
39
+ def parse(diff_content: str) -> List[PatchHunk]:
40
+ """
41
+ Parse unified diff format
42
+
43
+ Example:
44
+ --- a/file.py
45
+ +++ b/file.py
46
+ @@ -10,5 +10,6 @@
47
+ context line
48
+ -old line
49
+ +new line
50
+ context line
51
+ """
52
+ hunks = []
53
+ lines = diff_content.split("\n")
54
+
55
+ i = 0
56
+ while i < len(lines):
57
+ line = lines[i]
58
+
59
+ # Check for file headers
60
+ if line.startswith("--- "):
61
+ old_file = line[4:].split("\t")[0]
62
+ i += 1
63
+
64
+ if i < len(lines) and lines[i].startswith("+++ "):
65
+ new_file = lines[i][4:].split("\t")[0]
66
+ i += 1
67
+
68
+ # Parse hunks for this file
69
+ while i < len(lines) and lines[i].startswith("@@"):
70
+ match = re.match(
71
+ r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", lines[i]
72
+ )
73
+ if match:
74
+ old_start = int(match.group(1))
75
+ old_count = int(match.group(2) or 1)
76
+ new_start = int(match.group(3))
77
+ new_count = int(match.group(4) or 1)
78
+ i += 1
79
+
80
+ # Read hunk lines
81
+ hunk_lines = []
82
+ while (
83
+ i < len(lines)
84
+ and not lines[i].startswith("@@")
85
+ and not lines[i].startswith("--- ")
86
+ and not lines[i].startswith("+++")
87
+ and lines[i]
88
+ and lines[i][0] in [" ", "-", "+", "\\"]
89
+ ):
90
+ hunk_lines.append(lines[i])
91
+ i += 1
92
+
93
+ hunks.append(
94
+ PatchHunk(
95
+ old_file=old_file.lstrip("a/"),
96
+ new_file=new_file.lstrip("b/"),
97
+ old_start=old_start,
98
+ old_count=old_count,
99
+ new_start=new_start,
100
+ new_count=new_count,
101
+ lines=hunk_lines,
102
+ )
103
+ )
104
+ else:
105
+ i += 1
106
+ else:
107
+ i += 1
108
+
109
+ return hunks
110
+
111
+
112
+ class CodePatcher:
113
+ """Apply and manage code patches"""
114
+
115
+ BACKUP_DIR = Path.home() / ".voria" / "backups"
116
+
117
+ def __init__(self, repo_path: str = "."):
118
+ """Initialize patcher with repo path"""
119
+ self.repo_path = Path(repo_path)
120
+ self.backup_dir = self.BACKUP_DIR
121
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
122
+
123
+ async def apply_patch(
124
+ self, patch_content: str, strategy: str = "strict"
125
+ ) -> Dict[str, any]:
126
+ """
127
+ Apply a unified diff patch
128
+
129
+ Args:
130
+ patch_content: Unified diff format patch
131
+ strategy: "strict" (fail on conflict) or "fuzzy" (try best effort)
132
+
133
+ Returns:
134
+ Dict with status, modified_files, errors
135
+ """
136
+ try:
137
+ logger.info("Parsing patch...")
138
+ hunks = UnifiedDiffParser.parse(patch_content)
139
+
140
+ if not hunks:
141
+ return {
142
+ "status": "error",
143
+ "message": "No valid hunks found in patch",
144
+ "modified_files": [],
145
+ "errors": ["Empty patch"],
146
+ }
147
+
148
+ modified_files = {}
149
+ errors = []
150
+
151
+ # Group hunks by file
152
+ hunks_by_file = {}
153
+ for hunk in hunks:
154
+ key = hunk.new_file or hunk.old_file
155
+ if key not in hunks_by_file:
156
+ hunks_by_file[key] = []
157
+ hunks_by_file[key].append(hunk)
158
+
159
+ # Apply patches to each file
160
+ for file_path, file_hunks in hunks_by_file.items():
161
+ try:
162
+ result = await self._apply_file_patch(
163
+ file_path, file_hunks, strategy
164
+ )
165
+ modified_files[file_path] = result
166
+ except Exception as e:
167
+ errors.append(f"{file_path}: {str(e)}")
168
+ logger.error(f"Failed to patch {file_path}: {e}")
169
+
170
+ # Check results
171
+ failed_count = sum(1 for r in modified_files.values() if not r["success"])
172
+
173
+ return {
174
+ "status": "success" if failed_count == 0 else "partial",
175
+ "message": f"Patched {len(modified_files)} files, {failed_count} failed",
176
+ "modified_files": modified_files,
177
+ "errors": errors,
178
+ }
179
+
180
+ except Exception as e:
181
+ logger.error(f"Patch application failed: {e}")
182
+ return {
183
+ "status": "error",
184
+ "message": str(e),
185
+ "modified_files": {},
186
+ "errors": [str(e)],
187
+ }
188
+
189
+ async def _apply_file_patch(
190
+ self, file_path: str, hunks: List[PatchHunk], strategy: str
191
+ ) -> Dict[str, any]:
192
+ """Apply patch hunks to a single file"""
193
+
194
+ full_path = self.repo_path / file_path
195
+
196
+ # Check file exists
197
+ if not full_path.exists():
198
+ logger.warning(f"File not found, creating: {file_path}")
199
+ full_path.parent.mkdir(parents=True, exist_ok=True)
200
+ full_path.write_text("")
201
+
202
+ # Create backup
203
+ backup_path = await self._create_backup(full_path)
204
+
205
+ try:
206
+ # Read current content
207
+ content = full_path.read_text()
208
+ lines = content.split("\n")
209
+
210
+ # Apply hunks (process in reverse to preserve line numbers)
211
+ for hunk in reversed(sorted(hunks, key=lambda h: h.old_start)):
212
+ lines = await self._apply_hunk(lines, hunk, strategy)
213
+
214
+ # Write patched content
215
+ full_path.write_text("\n".join(lines))
216
+
217
+ logger.info(f"Successfully patched: {file_path}")
218
+
219
+ return {
220
+ "success": True,
221
+ "file": file_path,
222
+ "backup": str(backup_path),
223
+ "hunks_applied": len(hunks),
224
+ }
225
+
226
+ except Exception as e:
227
+ # Restore backup on failure
228
+ logger.error(f"Patch failed, restoring backup: {e}")
229
+ if backup_path.exists():
230
+ shutil.copy(backup_path, full_path)
231
+
232
+ return {
233
+ "success": False,
234
+ "file": file_path,
235
+ "error": str(e),
236
+ "backup_restored": True,
237
+ }
238
+
239
+ async def _apply_hunk(
240
+ self, lines: List[str], hunk: PatchHunk, strategy: str
241
+ ) -> List[str]:
242
+ """Apply a single hunk to lines"""
243
+
244
+ # Parse hunk lines
245
+ context_before = []
246
+ removals = []
247
+ additions = []
248
+ context_after = []
249
+
250
+ current = None
251
+
252
+ for line in hunk.lines:
253
+ if line.startswith(" "):
254
+ content = line[1:]
255
+ if not removals and not additions:
256
+ context_before.append(content)
257
+ else:
258
+ context_after.append(content)
259
+ elif line.startswith("-"):
260
+ removals.append(line[1:])
261
+ current = "removal"
262
+ elif line.startswith("+"):
263
+ additions.append(line[1:])
264
+ current = "addition"
265
+ elif line.startswith("\\"):
266
+ # ""
267
+ pass
268
+
269
+ # Find hunk location in lines
270
+ start_idx = hunk.old_start - 1
271
+
272
+ # Try to match context
273
+ if strategy == "strict":
274
+ # Verify exact match
275
+ for i, ctx_line in enumerate(context_before):
276
+ if start_idx + i >= len(lines) or lines[start_idx + i] != ctx_line:
277
+ raise ValueError(
278
+ f"Context mismatch at line {hunk.old_start + i}: "
279
+ f"expected '{ctx_line}', got '{lines[start_idx + i] if start_idx + i < len(lines) else 'EOF'}'"
280
+ )
281
+
282
+ # Apply changes
283
+ # Remove old lines
284
+ end_idx = start_idx + hunk.old_count
285
+ new_lines = lines[:start_idx] + additions + lines[end_idx:]
286
+
287
+ return new_lines
288
+
289
+ async def _create_backup(self, file_path: Path) -> Path:
290
+ """Create backup of file before patching"""
291
+
292
+ import time
293
+
294
+ timestamp = int(time.time())
295
+ backup_name = f"{file_path.stem}_{timestamp}.bak"
296
+ backup_path = self.backup_dir / backup_name
297
+
298
+ shutil.copy(file_path, backup_path)
299
+ logger.info(f"Backup created: {backup_path}")
300
+
301
+ return backup_path
302
+
303
+ async def rollback_patch(self, file_path: str, backup_path: str) -> bool:
304
+ """Rollback a patch using backup"""
305
+
306
+ try:
307
+ full_path = self.repo_path / file_path
308
+ backup = Path(backup_path)
309
+
310
+ if not backup.exists():
311
+ logger.error(f"Backup not found: {backup_path}")
312
+ return False
313
+
314
+ shutil.copy(backup, full_path)
315
+ logger.info(f"Rolled back: {file_path}")
316
+ return True
317
+
318
+ except Exception as e:
319
+ logger.error(f"Rollback failed: {e}")
320
+ return False
321
+
322
+ def cleanup_backups(self, keep_count: int = 10) -> int:
323
+ """Clean up old backups, keeping most recent"""
324
+
325
+ try:
326
+ if not self.backup_dir.exists():
327
+ return 0
328
+
329
+ # Get all backups sorted by modification time
330
+ backups = sorted(
331
+ self.backup_dir.glob("*.bak"),
332
+ key=lambda p: p.stat().st_mtime,
333
+ reverse=True,
334
+ )
335
+
336
+ # Remove old ones
337
+ removed = 0
338
+ for backup in backups[keep_count:]:
339
+ backup.unlink()
340
+ removed += 1
341
+
342
+ logger.info(f"Cleaned {removed} old backups")
343
+ return removed
344
+
345
+ except Exception as e:
346
+ logger.error(f"Backup cleanup failed: {e}")
347
+ return 0
348
+
349
+
350
+ async def test_patcher():
351
+ """Test the code patcher"""
352
+
353
+ # Example patch
354
+ patch = """--- a/example.py
355
+ +++ b/example.py
356
+ @@ -1,5 +1,6 @@
357
+ def hello():
358
+ - print("old")
359
+ + print("new")
360
+ return True
361
+
362
+ hello()
363
+ """
364
+
365
+ patcher = CodePatcher()
366
+ result = await patcher.apply_patch(patch)
367
+
368
+ print(f"Status: {result['status']}")
369
+ print(f"Message: {result['message']}")
370
+
371
+
372
+ if __name__ == "__main__":
373
+ import asyncio
374
+
375
+ asyncio.run(test_patcher())
@@ -0,0 +1 @@
1
+ """Planner module."""
@@ -0,0 +1,201 @@
1
+ """
2
+ Interactive setup CLI for configuring LLM providers.
3
+ Guides users through choosing provider, API key, and model selection.
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, Optional
10
+ import json
11
+ import logging
12
+
13
+ from voria.core.llm import LLMProviderFactory
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ProviderSetup:
19
+ """Interactive provider configuration setup"""
20
+
21
+ CONFIG_DIR = Path.home() / ".voria"
22
+ CONFIG_FILE = CONFIG_DIR / "providers.json"
23
+
24
+ def __init__(self):
25
+ """Initialize setup helper"""
26
+ self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
27
+ self.providers = {}
28
+ self._load_config()
29
+
30
+ def _load_config(self):
31
+ """Load saved provider configurations"""
32
+ if self.CONFIG_FILE.exists():
33
+ with open(self.CONFIG_FILE, "r") as f:
34
+ self.providers = json.load(f)
35
+
36
+ def _save_config(self):
37
+ """Save provider configurations"""
38
+ with open(self.CONFIG_FILE, "w") as f:
39
+ json.dump(self.providers, f, indent=2)
40
+ os.chmod(self.CONFIG_FILE, 0o600) # Restrict permissions
41
+
42
+ async def setup_provider(
43
+ self,
44
+ provider_name: Optional[str] = None,
45
+ api_key: Optional[str] = None,
46
+ ) -> Dict[str, str]:
47
+ """
48
+ Interactive setup for a provider
49
+
50
+ Args:
51
+ provider_name: Provider to setup (optional, will prompt if None)
52
+ api_key: API key (optional, will prompt if None)
53
+
54
+ Returns:
55
+ Dict with provider_name, api_key, and selected model
56
+ """
57
+ # Step 1: Choose provider
58
+ if not provider_name:
59
+ provider_name = await self._choose_provider()
60
+ provider_name = provider_name.lower()
61
+
62
+ # Step 2: Get API key
63
+ if not api_key:
64
+ api_key = await self._get_api_key(provider_name)
65
+
66
+ # Step 3: Discover models
67
+ print(f"\nšŸ” Fetching available {provider_name} models...")
68
+ try:
69
+ models = await LLMProviderFactory.discover_models(provider_name, api_key)
70
+ except Exception as e:
71
+ print(f"āŒ Failed to fetch models: {e}")
72
+ print("Using fallback models...")
73
+ from voria.core.llm import ModelDiscovery
74
+
75
+ if provider_name == "modal":
76
+ models = await ModelDiscovery._get_modal_fallback()
77
+ elif provider_name == "openai":
78
+ models = await ModelDiscovery._get_openai_fallback()
79
+ elif provider_name == "gemini":
80
+ models = await ModelDiscovery._get_gemini_fallback()
81
+ elif provider_name == "claude":
82
+ models = await ModelDiscovery._get_claude_fallback()
83
+
84
+ # Step 4: Choose model
85
+ chosen_model = await self._choose_model(models)
86
+
87
+ # Step 5: Save configuration
88
+ print(f"\nšŸ’¾ Saving configuration...")
89
+ self.providers[provider_name] = {
90
+ "api_key": api_key,
91
+ "model": chosen_model.name,
92
+ "model_name": chosen_model.display_name,
93
+ }
94
+ self._save_config()
95
+
96
+ print(f"āœ… {provider_name} configured successfully!")
97
+ print(f" Model: {chosen_model.display_name}")
98
+ print(f" Config saved to: {self.CONFIG_FILE}")
99
+
100
+ return {
101
+ "provider": provider_name,
102
+ "api_key": api_key,
103
+ "model": chosen_model.name,
104
+ "model_name": chosen_model.display_name,
105
+ }
106
+
107
+ async def _choose_provider(self) -> str:
108
+ """Interactive provider selection"""
109
+ providers = LLMProviderFactory.list_providers()
110
+
111
+ print("\nšŸ¤– Select LLM Provider:")
112
+ for i, provider in enumerate(providers, 1):
113
+ status = (
114
+ "āœ… Configured" if provider in self.providers else "āŒ Not configured"
115
+ )
116
+ print(f" {i}. {provider.upper()} {status}")
117
+
118
+ while True:
119
+ try:
120
+ choice = input("\nEnter number (1-4): ").strip()
121
+ idx = int(choice) - 1
122
+ if 0 <= idx < len(providers):
123
+ return providers[idx]
124
+ print("Invalid choice. Try again.")
125
+ except ValueError:
126
+ print("Please enter a valid number.")
127
+
128
+ async def _get_api_key(self, provider_name: str) -> str:
129
+ """Get API key from user or environment"""
130
+ # Check environment variables
131
+ env_vars = {
132
+ "modal": ["MODAL_API_KEY"],
133
+ "openai": ["OPENAI_API_KEY"],
134
+ "gemini": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
135
+ "claude": ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
136
+ }
137
+
138
+ for env_var in env_vars.get(provider_name, []):
139
+ if env_var in os.environ:
140
+ print(f"āœ… Using API key from ${env_var}")
141
+ return os.environ[env_var]
142
+
143
+ # Prompt user
144
+ while True:
145
+ api_key = input(f"\nšŸ”‘ Enter {provider_name.upper()} API key: ").strip()
146
+ if api_key:
147
+ # Ask to save to environment
148
+ save = input("Save to environment variable? (y/n): ").lower().strip()
149
+ if save == "y":
150
+ env_var = env_vars[provider_name][0]
151
+ print(f"Add to ~/.bashrc or ~/.zshrc:")
152
+ print(f"export {env_var}='{api_key}'")
153
+ return api_key
154
+ print("API key cannot be empty.")
155
+
156
+ async def _choose_model(self, models: list) -> object:
157
+ """Interactive model selection from discovered models"""
158
+ print("\nšŸ“¦ Available Models:")
159
+ for i, model in enumerate(models, 1):
160
+ print(f" {i}. {model.display_name}")
161
+ if model.description:
162
+ print(f" {model.description}")
163
+
164
+ while True:
165
+ try:
166
+ choice = input("\nSelect model number: ").strip()
167
+ idx = int(choice) - 1
168
+ if 0 <= idx < len(models):
169
+ return models[idx]
170
+ print("Invalid choice. Try again.")
171
+ except ValueError:
172
+ print("Please enter a valid number.")
173
+
174
+ def get_provider_config(self, provider_name: str) -> Optional[Dict]:
175
+ """Get saved configuration for a provider"""
176
+ return self.providers.get(provider_name.lower())
177
+
178
+ def list_configured(self) -> Dict:
179
+ """List all configured providers"""
180
+ return self.providers.copy()
181
+
182
+
183
+ async def interactive_setup():
184
+ """Run interactive setup for first-time users"""
185
+ setup = ProviderSetup()
186
+
187
+ print("\n" + "=" * 50)
188
+ print("šŸš€ voria LLM Provider Setup")
189
+ print("=" * 50)
190
+
191
+ config = await setup.setup_provider()
192
+
193
+ print("\n" + "=" * 50)
194
+ print("āœ… Setup Complete!")
195
+ print("=" * 50)
196
+
197
+ return config
198
+
199
+
200
+ if __name__ == "__main__":
201
+ asyncio.run(interactive_setup())
@@ -0,0 +1,29 @@
1
+ """Token Manager Module
2
+
3
+ Tracks LLM token usage across all providers and manages budget limits.
4
+
5
+ Usage:
6
+ from voria.core.token_manager import get_token_manager
7
+
8
+ manager = get_token_manager()
9
+ manager.record_usage("openai", "gpt-4", 500, 1000)
10
+ summary = manager.get_usage_summary()
11
+ """
12
+
13
+ from .manager import (
14
+ TokenManager,
15
+ TokenBudget,
16
+ TokenUsageRecord,
17
+ get_token_manager,
18
+ init_token_manager,
19
+ PRICING,
20
+ )
21
+
22
+ __all__ = [
23
+ "TokenManager",
24
+ "TokenBudget",
25
+ "TokenUsageRecord",
26
+ "get_token_manager",
27
+ "init_token_manager",
28
+ "PRICING",
29
+ ]