ai-coding-assistant 0.5.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 (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. coding_assistant/vcs/git.py +269 -0
@@ -0,0 +1,206 @@
1
+ """Secure API key storage for cloud LLM providers."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional, List, Dict
7
+
8
+
9
+ class KeyStore:
10
+ """
11
+ Secure storage for API keys.
12
+
13
+ Stores API keys in a JSON file with restricted permissions (600).
14
+ Keys are stored at: ~/.coding_assistant/api_keys.json
15
+ """
16
+
17
+ # Valid provider names (allowlist for security)
18
+ VALID_PROVIDERS = ["groq", "together", "gemini", "openai", "claude", "anthropic"]
19
+
20
+ def __init__(self, storage_dir: Optional[Path] = None):
21
+ """
22
+ Initialize keystore.
23
+
24
+ Args:
25
+ storage_dir: Directory to store keys (default: ~/.coding_assistant)
26
+ """
27
+ if storage_dir is None:
28
+ storage_dir = Path.home() / ".coding_assistant"
29
+
30
+ self.storage_dir = Path(storage_dir)
31
+ self.storage_dir.mkdir(parents=True, exist_ok=True)
32
+ self.keys_file = self.storage_dir / "api_keys.json"
33
+
34
+ # Ensure file has secure permissions if it exists
35
+ if self.keys_file.exists():
36
+ self._ensure_secure_permissions()
37
+
38
+ def set_key(self, provider: str, api_key: str) -> bool:
39
+ """
40
+ Store API key for a provider.
41
+
42
+ Args:
43
+ provider: Provider name (groq, together, openai, claude)
44
+ api_key: API key to store
45
+
46
+ Returns:
47
+ True if key was stored successfully
48
+
49
+ Raises:
50
+ ValueError: If provider is not in allowlist
51
+ """
52
+ # Validate provider
53
+ if provider.lower() not in self.VALID_PROVIDERS:
54
+ raise ValueError(
55
+ f"Invalid provider: {provider}. "
56
+ f"Must be one of: {', '.join(self.VALID_PROVIDERS)}"
57
+ )
58
+
59
+ # Load existing keys
60
+ keys = self._load_keys()
61
+
62
+ # Update key
63
+ keys[provider.lower()] = api_key
64
+
65
+ # Save with secure permissions
66
+ self._save_keys(keys)
67
+
68
+ return True
69
+
70
+ def get_key(self, provider: str) -> Optional[str]:
71
+ """
72
+ Get API key for a provider.
73
+
74
+ Args:
75
+ provider: Provider name
76
+
77
+ Returns:
78
+ API key if found, None otherwise
79
+ """
80
+ keys = self._load_keys()
81
+ return keys.get(provider.lower())
82
+
83
+ def remove_key(self, provider: str) -> bool:
84
+ """
85
+ Remove API key for a provider.
86
+
87
+ Args:
88
+ provider: Provider name
89
+
90
+ Returns:
91
+ True if key was removed, False if key didn't exist
92
+ """
93
+ keys = self._load_keys()
94
+
95
+ if provider.lower() in keys:
96
+ del keys[provider.lower()]
97
+ self._save_keys(keys)
98
+ return True
99
+
100
+ return False
101
+
102
+ def list_providers(self) -> List[str]:
103
+ """
104
+ List all providers with stored API keys.
105
+
106
+ Returns:
107
+ List of provider names
108
+ """
109
+ keys = self._load_keys()
110
+ return list(keys.keys())
111
+
112
+ def get_all_keys(self) -> Dict[str, str]:
113
+ """
114
+ Get all stored API keys.
115
+
116
+ Returns:
117
+ Dictionary of provider -> API key
118
+ """
119
+ return self._load_keys()
120
+
121
+ def clear_all(self) -> bool:
122
+ """
123
+ Remove all stored API keys.
124
+
125
+ Returns:
126
+ True if keys were cleared
127
+ """
128
+ self._save_keys({})
129
+ return True
130
+
131
+ def _load_keys(self) -> Dict[str, str]:
132
+ """
133
+ Load API keys from storage.
134
+
135
+ Returns:
136
+ Dictionary of provider -> API key
137
+ """
138
+ if not self.keys_file.exists():
139
+ return {}
140
+
141
+ try:
142
+ with open(self.keys_file, 'r') as f:
143
+ keys = json.load(f)
144
+ # Ensure it's a dict
145
+ if not isinstance(keys, dict):
146
+ return {}
147
+ return keys
148
+ except (json.JSONDecodeError, IOError):
149
+ # If file is corrupted, return empty dict
150
+ return {}
151
+
152
+ def _save_keys(self, keys: Dict[str, str]) -> None:
153
+ """
154
+ Save API keys to storage with secure permissions.
155
+
156
+ Args:
157
+ keys: Dictionary of provider -> API key
158
+ """
159
+ # Write to file
160
+ with open(self.keys_file, 'w') as f:
161
+ json.dump(keys, f, indent=2)
162
+
163
+ # Set secure permissions (owner read/write only)
164
+ self._ensure_secure_permissions()
165
+
166
+ def _ensure_secure_permissions(self) -> None:
167
+ """
168
+ Ensure keys file has secure permissions (600).
169
+
170
+ Only the owner should be able to read/write the file.
171
+ """
172
+ try:
173
+ # chmod 600 (owner read/write only)
174
+ os.chmod(self.keys_file, 0o600)
175
+ except Exception:
176
+ # If chmod fails (e.g., on Windows), continue anyway
177
+ # Windows doesn't use UNIX permissions
178
+ pass
179
+
180
+ def mask_key(self, api_key: str) -> str:
181
+ """
182
+ Mask an API key for display (show only first 8 and last 4 characters).
183
+
184
+ Args:
185
+ api_key: API key to mask
186
+
187
+ Returns:
188
+ Masked API key (e.g., "gsk_1234...xyz9")
189
+ """
190
+ if not api_key or len(api_key) < 12:
191
+ return "***"
192
+
193
+ return f"{api_key[:8]}...{api_key[-4:]}"
194
+
195
+ def get_masked_keys(self) -> Dict[str, str]:
196
+ """
197
+ Get all keys with masking for safe display.
198
+
199
+ Returns:
200
+ Dictionary of provider -> masked API key
201
+ """
202
+ keys = self._load_keys()
203
+ return {
204
+ provider: self.mask_key(key)
205
+ for provider, key in keys.items()
206
+ }
@@ -0,0 +1,32 @@
1
+ """Simple logging utilities."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+
7
+ def get_logger(name: str, level: Optional[int] = None) -> logging.Logger:
8
+ """
9
+ Get a configured logger instance.
10
+
11
+ Args:
12
+ name: Logger name (usually __name__)
13
+ level: Optional logging level (defaults to INFO)
14
+
15
+ Returns:
16
+ Configured logger instance
17
+ """
18
+ logger = logging.getLogger(name)
19
+
20
+ if not logger.handlers:
21
+ # Configure handler only if not already configured
22
+ handler = logging.StreamHandler()
23
+ formatter = logging.Formatter(
24
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
25
+ datefmt='%Y-%m-%d %H:%M:%S'
26
+ )
27
+ handler.setFormatter(formatter)
28
+ logger.addHandler(handler)
29
+
30
+ logger.setLevel(level or logging.INFO)
31
+
32
+ return logger
@@ -0,0 +1,311 @@
1
+ """Progress indicators and status utilities for consistent UX."""
2
+
3
+ from typing import Optional, Callable, Any
4
+ from contextlib import contextmanager
5
+ from rich.console import Console
6
+ from rich.progress import (
7
+ Progress,
8
+ SpinnerColumn,
9
+ TextColumn,
10
+ BarColumn,
11
+ TaskProgressColumn,
12
+ TimeRemainingColumn
13
+ )
14
+
15
+
16
+ class ProgressManager:
17
+ """
18
+ Centralized progress management for consistent UX.
19
+
20
+ Provides:
21
+ - Simple status spinners
22
+ - Detailed progress bars
23
+ - Consistent styling
24
+ """
25
+
26
+ def __init__(self, console: Optional[Console] = None):
27
+ """
28
+ Initialize progress manager.
29
+
30
+ Args:
31
+ console: Rich console (creates new one if not provided)
32
+ """
33
+ self.console = console or Console()
34
+
35
+ @contextmanager
36
+ def status(self, message: str, spinner: str = "dots"):
37
+ """
38
+ Show a simple status spinner.
39
+
40
+ Args:
41
+ message: Status message to display
42
+ spinner: Spinner style (dots, line, arc, etc.)
43
+
44
+ Usage:
45
+ with progress_manager.status("Processing..."):
46
+ # Long operation
47
+ pass
48
+ """
49
+ with self.console.status(f"[bold green]{message}[/bold green]", spinner=spinner):
50
+ yield
51
+
52
+ @contextmanager
53
+ def progress_bar(
54
+ self,
55
+ description: str,
56
+ total: Optional[int] = None,
57
+ show_time: bool = False
58
+ ):
59
+ """
60
+ Show a detailed progress bar.
61
+
62
+ Args:
63
+ description: Task description
64
+ total: Total steps (None for indeterminate)
65
+ show_time: Show time remaining
66
+
67
+ Usage:
68
+ with progress_manager.progress_bar("Indexing", total=100) as progress:
69
+ for i in range(100):
70
+ progress.update(i + 1)
71
+
72
+ Yields:
73
+ Progress updater function
74
+ """
75
+ columns = [
76
+ SpinnerColumn(),
77
+ TextColumn("[progress.description]{task.description}"),
78
+ ]
79
+
80
+ if total is not None:
81
+ columns.append(BarColumn())
82
+ columns.append(TaskProgressColumn())
83
+
84
+ if show_time:
85
+ columns.append(TimeRemainingColumn())
86
+
87
+ with Progress(*columns, console=self.console, transient=False) as progress:
88
+ task = progress.add_task(f"[cyan]{description}", total=total)
89
+
90
+ class Updater:
91
+ """Simple updater interface."""
92
+
93
+ def update(self, completed: Optional[int] = None, advance: Optional[int] = None):
94
+ """Update progress."""
95
+ if completed is not None:
96
+ progress.update(task, completed=completed)
97
+ elif advance is not None:
98
+ progress.advance(task, advance)
99
+
100
+ def set_description(self, description: str):
101
+ """Update description."""
102
+ progress.update(task, description=f"[cyan]{description}")
103
+
104
+ yield Updater()
105
+
106
+ def show_operation(
107
+ self,
108
+ operation_name: str,
109
+ operation_fn: Callable,
110
+ *args,
111
+ **kwargs
112
+ ) -> Any:
113
+ """
114
+ Execute an operation with automatic progress indication.
115
+
116
+ Args:
117
+ operation_name: Name of the operation
118
+ operation_fn: Function to execute
119
+ *args, **kwargs: Arguments for operation_fn
120
+
121
+ Returns:
122
+ Result of operation_fn
123
+
124
+ Usage:
125
+ result = progress_manager.show_operation(
126
+ "Indexing codebase",
127
+ retriever.index_codebase,
128
+ max_files=100
129
+ )
130
+ """
131
+ with self.status(operation_name):
132
+ return operation_fn(*args, **kwargs)
133
+
134
+ def show_steps(self, steps: list[tuple[str, Callable]]) -> list[Any]:
135
+ """
136
+ Execute multiple steps with progress tracking.
137
+
138
+ Args:
139
+ steps: List of (step_name, step_function) tuples
140
+
141
+ Returns:
142
+ List of results from each step
143
+
144
+ Usage:
145
+ results = progress_manager.show_steps([
146
+ ("Parsing code", parser.parse),
147
+ ("Generating embeddings", embedder.embed),
148
+ ("Storing results", storage.save)
149
+ ])
150
+ """
151
+ results = []
152
+
153
+ with self.progress_bar("Executing steps", total=len(steps)) as progress:
154
+ for i, (step_name, step_fn) in enumerate(steps, 1):
155
+ progress.set_description(f"{step_name}...")
156
+
157
+ try:
158
+ result = step_fn()
159
+ results.append(result)
160
+ except Exception as e:
161
+ self.console.print(f"[red]✗ {step_name} failed: {e}[/red]")
162
+ raise
163
+
164
+ progress.update(i)
165
+
166
+ return results
167
+
168
+
169
+ # Global instance for convenience
170
+ _default_progress_manager: Optional[ProgressManager] = None
171
+
172
+
173
+ def get_progress_manager(console: Optional[Console] = None) -> ProgressManager:
174
+ """
175
+ Get global progress manager instance.
176
+
177
+ Args:
178
+ console: Optional console to use
179
+
180
+ Returns:
181
+ ProgressManager instance
182
+ """
183
+ global _default_progress_manager
184
+
185
+ if _default_progress_manager is None or console is not None:
186
+ _default_progress_manager = ProgressManager(console)
187
+
188
+ return _default_progress_manager
189
+
190
+
191
+ # Convenience functions
192
+ def status(message: str, console: Optional[Console] = None):
193
+ """Context manager for simple status spinner."""
194
+ return get_progress_manager(console).status(message)
195
+
196
+
197
+ def progress_bar(
198
+ description: str,
199
+ total: Optional[int] = None,
200
+ show_time: bool = False,
201
+ console: Optional[Console] = None
202
+ ):
203
+ """Context manager for progress bar."""
204
+ return get_progress_manager(console).progress_bar(description, total, show_time)
205
+
206
+
207
+ def show_operation(
208
+ operation_name: str,
209
+ operation_fn: Callable,
210
+ *args,
211
+ console: Optional[Console] = None,
212
+ **kwargs
213
+ ) -> Any:
214
+ """Execute operation with progress."""
215
+ return get_progress_manager(console).show_operation(
216
+ operation_name, operation_fn, *args, **kwargs
217
+ )
218
+
219
+
220
+ # Progress callback helpers
221
+ class ProgressCallback:
222
+ """
223
+ Progress callback for long-running operations.
224
+
225
+ Usage:
226
+ callback = ProgressCallback(total=100)
227
+ for i in range(100):
228
+ # ... work ...
229
+ callback.update(i + 1)
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ total: int,
235
+ description: str = "Processing",
236
+ console: Optional[Console] = None
237
+ ):
238
+ """
239
+ Initialize progress callback.
240
+
241
+ Args:
242
+ total: Total number of items
243
+ description: Progress description
244
+ console: Rich console
245
+ """
246
+ self.total = total
247
+ self.description = description
248
+ self.console = console or Console()
249
+ self.current = 0
250
+
251
+ self.progress = Progress(
252
+ SpinnerColumn(),
253
+ TextColumn("[progress.description]{task.description}"),
254
+ BarColumn(),
255
+ TaskProgressColumn(),
256
+ console=self.console
257
+ )
258
+
259
+ self.task = None
260
+ self._started = False
261
+
262
+ def __enter__(self):
263
+ """Start progress display."""
264
+ self.progress.__enter__()
265
+ self.task = self.progress.add_task(
266
+ f"[cyan]{self.description}",
267
+ total=self.total
268
+ )
269
+ self._started = True
270
+ return self
271
+
272
+ def __exit__(self, *args):
273
+ """Stop progress display."""
274
+ self.progress.__exit__(*args)
275
+ self._started = False
276
+
277
+ def update(self, current: int, description: Optional[str] = None):
278
+ """
279
+ Update progress.
280
+
281
+ Args:
282
+ current: Current progress value
283
+ description: Optional new description
284
+ """
285
+ if not self._started:
286
+ return
287
+
288
+ self.current = current
289
+
290
+ if description:
291
+ self.progress.update(
292
+ self.task,
293
+ completed=current,
294
+ description=f"[cyan]{description}"
295
+ )
296
+ else:
297
+ self.progress.update(self.task, completed=current)
298
+
299
+ def advance(self, amount: int = 1):
300
+ """Advance progress by amount."""
301
+ if self._started:
302
+ self.current += amount
303
+ self.progress.advance(self.task, amount)
304
+
305
+ def set_description(self, description: str):
306
+ """Update description."""
307
+ if self._started:
308
+ self.progress.update(
309
+ self.task,
310
+ description=f"[cyan]{description}"
311
+ )
@@ -0,0 +1,13 @@
1
+ """Input validation utilities."""
2
+
3
+ from coding_assistant.validation.inputs import InputValidator
4
+ from coding_assistant.validation.files import FileValidator
5
+ from coding_assistant.validation.params import ParameterValidator
6
+ from coding_assistant.validation.sanitizers import InputSanitizer
7
+
8
+ __all__ = [
9
+ 'InputValidator',
10
+ 'FileValidator',
11
+ 'ParameterValidator',
12
+ 'InputSanitizer',
13
+ ]