tunacode-cli 0.0.67__py3-none-any.whl → 0.0.69__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 -0
- tunacode/cli/commands/implementations/__init__.py +2 -0
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- tunacode/cli/commands/implementations/quickstart.py +43 -0
- tunacode/cli/commands/registry.py +131 -1
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +134 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +399 -0
- tunacode/cli/main.py +4 -1
- tunacode/cli/repl.py +25 -0
- tunacode/configuration/defaults.py +1 -0
- tunacode/constants.py +1 -1
- tunacode/core/agents/agent_components/agent_helpers.py +14 -13
- tunacode/core/agents/main.py +1 -1
- tunacode/core/agents/utils.py +4 -3
- tunacode/core/setup/config_setup.py +231 -6
- tunacode/core/setup/coordinator.py +13 -5
- tunacode/core/setup/git_safety_setup.py +5 -1
- tunacode/exceptions.py +119 -5
- tunacode/setup.py +5 -2
- tunacode/tools/glob.py +9 -46
- tunacode/tools/grep.py +9 -51
- 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/ui/output.py +1 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.69.dist-info/METADATA +192 -0
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.69.dist-info}/RECORD +37 -24
- tunacode_cli-0.0.67.dist-info/METADATA +0 -327
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.69.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.69.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.69.dist-info}/licenses/LICENSE +0 -0
|
@@ -37,12 +37,16 @@ class ConfigSetup(BaseSetup):
|
|
|
37
37
|
"""Config setup should always run to load and merge configuration."""
|
|
38
38
|
return True
|
|
39
39
|
|
|
40
|
-
async def execute(self, force_setup: bool = False) -> None:
|
|
40
|
+
async def execute(self, force_setup: bool = False, wizard_mode: bool = False) -> None:
|
|
41
41
|
"""Setup configuration and run onboarding if needed, with config fingerprint fast path."""
|
|
42
42
|
import hashlib
|
|
43
43
|
|
|
44
44
|
self.state_manager.session.device_id = system.get_device_id()
|
|
45
45
|
loaded_config = user_configuration.load_config()
|
|
46
|
+
|
|
47
|
+
# Initialize first-time user settings if needed
|
|
48
|
+
user_configuration.initialize_first_time_user(self.state_manager)
|
|
49
|
+
|
|
46
50
|
# Fast path: if config fingerprint matches last loaded and config is already present, skip reprocessing
|
|
47
51
|
new_fp = None
|
|
48
52
|
if loaded_config:
|
|
@@ -52,6 +56,7 @@ class ConfigSetup(BaseSetup):
|
|
|
52
56
|
if (
|
|
53
57
|
loaded_config
|
|
54
58
|
and not force_setup
|
|
59
|
+
and not wizard_mode
|
|
55
60
|
and new_fp
|
|
56
61
|
and last_fp == new_fp
|
|
57
62
|
and getattr(self.state_manager, "_config_valid", False)
|
|
@@ -68,13 +73,17 @@ class ConfigSetup(BaseSetup):
|
|
|
68
73
|
await self._handle_cli_config(loaded_config)
|
|
69
74
|
return
|
|
70
75
|
|
|
71
|
-
if loaded_config and not force_setup:
|
|
76
|
+
if loaded_config and not force_setup and not wizard_mode:
|
|
72
77
|
# Silent loading
|
|
73
78
|
# Merge loaded config with defaults to ensure all required keys exist
|
|
74
79
|
self.state_manager.session.user_config = self._merge_with_defaults(loaded_config)
|
|
75
80
|
else:
|
|
76
|
-
if force_setup:
|
|
77
|
-
|
|
81
|
+
if force_setup or wizard_mode:
|
|
82
|
+
if wizard_mode:
|
|
83
|
+
await ui.muted("Running interactive setup wizard")
|
|
84
|
+
else:
|
|
85
|
+
await ui.muted("Running setup process, resetting config")
|
|
86
|
+
|
|
78
87
|
# Ensure user_config is properly initialized
|
|
79
88
|
if (
|
|
80
89
|
not hasattr(self.state_manager.session, "user_config")
|
|
@@ -89,7 +98,11 @@ class ConfigSetup(BaseSetup):
|
|
|
89
98
|
except ConfigurationError as e:
|
|
90
99
|
await ui.error(str(e))
|
|
91
100
|
raise
|
|
92
|
-
|
|
101
|
+
|
|
102
|
+
if wizard_mode:
|
|
103
|
+
await self._wizard_onboarding()
|
|
104
|
+
else:
|
|
105
|
+
await self._onboarding()
|
|
93
106
|
else:
|
|
94
107
|
# No config found - show CLI usage instead of onboarding
|
|
95
108
|
from tunacode.ui.console import console
|
|
@@ -107,9 +120,10 @@ class ConfigSetup(BaseSetup):
|
|
|
107
120
|
"--key 'your-key' --baseurl 'https://openrouter.ai/api/v1'[/green]"
|
|
108
121
|
)
|
|
109
122
|
console.print("\n[yellow]Run 'tunacode --help' for more options[/yellow]\n")
|
|
123
|
+
console.print("\n[cyan]Or use --wizard for guided setup[/cyan]\n")
|
|
110
124
|
|
|
111
125
|
raise ConfigurationError(
|
|
112
|
-
"No configuration found. Please use CLI flags to configure."
|
|
126
|
+
"No configuration found. Please use CLI flags to configure or --wizard for guided setup."
|
|
113
127
|
)
|
|
114
128
|
|
|
115
129
|
if not self.state_manager.session.user_config.get("default_model"):
|
|
@@ -339,3 +353,214 @@ class ConfigSetup(BaseSetup):
|
|
|
339
353
|
await ui.success(f"Configuration saved to: {self.config_file}")
|
|
340
354
|
except ConfigurationError as e:
|
|
341
355
|
await ui.error(str(e))
|
|
356
|
+
|
|
357
|
+
async def _wizard_onboarding(self):
|
|
358
|
+
"""Run enhanced wizard-style onboarding process for new users."""
|
|
359
|
+
initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
360
|
+
|
|
361
|
+
# Welcome message with provider guidance
|
|
362
|
+
await ui.panel(
|
|
363
|
+
"Welcome to TunaCode Setup Wizard!",
|
|
364
|
+
"This guided setup will help you configure TunaCode in under 5 minutes.\n"
|
|
365
|
+
"We'll help you choose a provider, set up your API keys, and configure your preferred model.",
|
|
366
|
+
border_style=UI_COLORS["primary"],
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Step 1: Provider selection with detailed guidance
|
|
370
|
+
await self._wizard_step1_provider_selection()
|
|
371
|
+
|
|
372
|
+
# Step 2: API key setup with provider-specific guidance
|
|
373
|
+
await self._wizard_step2_api_key_setup()
|
|
374
|
+
|
|
375
|
+
# Step 3: Model selection with smart recommendations
|
|
376
|
+
await self._wizard_step3_model_selection()
|
|
377
|
+
|
|
378
|
+
# Step 4: Optional settings configuration
|
|
379
|
+
await self._wizard_step4_optional_settings()
|
|
380
|
+
|
|
381
|
+
# Save configuration and finish
|
|
382
|
+
current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
383
|
+
if initial_config != current_config:
|
|
384
|
+
try:
|
|
385
|
+
user_configuration.save_config(self.state_manager)
|
|
386
|
+
await ui.panel(
|
|
387
|
+
"Setup Complete!",
|
|
388
|
+
f"Configuration saved to: [bold]{self.config_file}[/bold]\n\n"
|
|
389
|
+
"You're ready to start using TunaCode!\n"
|
|
390
|
+
"Use [green]/quickstart[/green] anytime for a tutorial.",
|
|
391
|
+
border_style=UI_COLORS["success"],
|
|
392
|
+
)
|
|
393
|
+
except ConfigurationError as e:
|
|
394
|
+
await ui.error(str(e))
|
|
395
|
+
|
|
396
|
+
async def _wizard_step1_provider_selection(self):
|
|
397
|
+
"""Wizard step 1: Provider selection with detailed explanations."""
|
|
398
|
+
provider_info = {
|
|
399
|
+
"1": {
|
|
400
|
+
"name": "OpenRouter",
|
|
401
|
+
"description": "Access to multiple models (GPT-4, Claude, Gemini, etc.)",
|
|
402
|
+
"signup": "https://openrouter.ai/",
|
|
403
|
+
"key_name": "OPENROUTER_API_KEY",
|
|
404
|
+
},
|
|
405
|
+
"2": {
|
|
406
|
+
"name": "OpenAI",
|
|
407
|
+
"description": "GPT-4 models",
|
|
408
|
+
"signup": "https://platform.openai.com/signup",
|
|
409
|
+
"key_name": "OPENAI_API_KEY",
|
|
410
|
+
},
|
|
411
|
+
"3": {
|
|
412
|
+
"name": "Anthropic",
|
|
413
|
+
"description": "Claude-3 models",
|
|
414
|
+
"signup": "https://console.anthropic.com/",
|
|
415
|
+
"key_name": "ANTHROPIC_API_KEY",
|
|
416
|
+
},
|
|
417
|
+
"4": {
|
|
418
|
+
"name": "Google",
|
|
419
|
+
"description": "Gemini models",
|
|
420
|
+
"signup": "https://ai.google.dev/",
|
|
421
|
+
"key_name": "GEMINI_API_KEY",
|
|
422
|
+
},
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
message = "Choose your AI provider:\n\n"
|
|
426
|
+
for key, info in provider_info.items():
|
|
427
|
+
message += f" {key} - {info['name']}: {info['description']}\n"
|
|
428
|
+
|
|
429
|
+
await ui.panel("Provider Selection", message, border_style=UI_COLORS["primary"])
|
|
430
|
+
|
|
431
|
+
while True:
|
|
432
|
+
choice = await ui.input(
|
|
433
|
+
"wizard_provider",
|
|
434
|
+
pretext=" Choose provider (1-4): ",
|
|
435
|
+
state_manager=self.state_manager,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if choice.strip() in provider_info:
|
|
439
|
+
selected = provider_info[choice.strip()]
|
|
440
|
+
self._wizard_selected_provider = selected
|
|
441
|
+
|
|
442
|
+
await ui.success(f"Selected: {selected['name']}")
|
|
443
|
+
await ui.info(f"Sign up at: {selected['signup']}")
|
|
444
|
+
break
|
|
445
|
+
else:
|
|
446
|
+
await ui.error("Please enter 1, 2, 3, or 4")
|
|
447
|
+
|
|
448
|
+
async def _wizard_step2_api_key_setup(self):
|
|
449
|
+
"""Wizard step 2: API key setup with provider-specific guidance."""
|
|
450
|
+
provider = self._wizard_selected_provider
|
|
451
|
+
|
|
452
|
+
message = f"Enter your {provider['name']} API key:\n\n"
|
|
453
|
+
message += f"Get your key from: {provider['signup']}\n"
|
|
454
|
+
message += "Your key will be stored securely in your local config"
|
|
455
|
+
|
|
456
|
+
await ui.panel(f"{provider['name']} API Key", message, border_style=UI_COLORS["primary"])
|
|
457
|
+
|
|
458
|
+
while True:
|
|
459
|
+
api_key = await ui.input(
|
|
460
|
+
"wizard_api_key",
|
|
461
|
+
pretext=f" {provider['name']} API Key: ",
|
|
462
|
+
is_password=True,
|
|
463
|
+
state_manager=self.state_manager,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if api_key.strip():
|
|
467
|
+
# Ensure env dict exists
|
|
468
|
+
if "env" not in self.state_manager.session.user_config:
|
|
469
|
+
self.state_manager.session.user_config["env"] = {}
|
|
470
|
+
|
|
471
|
+
self.state_manager.session.user_config["env"][provider["key_name"]] = (
|
|
472
|
+
api_key.strip()
|
|
473
|
+
)
|
|
474
|
+
await ui.success("API key saved successfully!")
|
|
475
|
+
break
|
|
476
|
+
else:
|
|
477
|
+
await ui.error("API key cannot be empty")
|
|
478
|
+
|
|
479
|
+
async def _wizard_step3_model_selection(self):
|
|
480
|
+
"""Wizard step 3: Model selection with smart recommendations."""
|
|
481
|
+
provider = self._wizard_selected_provider
|
|
482
|
+
|
|
483
|
+
# Provide smart recommendations based on provider
|
|
484
|
+
recommendations = {
|
|
485
|
+
"OpenAI": [
|
|
486
|
+
("openai:gpt-4o", "GPT-4o flagship multimodal model (recommended)"),
|
|
487
|
+
("openai:gpt-4.1", "Latest GPT-4.1 with enhanced coding"),
|
|
488
|
+
("openai:o3", "Advanced reasoning model for complex tasks"),
|
|
489
|
+
],
|
|
490
|
+
"Anthropic": [
|
|
491
|
+
("anthropic:claude-sonnet-4", "Claude Sonnet 4 latest generation (recommended)"),
|
|
492
|
+
("anthropic:claude-opus-4.1", "Most capable Claude with extended thinking"),
|
|
493
|
+
("anthropic:claude-3.5-sonnet", "Claude 3.5 Sonnet proven performance"),
|
|
494
|
+
],
|
|
495
|
+
"OpenRouter": [
|
|
496
|
+
(
|
|
497
|
+
"openrouter:anthropic/claude-sonnet-4",
|
|
498
|
+
"Claude Sonnet 4 via OpenRouter (recommended)",
|
|
499
|
+
),
|
|
500
|
+
("openrouter:openai/gpt-4.1", "GPT-4.1 via OpenRouter"),
|
|
501
|
+
("openrouter:google/gemini-2.5-flash", "Google Gemini 2.5 Flash latest"),
|
|
502
|
+
],
|
|
503
|
+
"Google": [
|
|
504
|
+
(
|
|
505
|
+
"google:gemini-2.5-pro",
|
|
506
|
+
"Gemini 2.5 Pro with thinking capabilities (recommended)",
|
|
507
|
+
),
|
|
508
|
+
("google:gemini-2.5-flash", "Gemini 2.5 Flash best price-performance"),
|
|
509
|
+
("google:gemini-2.0-flash", "Gemini 2.0 Flash with native tool use"),
|
|
510
|
+
],
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
models = recommendations.get(provider["name"], [])
|
|
514
|
+
message = f"Choose your default {provider['name']} model:\n\n"
|
|
515
|
+
|
|
516
|
+
for i, (model_id, description) in enumerate(models, 1):
|
|
517
|
+
message += f" {i} - {description}\n"
|
|
518
|
+
|
|
519
|
+
message += "\nYou can change this later with [green]/model[/green]"
|
|
520
|
+
|
|
521
|
+
await ui.panel("Model Selection", message, border_style=UI_COLORS["primary"])
|
|
522
|
+
|
|
523
|
+
while True:
|
|
524
|
+
choice = await ui.input(
|
|
525
|
+
"wizard_model",
|
|
526
|
+
pretext=f" Choose model (1-{len(models)}): ",
|
|
527
|
+
state_manager=self.state_manager,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
index = int(choice.strip()) - 1
|
|
532
|
+
if 0 <= index < len(models):
|
|
533
|
+
selected_model = models[index][0]
|
|
534
|
+
self.state_manager.session.user_config["default_model"] = selected_model
|
|
535
|
+
await ui.success(f"Selected: {selected_model}")
|
|
536
|
+
break
|
|
537
|
+
else:
|
|
538
|
+
await ui.error(f"Please enter a number between 1 and {len(models)}")
|
|
539
|
+
except ValueError:
|
|
540
|
+
await ui.error("Please enter a valid number")
|
|
541
|
+
|
|
542
|
+
async def _wizard_step4_optional_settings(self):
|
|
543
|
+
"""Wizard step 4: Optional settings configuration."""
|
|
544
|
+
message = "Configure optional settings:\n\n"
|
|
545
|
+
message += "• Tutorial: Enable interactive tutorial for new users\n"
|
|
546
|
+
message += "\nSkip this step to use recommended defaults"
|
|
547
|
+
|
|
548
|
+
await ui.panel("Optional Settings", message, border_style=UI_COLORS["primary"])
|
|
549
|
+
|
|
550
|
+
# Ask about tutorial
|
|
551
|
+
tutorial_choice = await ui.input(
|
|
552
|
+
"wizard_tutorial",
|
|
553
|
+
pretext=" Enable tutorial for new users? [Y/n]: ",
|
|
554
|
+
state_manager=self.state_manager,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
enable_tutorial = tutorial_choice.strip().lower() not in ["n", "no", "false"]
|
|
558
|
+
|
|
559
|
+
if "settings" not in self.state_manager.session.user_config:
|
|
560
|
+
self.state_manager.session.user_config["settings"] = {}
|
|
561
|
+
|
|
562
|
+
self.state_manager.session.user_config["settings"]["enable_tutorial"] = enable_tutorial
|
|
563
|
+
|
|
564
|
+
# Streaming is always enabled - no user choice needed
|
|
565
|
+
|
|
566
|
+
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
|
|
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,19 @@ 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
|
-
|
|
40
|
+
# Run steps sequentially to respect dependencies (ConfigSetup must complete before EnvironmentSetup)
|
|
41
|
+
for step in steps_to_run:
|
|
42
|
+
# Check if the step's execute method supports wizard_mode
|
|
43
|
+
import inspect
|
|
44
|
+
|
|
45
|
+
sig = inspect.signature(step.execute)
|
|
46
|
+
if "wizard_mode" in sig.parameters:
|
|
47
|
+
await step.execute(force_setup, wizard_mode=wizard_mode)
|
|
48
|
+
else:
|
|
49
|
+
await step.execute(force_setup)
|
|
50
|
+
|
|
43
51
|
# Now validate all sequentially: if any fail, raise error
|
|
44
52
|
for step in steps_to_run:
|
|
45
53
|
if not await step.validate():
|
|
@@ -37,8 +37,12 @@ class GitSafetySetup(BaseSetup):
|
|
|
37
37
|
# Always run unless user has explicitly disabled it
|
|
38
38
|
return not self.state_manager.session.user_config.get("skip_git_safety", False)
|
|
39
39
|
|
|
40
|
-
async def execute(self, _force: bool = False) -> None:
|
|
40
|
+
async def execute(self, _force: bool = False, wizard_mode: bool = False) -> None:
|
|
41
41
|
"""Create a safety branch for TunaCode operations."""
|
|
42
|
+
# Skip git safety during wizard mode to avoid UI interference
|
|
43
|
+
if wizard_mode:
|
|
44
|
+
return
|
|
45
|
+
|
|
42
46
|
try:
|
|
43
47
|
# Check if git is installed
|
|
44
48
|
result = subprocess.run(
|
tunacode/exceptions.py
CHANGED
|
@@ -18,7 +18,18 @@ class TunaCodeError(Exception):
|
|
|
18
18
|
class ConfigurationError(TunaCodeError):
|
|
19
19
|
"""Raised when there's a configuration issue."""
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def __init__(self, message: str, suggested_fix: str = None, help_url: str = None):
|
|
22
|
+
self.suggested_fix = suggested_fix
|
|
23
|
+
self.help_url = help_url
|
|
24
|
+
|
|
25
|
+
# Build enhanced error message with actionable guidance
|
|
26
|
+
full_message = message
|
|
27
|
+
if suggested_fix:
|
|
28
|
+
full_message += f"\n\n💡 Suggested fix: {suggested_fix}"
|
|
29
|
+
if help_url:
|
|
30
|
+
full_message += f"\n📖 More help: {help_url}"
|
|
31
|
+
|
|
32
|
+
super().__init__(full_message)
|
|
22
33
|
|
|
23
34
|
|
|
24
35
|
# User Interaction Exceptions
|
|
@@ -31,7 +42,19 @@ class UserAbortError(TunaCodeError):
|
|
|
31
42
|
class ValidationError(TunaCodeError):
|
|
32
43
|
"""Raised when input validation fails."""
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
def __init__(self, message: str, suggested_fix: str = None, valid_examples: list = None):
|
|
46
|
+
self.suggested_fix = suggested_fix
|
|
47
|
+
self.valid_examples = valid_examples or []
|
|
48
|
+
|
|
49
|
+
# Build enhanced error message with actionable guidance
|
|
50
|
+
full_message = f"Validation failed: {message}"
|
|
51
|
+
if suggested_fix:
|
|
52
|
+
full_message += f"\n\n💡 Suggested fix: {suggested_fix}"
|
|
53
|
+
if valid_examples:
|
|
54
|
+
examples_text = "\n".join(f" • {example}" for example in valid_examples)
|
|
55
|
+
full_message += f"\n\n✅ Valid examples:\n{examples_text}"
|
|
56
|
+
|
|
57
|
+
super().__init__(full_message)
|
|
35
58
|
|
|
36
59
|
|
|
37
60
|
# Tool and Agent Exceptions
|
|
@@ -39,17 +62,47 @@ class ToolExecutionError(TunaCodeError):
|
|
|
39
62
|
"""Raised when a tool fails to execute."""
|
|
40
63
|
|
|
41
64
|
def __init__(
|
|
42
|
-
self,
|
|
65
|
+
self,
|
|
66
|
+
tool_name: ToolName,
|
|
67
|
+
message: ErrorMessage,
|
|
68
|
+
original_error: OriginalError = None,
|
|
69
|
+
suggested_fix: str = None,
|
|
70
|
+
recovery_commands: list = None,
|
|
43
71
|
):
|
|
44
72
|
self.tool_name = tool_name
|
|
45
73
|
self.original_error = original_error
|
|
46
|
-
|
|
74
|
+
self.suggested_fix = suggested_fix
|
|
75
|
+
self.recovery_commands = recovery_commands or []
|
|
76
|
+
|
|
77
|
+
# Build enhanced error message
|
|
78
|
+
full_message = f"Tool '{tool_name}' failed: {message}"
|
|
79
|
+
if suggested_fix:
|
|
80
|
+
full_message += f"\n\n💡 Suggested fix: {suggested_fix}"
|
|
81
|
+
if recovery_commands:
|
|
82
|
+
commands_text = "\n".join(f" • {cmd}" for cmd in recovery_commands)
|
|
83
|
+
full_message += f"\n\n🔧 Recovery commands:\n{commands_text}"
|
|
84
|
+
|
|
85
|
+
super().__init__(full_message)
|
|
47
86
|
|
|
48
87
|
|
|
49
88
|
class AgentError(TunaCodeError):
|
|
50
89
|
"""Raised when agent operations fail."""
|
|
51
90
|
|
|
52
|
-
|
|
91
|
+
def __init__(self, message: str, suggested_fix: str = None, troubleshooting_steps: list = None):
|
|
92
|
+
self.suggested_fix = suggested_fix
|
|
93
|
+
self.troubleshooting_steps = troubleshooting_steps or []
|
|
94
|
+
|
|
95
|
+
# Build enhanced error message
|
|
96
|
+
full_message = f"Agent error: {message}"
|
|
97
|
+
if suggested_fix:
|
|
98
|
+
full_message += f"\n\n💡 Suggested fix: {suggested_fix}"
|
|
99
|
+
if troubleshooting_steps:
|
|
100
|
+
steps_text = "\n".join(
|
|
101
|
+
f" {i + 1}. {step}" for i, step in enumerate(troubleshooting_steps)
|
|
102
|
+
)
|
|
103
|
+
full_message += f"\n\n🔍 Troubleshooting steps:\n{steps_text}"
|
|
104
|
+
|
|
105
|
+
super().__init__(full_message)
|
|
53
106
|
|
|
54
107
|
|
|
55
108
|
# State Management Exceptions
|
|
@@ -103,6 +156,67 @@ class FileOperationError(TunaCodeError):
|
|
|
103
156
|
super().__init__(f"File {operation} failed for '{path}': {message}")
|
|
104
157
|
|
|
105
158
|
|
|
159
|
+
# Additional specialized exception classes for onboarding scenarios
|
|
160
|
+
class OnboardingError(TunaCodeError):
|
|
161
|
+
"""Raised when onboarding process encounters issues."""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self, message: str, step: str = None, suggested_fix: str = None, help_command: str = None
|
|
165
|
+
):
|
|
166
|
+
self.step = step
|
|
167
|
+
self.suggested_fix = suggested_fix
|
|
168
|
+
self.help_command = help_command
|
|
169
|
+
|
|
170
|
+
# Build enhanced error message
|
|
171
|
+
full_message = f"Onboarding failed: {message}"
|
|
172
|
+
if step:
|
|
173
|
+
full_message = f"Onboarding failed at step '{step}': {message}"
|
|
174
|
+
if suggested_fix:
|
|
175
|
+
full_message += f"\n\n💡 Suggested fix: {suggested_fix}"
|
|
176
|
+
if help_command:
|
|
177
|
+
full_message += f"\n🆘 For help: {help_command}"
|
|
178
|
+
|
|
179
|
+
super().__init__(full_message)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ModelConfigurationError(ConfigurationError):
|
|
183
|
+
"""Raised when model configuration is invalid."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, model: str, issue: str, valid_models: list = None):
|
|
186
|
+
self.model = model
|
|
187
|
+
self.issue = issue
|
|
188
|
+
self.valid_models = valid_models or []
|
|
189
|
+
|
|
190
|
+
suggested_fix = "Use --wizard for guided setup or --model with a valid model name"
|
|
191
|
+
help_url = "https://docs.anthropic.com/en/docs/claude-code"
|
|
192
|
+
|
|
193
|
+
message = f"Model '{model}' configuration error: {issue}"
|
|
194
|
+
if valid_models:
|
|
195
|
+
examples = valid_models[:3] # Show first 3 examples
|
|
196
|
+
suggested_fix += f"\n\nValid examples: {', '.join(examples)}"
|
|
197
|
+
|
|
198
|
+
super().__init__(message, suggested_fix=suggested_fix, help_url=help_url)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class SetupValidationError(ValidationError):
|
|
202
|
+
"""Raised when setup validation fails."""
|
|
203
|
+
|
|
204
|
+
def __init__(self, validation_type: str, details: str, quick_fixes: list = None):
|
|
205
|
+
self.validation_type = validation_type
|
|
206
|
+
self.details = details
|
|
207
|
+
self.quick_fixes = quick_fixes or []
|
|
208
|
+
|
|
209
|
+
suggested_fix = "Run 'tunacode --wizard' for guided setup"
|
|
210
|
+
if quick_fixes:
|
|
211
|
+
suggested_fix = f"Try these quick fixes: {', '.join(quick_fixes)}"
|
|
212
|
+
|
|
213
|
+
super().__init__(
|
|
214
|
+
f"{validation_type} validation failed: {details}",
|
|
215
|
+
suggested_fix=suggested_fix,
|
|
216
|
+
valid_examples=["tunacode --wizard", "tunacode --setup", "tunacode --help"],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
106
220
|
class TooBroadPatternError(ToolExecutionError):
|
|
107
221
|
"""Raised when a search pattern is too broad and times out."""
|
|
108
222
|
|
tunacode/setup.py
CHANGED
|
@@ -18,7 +18,9 @@ from tunacode.core.setup import (
|
|
|
18
18
|
from tunacode.core.state import StateManager
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
async def setup(
|
|
21
|
+
async def setup(
|
|
22
|
+
run_setup: bool, state_manager: StateManager, cli_config: dict = None, wizard_mode: bool = False
|
|
23
|
+
) -> None:
|
|
22
24
|
"""
|
|
23
25
|
Setup TunaCode on startup using the new setup coordinator.
|
|
24
26
|
|
|
@@ -26,6 +28,7 @@ async def setup(run_setup: bool, state_manager: StateManager, cli_config: dict =
|
|
|
26
28
|
run_setup (bool): If True, force run the setup process, resetting current config.
|
|
27
29
|
state_manager (StateManager): The state manager instance.
|
|
28
30
|
cli_config (dict): Optional CLI configuration with baseurl, model, and key.
|
|
31
|
+
wizard_mode (bool): If True, run interactive setup wizard.
|
|
29
32
|
"""
|
|
30
33
|
coordinator = SetupCoordinator(state_manager)
|
|
31
34
|
|
|
@@ -39,7 +42,7 @@ async def setup(run_setup: bool, state_manager: StateManager, cli_config: dict =
|
|
|
39
42
|
coordinator.register_step(GitSafetySetup(state_manager))
|
|
40
43
|
|
|
41
44
|
# Run all setup steps
|
|
42
|
-
await coordinator.run_setup(force_setup=run_setup)
|
|
45
|
+
await coordinator.run_setup(force_setup=run_setup, wizard_mode=wizard_mode)
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
async def setup_agent(agent: Optional[Any], state_manager: StateManager) -> None:
|
tunacode/tools/glob.py
CHANGED
|
@@ -14,11 +14,10 @@ from functools import lru_cache
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Dict, List, Optional, Set, Union
|
|
16
16
|
|
|
17
|
-
import defusedxml.ElementTree as ET
|
|
18
|
-
|
|
19
17
|
from tunacode.core.code_index import CodeIndex
|
|
20
18
|
from tunacode.exceptions import ToolExecutionError
|
|
21
19
|
from tunacode.tools.base import BaseTool
|
|
20
|
+
from tunacode.tools.xml_helper import load_parameters_schema_from_xml, load_prompt_from_xml
|
|
22
21
|
|
|
23
22
|
# Configuration
|
|
24
23
|
MAX_RESULTS = 5000 # Maximum files to return
|
|
@@ -70,17 +69,10 @@ class GlobTool(BaseTool):
|
|
|
70
69
|
Returns:
|
|
71
70
|
str: The loaded prompt from XML or a default prompt
|
|
72
71
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tree = ET.parse(prompt_file)
|
|
78
|
-
root = tree.getroot()
|
|
79
|
-
description = root.find("description")
|
|
80
|
-
if description is not None:
|
|
81
|
-
return description.text.strip()
|
|
82
|
-
except Exception:
|
|
83
|
-
pass # Fall through to default
|
|
72
|
+
# Try to load from XML helper
|
|
73
|
+
prompt = load_prompt_from_xml("glob")
|
|
74
|
+
if prompt:
|
|
75
|
+
return prompt
|
|
84
76
|
|
|
85
77
|
# Fallback to default prompt
|
|
86
78
|
return """Fast file pattern matching tool
|
|
@@ -92,39 +84,10 @@ class GlobTool(BaseTool):
|
|
|
92
84
|
@lru_cache(maxsize=1)
|
|
93
85
|
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
94
86
|
"""Get the parameters schema for the glob tool."""
|
|
95
|
-
# Try to load from XML
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
tree = ET.parse(prompt_file)
|
|
100
|
-
root = tree.getroot()
|
|
101
|
-
parameters = root.find("parameters")
|
|
102
|
-
if parameters is not None:
|
|
103
|
-
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
104
|
-
required_fields: List[str] = []
|
|
105
|
-
|
|
106
|
-
for param in parameters.findall("parameter"):
|
|
107
|
-
name = param.get("name")
|
|
108
|
-
required = param.get("required", "false").lower() == "true"
|
|
109
|
-
param_type = param.find("type")
|
|
110
|
-
description = param.find("description")
|
|
111
|
-
|
|
112
|
-
if name and param_type is not None:
|
|
113
|
-
prop = {
|
|
114
|
-
"type": param_type.text.strip(),
|
|
115
|
-
"description": description.text.strip()
|
|
116
|
-
if description is not None
|
|
117
|
-
else "",
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
schema["properties"][name] = prop
|
|
121
|
-
if required:
|
|
122
|
-
required_fields.append(name)
|
|
123
|
-
|
|
124
|
-
schema["required"] = required_fields
|
|
125
|
-
return schema
|
|
126
|
-
except Exception:
|
|
127
|
-
pass # Fall through to hardcoded schema
|
|
87
|
+
# Try to load from XML helper
|
|
88
|
+
schema = load_parameters_schema_from_xml("glob")
|
|
89
|
+
if schema:
|
|
90
|
+
return schema
|
|
128
91
|
|
|
129
92
|
# Fallback to hardcoded schema
|
|
130
93
|
return {
|