tunacode-cli 0.0.1__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 (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
tunacode/__init__.py ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ # CLI package
2
+ from .main import app
3
+
4
+ __all__ = ["app"]
@@ -0,0 +1,632 @@
1
+ """Command system for Sidekick CLI."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import Any, Dict, List, Optional, Type
7
+
8
+ from .. import utils
9
+ from ..configuration.models import ModelRegistry
10
+ from ..exceptions import ValidationError
11
+ from ..services.undo_service import perform_undo
12
+ from ..types import CommandArgs, CommandContext, CommandResult, ProcessRequestCallback
13
+ from ..ui import console as ui
14
+
15
+
16
+ class CommandCategory(Enum):
17
+ """Categories for organizing commands."""
18
+
19
+ SYSTEM = "system"
20
+ NAVIGATION = "navigation"
21
+ DEVELOPMENT = "development"
22
+ MODEL = "model"
23
+ DEBUG = "debug"
24
+
25
+
26
+ class Command(ABC):
27
+ """Base class for all commands."""
28
+
29
+ @property
30
+ @abstractmethod
31
+ def name(self) -> str:
32
+ """The primary name of the command."""
33
+ pass
34
+
35
+ @property
36
+ @abstractmethod
37
+ def aliases(self) -> CommandArgs:
38
+ """Alternative names/aliases for the command."""
39
+ pass
40
+
41
+ @property
42
+ def description(self) -> str:
43
+ """Description of what the command does."""
44
+ return ""
45
+
46
+ @property
47
+ def category(self) -> CommandCategory:
48
+ """Category this command belongs to."""
49
+ return CommandCategory.SYSTEM
50
+
51
+ @abstractmethod
52
+ async def execute(self, args: CommandArgs, context: CommandContext) -> CommandResult:
53
+ """
54
+ Execute the command.
55
+
56
+ Args:
57
+ args: Command arguments (excluding the command name)
58
+ context: Execution context with state and config
59
+
60
+ Returns:
61
+ Command-specific return value
62
+ """
63
+ pass
64
+
65
+
66
+ @dataclass
67
+ class CommandSpec:
68
+ """Specification for a command's metadata."""
69
+
70
+ name: str
71
+ aliases: List[str]
72
+ description: str
73
+ category: CommandCategory = CommandCategory.SYSTEM
74
+
75
+
76
+ class SimpleCommand(Command):
77
+ """Base class for simple commands without complex logic."""
78
+
79
+ def __init__(self, spec: CommandSpec):
80
+ self.spec = spec
81
+
82
+ @property
83
+ def name(self) -> str:
84
+ """The primary name of the command."""
85
+ return self.spec.name
86
+
87
+ @property
88
+ def aliases(self) -> CommandArgs:
89
+ """Alternative names/aliases for the command."""
90
+ return self.spec.aliases
91
+
92
+ @property
93
+ def description(self) -> str:
94
+ """Description of what the command does."""
95
+ return self.spec.description
96
+
97
+ @property
98
+ def category(self) -> CommandCategory:
99
+ """Category this command belongs to."""
100
+ return self.spec.category
101
+
102
+
103
+ class YoloCommand(SimpleCommand):
104
+ """Toggle YOLO mode (skip confirmations)."""
105
+
106
+ def __init__(self):
107
+ super().__init__(
108
+ CommandSpec(
109
+ name="yolo",
110
+ aliases=["/yolo"],
111
+ description="Toggle YOLO mode (skip tool confirmations)",
112
+ category=CommandCategory.DEVELOPMENT,
113
+ )
114
+ )
115
+
116
+ async def execute(self, args: List[str], context: CommandContext) -> None:
117
+ state = context.state_manager.session
118
+ state.yolo = not state.yolo
119
+ if state.yolo:
120
+ await ui.success("Ooh shit, its YOLO time!\n")
121
+ else:
122
+ await ui.info("Pfft, boring...\n")
123
+
124
+
125
+ class DumpCommand(SimpleCommand):
126
+ """Dump message history."""
127
+
128
+ def __init__(self):
129
+ super().__init__(
130
+ CommandSpec(
131
+ name="dump",
132
+ aliases=["/dump"],
133
+ description="Dump the current message history",
134
+ category=CommandCategory.DEBUG,
135
+ )
136
+ )
137
+
138
+ async def execute(self, args: List[str], context: CommandContext) -> None:
139
+ await ui.dump_messages(context.state_manager.session.messages)
140
+
141
+
142
+ class ClearCommand(SimpleCommand):
143
+ """Clear screen and message history."""
144
+
145
+ def __init__(self):
146
+ super().__init__(
147
+ CommandSpec(
148
+ name="clear",
149
+ aliases=["/clear"],
150
+ description="Clear the screen and message history",
151
+ category=CommandCategory.NAVIGATION,
152
+ )
153
+ )
154
+
155
+ async def execute(self, args: List[str], context: CommandContext) -> None:
156
+ await ui.clear()
157
+ context.state_manager.session.messages = []
158
+
159
+
160
+ class TunaCodeCommand(SimpleCommand):
161
+ """Use BM25 to inspect the codebase and read relevant files."""
162
+
163
+ def __init__(self):
164
+ super().__init__(
165
+ CommandSpec(
166
+ name="tunaCode",
167
+ aliases=["/tunaCode"],
168
+ description="Scan repo with BM25 and display key files",
169
+ category=CommandCategory.DEVELOPMENT,
170
+ )
171
+ )
172
+
173
+ async def execute(self, args: List[str], context: CommandContext) -> None:
174
+ from pathlib import Path
175
+
176
+ from tunacode.constants import UI_COLORS
177
+ from tunacode.utils.file_utils import DotDict
178
+
179
+ from ..tools.read_file import read_file
180
+ from ..utils.bm25 import BM25, tokenize
181
+ from ..utils.text_utils import ext_to_lang
182
+
183
+ colors = DotDict(UI_COLORS)
184
+
185
+ query = " ".join(args) if args else "overview"
186
+ await ui.info("Building BM25 index of repository")
187
+
188
+ docs: List[str] = []
189
+ paths: List[Path] = []
190
+ exts = {".py", ".js", ".ts", ".java", ".c", ".cpp", ".md", ".txt"}
191
+ for path in Path(".").rglob("*"):
192
+ if path.is_file() and path.suffix in exts:
193
+ try:
194
+ docs.append(path.read_text(encoding="utf-8"))
195
+ paths.append(path)
196
+ except Exception:
197
+ continue
198
+
199
+ if not docs:
200
+ await ui.error("No files found to index")
201
+ return
202
+
203
+ bm25 = BM25(docs)
204
+ scores = bm25.get_scores(tokenize(query))
205
+ ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:5]
206
+
207
+ for idx in ranked:
208
+ file_path = paths[idx]
209
+ content = await read_file(str(file_path))
210
+ lang = ext_to_lang(str(file_path))
211
+ await ui.panel(
212
+ str(file_path),
213
+ f"```{lang}\n{content}\n```",
214
+ border_style=colors.muted,
215
+ )
216
+
217
+
218
+ class HelpCommand(SimpleCommand):
219
+ """Show help information."""
220
+
221
+ def __init__(self, command_registry=None):
222
+ super().__init__(
223
+ CommandSpec(
224
+ name="help",
225
+ aliases=["/help"],
226
+ description="Show help information",
227
+ category=CommandCategory.SYSTEM,
228
+ )
229
+ )
230
+ self._command_registry = command_registry
231
+
232
+ async def execute(self, args: List[str], context: CommandContext) -> None:
233
+ await ui.help(self._command_registry)
234
+
235
+
236
+ class UndoCommand(SimpleCommand):
237
+ """Undo the last file operation."""
238
+
239
+ def __init__(self):
240
+ super().__init__(
241
+ CommandSpec(
242
+ name="undo",
243
+ aliases=["/undo"],
244
+ description="Undo the last file operation",
245
+ category=CommandCategory.DEVELOPMENT,
246
+ )
247
+ )
248
+
249
+ async def execute(self, args: List[str], context: CommandContext) -> None:
250
+ success, message = perform_undo(context.state_manager)
251
+ if success:
252
+ await ui.success(message)
253
+ else:
254
+ # Provide more helpful information when undo fails
255
+ await ui.warning(message)
256
+ if "not in a git repository" in message.lower():
257
+ await ui.muted("💡 To enable undo functionality:")
258
+ await ui.muted(" • Run 'git init' to initialize a git repository")
259
+ await ui.muted(" • Or work in a directory that's already a git repository")
260
+ await ui.muted(" • File operations will still work, but can't be undone")
261
+
262
+
263
+
264
+ class BranchCommand(SimpleCommand):
265
+ """Create and switch to a new git branch."""
266
+
267
+ def __init__(self):
268
+ super().__init__(
269
+ CommandSpec(
270
+ name="branch",
271
+ aliases=["/branch"],
272
+ description="Create and switch to a new git branch",
273
+ category=CommandCategory.DEVELOPMENT,
274
+ )
275
+ )
276
+
277
+ async def execute(self, args: List[str], context: CommandContext) -> None:
278
+ import subprocess
279
+
280
+ from ..services.undo_service import is_in_git_project
281
+
282
+ if not args:
283
+ await ui.error("Usage: /branch <branch-name>")
284
+ return
285
+
286
+ if not is_in_git_project():
287
+ await ui.error("Not a git repository")
288
+ return
289
+
290
+ branch_name = args[0]
291
+
292
+ try:
293
+ subprocess.run(
294
+ ["git", "checkout", "-b", branch_name],
295
+ capture_output=True,
296
+ text=True,
297
+ check=True,
298
+ timeout=5,
299
+ )
300
+ await ui.success(f"Switched to new branch '{branch_name}'")
301
+ except subprocess.TimeoutExpired:
302
+ await ui.error("Git command timed out")
303
+ except subprocess.CalledProcessError as e:
304
+ error_msg = e.stderr.strip() if e.stderr else str(e)
305
+ await ui.error(f"Git error: {error_msg}")
306
+ except FileNotFoundError:
307
+ await ui.error("Git executable not found")
308
+
309
+
310
+ class InitCommand(SimpleCommand):
311
+ """Analyse the repository and generate TUNACODE.md."""
312
+
313
+ def __init__(self):
314
+ super().__init__(
315
+ CommandSpec(
316
+ name="init",
317
+ aliases=["/init"],
318
+ description="Analyse the repo and create TUNACODE.md",
319
+ category=CommandCategory.DEVELOPMENT,
320
+ )
321
+ )
322
+
323
+ async def execute(self, args: List[str], context: CommandContext) -> None:
324
+ import json
325
+ from pathlib import Path
326
+
327
+ from .. import context as ctx
328
+
329
+ await ui.info("Gathering repository context")
330
+ data = await ctx.get_context()
331
+
332
+ prompt = (
333
+ "Using the following repository context, summarise build commands "
334
+ "and coding conventions. Return markdown for a TUNACODE.md file.\n\n"
335
+ + json.dumps(data, indent=2)
336
+ )
337
+
338
+ process_request = context.process_request
339
+ content = ""
340
+ if process_request:
341
+ res = await process_request(prompt, context.state_manager, output=False)
342
+ try:
343
+ content = res.result.output
344
+ except Exception:
345
+ content = ""
346
+
347
+ if not content:
348
+ content = "# TUNACODE\n\n" + json.dumps(data, indent=2)
349
+
350
+ Path("TUNACODE.md").write_text(content, encoding="utf-8")
351
+ await ui.success("TUNACODE.md written")
352
+
353
+
354
+ class CompactCommand(SimpleCommand):
355
+ """Compact conversation context."""
356
+
357
+ def __init__(self, process_request_callback: Optional[ProcessRequestCallback] = None):
358
+ super().__init__(
359
+ CommandSpec(
360
+ name="compact",
361
+ aliases=["/compact"],
362
+ description="Summarize and compact the conversation history",
363
+ category=CommandCategory.SYSTEM,
364
+ )
365
+ )
366
+ self._process_request = process_request_callback
367
+
368
+ async def execute(self, args: List[str], context: CommandContext) -> None:
369
+ # Use the injected callback or get it from context
370
+ process_request = self._process_request or context.process_request
371
+
372
+ if not process_request:
373
+ await ui.error("Compact command not available - process_request not configured")
374
+ return
375
+
376
+ # Get the current agent, create a summary of context, and trim message history
377
+ await process_request(
378
+ "Summarize the conversation so far", context.state_manager, output=False
379
+ )
380
+ await ui.success("Context history has been summarized and truncated.")
381
+ context.state_manager.session.messages = context.state_manager.session.messages[-2:]
382
+
383
+
384
+ class ModelCommand(SimpleCommand):
385
+ """Manage model selection."""
386
+
387
+ def __init__(self):
388
+ super().__init__(
389
+ CommandSpec(
390
+ name="model",
391
+ aliases=["/model"],
392
+ description="List models or select a model (e.g., /model 3 or /model 3 default)",
393
+ category=CommandCategory.MODEL,
394
+ )
395
+ )
396
+
397
+ async def execute(self, args: CommandArgs, context: CommandContext) -> Optional[str]:
398
+ if not args:
399
+ # No arguments - list models
400
+ await ui.models(context.state_manager)
401
+ return None
402
+
403
+ # Parse model index
404
+ try:
405
+ model_index = int(args[0])
406
+ except ValueError:
407
+ await ui.error(f"Invalid model index: {args[0]}")
408
+ return None
409
+
410
+ # Get model list
411
+ model_registry = ModelRegistry()
412
+ models = list(model_registry.list_models().keys())
413
+ if model_index < 0 or model_index >= len(models):
414
+ await ui.error(f"Model index {model_index} out of range")
415
+ return None
416
+
417
+ # Set the model
418
+ model = models[model_index]
419
+ context.state_manager.session.current_model = model
420
+
421
+ # Check if setting as default
422
+ if len(args) > 1 and args[1] == "default":
423
+ utils.user_configuration.set_default_model(model, context.state_manager)
424
+ await ui.muted("Updating default model")
425
+ return "restart"
426
+ else:
427
+ # Show success message with the new model
428
+ await ui.success(f"Switched to model: {model}")
429
+ return None
430
+
431
+
432
+ @dataclass
433
+ class CommandDependencies:
434
+ """Container for command dependencies."""
435
+
436
+ process_request_callback: Optional[ProcessRequestCallback] = None
437
+ command_registry: Optional[Any] = None # Reference to the registry itself
438
+
439
+
440
+ class CommandFactory:
441
+ """Factory for creating commands with proper dependency injection."""
442
+
443
+ def __init__(self, dependencies: Optional[CommandDependencies] = None):
444
+ self.dependencies = dependencies or CommandDependencies()
445
+
446
+ def create_command(self, command_class: Type[Command]) -> Command:
447
+ """Create a command instance with proper dependencies."""
448
+ # Special handling for commands that need dependencies
449
+ if command_class == CompactCommand:
450
+ return CompactCommand(self.dependencies.process_request_callback)
451
+ elif command_class == HelpCommand:
452
+ return HelpCommand(self.dependencies.command_registry)
453
+
454
+ # Default creation for commands without dependencies
455
+ return command_class()
456
+
457
+ def update_dependencies(self, **kwargs) -> None:
458
+ """Update factory dependencies."""
459
+ for key, value in kwargs.items():
460
+ if hasattr(self.dependencies, key):
461
+ setattr(self.dependencies, key, value)
462
+
463
+
464
+ class CommandRegistry:
465
+ """Registry for managing commands with auto-discovery and categories."""
466
+
467
+ def __init__(self, factory: Optional[CommandFactory] = None):
468
+ self._commands: Dict[str, Command] = {}
469
+ self._categories: Dict[CommandCategory, List[Command]] = {
470
+ category: [] for category in CommandCategory
471
+ }
472
+ self._factory = factory or CommandFactory()
473
+ self._discovered = False
474
+
475
+ # Set registry reference in factory dependencies
476
+ self._factory.update_dependencies(command_registry=self)
477
+
478
+ def register(self, command: Command) -> None:
479
+ """Register a command and its aliases."""
480
+ # Register by primary name
481
+ self._commands[command.name] = command
482
+
483
+ # Register all aliases
484
+ for alias in command.aliases:
485
+ self._commands[alias.lower()] = command
486
+
487
+ # Add to category (remove existing instance first to prevent duplicates)
488
+ category_commands = self._categories[command.category]
489
+ # Remove any existing instance of this command class
490
+ self._categories[command.category] = [
491
+ cmd for cmd in category_commands
492
+ if cmd.__class__ != command.__class__
493
+ ]
494
+ # Add the new instance
495
+ self._categories[command.category].append(command)
496
+
497
+ def register_command_class(self, command_class: Type[Command]) -> None:
498
+ """Register a command class using the factory."""
499
+ command = self._factory.create_command(command_class)
500
+ self.register(command)
501
+
502
+ def discover_commands(self) -> None:
503
+ """Auto-discover and register all command classes."""
504
+ if self._discovered:
505
+ return
506
+
507
+ # List of all command classes to register
508
+ command_classes = [
509
+ YoloCommand,
510
+ DumpCommand,
511
+ ClearCommand,
512
+ HelpCommand,
513
+ UndoCommand,
514
+ BranchCommand,
515
+ InitCommand,
516
+ # TunaCodeCommand, # TODO: Temporarily disabled
517
+ CompactCommand,
518
+ ModelCommand,
519
+ ]
520
+
521
+ # Register all discovered commands
522
+ for command_class in command_classes:
523
+ self.register_command_class(command_class)
524
+
525
+ self._discovered = True
526
+
527
+ def register_all_default_commands(self) -> None:
528
+ """Register all default commands (backward compatibility)."""
529
+ self.discover_commands()
530
+
531
+ def set_process_request_callback(self, callback: ProcessRequestCallback) -> None:
532
+ """Set the process_request callback for commands that need it."""
533
+ # Only update if callback has changed
534
+ if self._factory.dependencies.process_request_callback == callback:
535
+ return
536
+
537
+ self._factory.update_dependencies(process_request_callback=callback)
538
+
539
+ # Re-register CompactCommand with new dependency if already registered
540
+ if "compact" in self._commands:
541
+ self.register_command_class(CompactCommand)
542
+
543
+ async def execute(self, command_text: str, context: CommandContext) -> Any:
544
+ """
545
+ Execute a command.
546
+
547
+ Args:
548
+ command_text: The full command text
549
+ context: Execution context
550
+
551
+ Returns:
552
+ Command-specific return value, or None if command not found
553
+
554
+ Raises:
555
+ ValidationError: If command is not found or empty
556
+ """
557
+ # Ensure commands are discovered
558
+ self.discover_commands()
559
+
560
+ parts = command_text.split()
561
+ if not parts:
562
+ raise ValidationError("Empty command")
563
+
564
+ command_name = parts[0].lower()
565
+ args = parts[1:]
566
+
567
+ # First try exact match
568
+ if command_name in self._commands:
569
+ command = self._commands[command_name]
570
+ return await command.execute(args, context)
571
+
572
+ # Try partial matching
573
+ matches = self.find_matching_commands(command_name)
574
+
575
+ if not matches:
576
+ raise ValidationError(f"Unknown command: {command_name}")
577
+ elif len(matches) == 1:
578
+ # Unambiguous match
579
+ command = self._commands[matches[0]]
580
+ return await command.execute(args, context)
581
+ else:
582
+ # Ambiguous - show possibilities
583
+ raise ValidationError(
584
+ f"Ambiguous command '{command_name}'. Did you mean: {', '.join(sorted(set(matches)))}?"
585
+ )
586
+
587
+ def find_matching_commands(self, partial_command: str) -> List[str]:
588
+ """
589
+ Find all commands that start with the given partial command.
590
+
591
+ Args:
592
+ partial_command: The partial command to match
593
+
594
+ Returns:
595
+ List of matching command names
596
+ """
597
+ self.discover_commands()
598
+ partial = partial_command.lower()
599
+ return [cmd for cmd in self._commands.keys() if cmd.startswith(partial)]
600
+
601
+ def is_command(self, text: str) -> bool:
602
+ """Check if text starts with a registered command (supports partial matching)."""
603
+ if not text:
604
+ return False
605
+
606
+ parts = text.split()
607
+ if not parts:
608
+ return False
609
+
610
+ command_name = parts[0].lower()
611
+
612
+ # Check exact match first
613
+ if command_name in self._commands:
614
+ return True
615
+
616
+ # Check partial match
617
+ return len(self.find_matching_commands(command_name)) > 0
618
+
619
+ def get_command_names(self) -> CommandArgs:
620
+ """Get all registered command names (including aliases)."""
621
+ self.discover_commands()
622
+ return sorted(self._commands.keys())
623
+
624
+ def get_commands_by_category(self, category: CommandCategory) -> List[Command]:
625
+ """Get all commands in a specific category."""
626
+ self.discover_commands()
627
+ return self._categories.get(category, [])
628
+
629
+ def get_all_categories(self) -> Dict[CommandCategory, List[Command]]:
630
+ """Get all commands organized by category."""
631
+ self.discover_commands()
632
+ return self._categories.copy()
tunacode/cli/main.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ Module: sidekick.cli.main
3
+
4
+ CLI entry point and main command handling for the Sidekick application.
5
+ Manages application startup, version checking, and REPL initialization.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import typer
11
+
12
+ from tunacode.cli.repl import repl
13
+ from tunacode.configuration.settings import ApplicationSettings
14
+ from tunacode.core.state import StateManager
15
+ from tunacode.setup import setup
16
+ from tunacode.ui import console as ui
17
+ from tunacode.utils.system import check_for_updates
18
+
19
+ app_settings = ApplicationSettings()
20
+ app = typer.Typer(help=app_settings.name)
21
+ state_manager = StateManager()
22
+
23
+
24
+ @app.command()
25
+ def main(
26
+ version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
27
+ run_setup: bool = typer.Option(False, "--setup", help="Run setup process."),
28
+ ):
29
+ if version:
30
+ asyncio.run(ui.version())
31
+ return
32
+
33
+ asyncio.run(ui.banner())
34
+
35
+ has_update, latest_version = check_for_updates()
36
+ if has_update:
37
+ asyncio.run(ui.show_update_message(latest_version))
38
+
39
+ try:
40
+ asyncio.run(setup(run_setup, state_manager))
41
+ asyncio.run(repl(state_manager))
42
+ except Exception as e:
43
+ asyncio.run(ui.error(str(e)))
44
+
45
+
46
+ if __name__ == "__main__":
47
+ app()