tsugite-cli 0.3.3__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.
Files changed (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
tsugite/cli/history.py ADDED
@@ -0,0 +1,193 @@
1
+ """History management CLI commands."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from tsugite.history import (
11
+ ConversationMetadata,
12
+ Turn,
13
+ get_history_dir,
14
+ load_conversation,
15
+ query_index,
16
+ rebuild_index,
17
+ )
18
+ from tsugite.ui.chat_history import format_conversation_for_display
19
+
20
+ console = Console()
21
+
22
+ history_app = typer.Typer(help="Manage conversation history")
23
+
24
+
25
+ @history_app.command("list")
26
+ def history_list(
27
+ machine: str = typer.Option(None, "--machine", help="Filter by machine name"),
28
+ agent: str = typer.Option(None, "--agent", help="Filter by agent name"),
29
+ limit: int = typer.Option(20, "--limit", "-n", help="Maximum number of results"),
30
+ ):
31
+ """List conversations with optional filters.
32
+
33
+ Examples:
34
+ tsugite history list
35
+ tsugite history list --machine laptop
36
+ tsugite history list --agent chat_assistant --limit 10
37
+ """
38
+ try:
39
+ conversations = query_index(machine=machine, agent=agent, limit=limit)
40
+
41
+ if not conversations:
42
+ console.print("[yellow]No conversations found[/yellow]")
43
+
44
+ history_dir = get_history_dir()
45
+ if not history_dir.exists():
46
+ console.print(f"\nHistory directory doesn't exist yet: {history_dir}")
47
+ console.print("Conversations will be saved automatically when you use chat mode.")
48
+ return
49
+
50
+ table = Table(title=f"Conversation History ({len(conversations)} found)")
51
+ table.add_column("ID", style="cyan", no_wrap=True)
52
+ table.add_column("Agent", style="green")
53
+ table.add_column("Machine", style="dim")
54
+ table.add_column("Turns", justify="right")
55
+ table.add_column("Tokens", justify="right", style="dim")
56
+ table.add_column("Updated", style="dim")
57
+
58
+ for conv in conversations:
59
+ conv_id = conv.get("conversation_id", "unknown")
60
+ agent_name = conv.get("agent", "unknown")
61
+ machine_name = conv.get("machine", "unknown")
62
+ turn_count = conv.get("turn_count", 0)
63
+ total_tokens = conv.get("total_tokens", 0)
64
+ updated_at = conv.get("updated_at", "unknown")
65
+
66
+ # Format date
67
+ try:
68
+ dt = datetime.fromisoformat(updated_at)
69
+ updated_str = dt.strftime("%Y-%m-%d %H:%M")
70
+ except (ValueError, TypeError):
71
+ updated_str = updated_at[:16] if len(updated_at) > 16 else updated_at
72
+
73
+ table.add_row(
74
+ conv_id,
75
+ agent_name,
76
+ machine_name,
77
+ str(turn_count),
78
+ f"{total_tokens:,}" if total_tokens > 0 else "-",
79
+ updated_str,
80
+ )
81
+
82
+ console.print(table)
83
+
84
+ # Show usage hint
85
+ console.print("\n[dim]Use 'tsugite history show CONVERSATION_ID' to view details[/dim]")
86
+
87
+ except Exception as e:
88
+ console.print(f"[red]Failed to list conversations: {e}[/red]")
89
+ raise typer.Exit(1)
90
+
91
+
92
+ @history_app.command("show")
93
+ def history_show(
94
+ conversation_id: str = typer.Argument(help="Conversation ID to show"),
95
+ format: str = typer.Option("plain", "--format", "-f", help="Output format (plain, json, markdown)"),
96
+ ):
97
+ """Show full conversation details.
98
+
99
+ Examples:
100
+ tsugite history show 20251024_103000_chat_abc123
101
+ tsugite history show 20251024_103000_chat_abc123 --format json
102
+ tsugite history show 20251024_103000_chat_abc123 --format markdown
103
+ """
104
+ try:
105
+ turns = load_conversation(conversation_id)
106
+
107
+ if not turns:
108
+ console.print(f"[yellow]Conversation '{conversation_id}' is empty[/yellow]")
109
+ return
110
+
111
+ if format == "json":
112
+ # JSON output - convert Pydantic models to dicts
113
+ turns_as_dicts = [t.model_dump(mode="json") for t in turns]
114
+ output = json.dumps(turns_as_dicts, indent=2, ensure_ascii=False)
115
+ # Use plain print to avoid Rich wrapping that breaks JSON
116
+ print(output)
117
+
118
+ elif format == "markdown":
119
+ # Markdown output
120
+ metadata = next((t for t in turns if isinstance(t, ConversationMetadata)), None)
121
+
122
+ console.print(f"# Conversation: {conversation_id}\n")
123
+ if metadata:
124
+ console.print(f"- **Agent**: {metadata.agent}")
125
+ console.print(f"- **Model**: {metadata.model}")
126
+ console.print(f"- **Machine**: {metadata.machine}")
127
+ console.print(f"- **Created**: {metadata.created_at}\n")
128
+ console.print("---\n")
129
+
130
+ for turn in turns:
131
+ if isinstance(turn, Turn):
132
+ console.print(f"## Turn ({turn.timestamp})\n")
133
+ console.print(f"**User**: {turn.user}\n")
134
+ console.print(f"**Assistant**: {turn.assistant}\n")
135
+
136
+ if turn.tools:
137
+ console.print(f"*Tools*: {', '.join(turn.tools)}\n")
138
+
139
+ # Display execution steps if available
140
+ if turn.steps:
141
+ console.print("### Execution Steps\n")
142
+ for step in turn.steps:
143
+ step_num = step.get("step_number", "?")
144
+ thought = step.get("thought", "").strip()
145
+ code = step.get("code", "").strip()
146
+ output = step.get("output", "").strip()
147
+ error = step.get("error")
148
+ tools_called = step.get("tools_called", [])
149
+
150
+ console.print(f"**Step {step_num}**\n")
151
+ if thought:
152
+ console.print(f"*Thought*: {thought}\n")
153
+ if tools_called:
154
+ console.print(f"*Tools used*: {', '.join(tools_called)}\n")
155
+ if code:
156
+ console.print("*Code*:")
157
+ console.print(f"```python\n{code}\n```\n")
158
+ if output:
159
+ console.print(f"*Output*: {output}\n")
160
+ if error:
161
+ console.print(f"*Error*: {error}\n")
162
+
163
+ console.print(f"*Tokens*: {turn.tokens or 0} | *Cost*: ${turn.cost or 0.0:.4f}\n")
164
+ console.print("---\n")
165
+
166
+ else:
167
+ # Plain text output (default)
168
+ output = format_conversation_for_display(turns)
169
+ console.print(output)
170
+
171
+ except FileNotFoundError:
172
+ console.print(f"[red]Conversation '{conversation_id}' not found[/red]")
173
+ raise typer.Exit(1)
174
+ except Exception as e:
175
+ console.print(f"[red]Failed to load conversation: {e}[/red]")
176
+ raise typer.Exit(1)
177
+
178
+
179
+ @history_app.command("rebuild-index")
180
+ def history_rebuild_index():
181
+ """Rebuild conversation index from JSONL files.
182
+
183
+ Scans all conversation files and rebuilds the index.
184
+ Useful after manual file changes or index corruption.
185
+ """
186
+ try:
187
+ console.print("Rebuilding conversation index...")
188
+ count = rebuild_index()
189
+ console.print(f"[green]✓ Indexed {count} conversations[/green]")
190
+
191
+ except Exception as e:
192
+ console.print(f"[red]Failed to rebuild index: {e}[/red]")
193
+ raise typer.Exit(1)
tsugite/cli/init.py ADDED
@@ -0,0 +1,387 @@
1
+ """Initialization command for first-time setup."""
2
+
3
+ import logging
4
+ import shutil
5
+ import subprocess
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+ import questionary
11
+ import typer
12
+ from prompt_toolkit.styles import Style
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.prompt import Confirm
16
+
17
+ from tsugite.config import get_config_path, load_config, save_config
18
+
19
+ console = Console()
20
+
21
+ # Custom questionary style for better readability
22
+ # Uses dark background for popup with light text for good contrast
23
+ questionary_style = Style(
24
+ [
25
+ ("qmark", "fg:ansicyan bold"), # Question mark
26
+ ("question", "bold"), # Question text
27
+ ("answer", "fg:ansicyan bold"), # Selected answer
28
+ ("pointer", "fg:ansicyan bold"), # Pointer arrow
29
+ ("highlighted", "fg:ansiwhite bg:ansiblue"), # Highlighted choice - white on blue
30
+ ("selected", "fg:ansicyan"), # Selected text
31
+ ("separator", "fg:ansiblack"), # Separator
32
+ ("instruction", ""), # Instructions
33
+ ("text", ""), # Default text
34
+ # Autocomplete-specific styles
35
+ ("completion-menu", "bg:ansiblack fg:ansiwhite"), # Popup background
36
+ ("completion-menu.completion", "fg:ansiwhite bg:ansiblack"), # Individual items
37
+ ("completion-menu.completion.current", "fg:ansiblack bg:ansicyan bold"), # Current selection - black on cyan
38
+ ]
39
+ )
40
+
41
+
42
+ @contextmanager
43
+ def suppress_litellm_warnings():
44
+ """Temporarily suppress LiteLLM warnings."""
45
+ litellm_logger = logging.getLogger("LiteLLM")
46
+ original_level = litellm_logger.level
47
+ litellm_logger.setLevel(logging.ERROR)
48
+ try:
49
+ yield
50
+ finally:
51
+ litellm_logger.setLevel(original_level)
52
+
53
+
54
+ def detect_ollama() -> bool:
55
+ """Check if Ollama is running."""
56
+ try:
57
+ result = subprocess.run(
58
+ ["ollama", "list"],
59
+ capture_output=True,
60
+ timeout=2,
61
+ check=False,
62
+ )
63
+ return result.returncode == 0
64
+ except (FileNotFoundError, subprocess.TimeoutExpired):
65
+ return False
66
+
67
+
68
+ def fetch_provider_models(provider: str) -> List[str]:
69
+ """Get available models for a provider using LiteLLM.
70
+
71
+ Args:
72
+ provider: Provider name (ollama, openai, anthropic, google)
73
+
74
+ Returns:
75
+ List of model strings in LiteLLM format (e.g., "ollama/qwen2.5-coder:7b")
76
+ """
77
+ try:
78
+ from litellm import get_valid_models
79
+
80
+ with suppress_litellm_warnings():
81
+ # For ollama, check endpoint to get locally installed models
82
+ if provider == "ollama":
83
+ models = get_valid_models(
84
+ custom_llm_provider="ollama",
85
+ check_provider_endpoint=True,
86
+ )
87
+ # For cloud providers, get models based on API key presence
88
+ elif provider == "openai":
89
+ models = get_valid_models(custom_llm_provider="openai")
90
+ elif provider == "anthropic":
91
+ models = get_valid_models(custom_llm_provider="anthropic")
92
+ elif provider == "google":
93
+ # Use "gemini" instead of "google" for LiteLLM
94
+ models = get_valid_models(custom_llm_provider="gemini")
95
+ # Convert gemini/* to google:* for consistency
96
+ models = [m.replace("gemini/", "google/") for m in models]
97
+ else:
98
+ return []
99
+
100
+ return models if models else []
101
+ except Exception:
102
+ # Silently return empty list on error
103
+ return []
104
+
105
+
106
+ def litellm_to_tsugite_format(model: str) -> str:
107
+ """Convert LiteLLM model format to Tsugite format.
108
+
109
+ Args:
110
+ model: Model in LiteLLM format (e.g., "ollama/qwen2.5-coder:7b")
111
+
112
+ Returns:
113
+ Model in Tsugite format (e.g., "ollama:qwen2.5-coder:7b")
114
+ """
115
+ # Replace first / with :
116
+ return model.replace("/", ":", 1)
117
+
118
+
119
+ def detect_available_providers() -> dict[str, int]:
120
+ """Detect which LLM providers are available and count their models.
121
+
122
+ Returns:
123
+ Dict of provider name -> model count (0 if unavailable)
124
+ """
125
+ providers = {}
126
+
127
+ # Check each provider
128
+ for provider_name in ["ollama", "openai", "anthropic", "google"]:
129
+ models = fetch_provider_models(provider_name)
130
+ providers[provider_name] = len(models)
131
+
132
+ return providers
133
+
134
+
135
+ def prompt_for_model(providers: dict[str, int], skip_prompt: bool = False, default_model: Optional[str] = None) -> str:
136
+ """Prompt user to select a provider and then a model with arrow-key navigation.
137
+
138
+ Args:
139
+ providers: Dict of provider name -> model count
140
+ skip_prompt: If True, auto-select first available provider and model
141
+ default_model: If provided, skip prompts entirely
142
+
143
+ Returns:
144
+ Model string in Tsugite format (e.g., "ollama:qwen2.5-coder:7b")
145
+ """
146
+ if default_model:
147
+ return default_model
148
+
149
+ console.print("\n[bold cyan]Model Selection[/bold cyan]")
150
+
151
+ # Filter available providers
152
+ provider_choices = [(name, count) for name, count in providers.items() if count > 0]
153
+
154
+ if not provider_choices:
155
+ console.print("\n[yellow]No providers detected. You can still configure manually.[/yellow]")
156
+ if skip_prompt:
157
+ return "ollama:qwen2.5-coder:7b"
158
+
159
+ model = questionary.text(
160
+ "Enter model string (e.g., 'ollama:qwen2.5-coder:7b', 'openai:gpt-4o-mini'):",
161
+ default="ollama:qwen2.5-coder:7b",
162
+ style=questionary_style,
163
+ ).ask()
164
+ return model or "ollama:qwen2.5-coder:7b"
165
+
166
+ # Loop to allow going back
167
+ while True:
168
+ # Step 1: Select Provider
169
+ if skip_prompt:
170
+ selected_provider = provider_choices[0][0]
171
+ else:
172
+ provider_options = [f"{name} ({count} models)" for name, count in provider_choices]
173
+
174
+ selected_display = questionary.select(
175
+ "Select a provider:",
176
+ choices=provider_options,
177
+ style=questionary_style,
178
+ ).ask()
179
+
180
+ if not selected_display: # User cancelled (Ctrl+C)
181
+ raise KeyboardInterrupt
182
+
183
+ # Extract provider name from display string
184
+ selected_provider = selected_display.split(" (")[0]
185
+
186
+ # Step 2: Select Model from Provider
187
+ console.print(f"\n[bold cyan]Fetching {selected_provider} models...[/bold cyan]")
188
+ models = fetch_provider_models(selected_provider)
189
+
190
+ if not models:
191
+ console.print(f"[yellow]No models found for {selected_provider}[/yellow]")
192
+ if skip_prompt:
193
+ return f"{selected_provider}:default"
194
+
195
+ model = questionary.text(
196
+ f"Enter {selected_provider} model name:",
197
+ default="default",
198
+ style=questionary_style,
199
+ ).ask()
200
+ return f"{selected_provider}:{model or 'default'}"
201
+
202
+ # Convert to Tsugite format
203
+ tsugite_models = [litellm_to_tsugite_format(m) for m in models]
204
+
205
+ if skip_prompt:
206
+ return tsugite_models[0]
207
+
208
+ # Build model choices with Back option at the end
209
+ special_choices = ["← Back to provider selection", "✎ Enter custom model name"]
210
+ model_choices = tsugite_models + special_choices
211
+
212
+ # Use autocomplete for searchable/filterable list
213
+ console.print("\n[dim]💡 Tip: Type to filter models, use arrow keys to navigate[/dim]")
214
+ selected_model = questionary.autocomplete(
215
+ f"Select a model from {selected_provider}:",
216
+ choices=model_choices,
217
+ match_middle=True, # Allow matching in middle of string
218
+ style=questionary_style,
219
+ ).ask()
220
+
221
+ if not selected_model: # User cancelled
222
+ raise KeyboardInterrupt
223
+
224
+ if selected_model == "← Back to provider selection":
225
+ # Go back to provider selection
226
+ continue
227
+ elif selected_model == "✎ Enter custom model name":
228
+ # Custom model name
229
+ model_name = questionary.text(
230
+ f"Enter {selected_provider} model name:",
231
+ style=questionary_style,
232
+ ).ask()
233
+ if model_name:
234
+ return f"{selected_provider}:{model_name}"
235
+ else:
236
+ continue # Go back if empty
237
+ else:
238
+ # Regular model selected
239
+ return selected_model
240
+
241
+
242
+ def copy_template_agent(config_dir: Path) -> Path:
243
+ """Copy template assistant agent to config directory."""
244
+ agents_dir = config_dir / "agents"
245
+ agents_dir.mkdir(parents=True, exist_ok=True)
246
+
247
+ template_path = Path(__file__).parent.parent / "templates" / "assistant.md"
248
+ destination = agents_dir / "assistant.md"
249
+
250
+ if not destination.exists():
251
+ shutil.copy(template_path, destination)
252
+ console.print(f"[green]✓[/green] Created default agent: {destination}")
253
+ else:
254
+ console.print(f"[dim]Agent already exists: {destination}[/dim]")
255
+
256
+ return destination
257
+
258
+
259
+ def test_setup(model: str, skip_test: bool = False) -> bool:
260
+ """Run a quick test to verify setup works."""
261
+ if skip_test:
262
+ return True
263
+
264
+ console.print("\n[bold cyan]Testing Setup[/bold cyan]")
265
+
266
+ if not Confirm.ask("Run a quick test?", default=True):
267
+ return True
268
+
269
+ try:
270
+ from io import StringIO
271
+
272
+ from rich.console import Console as RichConsole
273
+
274
+ from tsugite.agent_runner import run_agent
275
+ from tsugite.ui import custom_agent_ui
276
+
277
+ # Find the assistant agent
278
+ config_dir = get_config_path().parent
279
+ agent_path = config_dir / "agents" / "assistant.md"
280
+
281
+ if not agent_path.exists():
282
+ console.print("[yellow]Warning: Assistant agent not found, skipping test[/yellow]")
283
+ return False
284
+
285
+ console.print("[dim]Running: tsugite run +assistant 'say hello'[/dim]")
286
+
287
+ # Create a quiet logger that doesn't output anything
288
+ quiet_console = RichConsole(file=StringIO())
289
+ with custom_agent_ui(
290
+ quiet_console,
291
+ show_code=False,
292
+ show_observations=False,
293
+ show_progress=False,
294
+ show_llm_messages=False,
295
+ show_execution_results=False,
296
+ show_execution_logs=False,
297
+ show_panels=False,
298
+ ) as logger:
299
+ result = run_agent(
300
+ agent_path=agent_path,
301
+ prompt="Say hello in one sentence",
302
+ model_override=model,
303
+ debug=False,
304
+ custom_logger=logger,
305
+ )
306
+
307
+ console.print("[green]✓[/green] Test successful!")
308
+ console.print(
309
+ f"[dim]Response: {result[:100]}...[/dim]" if len(result) > 100 else f"[dim]Response: {result}[/dim]"
310
+ )
311
+ return True
312
+
313
+ except Exception as e:
314
+ console.print(f"[yellow]Warning: Test failed: {e}[/yellow]")
315
+ console.print("[dim]You can still use tsugite, but you may need to configure your model.[/dim]")
316
+ return False
317
+
318
+
319
+ def init(
320
+ model: Optional[str] = typer.Option(None, "--model", help="Model to use (skip prompt)"),
321
+ force: bool = typer.Option(False, "--force", help="Overwrite existing configuration"),
322
+ ):
323
+ """Initialize tsugite with global configuration."""
324
+ from tsugite.cli.helpers import get_logo
325
+
326
+ # Show welcome
327
+ console.print(get_logo(console), style="cyan")
328
+ console.print()
329
+ console.print(
330
+ Panel(
331
+ "[bold]Welcome to Tsugite![/bold]\n\n"
332
+ "This wizard will set up your global configuration.\n"
333
+ "You can reconfigure anytime with: [cyan]tsugite config[/cyan]",
334
+ border_style="blue",
335
+ title="First-Time Setup",
336
+ )
337
+ )
338
+
339
+ # Check if already configured
340
+ config_path = get_config_path()
341
+ config = load_config()
342
+
343
+ if config_path.exists() and config.default_model and not force:
344
+ console.print(f"\n[yellow]Configuration already exists at: {config_path}[/yellow]")
345
+ console.print(f"Current default model: [cyan]{config.default_model}[/cyan]")
346
+
347
+ if not Confirm.ask("Reconfigure?", default=False):
348
+ console.print("\n[dim]Setup cancelled. Use --force to overwrite.[/dim]")
349
+ return
350
+
351
+ # Detect providers
352
+ console.print("\n[bold cyan]Detecting LLM Providers[/bold cyan]")
353
+ providers = detect_available_providers()
354
+
355
+ # Prompt for model
356
+ selected_model = prompt_for_model(providers, skip_prompt=bool(model), default_model=model)
357
+
358
+ # Create config
359
+ console.print("\n[bold cyan]Creating Configuration[/bold cyan]")
360
+ config.default_model = selected_model
361
+ save_config(config)
362
+
363
+ console.print(f"[green]✓[/green] Configuration saved to: [dim]{config_path}[/dim]")
364
+ console.print(f"[green]✓[/green] Default model: [cyan]{selected_model}[/cyan]")
365
+
366
+ # Copy template agent
367
+ console.print("\n[bold cyan]Setting Up Default Agent[/bold cyan]")
368
+ config_dir = config_path.parent
369
+ agent_path = copy_template_agent(config_dir)
370
+
371
+ # Show next steps
372
+ console.print("\n" + "=" * 60)
373
+ console.print("[bold green]Setup Complete![/bold green]")
374
+ console.print("=" * 60)
375
+
376
+ console.print("\n[bold]Next Steps:[/bold]")
377
+ console.print(" 1. Run the assistant: [cyan]tsugite run +assistant 'your task'[/cyan]")
378
+ console.print(" 2. View configuration: [cyan]tsugite config show[/cyan]")
379
+ console.print(" 3. Explore examples: [cyan]tsugite agents list[/cyan]")
380
+ console.print(f" 4. Customize your agent: [cyan]{agent_path}[/cyan]")
381
+
382
+ console.print("\n[bold]Documentation:[/bold]")
383
+ console.print(" • Quick start: See README.md")
384
+ console.print(" • Agent guide: See CLAUDE.md")
385
+ console.print(" • Examples: See examples/ directory")
386
+
387
+ console.print()