kollabor 0.4.9__py3-none-any.whl → 0.4.15__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 (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -72,6 +72,9 @@ class ModalStateManager:
72
72
  self.saved_snapshot: Optional[TerminalSnapshot] = None
73
73
  self.current_layout: Optional[ModalLayout] = None
74
74
  self.modal_content_cache: List[str] = []
75
+ # Track max dimensions seen during this modal session for proper clearing
76
+ self._max_rendered_height: int = 0
77
+ self._max_rendered_width: int = 0
75
78
 
76
79
  def _strip_ansi(self, text: str) -> str:
77
80
  """Remove ANSI escape codes from text.
@@ -141,7 +144,18 @@ class ModalStateManager:
141
144
  # Cache content for refresh operations
142
145
  self.modal_content_cache = content_lines.copy()
143
146
 
144
- # Clear previous modal content
147
+ # Track max dimensions for proper clearing when content shrinks
148
+ current_height = len(content_lines)
149
+ if current_height > self._max_rendered_height:
150
+ self._max_rendered_height = current_height
151
+
152
+ # Track max visible width (strip ANSI codes for accurate width)
153
+ if content_lines:
154
+ max_visible_width = max(len(self._strip_ansi(line)) for line in content_lines)
155
+ if max_visible_width > self._max_rendered_width:
156
+ self._max_rendered_width = max_visible_width
157
+
158
+ # Clear previous modal content (uses max dimensions to clear all artifacts)
145
159
  self._clear_modal_content_area()
146
160
 
147
161
  # Render new content using direct terminal output
@@ -384,6 +398,10 @@ class ModalStateManager:
384
398
  def _clear_modal_content_area(self) -> bool:
385
399
  """Clear the modal content area.
386
400
 
401
+ Uses _max_rendered_height and _max_rendered_width to ensure all
402
+ previously rendered content is cleared, even when content shrinks
403
+ (e.g., scrolling to the bottom of a list).
404
+
387
405
  Returns:
388
406
  True if area was cleared successfully.
389
407
  """
@@ -393,8 +411,13 @@ class ModalStateManager:
393
411
 
394
412
  layout = self.current_layout
395
413
 
414
+ # Use max rendered dimensions to clear all possible artifacts
415
+ # This handles the case where content shrinks (e.g., at bottom of list)
416
+ clear_height = max(layout.height, self._max_rendered_height)
417
+ clear_width = max(layout.width, self._max_rendered_width)
418
+
396
419
  # Clear each line of the modal area with spaces (overwrite modal content)
397
- for i in range(layout.height):
420
+ for i in range(clear_height):
398
421
  row = layout.start_row + i + 1 # 1-based row positioning
399
422
  col = layout.start_col + 1 # 1-based col positioning
400
423
 
@@ -402,10 +425,10 @@ class ModalStateManager:
402
425
  self.terminal_state.write_raw(f"\033[{row};{col}H")
403
426
 
404
427
  # Write spaces to overwrite modal content for this line
405
- spaces = " " * layout.width
428
+ spaces = " " * clear_width
406
429
  self.terminal_state.write_raw(spaces)
407
430
 
408
- logger.debug("Modal content area cleared")
431
+ logger.debug(f"Modal content area cleared ({clear_height}x{clear_width})")
409
432
  return True
410
433
 
411
434
  except Exception as e:
@@ -419,6 +442,8 @@ class ModalStateManager:
419
442
  self.current_layout = None
420
443
  self.modal_content_cache = []
421
444
  self.display_mode = ModalDisplayMode.OVERLAY
445
+ self._max_rendered_height = 0
446
+ self._max_rendered_width = 0
422
447
 
423
448
  def get_modal_state_info(self) -> Dict[str, Any]:
424
449
  """Get current modal state information.
@@ -53,6 +53,13 @@ class BaseWidget(ABC):
53
53
  Returns:
54
54
  Current configuration value for this widget's config path.
55
55
  """
56
+ # First check if widget config has an explicit 'value' or 'current_value' field
57
+ # This is used for form modals with pre-populated data
58
+ if "value" in self.config:
59
+ return self.config["value"]
60
+ if "current_value" in self.config:
61
+ return self.config["current_value"]
62
+
56
63
  # Try to get real value from config service
57
64
  if self.config_service:
58
65
  try:
@@ -0,0 +1,10 @@
1
+ """Release update checking system for Kollabor CLI.
2
+
3
+ This module provides functionality to check for new releases via GitHub API,
4
+ cache results, and notify users of available updates.
5
+ """
6
+
7
+ from .version_check_service import VersionCheckService, ReleaseInfo
8
+ from .version_comparator import is_newer_version, compare_versions
9
+
10
+ __all__ = ["VersionCheckService", "ReleaseInfo", "is_newer_version", "compare_versions"]
@@ -0,0 +1,348 @@
1
+ """Version check service for Kollabor CLI.
2
+
3
+ Checks GitHub Releases API for new versions, caches results in config,
4
+ and provides release information for user notifications.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import time
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+
13
+ import aiohttp
14
+
15
+ from .version_comparator import is_newer_version
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ReleaseInfo:
22
+ """Information about a GitHub release.
23
+
24
+ Attributes:
25
+ version: Semantic version number (e.g., "0.5.0")
26
+ tag_name: Git tag name (e.g., "v0.5.0")
27
+ name: Human-readable release name (e.g., "Version 0.5.0")
28
+ url: GitHub release page URL
29
+ is_prerelease: Whether this is a pre-release version
30
+ """
31
+
32
+ version: str
33
+ tag_name: str
34
+ name: str
35
+ url: str
36
+ is_prerelease: bool
37
+
38
+
39
+ class VersionCheckService:
40
+ """Service for checking GitHub releases and notifying of updates.
41
+
42
+ This service:
43
+ - Fetches latest release info from GitHub API
44
+ - Caches results in config for configurable TTL (default 24 hours)
45
+ - Compares versions using semantic versioning
46
+ - Handles network errors gracefully
47
+ - Never blocks application startup
48
+
49
+ Configuration keys:
50
+ core.updates.check_enabled (bool): Enable update checking
51
+ core.updates.check_interval_hours (int): Cache TTL in hours
52
+ core.updates.github_repo (str): Repository path (owner/repo)
53
+ core.updates.timeout_seconds (int): HTTP request timeout
54
+ core.updates.include_prereleases (bool): Include pre-release versions
55
+
56
+ Cache keys (stored in config):
57
+ core.updates.last_check_timestamp (int): Unix timestamp of last check
58
+ core.updates.cached_latest_version (str): Cached version string
59
+ core.updates.cached_release_url (str): Cached release URL
60
+ core.updates.cached_release_name (str): Cached release name
61
+ """
62
+
63
+ def __init__(self, config, current_version: str):
64
+ """Initialize version check service.
65
+
66
+ Args:
67
+ config: ConfigService instance for settings and cache storage
68
+ current_version: Current application version string
69
+ """
70
+ self.config = config
71
+ self.current_version = current_version
72
+
73
+ # Load configuration with defaults
74
+ self.check_enabled = config.get("core.updates.check_enabled", True)
75
+ self.check_interval_hours = config.get("core.updates.check_interval_hours", 24)
76
+ self.github_repo = config.get(
77
+ "core.updates.github_repo", "kollaborai/kollabor-cli"
78
+ )
79
+ self.timeout_seconds = config.get("core.updates.timeout_seconds", 5)
80
+ self.include_prereleases = config.get(
81
+ "core.updates.include_prereleases", False
82
+ )
83
+
84
+ # HTTP session (initialized in initialize())
85
+ self.session: Optional[aiohttp.ClientSession] = None
86
+
87
+ logger.debug(
88
+ f"VersionCheckService initialized: "
89
+ f"enabled={self.check_enabled}, "
90
+ f"interval={self.check_interval_hours}h, "
91
+ f"repo={self.github_repo}"
92
+ )
93
+
94
+ async def initialize(self) -> None:
95
+ """Initialize HTTP session and load cached data.
96
+
97
+ Creates aiohttp ClientSession with configured timeout and headers.
98
+ """
99
+ if not self.check_enabled:
100
+ logger.debug("Update checking disabled in config")
101
+ return
102
+
103
+ # Create HTTP session with timeout
104
+ timeout = aiohttp.ClientTimeout(total=self.timeout_seconds)
105
+
106
+ self.session = aiohttp.ClientSession(
107
+ timeout=timeout,
108
+ headers={
109
+ "Accept": "application/vnd.github+json",
110
+ "User-Agent": f"Kollabor-CLI/{self.current_version}",
111
+ },
112
+ )
113
+
114
+ logger.debug("VersionCheckService session initialized")
115
+
116
+ async def check_for_updates(self) -> Optional[ReleaseInfo]:
117
+ """Check if a newer version is available.
118
+
119
+ Workflow:
120
+ 1. Return None if disabled in config
121
+ 2. Check cache validity (24h TTL)
122
+ 3. If cache valid, return cached data
123
+ 4. If cache invalid/missing, fetch from GitHub API
124
+ 5. Parse response and compare versions
125
+ 6. Update cache if newer version found
126
+ 7. Return ReleaseInfo or None
127
+
128
+ Returns:
129
+ ReleaseInfo if newer version available, None otherwise
130
+
131
+ Note:
132
+ All errors are caught and logged as warnings. This method
133
+ never raises exceptions to avoid blocking startup.
134
+ """
135
+ try:
136
+ # Check if enabled
137
+ if not self.check_enabled:
138
+ logger.debug("Update checking disabled")
139
+ return None
140
+
141
+ # Check cache validity
142
+ if self._is_cache_valid():
143
+ logger.debug("Using cached release data")
144
+ cached_release = self._get_cached_release()
145
+ if cached_release and self._is_update_available(cached_release):
146
+ return cached_release
147
+ return None
148
+
149
+ # Fetch from GitHub API
150
+ logger.debug(f"Fetching latest release from GitHub: {self.github_repo}")
151
+ release_data = await self._fetch_latest_release()
152
+
153
+ if not release_data:
154
+ # API call failed, try cached data
155
+ logger.debug("API call failed, attempting to use cached data")
156
+ cached_release = self._get_cached_release()
157
+ if cached_release and self._is_update_available(cached_release):
158
+ return cached_release
159
+ return None
160
+
161
+ # Parse release data
162
+ release_info = self._parse_release_data(release_data)
163
+ if not release_info:
164
+ return None
165
+
166
+ # Update cache
167
+ self._update_cache(release_info)
168
+
169
+ # Check if update available
170
+ if self._is_update_available(release_info):
171
+ logger.info(f"New version available: {release_info.version}")
172
+ return release_info
173
+
174
+ logger.debug(f"Current version {self.current_version} is up to date")
175
+ return None
176
+
177
+ except Exception as e:
178
+ # Catch-all for unexpected errors
179
+ logger.warning(f"Unexpected error during update check: {e}")
180
+ return None
181
+
182
+ async def _fetch_latest_release(self) -> Optional[dict]:
183
+ """Fetch latest release from GitHub API.
184
+
185
+ Returns:
186
+ Parsed JSON response or None on error
187
+ """
188
+ if not self.session:
189
+ logger.warning("HTTP session not initialized")
190
+ return None
191
+
192
+ url = f"https://api.github.com/repos/{self.github_repo}/releases/latest"
193
+
194
+ try:
195
+ async with self.session.get(url) as response:
196
+ if response.status == 200:
197
+ data = await response.json()
198
+ logger.debug(f"GitHub API returned: {data.get('tag_name')}")
199
+ return data
200
+ elif response.status == 404:
201
+ logger.warning(f"Repository {self.github_repo} not found")
202
+ return None
203
+ elif response.status == 403:
204
+ logger.warning("GitHub API rate limit exceeded")
205
+ return None
206
+ else:
207
+ logger.warning(f"GitHub API returned status {response.status}")
208
+ return None
209
+
210
+ except asyncio.TimeoutError:
211
+ logger.warning(
212
+ f"GitHub API timeout after {self.timeout_seconds}s - using cached data"
213
+ )
214
+ return None
215
+
216
+ except aiohttp.ClientError as e:
217
+ logger.warning(f"GitHub API error: {e} - using cached data")
218
+ return None
219
+
220
+ except Exception as e:
221
+ logger.warning(f"Unexpected error fetching release: {e}")
222
+ return None
223
+
224
+ def _parse_release_data(self, release_json: dict) -> Optional[ReleaseInfo]:
225
+ """Parse GitHub release API response.
226
+
227
+ Args:
228
+ release_json: Parsed JSON response from GitHub API
229
+
230
+ Returns:
231
+ ReleaseInfo instance or None on error
232
+ """
233
+ try:
234
+ tag_name = release_json["tag_name"]
235
+ name = release_json.get("name", tag_name)
236
+ url = release_json["html_url"]
237
+ is_prerelease = release_json.get("prerelease", False)
238
+
239
+ # Strip 'v' prefix from tag to get version
240
+ version = tag_name.lstrip("v")
241
+
242
+ # Filter pre-releases if configured
243
+ if is_prerelease and not self.include_prereleases:
244
+ logger.debug(f"Skipping pre-release version: {version}")
245
+ return None
246
+
247
+ release_info = ReleaseInfo(
248
+ version=version,
249
+ tag_name=tag_name,
250
+ name=name,
251
+ url=url,
252
+ is_prerelease=is_prerelease,
253
+ )
254
+
255
+ logger.debug(f"Parsed release info: {version}")
256
+ return release_info
257
+
258
+ except KeyError as e:
259
+ logger.warning(f"Invalid release data: missing field {e}")
260
+ return None
261
+
262
+ except Exception as e:
263
+ logger.warning(f"Error parsing release data: {e}")
264
+ return None
265
+
266
+ def _is_update_available(self, release_info: ReleaseInfo) -> bool:
267
+ """Check if release is newer than current version.
268
+
269
+ Args:
270
+ release_info: Release information to check
271
+
272
+ Returns:
273
+ True if release is newer than current version
274
+ """
275
+ try:
276
+ return is_newer_version(self.current_version, release_info.version)
277
+ except ValueError as e:
278
+ logger.warning(f"Version comparison failed: {e}")
279
+ return False
280
+
281
+ def _is_cache_valid(self) -> bool:
282
+ """Check if cached data is still valid based on TTL.
283
+
284
+ Returns:
285
+ True if cache is valid (age < TTL)
286
+ """
287
+ last_check = self.config.get("core.updates.last_check_timestamp", 0)
288
+ current_time = int(time.time())
289
+ ttl_seconds = self.check_interval_hours * 3600
290
+
291
+ age_seconds = current_time - last_check
292
+ is_valid = age_seconds < ttl_seconds
293
+
294
+ logger.debug(
295
+ f"Cache validity: age={age_seconds}s, ttl={ttl_seconds}s, valid={is_valid}"
296
+ )
297
+
298
+ return is_valid
299
+
300
+ def _get_cached_release(self) -> Optional[ReleaseInfo]:
301
+ """Retrieve cached release information from config.
302
+
303
+ Returns:
304
+ ReleaseInfo from cache or None if not cached
305
+ """
306
+ version = self.config.get("core.updates.cached_latest_version")
307
+ if not version:
308
+ logger.debug("No cached release data found")
309
+ return None
310
+
311
+ url = self.config.get("core.updates.cached_release_url", "")
312
+ name = self.config.get(
313
+ "core.updates.cached_release_name", f"Version {version}"
314
+ )
315
+
316
+ release_info = ReleaseInfo(
317
+ version=version,
318
+ tag_name=f"v{version}",
319
+ name=name,
320
+ url=url,
321
+ is_prerelease=False,
322
+ )
323
+
324
+ logger.debug(f"Retrieved cached release: {version}")
325
+ return release_info
326
+
327
+ def _update_cache(self, release_info: ReleaseInfo) -> None:
328
+ """Update cached release information in config.
329
+
330
+ Args:
331
+ release_info: Release information to cache
332
+ """
333
+ current_time = int(time.time())
334
+
335
+ self.config.set("core.updates.last_check_timestamp", current_time)
336
+ self.config.set("core.updates.cached_latest_version", release_info.version)
337
+ self.config.set("core.updates.cached_release_url", release_info.url)
338
+ self.config.set("core.updates.cached_release_name", release_info.name)
339
+
340
+ logger.debug(
341
+ f"Updated cache with version {release_info.version} at timestamp {current_time}"
342
+ )
343
+
344
+ async def shutdown(self) -> None:
345
+ """Close HTTP session and cleanup resources."""
346
+ if self.session:
347
+ await self.session.close()
348
+ logger.debug("VersionCheckService session closed")
@@ -0,0 +1,103 @@
1
+ """Semantic version comparison utilities for Kollabor CLI.
2
+
3
+ Provides version comparison logic using the packaging library to handle
4
+ semantic versioning correctly (including pre-releases, build metadata, etc.).
5
+ """
6
+
7
+ import logging
8
+ from packaging.version import Version, InvalidVersion
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def is_newer_version(current: str, latest: str) -> bool:
14
+ """Check if latest version is newer than current version.
15
+
16
+ Uses semantic versioning comparison via the packaging library.
17
+ Handles version prefixes (v0.4.11), pre-releases (0.5.0-beta),
18
+ and build metadata (0.5.0+build123).
19
+
20
+ Args:
21
+ current: Current version string (e.g., "0.4.11" or "v0.4.11")
22
+ latest: Latest version string from GitHub API
23
+
24
+ Returns:
25
+ True if latest > current, False otherwise
26
+
27
+ Raises:
28
+ ValueError: If version format is invalid
29
+
30
+ Examples:
31
+ >>> is_newer_version("0.4.11", "0.5.0")
32
+ True
33
+ >>> is_newer_version("v0.5.0", "0.4.11")
34
+ False
35
+ >>> is_newer_version("0.5.0-beta", "0.5.0")
36
+ True
37
+ """
38
+ try:
39
+ # Normalize versions (strip 'v' prefix if present)
40
+ current_clean = current.lstrip("v")
41
+ latest_clean = latest.lstrip("v")
42
+
43
+ # Use packaging.version for robust semantic versioning
44
+ current_ver = Version(current_clean)
45
+ latest_ver = Version(latest_clean)
46
+
47
+ result = latest_ver > current_ver
48
+
49
+ logger.debug(
50
+ f"Version comparison: {current} vs {latest} -> "
51
+ f"latest is newer: {result}"
52
+ )
53
+
54
+ return result
55
+
56
+ except InvalidVersion as e:
57
+ error_msg = f"Invalid version format: {e}"
58
+ logger.warning(error_msg)
59
+ raise ValueError(error_msg) from e
60
+
61
+
62
+ def compare_versions(current: str, latest: str) -> int:
63
+ """Compare two version strings using semantic versioning.
64
+
65
+ Args:
66
+ current: Current version string
67
+ latest: Latest version string
68
+
69
+ Returns:
70
+ -1 if current < latest
71
+ 0 if current == latest
72
+ 1 if current > latest
73
+
74
+ Raises:
75
+ ValueError: If version format is invalid
76
+
77
+ Examples:
78
+ >>> compare_versions("0.4.11", "0.5.0")
79
+ -1
80
+ >>> compare_versions("0.5.0", "0.5.0")
81
+ 0
82
+ >>> compare_versions("0.6.0", "0.5.0")
83
+ 1
84
+ """
85
+ try:
86
+ # Normalize versions
87
+ current_clean = current.lstrip("v")
88
+ latest_clean = latest.lstrip("v")
89
+
90
+ current_ver = Version(current_clean)
91
+ latest_ver = Version(latest_clean)
92
+
93
+ if current_ver < latest_ver:
94
+ return -1
95
+ elif current_ver > latest_ver:
96
+ return 1
97
+ else:
98
+ return 0
99
+
100
+ except InvalidVersion as e:
101
+ error_msg = f"Invalid version format: {e}"
102
+ logger.warning(error_msg)
103
+ raise ValueError(error_msg) from e