tunacode-cli 0.0.55__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.
- tunacode/cli/commands/__init__.py +2 -2
- tunacode/cli/commands/implementations/__init__.py +2 -3
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/quickstart.py +43 -0
- tunacode/cli/commands/implementations/system.py +96 -3
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +139 -5
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +135 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +400 -0
- tunacode/cli/main.py +23 -2
- tunacode/cli/repl.py +217 -190
- tunacode/cli/repl_components/command_parser.py +38 -4
- tunacode/cli/repl_components/error_recovery.py +85 -4
- tunacode/cli/repl_components/output_display.py +12 -1
- tunacode/cli/repl_components/tool_executor.py +1 -1
- tunacode/configuration/defaults.py +12 -3
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +12 -40
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +249 -55
- tunacode/core/agents/agent_components/agent_helpers.py +43 -13
- tunacode/core/agents/agent_components/node_processor.py +179 -139
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -121
- tunacode/core/code_index.py +83 -29
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +110 -20
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +14 -5
- tunacode/core/state.py +16 -20
- tunacode/core/token_usage/usage_tracker.py +5 -3
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -40
- tunacode/exceptions.py +119 -5
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +125 -7
- tunacode/setup.py +5 -25
- tunacode/tools/base.py +163 -0
- tunacode/tools/bash.py +110 -1
- tunacode/tools/glob.py +332 -34
- tunacode/tools/grep.py +179 -82
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/prompts/bash_prompt.xml +72 -0
- tunacode/tools/prompts/glob_prompt.xml +45 -0
- tunacode/tools/prompts/grep_prompt.xml +98 -0
- tunacode/tools/prompts/list_dir_prompt.xml +31 -0
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +54 -0
- tunacode/tools/prompts/run_command_prompt.xml +64 -0
- tunacode/tools/prompts/update_file_prompt.xml +53 -0
- tunacode/tools/prompts/write_file_prompt.xml +37 -0
- tunacode/tools/react.py +153 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +114 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/tools/xml_helper.py +83 -0
- tunacode/tutorial/__init__.py +9 -0
- tunacode/tutorial/content.py +98 -0
- tunacode/tutorial/manager.py +182 -0
- tunacode/tutorial/steps.py +124 -0
- tunacode/types.py +20 -27
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +20 -3
- tunacode/ui/keybindings.py +7 -4
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +212 -43
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +5 -1
- tunacode/ui/tool_ui.py +33 -10
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/ripgrep.py +332 -9
- tunacode/utils/text_utils.py +18 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
- tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -182
- tunacode/prompts/system.md +0 -731
- tunacode/tools/read_file_async_poc.py +0 -196
- tunacode/tools/todo.py +0 -349
- tunacode_cli-0.0.55.dist-info/METADATA +0 -322
- tunacode_cli-0.0.55.dist-info/RECORD +0 -126
- tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,6 +18,7 @@ from .base import Command, CommandCategory, CommandSpec, SimpleCommand
|
|
|
18
18
|
from .implementations import (
|
|
19
19
|
BranchCommand,
|
|
20
20
|
ClearCommand,
|
|
21
|
+
CommandReloadCommand,
|
|
21
22
|
CompactCommand,
|
|
22
23
|
DumpCommand,
|
|
23
24
|
FixCommand,
|
|
@@ -28,7 +29,6 @@ from .implementations import (
|
|
|
28
29
|
ParseToolsCommand,
|
|
29
30
|
RefreshConfigCommand,
|
|
30
31
|
ThoughtsCommand,
|
|
31
|
-
TodoCommand,
|
|
32
32
|
UpdateCommand,
|
|
33
33
|
YoloCommand,
|
|
34
34
|
)
|
|
@@ -62,5 +62,5 @@ __all__ = [
|
|
|
62
62
|
"UpdateCommand",
|
|
63
63
|
"ModelCommand",
|
|
64
64
|
"InitCommand",
|
|
65
|
-
"
|
|
65
|
+
"CommandReloadCommand",
|
|
66
66
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command implementations for TunaCode CLI."""
|
|
2
2
|
|
|
3
3
|
# Import all command classes for easy access
|
|
4
|
+
from .command_reload import CommandReloadCommand
|
|
4
5
|
from .conversation import CompactCommand
|
|
5
6
|
from .debug import (
|
|
6
7
|
DumpCommand,
|
|
@@ -19,7 +20,6 @@ from .system import (
|
|
|
19
20
|
StreamingCommand,
|
|
20
21
|
UpdateCommand,
|
|
21
22
|
)
|
|
22
|
-
from .todo import TodoCommand
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
# System commands
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"RefreshConfigCommand",
|
|
29
29
|
"StreamingCommand",
|
|
30
30
|
"UpdateCommand",
|
|
31
|
+
"CommandReloadCommand",
|
|
31
32
|
# Debug commands
|
|
32
33
|
"YoloCommand",
|
|
33
34
|
"DumpCommand",
|
|
@@ -42,6 +43,4 @@ __all__ = [
|
|
|
42
43
|
"ModelCommand",
|
|
43
44
|
# Conversation commands
|
|
44
45
|
"CompactCommand",
|
|
45
|
-
# Todo commands
|
|
46
|
-
"TodoCommand",
|
|
47
46
|
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Command reload implementation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from ....types import CommandContext
|
|
7
|
+
from ....ui import console as ui
|
|
8
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CommandReloadCommand(SimpleCommand):
|
|
12
|
+
"""Reload slash commands to discover new commands."""
|
|
13
|
+
|
|
14
|
+
spec = CommandSpec(
|
|
15
|
+
name="command-reload",
|
|
16
|
+
aliases=["/command-reload"],
|
|
17
|
+
description="Reload slash commands to discover newly added commands",
|
|
18
|
+
category=CommandCategory.DEVELOPMENT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def __init__(self, command_registry=None):
|
|
22
|
+
self._command_registry = command_registry
|
|
23
|
+
|
|
24
|
+
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
25
|
+
# Check if any command directories exist
|
|
26
|
+
command_dirs = [
|
|
27
|
+
Path(".tunacode/commands"),
|
|
28
|
+
Path(".claude/commands"),
|
|
29
|
+
Path.home() / ".tunacode/commands",
|
|
30
|
+
Path.home() / ".claude/commands",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
dirs_exist = any(cmd_dir.exists() for cmd_dir in command_dirs)
|
|
34
|
+
|
|
35
|
+
if not dirs_exist:
|
|
36
|
+
await ui.info("No commands directory found")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Reload commands using registry
|
|
40
|
+
if self._command_registry:
|
|
41
|
+
try:
|
|
42
|
+
self._command_registry.reload_slash_commands()
|
|
43
|
+
await ui.success("Commands reloaded")
|
|
44
|
+
return
|
|
45
|
+
except Exception as e:
|
|
46
|
+
await ui.error(f"Reload failed: {e}")
|
|
47
|
+
else:
|
|
48
|
+
await ui.error("Command registry not available")
|
|
@@ -122,7 +122,7 @@ class FixCommand(SimpleCommand):
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
125
|
-
from tunacode.core.agents
|
|
125
|
+
from tunacode.core.agents import patch_tool_messages
|
|
126
126
|
|
|
127
127
|
# Count current messages
|
|
128
128
|
before_count = len(context.state_manager.session.messages)
|
|
@@ -152,7 +152,7 @@ class ParseToolsCommand(SimpleCommand):
|
|
|
152
152
|
)
|
|
153
153
|
|
|
154
154
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
155
|
-
from tunacode.core.agents
|
|
155
|
+
from tunacode.core.agents import extract_and_execute_tool_calls
|
|
156
156
|
|
|
157
157
|
# Find the last model response in messages
|
|
158
158
|
messages = context.state_manager.session.messages
|
|
@@ -49,27 +49,29 @@ class BranchCommand(SimpleCommand):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class InitCommand(SimpleCommand):
|
|
52
|
-
"""Creates or updates
|
|
52
|
+
"""Creates or updates AGENTS.md with project-specific context."""
|
|
53
53
|
|
|
54
54
|
spec = CommandSpec(
|
|
55
55
|
name="/init",
|
|
56
56
|
aliases=[],
|
|
57
|
-
description="Analyze codebase and create/update
|
|
57
|
+
description="Analyze codebase and create/update AGENTS.md file",
|
|
58
58
|
category=CommandCategory.DEVELOPMENT,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
async def execute(self, args, context: CommandContext) -> CommandResult:
|
|
62
62
|
"""Execute the init command."""
|
|
63
63
|
# Minimal implementation to make test pass
|
|
64
|
-
prompt = """Please analyze this codebase and create a
|
|
64
|
+
prompt = """Please analyze this codebase and create a AGENTS.md file containing:
|
|
65
65
|
1. Build/lint/test commands - especially for running a single test
|
|
66
|
-
2. Code style guidelines including imports, formatting, types, naming conventions,
|
|
66
|
+
2. Code style guidelines including imports, formatting, types, naming conventions,
|
|
67
|
+
error handling, etc.
|
|
67
68
|
|
|
68
|
-
The file you create will be given to agentic coding agents (such as yourself)
|
|
69
|
+
The file you create will be given to agentic coding agents (such as yourself)
|
|
70
|
+
that operate in this repository.
|
|
69
71
|
Make it about 20 lines long.
|
|
70
|
-
If there's already a
|
|
71
|
-
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules
|
|
72
|
-
make sure to include them."""
|
|
72
|
+
If there's already a AGENTS.md, improve it.
|
|
73
|
+
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules
|
|
74
|
+
(in .github/copilot-instructions.md), make sure to include them."""
|
|
73
75
|
|
|
74
76
|
# Call the agent to analyze and create/update the file
|
|
75
77
|
if context.process_request:
|
|
@@ -1,61 +1,389 @@
|
|
|
1
1
|
"""Model management commands for TunaCode CLI."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from .... import utils
|
|
6
5
|
from ....exceptions import ConfigurationError
|
|
7
6
|
from ....types import CommandArgs, CommandContext
|
|
8
7
|
from ....ui import console as ui
|
|
8
|
+
from ....ui.model_selector import select_model_interactive
|
|
9
|
+
from ....utils import user_configuration
|
|
10
|
+
from ....utils.models_registry import ModelInfo, ModelsRegistry
|
|
9
11
|
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class ModelCommand(SimpleCommand):
|
|
13
|
-
"""Manage model selection."""
|
|
15
|
+
"""Manage model selection with models.dev integration."""
|
|
14
16
|
|
|
15
17
|
spec = CommandSpec(
|
|
16
18
|
name="model",
|
|
17
19
|
aliases=["/model"],
|
|
18
|
-
description="Switch model
|
|
20
|
+
description="Switch model with interactive selection or search",
|
|
19
21
|
category=CommandCategory.MODEL,
|
|
20
22
|
)
|
|
21
23
|
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize the model command."""
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.registry = ModelsRegistry()
|
|
28
|
+
self._registry_loaded = False
|
|
29
|
+
|
|
30
|
+
async def _ensure_registry(self) -> bool:
|
|
31
|
+
"""Ensure the models registry is loaded."""
|
|
32
|
+
if not self._registry_loaded:
|
|
33
|
+
self._registry_loaded = await self.registry.load()
|
|
34
|
+
return self._registry_loaded
|
|
35
|
+
|
|
22
36
|
async def execute(self, args: CommandArgs, context: CommandContext) -> Optional[str]:
|
|
23
|
-
#
|
|
37
|
+
# Handle special flags
|
|
38
|
+
if args and args[0] in ["--list", "-l"]:
|
|
39
|
+
return await self._list_models()
|
|
40
|
+
|
|
41
|
+
if args and args[0] in ["--info", "-i"]:
|
|
42
|
+
if len(args) < 2:
|
|
43
|
+
await ui.error("Usage: /model --info <model-id>")
|
|
44
|
+
return None
|
|
45
|
+
return await self._show_model_info(args[1])
|
|
46
|
+
|
|
47
|
+
# No arguments - show interactive selector
|
|
24
48
|
if not args:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
return await self._interactive_select(context)
|
|
50
|
+
|
|
51
|
+
# Single argument - could be search query or model ID
|
|
52
|
+
model_query = args[0]
|
|
53
|
+
|
|
54
|
+
# Check for flags
|
|
55
|
+
if model_query in ["--search", "-s"]:
|
|
56
|
+
search_query = " ".join(args[1:]) if len(args) > 1 else ""
|
|
57
|
+
return await self._interactive_select(context, search_query)
|
|
58
|
+
|
|
59
|
+
# Direct model specification
|
|
60
|
+
return await self._set_model(model_query, args[1:], context)
|
|
61
|
+
|
|
62
|
+
async def _interactive_select(
|
|
63
|
+
self, context: CommandContext, initial_query: str = ""
|
|
64
|
+
) -> Optional[str]:
|
|
65
|
+
"""Show interactive model selector."""
|
|
66
|
+
await self._ensure_registry()
|
|
67
|
+
|
|
68
|
+
# Show current model
|
|
69
|
+
current_model = context.state_manager.session.current_model
|
|
70
|
+
await ui.info(f"Current model: {current_model}")
|
|
71
|
+
|
|
72
|
+
# Check if we have models loaded
|
|
73
|
+
if not self.registry.models:
|
|
74
|
+
await ui.error("No models available. Try /model --list to see if models can be loaded.")
|
|
29
75
|
return None
|
|
30
76
|
|
|
31
|
-
#
|
|
32
|
-
|
|
77
|
+
# For now, use a simple text-based approach instead of complex UI
|
|
78
|
+
# This avoids prompt_toolkit compatibility issues
|
|
79
|
+
if initial_query:
|
|
80
|
+
models = self.registry.search_models(initial_query)
|
|
81
|
+
if not models:
|
|
82
|
+
await ui.error(f"No models found matching '{initial_query}'")
|
|
83
|
+
return None
|
|
84
|
+
else:
|
|
85
|
+
# Show popular models for quick selection
|
|
86
|
+
popular_searches = ["gpt", "claude", "gemini"]
|
|
87
|
+
await ui.info("Popular model searches:")
|
|
88
|
+
for search in popular_searches:
|
|
89
|
+
models = self.registry.search_models(search)[:3] # Top 3
|
|
90
|
+
if models:
|
|
91
|
+
await ui.info(f"\n{search.upper()} models:")
|
|
92
|
+
for model in models:
|
|
93
|
+
await ui.muted(f" • {model.full_id} - {model.name}")
|
|
33
94
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await ui.
|
|
37
|
-
await ui.muted("
|
|
38
|
-
await ui.muted(
|
|
39
|
-
|
|
40
|
-
|
|
95
|
+
await ui.info("\nUsage:")
|
|
96
|
+
await ui.muted(" /model <search-term> - Search for models")
|
|
97
|
+
await ui.muted(" /model --list - Show all models")
|
|
98
|
+
await ui.muted(" /model --info <id> - Show model details")
|
|
99
|
+
await ui.muted(" /model <provider:id> - Set model directly")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Show search results
|
|
103
|
+
if len(models) == 1:
|
|
104
|
+
# Auto-select single result
|
|
105
|
+
model = models[0]
|
|
106
|
+
context.state_manager.session.current_model = model.full_id
|
|
107
|
+
# Persist selection to config by default
|
|
108
|
+
try:
|
|
109
|
+
user_configuration.set_default_model(model.full_id, context.state_manager)
|
|
110
|
+
await ui.success(
|
|
111
|
+
f"Switched to model: {model.full_id} - {model.name} (saved as default)"
|
|
112
|
+
)
|
|
113
|
+
except ConfigurationError as e:
|
|
114
|
+
await ui.error(str(e))
|
|
115
|
+
await ui.warning("Model switched for this session only; failed to save default.")
|
|
41
116
|
return None
|
|
42
117
|
|
|
43
|
-
#
|
|
44
|
-
await ui.
|
|
118
|
+
# Show multiple results
|
|
119
|
+
await ui.info(f"Found {len(models)} models:")
|
|
120
|
+
for i, model in enumerate(models[:10], 1): # Show top 10
|
|
121
|
+
details = []
|
|
122
|
+
if model.cost.input is not None:
|
|
123
|
+
details.append(f"${model.cost.input}/{model.cost.output}")
|
|
124
|
+
if model.limits.context:
|
|
125
|
+
details.append(f"{model.limits.context // 1000}k")
|
|
126
|
+
detail_str = f" ({', '.join(details)})" if details else ""
|
|
45
127
|
|
|
46
|
-
|
|
47
|
-
context.state_manager.session.current_model = model_name
|
|
128
|
+
await ui.info(f"{i:2d}. {model.full_id} - {model.name}{detail_str}")
|
|
48
129
|
|
|
49
|
-
|
|
50
|
-
|
|
130
|
+
if len(models) > 10:
|
|
131
|
+
await ui.muted(f"... and {len(models) - 10} more")
|
|
132
|
+
|
|
133
|
+
await ui.muted("Use '/model <provider:model-id>' to select a specific model")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
async def _set_model(
|
|
137
|
+
self, model_name: str, extra_args: CommandArgs, context: CommandContext
|
|
138
|
+
) -> Optional[str]:
|
|
139
|
+
"""Set model directly or by search."""
|
|
140
|
+
# Load registry for validation
|
|
141
|
+
await self._ensure_registry()
|
|
142
|
+
|
|
143
|
+
# Check if it's a direct model ID
|
|
144
|
+
if ":" in model_name:
|
|
145
|
+
# Validate against registry if loaded
|
|
146
|
+
if self._registry_loaded:
|
|
147
|
+
model_info = self.registry.get_model(model_name)
|
|
148
|
+
if not model_info:
|
|
149
|
+
# Search for similar models
|
|
150
|
+
similar = self.registry.search_models(model_name.split(":")[-1])
|
|
151
|
+
if similar:
|
|
152
|
+
await ui.warning(f"Model '{model_name}' not found in registry")
|
|
153
|
+
await ui.muted("Did you mean one of these?")
|
|
154
|
+
for model in similar[:5]:
|
|
155
|
+
await ui.muted(f" • {model.full_id} - {model.name}")
|
|
156
|
+
return None
|
|
157
|
+
else:
|
|
158
|
+
await ui.warning("Model not found in registry - setting anyway")
|
|
159
|
+
else:
|
|
160
|
+
# Show model info
|
|
161
|
+
await ui.info(f"Selected: {model_info.name}")
|
|
162
|
+
if model_info.cost.input is not None:
|
|
163
|
+
await ui.muted(f" Pricing: {model_info.cost.format_cost()}")
|
|
164
|
+
if model_info.limits.context:
|
|
165
|
+
await ui.muted(f" Limits: {model_info.limits.format_limits()}")
|
|
166
|
+
|
|
167
|
+
# Set the model
|
|
168
|
+
context.state_manager.session.current_model = model_name
|
|
169
|
+
|
|
170
|
+
# Check if setting as default (preserve existing behavior)
|
|
171
|
+
if extra_args and extra_args[0] == "default":
|
|
172
|
+
try:
|
|
173
|
+
user_configuration.set_default_model(model_name, context.state_manager)
|
|
174
|
+
await ui.muted("Updating default model")
|
|
175
|
+
return "restart"
|
|
176
|
+
except ConfigurationError as e:
|
|
177
|
+
await ui.error(str(e))
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
# Persist selection to config by default (auto-persist)
|
|
181
|
+
try:
|
|
182
|
+
user_configuration.set_default_model(model_name, context.state_manager)
|
|
183
|
+
await ui.success(f"Switched to model: {model_name} (saved as default)")
|
|
184
|
+
except ConfigurationError as e:
|
|
185
|
+
await ui.error(str(e))
|
|
186
|
+
await ui.warning("Model switched for this session only; failed to save default.")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# No colon - treat as search query
|
|
190
|
+
models = self.registry.search_models(model_name)
|
|
191
|
+
|
|
192
|
+
if not models:
|
|
193
|
+
await ui.error(f"No models found matching '{model_name}'")
|
|
194
|
+
await ui.muted("Try /model --list to see all available models")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
if len(models) == 1:
|
|
198
|
+
# Single match - use it
|
|
199
|
+
model = models[0]
|
|
200
|
+
context.state_manager.session.current_model = model.full_id
|
|
201
|
+
# Persist selection to config by default
|
|
51
202
|
try:
|
|
52
|
-
|
|
53
|
-
await ui.
|
|
54
|
-
|
|
203
|
+
user_configuration.set_default_model(model.full_id, context.state_manager)
|
|
204
|
+
await ui.success(
|
|
205
|
+
f"Switched to model: {model.full_id} - {model.name} (saved as default)"
|
|
206
|
+
)
|
|
55
207
|
except ConfigurationError as e:
|
|
56
208
|
await ui.error(str(e))
|
|
209
|
+
await ui.warning("Model switched for this session only; failed to save default.")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# Multiple matches - show interactive selector with results
|
|
213
|
+
await ui.info(f"Found {len(models)} models matching '{model_name}'")
|
|
214
|
+
selected_model = await select_model_interactive(self.registry, model_name)
|
|
215
|
+
|
|
216
|
+
if selected_model:
|
|
217
|
+
context.state_manager.session.current_model = selected_model
|
|
218
|
+
# Persist selection to config by default
|
|
219
|
+
try:
|
|
220
|
+
user_configuration.set_default_model(selected_model, context.state_manager)
|
|
221
|
+
await ui.success(f"Switched to model: {selected_model} (saved as default)")
|
|
222
|
+
except ConfigurationError as e:
|
|
223
|
+
await ui.error(str(e))
|
|
224
|
+
await ui.warning("Model switched for this session only; failed to save default.")
|
|
225
|
+
else:
|
|
226
|
+
await ui.info("Model selection cancelled")
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
async def _list_models(self) -> Optional[str]:
|
|
231
|
+
"""List all available models."""
|
|
232
|
+
await self._ensure_registry()
|
|
233
|
+
|
|
234
|
+
if not self.registry.models:
|
|
235
|
+
await ui.error("No models available")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Group by provider
|
|
239
|
+
providers: Dict[str, List[ModelInfo]] = {}
|
|
240
|
+
for model in self.registry.models.values():
|
|
241
|
+
if model.provider not in providers:
|
|
242
|
+
providers[model.provider] = []
|
|
243
|
+
providers[model.provider].append(model)
|
|
244
|
+
|
|
245
|
+
# Display models
|
|
246
|
+
await ui.info(f"Available models ({len(self.registry.models)} total):")
|
|
247
|
+
|
|
248
|
+
for provider_id in sorted(providers.keys()):
|
|
249
|
+
provider_info = self.registry.providers.get(provider_id)
|
|
250
|
+
provider_name = provider_info.name if provider_info else provider_id
|
|
251
|
+
|
|
252
|
+
await ui.print(f"\n{provider_name}:")
|
|
253
|
+
|
|
254
|
+
for model in sorted(providers[provider_id], key=lambda m: m.name):
|
|
255
|
+
line = f" • {model.id}"
|
|
256
|
+
if model.cost.input is not None:
|
|
257
|
+
line += f" (${model.cost.input}/{model.cost.output})"
|
|
258
|
+
if model.limits.context:
|
|
259
|
+
line += f" [{model.limits.context // 1000}k]"
|
|
260
|
+
await ui.muted(line)
|
|
261
|
+
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
async def _show_model_info(self, model_id: str) -> Optional[str]:
|
|
265
|
+
"""Show detailed information about a model."""
|
|
266
|
+
await self._ensure_registry()
|
|
267
|
+
|
|
268
|
+
model = self.registry.get_model(model_id)
|
|
269
|
+
if not model:
|
|
270
|
+
# Try to find similar models or routing options
|
|
271
|
+
base_name = self.registry._extract_base_model_name(model_id)
|
|
272
|
+
variants = self.registry.get_model_variants(base_name)
|
|
273
|
+
if variants:
|
|
274
|
+
await ui.warning(f"Model '{model_id}' not found directly")
|
|
275
|
+
await ui.info(f"Found routing options for '{base_name}':")
|
|
276
|
+
|
|
277
|
+
# Sort variants by cost (FREE first)
|
|
278
|
+
sorted_variants = sorted(
|
|
279
|
+
variants,
|
|
280
|
+
key=lambda m: (
|
|
281
|
+
0 if m.cost.input == 0 else 1, # FREE first
|
|
282
|
+
m.cost.input or float("inf"), # Then by cost
|
|
283
|
+
m.provider, # Then by provider name
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for variant in sorted_variants:
|
|
288
|
+
cost_display = (
|
|
289
|
+
"FREE"
|
|
290
|
+
if variant.cost.input == 0
|
|
291
|
+
else f"${variant.cost.input}/{variant.cost.output}"
|
|
292
|
+
)
|
|
293
|
+
provider_name = self._get_provider_display_name(variant.provider)
|
|
294
|
+
|
|
295
|
+
await ui.muted(f" • {variant.full_id} - {provider_name} ({cost_display})")
|
|
296
|
+
|
|
297
|
+
await ui.muted(
|
|
298
|
+
"\nUse '/model <provider:model-id>' to select a specific routing option"
|
|
299
|
+
)
|
|
57
300
|
return None
|
|
301
|
+
else:
|
|
302
|
+
await ui.error(f"Model '{model_id}' not found")
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
# Display model information
|
|
306
|
+
await ui.info(f"{model.name}")
|
|
307
|
+
await ui.muted(f"ID: {model.full_id}")
|
|
308
|
+
|
|
309
|
+
# Show routing alternatives for this base model
|
|
310
|
+
base_name = self.registry._extract_base_model_name(model)
|
|
311
|
+
variants = self.registry.get_model_variants(base_name)
|
|
312
|
+
if len(variants) > 1:
|
|
313
|
+
await ui.print("\nRouting Options:")
|
|
314
|
+
|
|
315
|
+
# Sort variants by cost (FREE first)
|
|
316
|
+
sorted_variants = sorted(
|
|
317
|
+
variants,
|
|
318
|
+
key=lambda m: (
|
|
319
|
+
0 if m.cost.input == 0 else 1, # FREE first
|
|
320
|
+
m.cost.input or float("inf"), # Then by cost
|
|
321
|
+
m.provider, # Then by provider name
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
for variant in sorted_variants:
|
|
326
|
+
cost_display = (
|
|
327
|
+
"FREE"
|
|
328
|
+
if variant.cost.input == 0
|
|
329
|
+
else f"${variant.cost.input}/{variant.cost.output}"
|
|
330
|
+
)
|
|
331
|
+
provider_name = self._get_provider_display_name(variant.provider)
|
|
332
|
+
|
|
333
|
+
# Highlight current selection
|
|
334
|
+
prefix = "→ " if variant.full_id == model.full_id else " "
|
|
335
|
+
free_indicator = " ⭐" if variant.cost.input == 0 else ""
|
|
336
|
+
|
|
337
|
+
await ui.muted(
|
|
338
|
+
f"{prefix}{variant.full_id} - {provider_name} ({cost_display}){free_indicator}"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
if model.cost.input is not None:
|
|
342
|
+
await ui.print("\nPricing:")
|
|
343
|
+
await ui.muted(f" Input: ${model.cost.input} per 1M tokens")
|
|
344
|
+
await ui.muted(f" Output: ${model.cost.output} per 1M tokens")
|
|
345
|
+
|
|
346
|
+
if model.limits.context or model.limits.output:
|
|
347
|
+
await ui.print("\nLimits:")
|
|
348
|
+
if model.limits.context:
|
|
349
|
+
await ui.muted(f" Context: {model.limits.context:,} tokens")
|
|
350
|
+
if model.limits.output:
|
|
351
|
+
await ui.muted(f" Output: {model.limits.output:,} tokens")
|
|
352
|
+
|
|
353
|
+
caps = []
|
|
354
|
+
if model.capabilities.attachment:
|
|
355
|
+
caps.append("Attachments")
|
|
356
|
+
if model.capabilities.reasoning:
|
|
357
|
+
caps.append("Reasoning")
|
|
358
|
+
if model.capabilities.tool_call:
|
|
359
|
+
caps.append("Tool calling")
|
|
360
|
+
|
|
361
|
+
if caps:
|
|
362
|
+
await ui.print("\nCapabilities:")
|
|
363
|
+
for cap in caps:
|
|
364
|
+
await ui.muted(f" ✓ {cap}")
|
|
365
|
+
|
|
366
|
+
if model.capabilities.knowledge:
|
|
367
|
+
await ui.print(f"\nKnowledge cutoff: {model.capabilities.knowledge}")
|
|
58
368
|
|
|
59
|
-
# Show success message with the new model
|
|
60
|
-
await ui.success(f"Switched to model: {model_name}")
|
|
61
369
|
return None
|
|
370
|
+
|
|
371
|
+
def _get_provider_display_name(self, provider: str) -> str:
|
|
372
|
+
"""Get a user-friendly provider display name."""
|
|
373
|
+
provider_names = {
|
|
374
|
+
"openai": "OpenAI Direct",
|
|
375
|
+
"anthropic": "Anthropic Direct",
|
|
376
|
+
"google": "Google Direct",
|
|
377
|
+
"google-gla": "Google Labs",
|
|
378
|
+
"openrouter": "OpenRouter",
|
|
379
|
+
"github-models": "GitHub Models (FREE)",
|
|
380
|
+
"azure": "Azure OpenAI",
|
|
381
|
+
"fastrouter": "FastRouter",
|
|
382
|
+
"requesty": "Requesty",
|
|
383
|
+
"cloudflare-workers-ai": "Cloudflare",
|
|
384
|
+
"amazon-bedrock": "AWS Bedrock",
|
|
385
|
+
"chutes": "Chutes AI",
|
|
386
|
+
"deepinfra": "DeepInfra",
|
|
387
|
+
"venice": "Venice AI",
|
|
388
|
+
}
|
|
389
|
+
return provider_names.get(provider, provider.title())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.cli.commands.implementations.quickstart
|
|
3
|
+
|
|
4
|
+
QuickStart command implementation for interactive tutorial system.
|
|
5
|
+
Provides guided introduction to TunaCode for new users.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from tunacode.types import CommandContext, CommandResult
|
|
11
|
+
|
|
12
|
+
from ..base import CommandCategory, CommandSpec, SimpleCommand
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QuickStartCommand(SimpleCommand):
|
|
18
|
+
"""Interactive quickstart tutorial command."""
|
|
19
|
+
|
|
20
|
+
spec = CommandSpec(
|
|
21
|
+
name="quickstart",
|
|
22
|
+
aliases=["/quickstart", "/qs"],
|
|
23
|
+
description="Interactive tutorial for getting started with TunaCode",
|
|
24
|
+
category=CommandCategory.SYSTEM,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def execute(self, _args: list[str], context: CommandContext) -> CommandResult:
|
|
28
|
+
"""Execute the quickstart tutorial."""
|
|
29
|
+
try:
|
|
30
|
+
from ....tutorial import TutorialManager
|
|
31
|
+
except ImportError:
|
|
32
|
+
await context.ui.error("Tutorial system is not available")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
tutorial_manager = TutorialManager(context.state_manager)
|
|
36
|
+
|
|
37
|
+
# Always run tutorial when explicitly requested
|
|
38
|
+
success = await tutorial_manager.run_tutorial()
|
|
39
|
+
|
|
40
|
+
if success:
|
|
41
|
+
await context.ui.success("✅ Tutorial completed successfully!")
|
|
42
|
+
else:
|
|
43
|
+
await context.ui.info("Tutorial was cancelled or interrupted")
|