tunacode-cli 0.0.75__py3-none-any.whl → 0.0.76.1__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/implementations/model.py +33 -5
- tunacode/cli/main.py +10 -0
- tunacode/cli/repl.py +17 -1
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +275 -0
- tunacode/constants.py +1 -1
- tunacode/core/agents/agent_components/node_processor.py +40 -4
- tunacode/core/agents/agent_components/streaming.py +268 -0
- tunacode/core/agents/main.py +30 -15
- tunacode/core/setup/config_setup.py +67 -224
- tunacode/core/setup/config_wizard.py +229 -0
- tunacode/core/state.py +3 -1
- tunacode/prompts/system.md +43 -29
- tunacode/ui/config_dashboard.py +567 -0
- tunacode/ui/panels.py +92 -9
- tunacode/utils/config_comparator.py +340 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/METADATA +63 -6
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/RECORD +21 -16
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/licenses/LICENSE +0 -0
tunacode/ui/panels.py
CHANGED
|
@@ -92,7 +92,7 @@ class StreamingAgentPanel:
|
|
|
92
92
|
_dots_count: int
|
|
93
93
|
_show_dots: bool
|
|
94
94
|
|
|
95
|
-
def __init__(self, bottom: int = 1):
|
|
95
|
+
def __init__(self, bottom: int = 1, debug: bool = False):
|
|
96
96
|
self.bottom = bottom
|
|
97
97
|
self.title = f"[bold {colors.primary}]●[/bold {colors.primary}] {APP_NAME}"
|
|
98
98
|
self.content = ""
|
|
@@ -101,6 +101,24 @@ class StreamingAgentPanel:
|
|
|
101
101
|
self._dots_task = None
|
|
102
102
|
self._dots_count = 0
|
|
103
103
|
self._show_dots = True # Start with dots enabled for "Thinking..."
|
|
104
|
+
# Debug/diagnostic instrumentation (printed after stop to avoid Live interference)
|
|
105
|
+
self._debug_enabled = debug
|
|
106
|
+
self._debug_events: list[str] = []
|
|
107
|
+
self._update_count: int = 0
|
|
108
|
+
self._first_update_done: bool = False
|
|
109
|
+
self._dots_tick_count: int = 0
|
|
110
|
+
self._max_logged_dots: int = 10
|
|
111
|
+
|
|
112
|
+
def _log_debug(self, label: str, **data: Any) -> None:
|
|
113
|
+
if not self._debug_enabled:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
ts = time.perf_counter_ns()
|
|
117
|
+
except Exception:
|
|
118
|
+
ts = 0
|
|
119
|
+
payload = ", ".join(f"{k}={repr(v)}" for k, v in data.items()) if data else ""
|
|
120
|
+
line = f"[ui] {label} ts_ns={ts}{(' ' + payload) if payload else ''}"
|
|
121
|
+
self._debug_events.append(line)
|
|
104
122
|
|
|
105
123
|
def _create_panel(self) -> Padding:
|
|
106
124
|
"""Create a Rich panel with current content."""
|
|
@@ -159,6 +177,14 @@ class StreamingAgentPanel:
|
|
|
159
177
|
if current_time - self._last_update_time > delay_threshold:
|
|
160
178
|
self._show_dots = True
|
|
161
179
|
self._dots_count += 1
|
|
180
|
+
# Log only a few initial ticks to avoid noise
|
|
181
|
+
if (
|
|
182
|
+
self._debug_enabled
|
|
183
|
+
and not self.content
|
|
184
|
+
and self._dots_tick_count < self._max_logged_dots
|
|
185
|
+
):
|
|
186
|
+
self._dots_tick_count += 1
|
|
187
|
+
self._log_debug("dots_tick", n=self._dots_count)
|
|
162
188
|
if self.live:
|
|
163
189
|
self.live.update(self._create_panel())
|
|
164
190
|
else:
|
|
@@ -173,6 +199,7 @@ class StreamingAgentPanel:
|
|
|
173
199
|
|
|
174
200
|
self.live = Live(self._create_panel(), console=console, refresh_per_second=4)
|
|
175
201
|
self.live.start()
|
|
202
|
+
self._log_debug("start")
|
|
176
203
|
# For "Thinking...", set time in past to trigger dots immediately
|
|
177
204
|
if not self.content:
|
|
178
205
|
self._last_update_time = time.time() - 0.4 # Triggers dots on first cycle
|
|
@@ -188,45 +215,83 @@ class StreamingAgentPanel:
|
|
|
188
215
|
content_chunk = ""
|
|
189
216
|
|
|
190
217
|
# Filter out plan mode system prompts and tool definitions from streaming
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
# Use more precise filtering to avoid false positives
|
|
219
|
+
content_str = str(content_chunk).strip()
|
|
220
|
+
if content_str and any(
|
|
221
|
+
content_str.startswith(phrase) or phrase in content_str
|
|
193
222
|
for phrase in [
|
|
194
223
|
"🔧 PLAN MODE",
|
|
195
224
|
"TOOL EXECUTION ONLY",
|
|
196
225
|
"planning assistant that ONLY communicates",
|
|
197
226
|
"namespace functions {",
|
|
198
227
|
"namespace multi_tool_use {",
|
|
199
|
-
"You are trained on data up to",
|
|
200
228
|
]
|
|
201
229
|
):
|
|
202
230
|
return
|
|
203
231
|
|
|
232
|
+
# Special handling for the training data phrase - only filter if it's a complete system message
|
|
233
|
+
if "You are trained on data up to" in content_str and len(content_str) > 50:
|
|
234
|
+
# Only filter if this looks like a complete system message, not user content
|
|
235
|
+
if (
|
|
236
|
+
content_str.startswith("You are trained on data up to")
|
|
237
|
+
or "The current date is" in content_str
|
|
238
|
+
):
|
|
239
|
+
return
|
|
240
|
+
|
|
204
241
|
# Ensure type safety for concatenation
|
|
205
|
-
|
|
242
|
+
incoming = str(content_chunk)
|
|
243
|
+
# First-chunk diagnostics
|
|
244
|
+
is_first_chunk = (not self.content) and bool(incoming)
|
|
245
|
+
if is_first_chunk:
|
|
246
|
+
self._log_debug(
|
|
247
|
+
"first_chunk_received",
|
|
248
|
+
chunk_repr=incoming[:5],
|
|
249
|
+
chunk_len=len(incoming),
|
|
250
|
+
)
|
|
251
|
+
self.content = (self.content or "") + incoming
|
|
206
252
|
|
|
207
253
|
# Reset the update timer when we get new content
|
|
208
254
|
self._last_update_time = time.time()
|
|
209
|
-
|
|
255
|
+
# Hide dots immediately when new content arrives
|
|
256
|
+
if self._show_dots:
|
|
257
|
+
self._log_debug("disable_dots_called")
|
|
258
|
+
self._show_dots = False
|
|
210
259
|
|
|
211
260
|
if self.live:
|
|
261
|
+
# Log timing around the first two live.update() calls
|
|
262
|
+
self._update_count += 1
|
|
263
|
+
if self._update_count <= 2:
|
|
264
|
+
self._log_debug("live_update.start", update_index=self._update_count)
|
|
212
265
|
self.live.update(self._create_panel())
|
|
266
|
+
if self._update_count <= 2:
|
|
267
|
+
self._log_debug("live_update.end", update_index=self._update_count)
|
|
213
268
|
|
|
214
269
|
async def set_content(self, content: str):
|
|
215
270
|
"""Set the complete content (overwrites previous)."""
|
|
216
271
|
# Filter out plan mode system prompts and tool definitions
|
|
217
|
-
|
|
218
|
-
|
|
272
|
+
# Use more precise filtering to avoid false positives
|
|
273
|
+
content_str = str(content).strip()
|
|
274
|
+
if content_str and any(
|
|
275
|
+
content_str.startswith(phrase) or phrase in content_str
|
|
219
276
|
for phrase in [
|
|
220
277
|
"🔧 PLAN MODE",
|
|
221
278
|
"TOOL EXECUTION ONLY",
|
|
222
279
|
"planning assistant that ONLY communicates",
|
|
223
280
|
"namespace functions {",
|
|
224
281
|
"namespace multi_tool_use {",
|
|
225
|
-
"You are trained on data up to",
|
|
226
282
|
]
|
|
227
283
|
):
|
|
228
284
|
return
|
|
229
285
|
|
|
286
|
+
# Special handling for the training data phrase - only filter if it's a complete system message
|
|
287
|
+
if "You are trained on data up to" in content_str and len(content_str) > 50:
|
|
288
|
+
# Only filter if this looks like a complete system message, not user content
|
|
289
|
+
if (
|
|
290
|
+
content_str.startswith("You are trained on data up to")
|
|
291
|
+
or "The current date is" in content_str
|
|
292
|
+
):
|
|
293
|
+
return
|
|
294
|
+
|
|
230
295
|
self.content = content
|
|
231
296
|
if self.live:
|
|
232
297
|
self.live.update(self._create_panel())
|
|
@@ -240,6 +305,8 @@ class StreamingAgentPanel:
|
|
|
240
305
|
await self._dots_task
|
|
241
306
|
except asyncio.CancelledError:
|
|
242
307
|
pass
|
|
308
|
+
finally:
|
|
309
|
+
self._log_debug("dots_task_cancelled")
|
|
243
310
|
|
|
244
311
|
if self.live:
|
|
245
312
|
# Get the console before stopping the live display
|
|
@@ -263,6 +330,22 @@ class StreamingAgentPanel:
|
|
|
263
330
|
|
|
264
331
|
self.live = None
|
|
265
332
|
|
|
333
|
+
# Emit debug diagnostics after Live has been stopped (to avoid interference)
|
|
334
|
+
if self._debug_enabled:
|
|
335
|
+
from .output import print as ui_print
|
|
336
|
+
|
|
337
|
+
# Summarize UI buffer state
|
|
338
|
+
ui_prefix = "[debug]"
|
|
339
|
+
ui_buffer_first5 = repr((self.content or "")[:5])
|
|
340
|
+
lines = [
|
|
341
|
+
f"{ui_prefix} ui_buffer_first5={ui_buffer_first5} total_len={len(self.content or '')}",
|
|
342
|
+
]
|
|
343
|
+
# Include recorded event lines
|
|
344
|
+
lines.extend(self._debug_events)
|
|
345
|
+
# Flush lines to console
|
|
346
|
+
for line in lines:
|
|
347
|
+
await ui_print(line)
|
|
348
|
+
|
|
266
349
|
|
|
267
350
|
async def agent_streaming(content_stream, bottom: int = 1):
|
|
268
351
|
"""Display an agent panel with streaming content updates.
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.utils.config_comparator
|
|
3
|
+
|
|
4
|
+
Configuration comparison utility for analyzing user configurations against defaults.
|
|
5
|
+
Provides detailed analysis of customizations, defaults, and configuration state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
12
|
+
|
|
13
|
+
from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
14
|
+
from tunacode.types import UserConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConfigDifference:
|
|
19
|
+
"""Represents a difference between user config and defaults."""
|
|
20
|
+
|
|
21
|
+
key_path: str
|
|
22
|
+
user_value: Any
|
|
23
|
+
default_value: Any
|
|
24
|
+
difference_type: str # "custom", "missing", "extra", "type_mismatch"
|
|
25
|
+
section: str
|
|
26
|
+
description: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ConfigAnalysis:
|
|
31
|
+
"""Complete analysis of configuration state."""
|
|
32
|
+
|
|
33
|
+
user_config: UserConfig
|
|
34
|
+
default_config: UserConfig
|
|
35
|
+
differences: List[ConfigDifference]
|
|
36
|
+
custom_keys: Set[str]
|
|
37
|
+
missing_keys: Set[str]
|
|
38
|
+
extra_keys: Set[str]
|
|
39
|
+
type_mismatches: Set[str]
|
|
40
|
+
sections_analyzed: Set[str]
|
|
41
|
+
total_keys: int
|
|
42
|
+
custom_percentage: float
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConfigComparator:
|
|
46
|
+
"""Compares user configuration against defaults to identify customizations."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, default_config: Optional[UserConfig] = None):
|
|
49
|
+
"""Initialize comparator with default configuration."""
|
|
50
|
+
self.default_config = default_config or DEFAULT_USER_CONFIG
|
|
51
|
+
|
|
52
|
+
def _get_key_description(self, key_path: str, difference_type: str) -> str:
|
|
53
|
+
"""Get a descriptive explanation for a configuration key difference."""
|
|
54
|
+
try:
|
|
55
|
+
from tunacode.configuration.key_descriptions import get_key_description
|
|
56
|
+
|
|
57
|
+
desc = get_key_description(key_path)
|
|
58
|
+
|
|
59
|
+
if desc:
|
|
60
|
+
if difference_type == "custom":
|
|
61
|
+
return f"Custom: {desc.description}"
|
|
62
|
+
elif difference_type == "missing":
|
|
63
|
+
return f"Missing: {desc.description}"
|
|
64
|
+
elif difference_type == "extra":
|
|
65
|
+
return f"Extra: {desc.description}"
|
|
66
|
+
elif difference_type == "type_mismatch":
|
|
67
|
+
return f"Type mismatch: {desc.description}"
|
|
68
|
+
|
|
69
|
+
except ImportError:
|
|
70
|
+
pass # Fall back to basic descriptions
|
|
71
|
+
|
|
72
|
+
# Fallback descriptions
|
|
73
|
+
if difference_type == "custom":
|
|
74
|
+
return f"Custom value: {key_path}"
|
|
75
|
+
elif difference_type == "missing":
|
|
76
|
+
return f"Missing configuration key: {key_path}"
|
|
77
|
+
elif difference_type == "extra":
|
|
78
|
+
return f"Extra configuration key: {key_path}"
|
|
79
|
+
elif difference_type == "type_mismatch":
|
|
80
|
+
return f"Type mismatch for: {key_path}"
|
|
81
|
+
|
|
82
|
+
return f"Configuration difference: {key_path}"
|
|
83
|
+
|
|
84
|
+
def analyze_config(self, user_config: UserConfig) -> ConfigAnalysis:
|
|
85
|
+
"""Perform complete analysis of user configuration."""
|
|
86
|
+
differences: list[ConfigDifference] = []
|
|
87
|
+
custom_keys: set[str] = set()
|
|
88
|
+
missing_keys: set[str] = set()
|
|
89
|
+
extra_keys: set[str] = set()
|
|
90
|
+
type_mismatches: set[str] = set()
|
|
91
|
+
|
|
92
|
+
# Analyze each section recursively
|
|
93
|
+
self._analyze_recursive(
|
|
94
|
+
user_config=user_config,
|
|
95
|
+
default_config=self.default_config,
|
|
96
|
+
current_path="",
|
|
97
|
+
differences=differences,
|
|
98
|
+
custom_keys=custom_keys,
|
|
99
|
+
missing_keys=missing_keys,
|
|
100
|
+
extra_keys=extra_keys,
|
|
101
|
+
type_mismatches=type_mismatches,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Calculate statistics
|
|
105
|
+
sections_analyzed: set[str] = set()
|
|
106
|
+
self._collect_sections(user_config, sections_analyzed)
|
|
107
|
+
self._collect_sections(self.default_config, sections_analyzed)
|
|
108
|
+
|
|
109
|
+
total_keys = len(custom_keys) + len(missing_keys) + len(extra_keys) + len(type_mismatches)
|
|
110
|
+
custom_percentage = (len(custom_keys) / total_keys * 100) if total_keys > 0 else 0
|
|
111
|
+
|
|
112
|
+
return ConfigAnalysis(
|
|
113
|
+
user_config=user_config,
|
|
114
|
+
default_config=self.default_config,
|
|
115
|
+
differences=differences,
|
|
116
|
+
custom_keys=custom_keys,
|
|
117
|
+
missing_keys=missing_keys,
|
|
118
|
+
extra_keys=extra_keys,
|
|
119
|
+
type_mismatches=type_mismatches,
|
|
120
|
+
sections_analyzed=sections_analyzed,
|
|
121
|
+
total_keys=total_keys,
|
|
122
|
+
custom_percentage=custom_percentage,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _analyze_recursive(
|
|
126
|
+
self,
|
|
127
|
+
user_config: Dict[str, Any],
|
|
128
|
+
default_config: Dict[str, Any],
|
|
129
|
+
current_path: str,
|
|
130
|
+
differences: List[ConfigDifference],
|
|
131
|
+
custom_keys: Set[str],
|
|
132
|
+
missing_keys: Set[str],
|
|
133
|
+
extra_keys: Set[str],
|
|
134
|
+
type_mismatches: Set[str],
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Recursively analyze configuration differences."""
|
|
137
|
+
|
|
138
|
+
# Check for missing keys (present in default but not in user)
|
|
139
|
+
for key, default_value in default_config.items():
|
|
140
|
+
full_key = f"{current_path}.{key}" if current_path else key
|
|
141
|
+
|
|
142
|
+
if key not in user_config:
|
|
143
|
+
missing_keys.add(full_key)
|
|
144
|
+
differences.append(
|
|
145
|
+
ConfigDifference(
|
|
146
|
+
key_path=full_key,
|
|
147
|
+
user_value=None,
|
|
148
|
+
default_value=default_value,
|
|
149
|
+
difference_type="missing",
|
|
150
|
+
section=current_path or "root",
|
|
151
|
+
description=self._get_key_description(full_key, "missing"),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
user_value = user_config[key]
|
|
157
|
+
|
|
158
|
+
# Recursively analyze nested dictionaries
|
|
159
|
+
if isinstance(default_value, dict) and isinstance(user_value, dict):
|
|
160
|
+
self._analyze_recursive(
|
|
161
|
+
user_config=user_value,
|
|
162
|
+
default_config=default_value,
|
|
163
|
+
current_path=full_key,
|
|
164
|
+
differences=differences,
|
|
165
|
+
custom_keys=custom_keys,
|
|
166
|
+
missing_keys=missing_keys,
|
|
167
|
+
extra_keys=extra_keys,
|
|
168
|
+
type_mismatches=type_mismatches,
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Check for type mismatches
|
|
173
|
+
if not isinstance(user_value, type(default_value)):
|
|
174
|
+
type_mismatches.add(full_key)
|
|
175
|
+
differences.append(
|
|
176
|
+
ConfigDifference(
|
|
177
|
+
key_path=full_key,
|
|
178
|
+
user_value=user_value,
|
|
179
|
+
default_value=default_value,
|
|
180
|
+
difference_type="type_mismatch",
|
|
181
|
+
section=current_path or "root",
|
|
182
|
+
description=self._get_key_description(full_key, "type_mismatch"),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Check for custom values
|
|
188
|
+
if user_value != default_value:
|
|
189
|
+
custom_keys.add(full_key)
|
|
190
|
+
differences.append(
|
|
191
|
+
ConfigDifference(
|
|
192
|
+
key_path=full_key,
|
|
193
|
+
user_value=user_value,
|
|
194
|
+
default_value=default_value,
|
|
195
|
+
difference_type="custom",
|
|
196
|
+
section=current_path or "root",
|
|
197
|
+
description=self._get_key_description(full_key, "custom"),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Check for extra keys (present in user but not in default)
|
|
202
|
+
for key, user_value in user_config.items():
|
|
203
|
+
if key not in default_config:
|
|
204
|
+
full_key = f"{current_path}.{key}" if current_path else key
|
|
205
|
+
extra_keys.add(full_key)
|
|
206
|
+
differences.append(
|
|
207
|
+
ConfigDifference(
|
|
208
|
+
key_path=full_key,
|
|
209
|
+
user_value=user_value,
|
|
210
|
+
default_value=None,
|
|
211
|
+
difference_type="extra",
|
|
212
|
+
section=current_path or "root",
|
|
213
|
+
description=self._get_key_description(full_key, "extra"),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _collect_sections(self, config: Dict[str, Any], sections: Set[str]) -> None:
|
|
218
|
+
"""Collect all section names from configuration."""
|
|
219
|
+
for key, value in config.items():
|
|
220
|
+
if isinstance(value, dict):
|
|
221
|
+
sections.add(key)
|
|
222
|
+
self._collect_sections(value, sections)
|
|
223
|
+
|
|
224
|
+
def get_summary_stats(self, analysis: ConfigAnalysis) -> Dict[str, Any]:
|
|
225
|
+
"""Get summary statistics for the configuration analysis."""
|
|
226
|
+
return {
|
|
227
|
+
"total_keys_analyzed": analysis.total_keys,
|
|
228
|
+
"custom_keys_count": len(analysis.custom_keys),
|
|
229
|
+
"missing_keys_count": len(analysis.missing_keys),
|
|
230
|
+
"extra_keys_count": len(analysis.extra_keys),
|
|
231
|
+
"type_mismatches_count": len(analysis.type_mismatches),
|
|
232
|
+
"custom_percentage": analysis.custom_percentage,
|
|
233
|
+
"sections_analyzed": len(analysis.sections_analyzed),
|
|
234
|
+
"has_issues": bool(analysis.missing_keys or analysis.type_mismatches),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def get_section_analysis(
|
|
238
|
+
self, analysis: ConfigAnalysis, section: str
|
|
239
|
+
) -> List[ConfigDifference]:
|
|
240
|
+
"""Get differences for a specific section."""
|
|
241
|
+
return [diff for diff in analysis.differences if diff.section == section]
|
|
242
|
+
|
|
243
|
+
def is_config_healthy(self, analysis: ConfigAnalysis) -> bool:
|
|
244
|
+
"""Check if configuration is healthy (no critical issues)."""
|
|
245
|
+
# Type mismatches are considered critical
|
|
246
|
+
if analysis.type_mismatches:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
# Missing keys might be acceptable depending on the context
|
|
250
|
+
# For now, we'll consider missing keys as warnings, not errors
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
def get_recommendations(self, analysis: ConfigAnalysis) -> List[str]:
|
|
254
|
+
"""Get recommendations based on configuration analysis."""
|
|
255
|
+
recommendations = []
|
|
256
|
+
|
|
257
|
+
if analysis.type_mismatches:
|
|
258
|
+
recommendations.append(
|
|
259
|
+
f"Fix {len(analysis.type_mismatches)} type mismatch(es) in configuration"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if analysis.missing_keys:
|
|
263
|
+
recommendations.append(
|
|
264
|
+
f"Consider adding {len(analysis.missing_keys)} missing configuration key(s)"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if analysis.custom_percentage > 80:
|
|
268
|
+
recommendations.append(
|
|
269
|
+
"High customization detected - consider documenting your configuration"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if analysis.extra_keys:
|
|
273
|
+
recommendations.append(
|
|
274
|
+
f"Found {len(analysis.extra_keys)} unrecognized configuration key(s)"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return recommendations
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def load_and_analyze_config(config_path: Optional[Union[str, Path]] = None) -> ConfigAnalysis:
|
|
281
|
+
"""Load configuration from file and analyze it."""
|
|
282
|
+
from tunacode.utils.user_configuration import load_config
|
|
283
|
+
|
|
284
|
+
if config_path:
|
|
285
|
+
try:
|
|
286
|
+
with open(config_path, "r") as f:
|
|
287
|
+
user_config = json.load(f)
|
|
288
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
289
|
+
raise ValueError(f"Failed to load config from {config_path}: {e}")
|
|
290
|
+
else:
|
|
291
|
+
user_config = load_config()
|
|
292
|
+
if user_config is None:
|
|
293
|
+
raise ValueError("No user configuration found")
|
|
294
|
+
|
|
295
|
+
comparator = ConfigComparator()
|
|
296
|
+
return comparator.analyze_config(user_config)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def create_config_report(analysis: ConfigAnalysis) -> str:
|
|
300
|
+
"""Create a human-readable report of configuration analysis."""
|
|
301
|
+
stats = ConfigComparator().get_summary_stats(analysis)
|
|
302
|
+
|
|
303
|
+
report = [
|
|
304
|
+
"Configuration Analysis Report",
|
|
305
|
+
"=" * 50,
|
|
306
|
+
f"Total keys analyzed: {stats['total_keys_analyzed']}",
|
|
307
|
+
f"Custom keys: {stats['custom_keys_count']} ({stats['custom_percentage']:.1f}%)",
|
|
308
|
+
f"Missing keys: {stats['missing_keys_count']}",
|
|
309
|
+
f"Extra keys: {stats['extra_keys_count']}",
|
|
310
|
+
f"Type mismatches: {stats['type_mismatches_count']}",
|
|
311
|
+
f"Sections analyzed: {stats['sections_analyzed']}",
|
|
312
|
+
f"Configuration healthy: {'Yes' if stats['has_issues'] else 'No'}",
|
|
313
|
+
"",
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
if analysis.custom_keys:
|
|
317
|
+
report.append("Custom Values:")
|
|
318
|
+
for key in sorted(analysis.custom_keys):
|
|
319
|
+
report.append(f" ✓ {key}")
|
|
320
|
+
report.append("")
|
|
321
|
+
|
|
322
|
+
if analysis.missing_keys:
|
|
323
|
+
report.append("Missing Keys:")
|
|
324
|
+
for key in sorted(analysis.missing_keys):
|
|
325
|
+
report.append(f" ⚠ {key}")
|
|
326
|
+
report.append("")
|
|
327
|
+
|
|
328
|
+
if analysis.type_mismatches:
|
|
329
|
+
report.append("Type Mismatches:")
|
|
330
|
+
for key in sorted(analysis.type_mismatches):
|
|
331
|
+
report.append(f" ✗ {key}")
|
|
332
|
+
report.append("")
|
|
333
|
+
|
|
334
|
+
recommendations = ConfigComparator().get_recommendations(analysis)
|
|
335
|
+
if recommendations:
|
|
336
|
+
report.append("Recommendations:")
|
|
337
|
+
for rec in recommendations:
|
|
338
|
+
report.append(f" • {rec}")
|
|
339
|
+
|
|
340
|
+
return "\n".join(report)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.76.1
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Project-URL: Homepage, https://tunacode.xyz/
|
|
6
6
|
Project-URL: Repository, https://github.com/alchemiststudiosDOTai/tunacode
|
|
@@ -75,6 +75,27 @@ pip install tunacode-cli
|
|
|
75
75
|
|
|
76
76
|
For detailed installation and configuration instructions, see the [**Getting Started Guide**](documentation/user/getting-started.md).
|
|
77
77
|
|
|
78
|
+
## Quickstart
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 1) Install
|
|
82
|
+
pip install tunacode-cli
|
|
83
|
+
|
|
84
|
+
# 2) Launch the CLI
|
|
85
|
+
tunacode --wizard # guided setup (enter an API key, pick a model)
|
|
86
|
+
|
|
87
|
+
# 3) Try common commands in the REPL
|
|
88
|
+
/help # see commands
|
|
89
|
+
/model # explore models and set a default
|
|
90
|
+
/plan # enter read-only Plan Mode
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Tip: You can also skip the wizard and set everything via flags:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
tunacode --model openai:gpt-4.1 --key sk-your-key
|
|
97
|
+
```
|
|
98
|
+
|
|
78
99
|
## Development Installation
|
|
79
100
|
|
|
80
101
|
For contributors and developers who want to work on TunaCode:
|
|
@@ -87,13 +108,13 @@ cd tunacode
|
|
|
87
108
|
# Quick setup (recommended)
|
|
88
109
|
./scripts/setup_dev_env.sh
|
|
89
110
|
|
|
90
|
-
# Or manual setup
|
|
91
|
-
|
|
92
|
-
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
93
|
-
pip install -e ".[dev]"
|
|
111
|
+
# Or manual setup with UV (recommended)
|
|
112
|
+
uv venv
|
|
113
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
114
|
+
uv pip install -e ".[dev]"
|
|
94
115
|
|
|
95
116
|
# Verify installation
|
|
96
|
-
|
|
117
|
+
tunacode --version
|
|
97
118
|
```
|
|
98
119
|
|
|
99
120
|
See the [Hatch Build System Guide](documentation/development/hatch-build-system.md) for detailed instructions on the development environment.
|
|
@@ -102,6 +123,42 @@ See the [Hatch Build System Guide](documentation/development/hatch-build-system.
|
|
|
102
123
|
|
|
103
124
|
Choose your AI provider and set your API key. For more details, see the [Configuration Section](documentation/user/getting-started.md#2-configuration) in the Getting Started Guide. For local models (LM Studio, Ollama, etc.), see the [Local Models Setup Guide](documentation/configuration/local-models.md).
|
|
104
125
|
|
|
126
|
+
### New: Enhanced Model Selection
|
|
127
|
+
|
|
128
|
+
TunaCode now automatically saves your model selection for future sessions. When you choose a model using `/model <provider:name>`, it will be remembered across restarts.
|
|
129
|
+
|
|
130
|
+
**If you encounter API key errors**, you can manually create a configuration file that matches the current schema:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Create the config file
|
|
134
|
+
cat > ~/.config/tunacode.json << 'EOF'
|
|
135
|
+
{
|
|
136
|
+
"default_model": "openai:gpt-4.1",
|
|
137
|
+
"env": {
|
|
138
|
+
"OPENAI_API_KEY": "your-openai-api-key-here",
|
|
139
|
+
"ANTHROPIC_API_KEY": "",
|
|
140
|
+
"GEMINI_API_KEY": "",
|
|
141
|
+
"OPENROUTER_API_KEY": ""
|
|
142
|
+
},
|
|
143
|
+
"settings": {
|
|
144
|
+
"enable_streaming": true,
|
|
145
|
+
"max_iterations": 40,
|
|
146
|
+
"context_window_size": 200000
|
|
147
|
+
},
|
|
148
|
+
"mcpServers": {}
|
|
149
|
+
}
|
|
150
|
+
EOF
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Replace the model and API key with your preferred provider and credentials. Examples:
|
|
154
|
+
- `openai:gpt-4.1` (requires OPENAI_API_KEY)
|
|
155
|
+
- `anthropic:claude-4-sonnet-20250522` (requires ANTHROPIC_API_KEY)
|
|
156
|
+
- `google:gemini-2.5-pro` (requires GEMINI_API_KEY)
|
|
157
|
+
|
|
158
|
+
### ⚠️ Important Notice
|
|
159
|
+
|
|
160
|
+
I apologize for any recent issues with model selection and configuration. I'm actively working to fix these problems and improve the overall stability of TunaCode. Your patience and feedback are greatly appreciated as I work to make the tool more reliable.
|
|
161
|
+
|
|
105
162
|
### Recommended Models
|
|
106
163
|
|
|
107
164
|
Based on extensive testing, these models provide the best performance:
|