tunacode-cli 0.0.70__py3-none-any.whl → 0.0.78.6__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 tunacode-cli might be problematic. Click here for more details.

Files changed (90) hide show
  1. tunacode/cli/commands/__init__.py +0 -2
  2. tunacode/cli/commands/implementations/__init__.py +0 -3
  3. tunacode/cli/commands/implementations/debug.py +2 -2
  4. tunacode/cli/commands/implementations/development.py +10 -8
  5. tunacode/cli/commands/implementations/model.py +357 -29
  6. tunacode/cli/commands/implementations/system.py +3 -2
  7. tunacode/cli/commands/implementations/template.py +0 -2
  8. tunacode/cli/commands/registry.py +8 -7
  9. tunacode/cli/commands/slash/loader.py +2 -1
  10. tunacode/cli/commands/slash/validator.py +2 -1
  11. tunacode/cli/main.py +19 -1
  12. tunacode/cli/repl.py +90 -229
  13. tunacode/cli/repl_components/command_parser.py +2 -1
  14. tunacode/cli/repl_components/error_recovery.py +8 -5
  15. tunacode/cli/repl_components/output_display.py +1 -10
  16. tunacode/cli/repl_components/tool_executor.py +1 -13
  17. tunacode/configuration/defaults.py +2 -2
  18. tunacode/configuration/key_descriptions.py +284 -0
  19. tunacode/configuration/settings.py +0 -1
  20. tunacode/constants.py +6 -42
  21. tunacode/core/agents/__init__.py +43 -2
  22. tunacode/core/agents/agent_components/__init__.py +7 -0
  23. tunacode/core/agents/agent_components/agent_config.py +162 -158
  24. tunacode/core/agents/agent_components/agent_helpers.py +31 -2
  25. tunacode/core/agents/agent_components/node_processor.py +180 -146
  26. tunacode/core/agents/agent_components/response_state.py +123 -6
  27. tunacode/core/agents/agent_components/state_transition.py +116 -0
  28. tunacode/core/agents/agent_components/streaming.py +296 -0
  29. tunacode/core/agents/agent_components/task_completion.py +19 -6
  30. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  31. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  32. tunacode/core/agents/main.py +522 -370
  33. tunacode/core/agents/main_legact.py +538 -0
  34. tunacode/core/agents/prompts.py +66 -0
  35. tunacode/core/agents/utils.py +29 -122
  36. tunacode/core/setup/__init__.py +0 -2
  37. tunacode/core/setup/config_setup.py +88 -227
  38. tunacode/core/setup/config_wizard.py +230 -0
  39. tunacode/core/setup/coordinator.py +2 -1
  40. tunacode/core/state.py +16 -64
  41. tunacode/core/token_usage/usage_tracker.py +3 -1
  42. tunacode/core/tool_authorization.py +352 -0
  43. tunacode/core/tool_handler.py +67 -60
  44. tunacode/prompts/system.xml +751 -0
  45. tunacode/services/mcp.py +97 -1
  46. tunacode/setup.py +0 -23
  47. tunacode/tools/base.py +54 -1
  48. tunacode/tools/bash.py +14 -0
  49. tunacode/tools/glob.py +4 -2
  50. tunacode/tools/grep.py +7 -17
  51. tunacode/tools/prompts/glob_prompt.xml +1 -1
  52. tunacode/tools/prompts/grep_prompt.xml +1 -0
  53. tunacode/tools/prompts/list_dir_prompt.xml +1 -1
  54. tunacode/tools/prompts/react_prompt.xml +23 -0
  55. tunacode/tools/prompts/read_file_prompt.xml +1 -1
  56. tunacode/tools/react.py +153 -0
  57. tunacode/tools/run_command.py +15 -0
  58. tunacode/types.py +14 -79
  59. tunacode/ui/completers.py +434 -50
  60. tunacode/ui/config_dashboard.py +585 -0
  61. tunacode/ui/console.py +63 -11
  62. tunacode/ui/input.py +8 -3
  63. tunacode/ui/keybindings.py +0 -18
  64. tunacode/ui/model_selector.py +395 -0
  65. tunacode/ui/output.py +40 -19
  66. tunacode/ui/panels.py +173 -49
  67. tunacode/ui/path_heuristics.py +91 -0
  68. tunacode/ui/prompt_manager.py +1 -20
  69. tunacode/ui/tool_ui.py +30 -8
  70. tunacode/utils/api_key_validation.py +93 -0
  71. tunacode/utils/config_comparator.py +340 -0
  72. tunacode/utils/models_registry.py +593 -0
  73. tunacode/utils/text_utils.py +18 -1
  74. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
  75. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
  76. tunacode/cli/commands/implementations/plan.py +0 -50
  77. tunacode/cli/commands/implementations/todo.py +0 -217
  78. tunacode/context.py +0 -71
  79. tunacode/core/setup/git_safety_setup.py +0 -186
  80. tunacode/prompts/system.md +0 -359
  81. tunacode/prompts/system.md.bak +0 -487
  82. tunacode/tools/exit_plan_mode.py +0 -273
  83. tunacode/tools/present_plan.py +0 -288
  84. tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
  85. tunacode/tools/prompts/present_plan_prompt.xml +0 -20
  86. tunacode/tools/prompts/todo_prompt.xml +0 -96
  87. tunacode/tools/todo.py +0 -456
  88. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
  89. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  90. {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/ui/input.py CHANGED
@@ -89,20 +89,25 @@ async def multiline_input(
89
89
  "<bold>Enter</bold> to submit • "
90
90
  "<bold>Esc + Enter</bold> for new line • "
91
91
  "<bold>Esc twice</bold> to cancel • "
92
- "<bold>Shift + Tab</bold> toggle plan mode • "
93
92
  "<bold>/help</bold> for commands"
94
93
  "</darkgrey>"
95
94
  )
96
95
  )
97
96
 
98
- # Display input area (Plan Mode indicator is handled dynamically in prompt manager)
97
+ # Create models registry for auto-completion (lazy loaded)
98
+ from ..utils.models_registry import ModelsRegistry
99
+
100
+ models_registry = ModelsRegistry()
101
+ # Note: Registry will be loaded lazily by the completer when needed
102
+
103
+ # Display input area
99
104
  result = await input(
100
105
  "multiline",
101
106
  pretext="> ",
102
107
  key_bindings=kb,
103
108
  multiline=True,
104
109
  placeholder=placeholder,
105
- completer=create_completer(command_registry),
110
+ completer=create_completer(command_registry, models_registry),
106
111
  lexer=FileReferenceLexer(),
107
112
  state_manager=state_manager,
108
113
  )
@@ -50,22 +50,4 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
50
50
 
51
51
  os.kill(os.getpid(), signal.SIGINT)
52
52
 
53
- @kb.add("s-tab") # shift+tab
54
- def _toggle_plan_mode(event):
55
- """Toggle between Plan Mode and normal mode."""
56
- if state_manager:
57
- # Toggle the state
58
- if state_manager.is_plan_mode():
59
- state_manager.exit_plan_mode()
60
- logger.debug("Toggled to normal mode via Shift+Tab")
61
- else:
62
- state_manager.enter_plan_mode()
63
- logger.debug("Toggled to Plan Mode via Shift+Tab")
64
-
65
- # Clear the current buffer and refresh the display
66
- event.current_buffer.reset()
67
-
68
- # Force a refresh of the application without exiting
69
- event.app.invalidate()
70
-
71
53
  return kb
@@ -0,0 +1,395 @@
1
+ """Interactive model selector UI component."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from prompt_toolkit.application import Application
6
+ from prompt_toolkit.buffer import Buffer
7
+ from prompt_toolkit.formatted_text import HTML, StyleAndTextTuples
8
+ from prompt_toolkit.key_binding import KeyBindings
9
+ from prompt_toolkit.layout import (
10
+ FormattedTextControl,
11
+ HSplit,
12
+ Layout,
13
+ VSplit,
14
+ Window,
15
+ WindowAlign,
16
+ )
17
+ from prompt_toolkit.layout.controls import BufferControl
18
+ from prompt_toolkit.layout.dimension import Dimension
19
+ from prompt_toolkit.search import SearchState
20
+ from prompt_toolkit.styles import Style
21
+ from prompt_toolkit.widgets import Frame
22
+
23
+ from ..utils.models_registry import ModelInfo, ModelsRegistry
24
+
25
+
26
+ class ModelSelector:
27
+ """Interactive model selector with search and navigation."""
28
+
29
+ def __init__(self, registry: ModelsRegistry):
30
+ """Initialize the model selector."""
31
+ self.registry = registry
32
+ self.models: List[ModelInfo] = []
33
+ self.filtered_models: List[ModelInfo] = []
34
+ self.selected_index = 0
35
+ self.search_text = ""
36
+ self.selected_model: Optional[ModelInfo] = None
37
+
38
+ # Create key bindings
39
+ self.kb = self._create_key_bindings()
40
+
41
+ # Create search buffer
42
+ self.search_buffer = Buffer(on_text_changed=self._on_search_changed)
43
+
44
+ # Search state
45
+ self.search_state = SearchState()
46
+
47
+ def _create_key_bindings(self) -> KeyBindings:
48
+ """Create key bindings for the selector."""
49
+ kb = KeyBindings()
50
+
51
+ @kb.add("up", "k")
52
+ def move_up(event):
53
+ """Move selection up."""
54
+ if self.selected_index > 0:
55
+ self.selected_index -= 1
56
+ self._update_display()
57
+
58
+ @kb.add("down", "j")
59
+ def move_down(event):
60
+ """Move selection down."""
61
+ if self.selected_index < len(self.filtered_models) - 1:
62
+ self.selected_index += 1
63
+ self._update_display()
64
+
65
+ @kb.add("pageup")
66
+ def page_up(event):
67
+ """Move selection up by page."""
68
+ self.selected_index = max(0, self.selected_index - 10)
69
+ self._update_display()
70
+
71
+ @kb.add("pagedown")
72
+ def page_down(event):
73
+ """Move selection down by page."""
74
+ self.selected_index = min(len(self.filtered_models) - 1, self.selected_index + 10)
75
+ self._update_display()
76
+
77
+ @kb.add("enter")
78
+ def select_model(event):
79
+ """Select the current model."""
80
+ if 0 <= self.selected_index < len(self.filtered_models):
81
+ self.selected_model = self.filtered_models[self.selected_index]
82
+ event.app.exit(result=self.selected_model)
83
+
84
+ @kb.add("c-c", "escape", "q")
85
+ def cancel(event):
86
+ """Cancel selection."""
87
+ event.app.exit(result=None)
88
+
89
+ @kb.add("/")
90
+ def focus_search(event):
91
+ """Focus the search input."""
92
+ event.app.layout.focus(self.search_buffer)
93
+
94
+ @kb.add("tab")
95
+ def next_provider(event):
96
+ """Jump to next provider."""
97
+ if not self.filtered_models:
98
+ return
99
+
100
+ current_provider = self.filtered_models[self.selected_index].provider
101
+ for i in range(self.selected_index + 1, len(self.filtered_models)):
102
+ if self.filtered_models[i].provider != current_provider:
103
+ self.selected_index = i
104
+ self._update_display()
105
+ break
106
+
107
+ @kb.add("s-tab")
108
+ def prev_provider(event):
109
+ """Jump to previous provider."""
110
+ if not self.filtered_models:
111
+ return
112
+
113
+ current_provider = self.filtered_models[self.selected_index].provider
114
+ for i in range(self.selected_index - 1, -1, -1):
115
+ if self.filtered_models[i].provider != current_provider:
116
+ self.selected_index = i
117
+ self._update_display()
118
+ break
119
+
120
+ return kb
121
+
122
+ def _on_search_changed(self, buffer: Buffer) -> None:
123
+ """Handle search text changes."""
124
+ self.search_text = buffer.text
125
+ self._filter_models()
126
+ self._update_display()
127
+
128
+ def _filter_models(self) -> None:
129
+ """Filter models based on search text."""
130
+ if not self.search_text:
131
+ self.filtered_models = self.models.copy()
132
+ else:
133
+ # Search and sort by relevance
134
+ self.filtered_models = self.registry.search_models(self.search_text)
135
+
136
+ # Reset selection
137
+ self.selected_index = 0 if self.filtered_models else -1
138
+
139
+ def _get_model_lines(self) -> List[StyleAndTextTuples]:
140
+ """Get formatted lines for model display."""
141
+ lines = []
142
+
143
+ if not self.filtered_models:
144
+ lines.append([("class:muted", "No models found")])
145
+ return lines
146
+
147
+ # Group models by provider
148
+ current_provider = None
149
+ for i, model in enumerate(self.filtered_models):
150
+ # Add provider header if changed
151
+ if model.provider != current_provider:
152
+ if current_provider is not None:
153
+ lines.append([]) # Empty line between providers
154
+
155
+ provider_info = self.registry.providers.get(model.provider)
156
+ provider_name = provider_info.name if provider_info else model.provider
157
+ lines.append([("class:provider", f"▼ {provider_name}")])
158
+ current_provider = model.provider
159
+
160
+ # Model line
161
+ is_selected = i == self.selected_index
162
+
163
+ # Build model display
164
+ parts = []
165
+
166
+ # Selection indicator
167
+ if is_selected:
168
+ parts.append(("class:selected", "→ "))
169
+ else:
170
+ parts.append(("", " "))
171
+
172
+ # Model ID and name
173
+ parts.append(
174
+ ("class:model-id" if not is_selected else "class:selected-id", f"{model.id}")
175
+ )
176
+ parts.append(("class:muted", " - "))
177
+ parts.append(
178
+ ("class:model-name" if not is_selected else "class:selected-name", model.name)
179
+ )
180
+
181
+ # Cost and limits
182
+ details = []
183
+ if model.cost.input is not None:
184
+ details.append(f"${model.cost.input}/{model.cost.output}")
185
+ if model.limits.context:
186
+ details.append(f"{model.limits.context // 1000}k")
187
+
188
+ if details:
189
+ parts.append(("class:muted", f" ({', '.join(details)})"))
190
+
191
+ # Capabilities badges
192
+ badges = []
193
+ if model.capabilities.attachment:
194
+ badges.append("📎")
195
+ if model.capabilities.reasoning:
196
+ badges.append("🧠")
197
+ if model.capabilities.tool_call:
198
+ badges.append("🔧")
199
+
200
+ if badges:
201
+ parts.append(("class:badges", " " + "".join(badges)))
202
+
203
+ lines.append(parts)
204
+
205
+ return lines
206
+
207
+ def _get_details_panel(self) -> StyleAndTextTuples:
208
+ """Get the details panel content for selected model."""
209
+ if not self.filtered_models or self.selected_index < 0:
210
+ return [("", "Select a model to see details")]
211
+
212
+ model = self.filtered_models[self.selected_index]
213
+ lines = []
214
+
215
+ # Model name and ID
216
+ lines.append([("class:title", model.name)])
217
+ lines.append([("class:muted", f"{model.full_id}")])
218
+ lines.append([])
219
+
220
+ # Pricing
221
+ lines.append([("class:section", "Pricing:")])
222
+ if model.cost.input is not None:
223
+ lines.append([("", f" Input: ${model.cost.input} per 1M tokens")])
224
+ lines.append([("", f" Output: ${model.cost.output} per 1M tokens")])
225
+ else:
226
+ lines.append([("class:muted", " Not available")])
227
+ lines.append([])
228
+
229
+ # Limits
230
+ lines.append([("class:section", "Limits:")])
231
+ if model.limits.context:
232
+ lines.append([("", f" Context: {model.limits.context:,} tokens")])
233
+ if model.limits.output:
234
+ lines.append([("", f" Output: {model.limits.output:,} tokens")])
235
+ if not model.limits.context and not model.limits.output:
236
+ lines.append([("class:muted", " Not specified")])
237
+ lines.append([])
238
+
239
+ # Capabilities
240
+ lines.append([("class:section", "Capabilities:")])
241
+ caps = []
242
+ if model.capabilities.attachment:
243
+ caps.append("Attachments")
244
+ if model.capabilities.reasoning:
245
+ caps.append("Reasoning")
246
+ if model.capabilities.tool_call:
247
+ caps.append("Tool calling")
248
+ if model.capabilities.temperature:
249
+ caps.append("Temperature control")
250
+
251
+ if caps:
252
+ for cap in caps:
253
+ lines.append([("", f" ✓ {cap}")])
254
+ else:
255
+ lines.append([("class:muted", " Basic text generation")])
256
+
257
+ if model.capabilities.knowledge:
258
+ lines.append([])
259
+ lines.append([("class:section", "Knowledge cutoff:")])
260
+ lines.append([("", f" {model.capabilities.knowledge}")])
261
+
262
+ # Modalities
263
+ if model.modalities:
264
+ lines.append([])
265
+ lines.append([("class:section", "Modalities:")])
266
+ if "input" in model.modalities:
267
+ lines.append([("", f" Input: {', '.join(model.modalities['input'])}")])
268
+ if "output" in model.modalities:
269
+ lines.append([("", f" Output: {', '.join(model.modalities['output'])}")])
270
+
271
+ return lines
272
+
273
+ def _update_display(self) -> None:
274
+ """Update the display (called on changes)."""
275
+ # This will trigger a redraw through prompt_toolkit's event system
276
+ if hasattr(self, "app"):
277
+ self.app.invalidate()
278
+
279
+ def _create_layout(self) -> Layout:
280
+ """Create the application layout."""
281
+ # Model list
282
+ model_list = FormattedTextControl(self._get_model_lines, focusable=False, show_cursor=False)
283
+
284
+ model_window = Window(
285
+ content=model_list,
286
+ width=Dimension(min=40, preferred=60),
287
+ height=Dimension(min=10, preferred=20),
288
+ scroll_offsets=True,
289
+ wrap_lines=False,
290
+ )
291
+
292
+ # Details panel
293
+ details_control = FormattedTextControl(
294
+ self._get_details_panel, focusable=False, show_cursor=False
295
+ )
296
+
297
+ details_window = Window(
298
+ content=details_control, width=Dimension(min=30, preferred=40), wrap_lines=True
299
+ )
300
+
301
+ # Search bar
302
+ search_field = Window(
303
+ BufferControl(buffer=self.search_buffer, focus_on_click=True), height=1
304
+ )
305
+
306
+ search_label = Window(
307
+ FormattedTextControl(HTML("<b>Search:</b> ")), width=8, height=1, dont_extend_width=True
308
+ )
309
+
310
+ search_bar = VSplit([search_label, search_field])
311
+
312
+ # Help text
313
+ help_text = Window(
314
+ FormattedTextControl(
315
+ HTML(
316
+ "<muted>↑↓: Navigate | Enter: Select | /: Search | Tab: Next provider | "
317
+ "Esc: Cancel</muted>"
318
+ )
319
+ ),
320
+ height=1,
321
+ align=WindowAlign.CENTER,
322
+ )
323
+
324
+ # Main content
325
+ content = VSplit(
326
+ [Frame(model_window, title="Select Model"), Frame(details_window, title="Details")]
327
+ )
328
+
329
+ # Root layout
330
+ root = HSplit(
331
+ [
332
+ search_bar,
333
+ Window(height=1), # Spacer
334
+ content,
335
+ Window(height=1), # Spacer
336
+ help_text,
337
+ ]
338
+ )
339
+
340
+ return Layout(root)
341
+
342
+ async def select_model(self, initial_query: str = "") -> Optional[ModelInfo]:
343
+ """Show the model selector and return selected model."""
344
+ # Load all models
345
+ self.models = list(self.registry.models.values())
346
+ self.search_buffer.text = initial_query
347
+
348
+ # Filter initially
349
+ self._filter_models()
350
+
351
+ # Create application
352
+ self.app = Application(
353
+ layout=self._create_layout(),
354
+ key_bindings=self.kb,
355
+ mouse_support=True,
356
+ full_screen=False,
357
+ style=self._get_style(),
358
+ )
359
+
360
+ # Run the selector
361
+ result = await self.app.run_async()
362
+ return result
363
+
364
+ def _get_style(self) -> Style:
365
+ """Get the style for the selector."""
366
+ return Style.from_dict(
367
+ {
368
+ "provider": "bold cyan",
369
+ "model-id": "white",
370
+ "model-name": "ansiwhite",
371
+ "selected": "reverse bold",
372
+ "selected-id": "reverse bold white",
373
+ "selected-name": "reverse bold ansiwhite",
374
+ "muted": "gray",
375
+ "badges": "yellow",
376
+ "title": "bold ansiwhite",
377
+ "section": "bold cyan",
378
+ }
379
+ )
380
+
381
+
382
+ async def select_model_interactive(
383
+ registry: Optional[ModelsRegistry] = None, initial_query: str = ""
384
+ ) -> Optional[str]:
385
+ """Show interactive model selector and return selected model ID."""
386
+ if registry is None:
387
+ registry = ModelsRegistry()
388
+ await registry.load()
389
+
390
+ selector = ModelSelector(registry)
391
+ model = await selector.select_model(initial_query)
392
+
393
+ if model:
394
+ return model.full_id
395
+ return None
tunacode/ui/output.py CHANGED
@@ -1,12 +1,10 @@
1
1
  """Output and display functions for TunaCode UI."""
2
2
 
3
- from typing import Optional
3
+ from typing import TYPE_CHECKING, Any, Optional
4
4
 
5
5
  from prompt_toolkit.application import run_in_terminal
6
- from rich.console import Console
7
6
  from rich.padding import Padding
8
7
 
9
- from tunacode.configuration.settings import ApplicationSettings
10
8
  from tunacode.constants import (
11
9
  MSG_UPDATE_AVAILABLE,
12
10
  MSG_UPDATE_INSTRUCTION,
@@ -22,27 +20,48 @@ from .constants import SPINNER_TYPE
22
20
  from .decorators import create_sync_wrapper
23
21
  from .logging_compat import ui_logger
24
22
 
25
- # Create console with explicit settings to ensure ANSI codes work properly
26
- console = Console()
23
+ # Lazy console initialization
24
+ if TYPE_CHECKING:
25
+ from rich.console import Console
26
+
27
+ _console: Optional["Console"] = None
28
+
29
+
30
+ def get_console() -> "Console":
31
+ """Get console instance lazily."""
32
+ from rich.console import Console
33
+
34
+ global _console
35
+ if _console is None:
36
+ _console = Console()
37
+ return _console
38
+
39
+
40
+ class _LazyConsole:
41
+ """Lightweight proxy that defers Console creation."""
42
+
43
+ def __getattr__(self, name: str) -> Any:
44
+ return getattr(get_console(), name)
45
+
46
+ def __str__(self) -> str:
47
+ return str(get_console())
48
+
49
+
50
+ console = _LazyConsole()
27
51
  colors = DotDict(UI_COLORS)
28
52
 
29
53
  BANNER = """[bold cyan]
30
- ████████╗██╗ ██╗███╗ ██╗ █████╗
31
- ╚══██╔══╝██║ ██║████╗ ██║██╔══██╗
32
- ██║ ██║ ██║██╔██╗ ██║███████║
33
- ██║ ██║ ██║██║╚██╗██║██╔══██║
34
- ██║ ╚██████╔╝██║ ╚████║██║ ██║
35
- ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
36
-
37
- ██████╗ ██████╗ ██████╗ ███████╗ dev
38
- ██╔════╝██╔═══██╗██╔══██╗██╔════╝
39
- ██║ ██║ ██║██║ ██║█████╗
40
- ██║ ██║ ██║██║ ██║██╔══╝
41
- ╚██████╗╚██████╔╝██████╔╝███████╗
42
- ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
54
+ ▐█████▌
55
+ ▐█▛█▛█▛█▛█▛█▌
56
+ ▐█████████████▌
57
+ ▐██ ██▌
58
+ ▐█████████████▌
59
+ ▐█▛█▛█▛█▛█▛█▌
60
+ ▐█████▌
61
+ TunaCode
43
62
  [/bold cyan]
44
63
 
45
- ● Caution: This tool can modify your codebase - always use git branches"""
64
+ """
46
65
 
47
66
 
48
67
  @create_sync_wrapper
@@ -83,6 +102,8 @@ async def usage(usage: str) -> None:
83
102
 
84
103
  async def version() -> None:
85
104
  """Print version information."""
105
+ from tunacode.configuration.settings import ApplicationSettings
106
+
86
107
  app_settings = ApplicationSettings()
87
108
  await info(MSG_VERSION_DISPLAY.format(version=app_settings.version))
88
109