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
@@ -0,0 +1,230 @@
1
+ """Module: tunacode.core.setup.config_wizard
2
+
3
+ Wizard-style onboarding helpers extracted from ConfigSetup to reduce file size
4
+ and keep responsibilities focused.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, Optional
10
+
11
+ from tunacode.constants import UI_COLORS
12
+ from tunacode.exceptions import ConfigurationError
13
+ from tunacode.ui import console as ui
14
+ from tunacode.utils import user_configuration
15
+
16
+
17
+ class ConfigWizard:
18
+ """Encapsulates the interactive configuration wizard flow."""
19
+
20
+ def __init__(self, state_manager, model_registry, config_file: Path):
21
+ self.state_manager = state_manager
22
+ self.model_registry = model_registry
23
+ self.config_file = config_file
24
+ self._wizard_selected_provider: Optional[Dict[str, str]] = None
25
+
26
+ async def run_onboarding(self) -> None:
27
+ """Run enhanced wizard-style onboarding process for new users."""
28
+ initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
29
+
30
+ # Welcome message with provider guidance
31
+ await ui.panel(
32
+ "Welcome to TunaCode Setup Wizard!",
33
+ "This guided setup will help you configure TunaCode in under 5 minutes.\n"
34
+ "We'll help you choose a provider, set up your API keys, and configure your "
35
+ "preferred model.",
36
+ border_style=UI_COLORS["primary"],
37
+ )
38
+
39
+ # Steps
40
+ await self._step1_provider_selection()
41
+ await self._step2_api_key_setup()
42
+ await self._step3_model_selection()
43
+ await self._step4_optional_settings()
44
+
45
+ # Save configuration and finish
46
+ current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
47
+ if initial_config != current_config:
48
+ try:
49
+ user_configuration.save_config(self.state_manager)
50
+ await ui.panel(
51
+ "Setup Complete!",
52
+ f"Configuration saved to: [bold]{self.config_file}[/bold]\n\n"
53
+ "You're ready to start using TunaCode!\n"
54
+ "Use [green]/quickstart[/green] anytime for a tutorial.",
55
+ border_style=UI_COLORS["success"],
56
+ )
57
+ except ConfigurationError as e:
58
+ await ui.error(str(e))
59
+
60
+ async def _step1_provider_selection(self) -> None:
61
+ """Wizard step 1: Provider selection with detailed explanations."""
62
+ provider_info = {
63
+ "1": {
64
+ "name": "OpenRouter",
65
+ "description": "Access to multiple models (GPT-4, Claude, Gemini, etc.)",
66
+ "signup": "https://openrouter.ai/",
67
+ "key_name": "OPENROUTER_API_KEY",
68
+ },
69
+ "2": {
70
+ "name": "OpenAI",
71
+ "description": "GPT-4 models",
72
+ "signup": "https://platform.openai.com/signup",
73
+ "key_name": "OPENAI_API_KEY",
74
+ },
75
+ "3": {
76
+ "name": "Anthropic",
77
+ "description": "Claude-3 models",
78
+ "signup": "https://console.anthropic.com/",
79
+ "key_name": "ANTHROPIC_API_KEY",
80
+ },
81
+ "4": {
82
+ "name": "Google",
83
+ "description": "Gemini models",
84
+ "signup": "https://ai.google.dev/",
85
+ "key_name": "GEMINI_API_KEY",
86
+ },
87
+ }
88
+
89
+ message = "Choose your AI provider:\n\n"
90
+ for key, info in provider_info.items():
91
+ message += f" {key} - {info['name']}: {info['description']}\n"
92
+
93
+ await ui.panel("Provider Selection", message, border_style=UI_COLORS["primary"])
94
+
95
+ while True:
96
+ choice = await ui.input(
97
+ "wizard_provider",
98
+ pretext=" Choose provider (1-4): ",
99
+ state_manager=self.state_manager,
100
+ )
101
+
102
+ if choice.strip() in provider_info:
103
+ selected = provider_info[choice.strip()]
104
+ self._wizard_selected_provider = selected
105
+
106
+ await ui.success(f"Selected: {selected['name']}")
107
+ await ui.info(f"Sign up at: {selected['signup']}")
108
+ break
109
+ else:
110
+ await ui.error("Please enter 1, 2, 3, or 4")
111
+
112
+ async def _step2_api_key_setup(self) -> None:
113
+ """Wizard step 2: API key setup with provider-specific guidance."""
114
+ provider = self._wizard_selected_provider
115
+
116
+ message = f"Enter your {provider['name']} API key:\n\n"
117
+ message += f"Get your key from: {provider['signup']}\n"
118
+ message += "Your key will be stored securely in your local config"
119
+
120
+ await ui.panel(f"{provider['name']} API Key", message, border_style=UI_COLORS["primary"])
121
+
122
+ while True:
123
+ api_key = await ui.input(
124
+ "wizard_api_key",
125
+ pretext=f" {provider['name']} API Key: ",
126
+ is_password=True,
127
+ state_manager=self.state_manager,
128
+ )
129
+
130
+ if api_key.strip():
131
+ # Ensure env dict exists
132
+ if "env" not in self.state_manager.session.user_config:
133
+ self.state_manager.session.user_config["env"] = {}
134
+
135
+ self.state_manager.session.user_config["env"][provider["key_name"]] = (
136
+ api_key.strip()
137
+ )
138
+ await ui.success("API key saved successfully!")
139
+ break
140
+ else:
141
+ await ui.error("API key cannot be empty")
142
+
143
+ async def _step3_model_selection(self) -> None:
144
+ """Wizard step 3: Model selection with smart recommendations."""
145
+ provider = self._wizard_selected_provider
146
+
147
+ # Provide smart recommendations based on provider
148
+ recommendations = {
149
+ "OpenAI": [
150
+ ("openai:gpt-4o", "GPT-4o flagship multimodal model (recommended)"),
151
+ ("openai:gpt-4.1", "Latest GPT-4.1 with enhanced coding"),
152
+ ("openai:o3", "Advanced reasoning model for complex tasks"),
153
+ ],
154
+ "Anthropic": [
155
+ ("anthropic:claude-sonnet-4", "Claude Sonnet 4 latest generation (recommended)"),
156
+ ("anthropic:claude-opus-4.1", "Most capable Claude with extended thinking"),
157
+ ("anthropic:claude-3.5-sonnet", "Claude 3.5 Sonnet proven performance"),
158
+ ],
159
+ "OpenRouter": [
160
+ (
161
+ "openrouter:anthropic/claude-sonnet-4",
162
+ "Claude Sonnet 4 via OpenRouter (recommended)",
163
+ ),
164
+ ("openrouter:openai/gpt-4.1", "GPT-4.1 via OpenRouter"),
165
+ ("openrouter:google/gemini-2.5-flash", "Google Gemini 2.5 Flash latest"),
166
+ ],
167
+ "Google": [
168
+ (
169
+ "google:gemini-2.5-pro",
170
+ "Gemini 2.5 Pro with thinking capabilities (recommended)",
171
+ ),
172
+ ("google:gemini-2.5-flash", "Gemini 2.5 Flash best price-performance"),
173
+ ("google:gemini-2.0-flash", "Gemini 2.0 Flash with native tool use"),
174
+ ],
175
+ }
176
+
177
+ models = recommendations.get(provider["name"], [])
178
+ message = f"Choose your default {provider['name']} model:\n\n"
179
+
180
+ for i, (model_id, description) in enumerate(models, 1):
181
+ message += f" {i} - {description}\n"
182
+
183
+ message += "\nYou can change this later with [green]/model[/green]"
184
+
185
+ await ui.panel("Model Selection", message, border_style=UI_COLORS["primary"])
186
+
187
+ while True:
188
+ choice = await ui.input(
189
+ "wizard_model",
190
+ pretext=f" Choose model (1-{len(models)}): ",
191
+ state_manager=self.state_manager,
192
+ )
193
+
194
+ try:
195
+ index = int(choice.strip()) - 1
196
+ if 0 <= index < len(models):
197
+ selected_model = models[index][0]
198
+ self.state_manager.session.user_config["default_model"] = selected_model
199
+ await ui.success(f"Selected: {selected_model}")
200
+ break
201
+ else:
202
+ await ui.error(f"Please enter a number between 1 and {len(models)}")
203
+ except ValueError:
204
+ await ui.error("Please enter a valid number")
205
+
206
+ async def _step4_optional_settings(self) -> None:
207
+ """Wizard step 4: Optional settings configuration."""
208
+ message = "Configure optional settings:\n\n"
209
+ message += "• Tutorial: Enable interactive tutorial for new users\n"
210
+ message += "\nSkip this step to use recommended defaults"
211
+
212
+ await ui.panel("Optional Settings", message, border_style=UI_COLORS["primary"])
213
+
214
+ # Ask about tutorial
215
+ tutorial_choice = await ui.input(
216
+ "wizard_tutorial",
217
+ pretext=" Enable tutorial for new users? [Y/n]: ",
218
+ state_manager=self.state_manager,
219
+ )
220
+
221
+ enable_tutorial = tutorial_choice.strip().lower() not in ["n", "no", "false"]
222
+
223
+ if "settings" not in self.state_manager.session.user_config:
224
+ self.state_manager.session.user_config["settings"] = {}
225
+
226
+ self.state_manager.session.user_config["settings"]["enable_tutorial"] = enable_tutorial
227
+
228
+ # Streaming is always enabled - no user choice needed
229
+
230
+ await ui.info("Optional settings configured!")
@@ -37,7 +37,8 @@ class SetupCoordinator:
37
37
  raise
38
38
 
39
39
  try:
40
- # Run steps sequentially to respect dependencies (ConfigSetup must complete before EnvironmentSetup)
40
+ # Run steps sequentially to respect dependencies
41
+ # (ConfigSetup must complete before EnvironmentSetup)
41
42
  for step in steps_to_run:
42
43
  # Check if the step's execute method supports wizard_mode
43
44
  import inspect
tunacode/core/state.py CHANGED
@@ -10,15 +10,13 @@ import uuid
10
10
  from dataclasses import dataclass, field
11
11
  from typing import TYPE_CHECKING, Any, Optional
12
12
 
13
+ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
13
14
  from tunacode.types import (
14
15
  DeviceId,
15
16
  InputSessions,
16
17
  MessageHistory,
17
18
  ModelName,
18
- PlanDoc,
19
- PlanPhase,
20
19
  SessionId,
21
- TodoItem,
22
20
  ToolName,
23
21
  UserConfig,
24
22
  )
@@ -39,7 +37,8 @@ class SessionState:
39
37
  ) # Keep as dict[str, Any] for agent instances
40
38
  messages: MessageHistory = field(default_factory=list)
41
39
  total_cost: float = 0.0
42
- current_model: ModelName = "openai:gpt-4o"
40
+ # Keep session default in sync with configuration default
41
+ current_model: ModelName = DEFAULT_USER_CONFIG["default_model"]
43
42
  spinner: Optional[Any] = None
44
43
  tool_ignore: list[ToolName] = field(default_factory=list)
45
44
  yolo: bool = False
@@ -49,7 +48,10 @@ class SessionState:
49
48
  device_id: Optional[DeviceId] = None
50
49
  input_sessions: InputSessions = field(default_factory=dict)
51
50
  current_task: Optional[Any] = None
52
- todos: list[TodoItem] = field(default_factory=list)
51
+ # CLAUDE_ANCHOR[react-scratchpad]: Session scratchpad for ReAct tooling
52
+ react_scratchpad: dict[str, Any] = field(default_factory=lambda: {"timeline": []})
53
+ react_forced_calls: int = 0
54
+ react_guidance: list[str] = field(default_factory=list)
53
55
  # Operation state tracking
54
56
  operation_cancelled: bool = False
55
57
  # Enhanced tracking for thoughts display
@@ -87,12 +89,6 @@ class SessionState:
87
89
  iteration_budgets: dict[str, int] = field(default_factory=dict)
88
90
  recursive_context_stack: list[dict[str, Any]] = field(default_factory=list)
89
91
 
90
- # Plan Mode state tracking
91
- plan_mode: bool = False
92
- plan_phase: Optional[PlanPhase] = None
93
- current_plan: Optional[PlanDoc] = None
94
- plan_approved: bool = False
95
-
96
92
  def update_token_count(self):
97
93
  """Calculates the total token count from messages and files in context."""
98
94
  message_contents = [get_message_content(msg) for msg in self.messages]
@@ -119,19 +115,6 @@ class StateManager:
119
115
  def set_tool_handler(self, handler: "ToolHandler") -> None:
120
116
  self._tool_handler = handler
121
117
 
122
- def add_todo(self, todo: TodoItem) -> None:
123
- self._session.todos.append(todo)
124
-
125
- def update_todo(self, todo_id: str, status: str) -> None:
126
- from datetime import datetime
127
-
128
- for todo in self._session.todos:
129
- if todo.id == todo_id:
130
- todo.status = status
131
- if status == "completed" and not todo.completed_at:
132
- todo.completed_at = datetime.now()
133
- break
134
-
135
118
  def push_recursive_context(self, context: dict[str, Any]) -> None:
136
119
  """Push a new context onto the recursive execution stack."""
137
120
  self._session.recursive_context_stack.append(context)
@@ -166,48 +149,17 @@ class StateManager:
166
149
  self._session.iteration_budgets.clear()
167
150
  self._session.recursive_context_stack.clear()
168
151
 
169
- def remove_todo(self, todo_id: str) -> None:
170
- self._session.todos = [todo for todo in self._session.todos if todo.id != todo_id]
152
+ # React scratchpad helpers
153
+ def get_react_scratchpad(self) -> dict[str, Any]:
154
+ return self._session.react_scratchpad
155
+
156
+ def append_react_entry(self, entry: dict[str, Any]) -> None:
157
+ timeline = self._session.react_scratchpad.setdefault("timeline", [])
158
+ timeline.append(entry)
171
159
 
172
- def clear_todos(self) -> None:
173
- self._session.todos = []
160
+ def clear_react_scratchpad(self) -> None:
161
+ self._session.react_scratchpad = {"timeline": []}
174
162
 
175
163
  def reset_session(self) -> None:
176
164
  """Reset the session to a fresh state."""
177
165
  self._session = SessionState()
178
-
179
- # Plan Mode methods
180
- def enter_plan_mode(self) -> None:
181
- """Enter plan mode - restricts to read-only operations."""
182
- self._session.plan_mode = True
183
- self._session.plan_phase = PlanPhase.PLANNING_RESEARCH
184
- self._session.current_plan = None
185
- self._session.plan_approved = False
186
- # Clear agent cache to force recreation with plan mode tools
187
- self._session.agents.clear()
188
-
189
- def exit_plan_mode(self, plan: Optional[PlanDoc] = None) -> None:
190
- """Exit plan mode with optional plan data."""
191
- self._session.plan_mode = False
192
- self._session.plan_phase = None
193
- self._session.current_plan = plan
194
- self._session.plan_approved = False
195
- # Clear agent cache to force recreation without plan mode tools
196
- self._session.agents.clear()
197
-
198
- def approve_plan(self) -> None:
199
- """Mark current plan as approved for execution."""
200
- self._session.plan_approved = True
201
- self._session.plan_mode = False
202
-
203
- def is_plan_mode(self) -> bool:
204
- """Check if currently in plan mode."""
205
- return self._session.plan_mode
206
-
207
- def set_current_plan(self, plan: PlanDoc) -> None:
208
- """Set the current plan data."""
209
- self._session.current_plan = plan
210
-
211
- def get_current_plan(self) -> Optional[PlanDoc]:
212
- """Get the current plan data."""
213
- return self._session.current_plan
@@ -136,8 +136,10 @@ class UsageTracker(UsageTrackerProtocol):
136
136
  last_cost_safe = last_cost if last_cost is not None else 0.0
137
137
  session_cost_safe = session_cost if session_cost is not None else 0.0
138
138
 
139
+ total_tokens = prompt_safe + completion_safe
139
140
  usage_summary = (
140
- f"[ Tokens: {prompt_safe + completion_safe:,} (P: {prompt_safe:,}, C: {completion_safe:,}) | "
141
+ f"[ Tokens: {total_tokens:,} "
142
+ f"(P: {prompt_safe:,}, C: {completion_safe:,}) | "
141
143
  f"Cost: ${last_cost_safe:.4f} | "
142
144
  f"Session Total: ${session_cost_safe:.4f} ]"
143
145
  )