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.

Files changed (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.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!")
@@ -22,8 +22,8 @@ class SetupCoordinator:
22
22
  """Register a setup step to be run."""
23
23
  self.setup_steps.append(step)
24
24
 
25
- async def run_setup(self, force_setup: bool = False) -> None:
26
- """Run all registered setup steps concurrently if possible."""
25
+ async def run_setup(self, force_setup: bool = False, wizard_mode: bool = False) -> None:
26
+ """Run all registered setup steps with proper dependency order."""
27
27
  # Run should_run checks sequentially (they may depend on order)
28
28
  steps_to_run = []
29
29
  for step in self.setup_steps:
@@ -35,11 +35,20 @@ class SetupCoordinator:
35
35
  f"Setup failed at step '{getattr(step, 'name', repr(step))}': {str(e)}"
36
36
  )
37
37
  raise
38
- # Run all .execute(force_setup) in parallel where possible (independent steps)
39
- from asyncio import gather
40
38
 
41
39
  try:
42
- await gather(*(step.execute(force_setup) for step in steps_to_run))
40
+ # Run steps sequentially to respect dependencies
41
+ # (ConfigSetup must complete before EnvironmentSetup)
42
+ for step in steps_to_run:
43
+ # Check if the step's execute method supports wizard_mode
44
+ import inspect
45
+
46
+ sig = inspect.signature(step.execute)
47
+ if "wizard_mode" in sig.parameters:
48
+ await step.execute(force_setup, wizard_mode=wizard_mode)
49
+ else:
50
+ await step.execute(force_setup)
51
+
43
52
  # Now validate all sequentially: if any fail, raise error
44
53
  for step in steps_to_run:
45
54
  if not await step.validate():
tunacode/core/state.py CHANGED
@@ -10,13 +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
19
  SessionId,
19
- TodoItem,
20
20
  ToolName,
21
21
  UserConfig,
22
22
  )
@@ -37,7 +37,8 @@ class SessionState:
37
37
  ) # Keep as dict[str, Any] for agent instances
38
38
  messages: MessageHistory = field(default_factory=list)
39
39
  total_cost: float = 0.0
40
- current_model: ModelName = "openai:gpt-4o"
40
+ # Keep session default in sync with configuration default
41
+ current_model: ModelName = DEFAULT_USER_CONFIG["default_model"]
41
42
  spinner: Optional[Any] = None
42
43
  tool_ignore: list[ToolName] = field(default_factory=list)
43
44
  yolo: bool = False
@@ -47,7 +48,10 @@ class SessionState:
47
48
  device_id: Optional[DeviceId] = None
48
49
  input_sessions: InputSessions = field(default_factory=dict)
49
50
  current_task: Optional[Any] = None
50
- 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)
51
55
  # Operation state tracking
52
56
  operation_cancelled: bool = False
53
57
  # Enhanced tracking for thoughts display
@@ -111,19 +115,6 @@ class StateManager:
111
115
  def set_tool_handler(self, handler: "ToolHandler") -> None:
112
116
  self._tool_handler = handler
113
117
 
114
- def add_todo(self, todo: TodoItem) -> None:
115
- self._session.todos.append(todo)
116
-
117
- def update_todo(self, todo_id: str, status: str) -> None:
118
- from datetime import datetime
119
-
120
- for todo in self._session.todos:
121
- if todo.id == todo_id:
122
- todo.status = status
123
- if status == "completed" and not todo.completed_at:
124
- todo.completed_at = datetime.now()
125
- break
126
-
127
118
  def push_recursive_context(self, context: dict[str, Any]) -> None:
128
119
  """Push a new context onto the recursive execution stack."""
129
120
  self._session.recursive_context_stack.append(context)
@@ -158,11 +149,16 @@ class StateManager:
158
149
  self._session.iteration_budgets.clear()
159
150
  self._session.recursive_context_stack.clear()
160
151
 
161
- def remove_todo(self, todo_id: str) -> None:
162
- 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)
163
159
 
164
- def clear_todos(self) -> None:
165
- self._session.todos = []
160
+ def clear_react_scratchpad(self) -> None:
161
+ self._session.react_scratchpad = {"timeline": []}
166
162
 
167
163
  def reset_session(self) -> None:
168
164
  """Reset the session to a fresh state."""
@@ -37,10 +37,10 @@ class UsageTracker(UsageTrackerProtocol):
37
37
  # 2. Calculate the cost
38
38
  cost = self._calculate_cost(parsed_data)
39
39
 
40
- # 3. Update the session state
40
+ # 3. Update the session state (always done to track totals for session cost display)
41
41
  self._update_state(parsed_data, cost)
42
42
 
43
- # 4. Display the summary if enabled
43
+ # 4. Display detailed per-call summary only if debugging enabled
44
44
  if self.state_manager.session.show_thoughts:
45
45
  await self._display_summary()
46
46
 
@@ -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
  )