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.
- tunacode/cli/commands/__init__.py +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- 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/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- 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 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/core/agents/utils.py
CHANGED
|
@@ -3,9 +3,8 @@ import importlib
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
-
import re
|
|
7
6
|
from collections.abc import Iterator
|
|
8
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime
|
|
9
8
|
from typing import Any
|
|
10
9
|
|
|
11
10
|
from tunacode.constants import (
|
|
@@ -14,13 +13,12 @@ from tunacode.constants import (
|
|
|
14
13
|
JSON_PARSE_MAX_RETRIES,
|
|
15
14
|
READ_ONLY_TOOLS,
|
|
16
15
|
)
|
|
16
|
+
|
|
17
|
+
# Re-export tool parsing functions from agent_components for backward compatibility
|
|
17
18
|
from tunacode.exceptions import ToolBatchingJSONError
|
|
18
19
|
from tunacode.types import (
|
|
19
|
-
ErrorMessage,
|
|
20
20
|
StateManager,
|
|
21
21
|
ToolCallback,
|
|
22
|
-
ToolCallId,
|
|
23
|
-
ToolName,
|
|
24
22
|
)
|
|
25
23
|
from tunacode.ui import console as ui
|
|
26
24
|
from tunacode.utils.retry import retry_json_parse_async
|
|
@@ -271,123 +269,32 @@ async def parse_json_tool_calls(
|
|
|
271
269
|
|
|
272
270
|
async def extract_and_execute_tool_calls(
|
|
273
271
|
text: str, tool_callback: ToolCallback | None, state_manager: StateManager
|
|
274
|
-
):
|
|
275
|
-
"""Extract tool calls from text content and execute them.
|
|
276
|
-
Supports multiple formats for maximum compatibility.
|
|
272
|
+
) -> int:
|
|
277
273
|
"""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# Format 2: Tool calls in code blocks
|
|
282
|
-
code_block_pattern = r'```json\s*(\{(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*"tool"(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*\})\s*```'
|
|
283
|
-
code_matches = re.findall(code_block_pattern, text, re.MULTILINE | re.DOTALL)
|
|
284
|
-
remaining_text = re.sub(code_block_pattern, "", text)
|
|
285
|
-
|
|
286
|
-
for match in code_matches:
|
|
287
|
-
try:
|
|
288
|
-
# Use retry logic for JSON parsing in code blocks
|
|
289
|
-
tool_data = await retry_json_parse_async(
|
|
290
|
-
match,
|
|
291
|
-
max_retries=JSON_PARSE_MAX_RETRIES,
|
|
292
|
-
base_delay=JSON_PARSE_BASE_DELAY,
|
|
293
|
-
max_delay=JSON_PARSE_MAX_DELAY,
|
|
294
|
-
)
|
|
295
|
-
if "tool" in tool_data and "args" in tool_data:
|
|
296
|
-
|
|
297
|
-
class MockToolCall:
|
|
298
|
-
def __init__(self, tool_name: str, args: dict):
|
|
299
|
-
self.tool_name = tool_name
|
|
300
|
-
self.args = args
|
|
301
|
-
self.tool_call_id = f"codeblock_{datetime.now().timestamp()}"
|
|
302
|
-
|
|
303
|
-
class MockNode:
|
|
304
|
-
pass
|
|
305
|
-
|
|
306
|
-
mock_call = MockToolCall(tool_data["tool"], tool_data["args"])
|
|
307
|
-
mock_node = MockNode()
|
|
308
|
-
|
|
309
|
-
await tool_callback(mock_call, mock_node)
|
|
310
|
-
|
|
311
|
-
if state_manager.session.show_thoughts:
|
|
312
|
-
await ui.muted(f"FALLBACK: Executed {tool_data['tool']} from code block")
|
|
313
|
-
|
|
314
|
-
except json.JSONDecodeError as e:
|
|
315
|
-
# After all retries failed
|
|
316
|
-
logger.error(
|
|
317
|
-
f"Code block JSON parsing failed after {JSON_PARSE_MAX_RETRIES} retries: {e}"
|
|
318
|
-
)
|
|
319
|
-
if state_manager.session.show_thoughts:
|
|
320
|
-
await ui.error(
|
|
321
|
-
f"Failed to parse code block tool JSON after {JSON_PARSE_MAX_RETRIES} retries"
|
|
322
|
-
)
|
|
323
|
-
# Raise custom exception for better error handling
|
|
324
|
-
raise ToolBatchingJSONError(
|
|
325
|
-
json_content=match,
|
|
326
|
-
retry_count=JSON_PARSE_MAX_RETRIES,
|
|
327
|
-
original_error=e,
|
|
328
|
-
) from e
|
|
329
|
-
except (KeyError, Exception) as e:
|
|
330
|
-
if state_manager.session.show_thoughts:
|
|
331
|
-
await ui.error(f"Error parsing code block tool call: {e!s}")
|
|
332
|
-
|
|
333
|
-
# Format 1: {"tool": "name", "args": {...}}
|
|
334
|
-
await parse_json_tool_calls(remaining_text, tool_callback, state_manager)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def patch_tool_messages(
|
|
338
|
-
error_message: ErrorMessage = "Tool operation failed",
|
|
339
|
-
state_manager: StateManager = None,
|
|
340
|
-
):
|
|
341
|
-
"""Find any tool calls without responses and add synthetic error responses for them.
|
|
342
|
-
Takes an error message to use in the synthesized tool response.
|
|
274
|
+
Extract tool calls from text content and execute them.
|
|
275
|
+
Supports multiple formats for maximum compatibility.
|
|
276
|
+
Uses the enhanced parse_json_tool_calls with retry logic.
|
|
343
277
|
|
|
344
|
-
|
|
345
|
-
|
|
278
|
+
Returns:
|
|
279
|
+
int: Number of tools successfully executed
|
|
346
280
|
"""
|
|
347
|
-
if
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
):
|
|
368
|
-
if part.part_kind == "tool-call":
|
|
369
|
-
tool_calls[part.tool_call_id] = part.tool_name
|
|
370
|
-
elif part.part_kind == "tool-return":
|
|
371
|
-
tool_returns.add(part.tool_call_id)
|
|
372
|
-
elif part.part_kind == "retry-prompt":
|
|
373
|
-
retry_prompts.add(part.tool_call_id)
|
|
374
|
-
|
|
375
|
-
# Identify orphaned tools (those without responses and not being retried)
|
|
376
|
-
for tool_call_id, tool_name in list(tool_calls.items()):
|
|
377
|
-
if tool_call_id not in tool_returns and tool_call_id not in retry_prompts:
|
|
378
|
-
# Import ModelRequest and ToolReturnPart lazily
|
|
379
|
-
model_request_cls, tool_return_part_cls, _ = get_model_messages()
|
|
380
|
-
messages.append(
|
|
381
|
-
model_request_cls(
|
|
382
|
-
parts=[
|
|
383
|
-
tool_return_part_cls(
|
|
384
|
-
tool_name=tool_name,
|
|
385
|
-
content=error_message,
|
|
386
|
-
tool_call_id=tool_call_id,
|
|
387
|
-
timestamp=datetime.now(timezone.utc),
|
|
388
|
-
part_kind="tool-return",
|
|
389
|
-
)
|
|
390
|
-
],
|
|
391
|
-
kind="request",
|
|
392
|
-
)
|
|
393
|
-
)
|
|
281
|
+
if not tool_callback:
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
tools_executed = 0
|
|
285
|
+
|
|
286
|
+
# Use the enhanced parse_json_tool_calls with retry logic
|
|
287
|
+
# We need to handle this differently since our parse_json_tool_calls doesn't return a count
|
|
288
|
+
try:
|
|
289
|
+
await parse_json_tool_calls(text, tool_callback, state_manager)
|
|
290
|
+
# If we get here without error, at least one tool was likely executed
|
|
291
|
+
# For simplicity, we'll assume 1 tool was executed if no error occurred
|
|
292
|
+
tools_executed = 1
|
|
293
|
+
except ToolBatchingJSONError:
|
|
294
|
+
# Re-raise the error for proper test handling
|
|
295
|
+
raise
|
|
296
|
+
except Exception:
|
|
297
|
+
# Other exceptions mean 0 tools executed
|
|
298
|
+
tools_executed = 0
|
|
299
|
+
|
|
300
|
+
return tools_executed
|
tunacode/core/setup/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@ from .base import BaseSetup
|
|
|
3
3
|
from .config_setup import ConfigSetup
|
|
4
4
|
from .coordinator import SetupCoordinator
|
|
5
5
|
from .environment_setup import EnvironmentSetup
|
|
6
|
-
from .git_safety_setup import GitSafetySetup
|
|
7
6
|
from .template_setup import TemplateSetup
|
|
8
7
|
|
|
9
8
|
__all__ = [
|
|
@@ -11,7 +10,6 @@ __all__ = [
|
|
|
11
10
|
"SetupCoordinator",
|
|
12
11
|
"ConfigSetup",
|
|
13
12
|
"EnvironmentSetup",
|
|
14
|
-
"GitSafetySetup",
|
|
15
13
|
"AgentSetup",
|
|
16
14
|
"TemplateSetup",
|
|
17
15
|
]
|
|
@@ -11,11 +11,16 @@ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
|
11
11
|
from tunacode.configuration.models import ModelRegistry
|
|
12
12
|
from tunacode.constants import APP_NAME, CONFIG_FILE_NAME, UI_COLORS
|
|
13
13
|
from tunacode.core.setup.base import BaseSetup
|
|
14
|
+
from tunacode.core.setup.config_wizard import ConfigWizard
|
|
14
15
|
from tunacode.core.state import StateManager
|
|
15
16
|
from tunacode.exceptions import ConfigurationError
|
|
16
17
|
from tunacode.types import ConfigFile, ConfigPath, UserConfig
|
|
17
18
|
from tunacode.ui import console as ui
|
|
18
19
|
from tunacode.utils import system, user_configuration
|
|
20
|
+
from tunacode.utils.api_key_validation import (
|
|
21
|
+
get_required_api_key_for_model,
|
|
22
|
+
validate_api_key_for_model,
|
|
23
|
+
)
|
|
19
24
|
from tunacode.utils.text_utils import key_to_title
|
|
20
25
|
|
|
21
26
|
|
|
@@ -51,7 +56,8 @@ class ConfigSetup(BaseSetup):
|
|
|
51
56
|
# Initialize first-time user settings if needed
|
|
52
57
|
user_configuration.initialize_first_time_user(self.state_manager)
|
|
53
58
|
|
|
54
|
-
# Fast path: if config fingerprint matches last loaded and config is already present,
|
|
59
|
+
# Fast path: if config fingerprint matches last loaded and config is already present,
|
|
60
|
+
# skip reprocessing
|
|
55
61
|
new_fp = None
|
|
56
62
|
if loaded_config:
|
|
57
63
|
b = json.dumps(loaded_config, sort_keys=True).encode()
|
|
@@ -104,14 +110,17 @@ class ConfigSetup(BaseSetup):
|
|
|
104
110
|
raise
|
|
105
111
|
|
|
106
112
|
if wizard_mode:
|
|
107
|
-
|
|
113
|
+
wizard = ConfigWizard(self.state_manager, self.model_registry, self.config_file)
|
|
114
|
+
await wizard.run_onboarding()
|
|
108
115
|
else:
|
|
109
116
|
await self._onboarding()
|
|
110
117
|
else:
|
|
111
|
-
# No config found - show CLI usage
|
|
118
|
+
# No config found - show CLI usage and continue with safe defaults (no crash)
|
|
112
119
|
from tunacode.ui.console import console
|
|
113
120
|
|
|
114
|
-
console.print(
|
|
121
|
+
console.print(
|
|
122
|
+
"\n[bold yellow]No configuration found — using safe defaults.[/bold yellow]"
|
|
123
|
+
)
|
|
115
124
|
console.print("\n[bold]Quick Setup:[/bold]")
|
|
116
125
|
console.print("Configure TunaCode using CLI flags:")
|
|
117
126
|
console.print("\n[blue]Examples:[/blue]")
|
|
@@ -126,23 +135,62 @@ class ConfigSetup(BaseSetup):
|
|
|
126
135
|
console.print("\n[yellow]Run 'tunacode --help' for more options[/yellow]\n")
|
|
127
136
|
console.print("\n[cyan]Or use --wizard for guided setup[/cyan]\n")
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
# Initialize in-memory defaults so we don't crash
|
|
139
|
+
self.state_manager.session.user_config = DEFAULT_USER_CONFIG.copy()
|
|
140
|
+
# Mark config as not fully validated for the fast path
|
|
141
|
+
setattr(self.state_manager, "_config_valid", False)
|
|
132
142
|
|
|
133
143
|
if not self.state_manager.session.user_config.get("default_model"):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
# Gracefully apply default model instead of crashing
|
|
145
|
+
self.state_manager.session.user_config["default_model"] = DEFAULT_USER_CONFIG[
|
|
146
|
+
"default_model"
|
|
147
|
+
]
|
|
148
|
+
await ui.warning(
|
|
149
|
+
"No default model set in config; applying safe default "
|
|
150
|
+
f"'{self.state_manager.session.user_config['default_model']}'."
|
|
139
151
|
)
|
|
140
152
|
|
|
141
|
-
#
|
|
153
|
+
# Validate API key exists for the selected model
|
|
154
|
+
model = self.state_manager.session.user_config["default_model"]
|
|
155
|
+
is_valid, error_msg = validate_api_key_for_model(
|
|
156
|
+
model, self.state_manager.session.user_config
|
|
157
|
+
)
|
|
158
|
+
if not is_valid:
|
|
159
|
+
# Try to pick a fallback model based on whichever provider has a key configured
|
|
160
|
+
fallback = self._pick_fallback_model(self.state_manager.session.user_config)
|
|
161
|
+
if fallback and fallback != model:
|
|
162
|
+
await ui.warning(
|
|
163
|
+
"API key missing for selected model; switching to configured provider: "
|
|
164
|
+
f"'{fallback}'."
|
|
165
|
+
)
|
|
166
|
+
self.state_manager.session.user_config["default_model"] = fallback
|
|
167
|
+
model = fallback
|
|
168
|
+
else:
|
|
169
|
+
# No suitable fallback; continue without crashing but mark invalid
|
|
170
|
+
await ui.warning(
|
|
171
|
+
(error_msg or "API key missing for model")
|
|
172
|
+
+ "\nContinuing without provider initialization; run 'tunacode --setup' later."
|
|
173
|
+
)
|
|
174
|
+
setattr(self.state_manager, "_config_valid", False)
|
|
142
175
|
|
|
143
|
-
self.state_manager.session.current_model =
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
self.state_manager.session.current_model = model
|
|
177
|
+
|
|
178
|
+
def _pick_fallback_model(self, user_config: UserConfig) -> str | None:
|
|
179
|
+
"""Select a reasonable fallback model based on configured API keys."""
|
|
180
|
+
env = (user_config or {}).get("env", {})
|
|
181
|
+
|
|
182
|
+
# Preference order: OpenAI → Anthropic → Google → OpenRouter
|
|
183
|
+
if env.get("OPENAI_API_KEY", "").strip():
|
|
184
|
+
return "openai:gpt-4o"
|
|
185
|
+
if env.get("ANTHROPIC_API_KEY", "").strip():
|
|
186
|
+
return "anthropic:claude-sonnet-4"
|
|
187
|
+
if env.get("GEMINI_API_KEY", "").strip():
|
|
188
|
+
return "google:gemini-2.5-flash"
|
|
189
|
+
if env.get("OPENROUTER_API_KEY", "").strip():
|
|
190
|
+
# Use the project default when OpenRouter is configured
|
|
191
|
+
return DEFAULT_USER_CONFIG.get("default_model", "openrouter:openai/gpt-4.1")
|
|
192
|
+
|
|
193
|
+
return None
|
|
146
194
|
|
|
147
195
|
async def validate(self) -> bool:
|
|
148
196
|
"""Validate that configuration is properly set up."""
|
|
@@ -152,6 +200,30 @@ class ConfigSetup(BaseSetup):
|
|
|
152
200
|
valid = False
|
|
153
201
|
elif not self.state_manager.session.user_config.get("default_model"):
|
|
154
202
|
valid = False
|
|
203
|
+
else:
|
|
204
|
+
# Validate API key exists for the selected model
|
|
205
|
+
model = self.state_manager.session.user_config.get("default_model")
|
|
206
|
+
is_valid, error_msg = validate_api_key_for_model(
|
|
207
|
+
model, self.state_manager.session.user_config
|
|
208
|
+
)
|
|
209
|
+
if not is_valid:
|
|
210
|
+
valid = False
|
|
211
|
+
# Store error message for later use
|
|
212
|
+
setattr(self.state_manager, "_config_error", error_msg)
|
|
213
|
+
|
|
214
|
+
# Provide actionable guidance for manual setup
|
|
215
|
+
required_key, provider_name = get_required_api_key_for_model(model)
|
|
216
|
+
setup_hint = (
|
|
217
|
+
f"Missing API key for {provider_name}.\n"
|
|
218
|
+
f"Either run 'tunacode --wizard' (recommended) or add it manually to: "
|
|
219
|
+
f"{self.config_file}\n\n"
|
|
220
|
+
"Example snippet (add under 'env'):\n"
|
|
221
|
+
' "env": {\n'
|
|
222
|
+
f' "{required_key or "PROVIDER_API_KEY"}": "your-key-here"\n'
|
|
223
|
+
" }\n"
|
|
224
|
+
)
|
|
225
|
+
await ui.error(setup_hint)
|
|
226
|
+
|
|
155
227
|
# Cache result for fastpath
|
|
156
228
|
if valid:
|
|
157
229
|
setattr(self.state_manager, "_config_valid", True)
|
|
@@ -357,214 +429,3 @@ class ConfigSetup(BaseSetup):
|
|
|
357
429
|
await ui.success(f"Configuration saved to: {self.config_file}")
|
|
358
430
|
except ConfigurationError as e:
|
|
359
431
|
await ui.error(str(e))
|
|
360
|
-
|
|
361
|
-
async def _wizard_onboarding(self):
|
|
362
|
-
"""Run enhanced wizard-style onboarding process for new users."""
|
|
363
|
-
initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
364
|
-
|
|
365
|
-
# Welcome message with provider guidance
|
|
366
|
-
await ui.panel(
|
|
367
|
-
"Welcome to TunaCode Setup Wizard!",
|
|
368
|
-
"This guided setup will help you configure TunaCode in under 5 minutes.\n"
|
|
369
|
-
"We'll help you choose a provider, set up your API keys, and configure your preferred model.",
|
|
370
|
-
border_style=UI_COLORS["primary"],
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
# Step 1: Provider selection with detailed guidance
|
|
374
|
-
await self._wizard_step1_provider_selection()
|
|
375
|
-
|
|
376
|
-
# Step 2: API key setup with provider-specific guidance
|
|
377
|
-
await self._wizard_step2_api_key_setup()
|
|
378
|
-
|
|
379
|
-
# Step 3: Model selection with smart recommendations
|
|
380
|
-
await self._wizard_step3_model_selection()
|
|
381
|
-
|
|
382
|
-
# Step 4: Optional settings configuration
|
|
383
|
-
await self._wizard_step4_optional_settings()
|
|
384
|
-
|
|
385
|
-
# Save configuration and finish
|
|
386
|
-
current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
387
|
-
if initial_config != current_config:
|
|
388
|
-
try:
|
|
389
|
-
user_configuration.save_config(self.state_manager)
|
|
390
|
-
await ui.panel(
|
|
391
|
-
"Setup Complete!",
|
|
392
|
-
f"Configuration saved to: [bold]{self.config_file}[/bold]\n\n"
|
|
393
|
-
"You're ready to start using TunaCode!\n"
|
|
394
|
-
"Use [green]/quickstart[/green] anytime for a tutorial.",
|
|
395
|
-
border_style=UI_COLORS["success"],
|
|
396
|
-
)
|
|
397
|
-
except ConfigurationError as e:
|
|
398
|
-
await ui.error(str(e))
|
|
399
|
-
|
|
400
|
-
async def _wizard_step1_provider_selection(self):
|
|
401
|
-
"""Wizard step 1: Provider selection with detailed explanations."""
|
|
402
|
-
provider_info = {
|
|
403
|
-
"1": {
|
|
404
|
-
"name": "OpenRouter",
|
|
405
|
-
"description": "Access to multiple models (GPT-4, Claude, Gemini, etc.)",
|
|
406
|
-
"signup": "https://openrouter.ai/",
|
|
407
|
-
"key_name": "OPENROUTER_API_KEY",
|
|
408
|
-
},
|
|
409
|
-
"2": {
|
|
410
|
-
"name": "OpenAI",
|
|
411
|
-
"description": "GPT-4 models",
|
|
412
|
-
"signup": "https://platform.openai.com/signup",
|
|
413
|
-
"key_name": "OPENAI_API_KEY",
|
|
414
|
-
},
|
|
415
|
-
"3": {
|
|
416
|
-
"name": "Anthropic",
|
|
417
|
-
"description": "Claude-3 models",
|
|
418
|
-
"signup": "https://console.anthropic.com/",
|
|
419
|
-
"key_name": "ANTHROPIC_API_KEY",
|
|
420
|
-
},
|
|
421
|
-
"4": {
|
|
422
|
-
"name": "Google",
|
|
423
|
-
"description": "Gemini models",
|
|
424
|
-
"signup": "https://ai.google.dev/",
|
|
425
|
-
"key_name": "GEMINI_API_KEY",
|
|
426
|
-
},
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
message = "Choose your AI provider:\n\n"
|
|
430
|
-
for key, info in provider_info.items():
|
|
431
|
-
message += f" {key} - {info['name']}: {info['description']}\n"
|
|
432
|
-
|
|
433
|
-
await ui.panel("Provider Selection", message, border_style=UI_COLORS["primary"])
|
|
434
|
-
|
|
435
|
-
while True:
|
|
436
|
-
choice = await ui.input(
|
|
437
|
-
"wizard_provider",
|
|
438
|
-
pretext=" Choose provider (1-4): ",
|
|
439
|
-
state_manager=self.state_manager,
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
if choice.strip() in provider_info:
|
|
443
|
-
selected = provider_info[choice.strip()]
|
|
444
|
-
self._wizard_selected_provider = selected
|
|
445
|
-
|
|
446
|
-
await ui.success(f"Selected: {selected['name']}")
|
|
447
|
-
await ui.info(f"Sign up at: {selected['signup']}")
|
|
448
|
-
break
|
|
449
|
-
else:
|
|
450
|
-
await ui.error("Please enter 1, 2, 3, or 4")
|
|
451
|
-
|
|
452
|
-
async def _wizard_step2_api_key_setup(self):
|
|
453
|
-
"""Wizard step 2: API key setup with provider-specific guidance."""
|
|
454
|
-
provider = self._wizard_selected_provider
|
|
455
|
-
|
|
456
|
-
message = f"Enter your {provider['name']} API key:\n\n"
|
|
457
|
-
message += f"Get your key from: {provider['signup']}\n"
|
|
458
|
-
message += "Your key will be stored securely in your local config"
|
|
459
|
-
|
|
460
|
-
await ui.panel(f"{provider['name']} API Key", message, border_style=UI_COLORS["primary"])
|
|
461
|
-
|
|
462
|
-
while True:
|
|
463
|
-
api_key = await ui.input(
|
|
464
|
-
"wizard_api_key",
|
|
465
|
-
pretext=f" {provider['name']} API Key: ",
|
|
466
|
-
is_password=True,
|
|
467
|
-
state_manager=self.state_manager,
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
if api_key.strip():
|
|
471
|
-
# Ensure env dict exists
|
|
472
|
-
if "env" not in self.state_manager.session.user_config:
|
|
473
|
-
self.state_manager.session.user_config["env"] = {}
|
|
474
|
-
|
|
475
|
-
self.state_manager.session.user_config["env"][provider["key_name"]] = (
|
|
476
|
-
api_key.strip()
|
|
477
|
-
)
|
|
478
|
-
await ui.success("API key saved successfully!")
|
|
479
|
-
break
|
|
480
|
-
else:
|
|
481
|
-
await ui.error("API key cannot be empty")
|
|
482
|
-
|
|
483
|
-
async def _wizard_step3_model_selection(self):
|
|
484
|
-
"""Wizard step 3: Model selection with smart recommendations."""
|
|
485
|
-
provider = self._wizard_selected_provider
|
|
486
|
-
|
|
487
|
-
# Provide smart recommendations based on provider
|
|
488
|
-
recommendations = {
|
|
489
|
-
"OpenAI": [
|
|
490
|
-
("openai:gpt-4o", "GPT-4o flagship multimodal model (recommended)"),
|
|
491
|
-
("openai:gpt-4.1", "Latest GPT-4.1 with enhanced coding"),
|
|
492
|
-
("openai:o3", "Advanced reasoning model for complex tasks"),
|
|
493
|
-
],
|
|
494
|
-
"Anthropic": [
|
|
495
|
-
("anthropic:claude-sonnet-4", "Claude Sonnet 4 latest generation (recommended)"),
|
|
496
|
-
("anthropic:claude-opus-4.1", "Most capable Claude with extended thinking"),
|
|
497
|
-
("anthropic:claude-3.5-sonnet", "Claude 3.5 Sonnet proven performance"),
|
|
498
|
-
],
|
|
499
|
-
"OpenRouter": [
|
|
500
|
-
(
|
|
501
|
-
"openrouter:anthropic/claude-sonnet-4",
|
|
502
|
-
"Claude Sonnet 4 via OpenRouter (recommended)",
|
|
503
|
-
),
|
|
504
|
-
("openrouter:openai/gpt-4.1", "GPT-4.1 via OpenRouter"),
|
|
505
|
-
("openrouter:google/gemini-2.5-flash", "Google Gemini 2.5 Flash latest"),
|
|
506
|
-
],
|
|
507
|
-
"Google": [
|
|
508
|
-
(
|
|
509
|
-
"google:gemini-2.5-pro",
|
|
510
|
-
"Gemini 2.5 Pro with thinking capabilities (recommended)",
|
|
511
|
-
),
|
|
512
|
-
("google:gemini-2.5-flash", "Gemini 2.5 Flash best price-performance"),
|
|
513
|
-
("google:gemini-2.0-flash", "Gemini 2.0 Flash with native tool use"),
|
|
514
|
-
],
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
models = recommendations.get(provider["name"], [])
|
|
518
|
-
message = f"Choose your default {provider['name']} model:\n\n"
|
|
519
|
-
|
|
520
|
-
for i, (model_id, description) in enumerate(models, 1):
|
|
521
|
-
message += f" {i} - {description}\n"
|
|
522
|
-
|
|
523
|
-
message += "\nYou can change this later with [green]/model[/green]"
|
|
524
|
-
|
|
525
|
-
await ui.panel("Model Selection", message, border_style=UI_COLORS["primary"])
|
|
526
|
-
|
|
527
|
-
while True:
|
|
528
|
-
choice = await ui.input(
|
|
529
|
-
"wizard_model",
|
|
530
|
-
pretext=f" Choose model (1-{len(models)}): ",
|
|
531
|
-
state_manager=self.state_manager,
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
try:
|
|
535
|
-
index = int(choice.strip()) - 1
|
|
536
|
-
if 0 <= index < len(models):
|
|
537
|
-
selected_model = models[index][0]
|
|
538
|
-
self.state_manager.session.user_config["default_model"] = selected_model
|
|
539
|
-
await ui.success(f"Selected: {selected_model}")
|
|
540
|
-
break
|
|
541
|
-
else:
|
|
542
|
-
await ui.error(f"Please enter a number between 1 and {len(models)}")
|
|
543
|
-
except ValueError:
|
|
544
|
-
await ui.error("Please enter a valid number")
|
|
545
|
-
|
|
546
|
-
async def _wizard_step4_optional_settings(self):
|
|
547
|
-
"""Wizard step 4: Optional settings configuration."""
|
|
548
|
-
message = "Configure optional settings:\n\n"
|
|
549
|
-
message += "• Tutorial: Enable interactive tutorial for new users\n"
|
|
550
|
-
message += "\nSkip this step to use recommended defaults"
|
|
551
|
-
|
|
552
|
-
await ui.panel("Optional Settings", message, border_style=UI_COLORS["primary"])
|
|
553
|
-
|
|
554
|
-
# Ask about tutorial
|
|
555
|
-
tutorial_choice = await ui.input(
|
|
556
|
-
"wizard_tutorial",
|
|
557
|
-
pretext=" Enable tutorial for new users? [Y/n]: ",
|
|
558
|
-
state_manager=self.state_manager,
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
enable_tutorial = tutorial_choice.strip().lower() not in ["n", "no", "false"]
|
|
562
|
-
|
|
563
|
-
if "settings" not in self.state_manager.session.user_config:
|
|
564
|
-
self.state_manager.session.user_config["settings"] = {}
|
|
565
|
-
|
|
566
|
-
self.state_manager.session.user_config["settings"]["enable_tutorial"] = enable_tutorial
|
|
567
|
-
|
|
568
|
-
# Streaming is always enabled - no user choice needed
|
|
569
|
-
|
|
570
|
-
await ui.info("Optional settings configured!")
|