tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands/__init__.py +2 -2
- tunacode/cli/commands/implementations/__init__.py +2 -3
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- 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/quickstart.py +43 -0
- tunacode/cli/commands/implementations/system.py +96 -3
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +139 -5
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +135 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +400 -0
- tunacode/cli/main.py +23 -2
- tunacode/cli/repl.py +217 -190
- tunacode/cli/repl_components/command_parser.py +38 -4
- tunacode/cli/repl_components/error_recovery.py +85 -4
- tunacode/cli/repl_components/output_display.py +12 -1
- tunacode/cli/repl_components/tool_executor.py +1 -1
- tunacode/configuration/defaults.py +12 -3
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +12 -40
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +249 -55
- tunacode/core/agents/agent_components/agent_helpers.py +43 -13
- tunacode/core/agents/agent_components/node_processor.py +179 -139
- 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 -121
- tunacode/core/code_index.py +83 -29
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +110 -20
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +14 -5
- tunacode/core/state.py +16 -20
- tunacode/core/token_usage/usage_tracker.py +5 -3
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -40
- tunacode/exceptions.py +119 -5
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +125 -7
- tunacode/setup.py +5 -25
- tunacode/tools/base.py +163 -0
- tunacode/tools/bash.py +110 -1
- tunacode/tools/glob.py +332 -34
- tunacode/tools/grep.py +179 -82
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/prompts/bash_prompt.xml +72 -0
- tunacode/tools/prompts/glob_prompt.xml +45 -0
- tunacode/tools/prompts/grep_prompt.xml +98 -0
- tunacode/tools/prompts/list_dir_prompt.xml +31 -0
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +54 -0
- tunacode/tools/prompts/run_command_prompt.xml +64 -0
- tunacode/tools/prompts/update_file_prompt.xml +53 -0
- tunacode/tools/prompts/write_file_prompt.xml +37 -0
- tunacode/tools/react.py +153 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +114 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- 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/types.py +20 -27
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +20 -3
- tunacode/ui/keybindings.py +7 -4
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +212 -43
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +5 -1
- tunacode/ui/tool_ui.py +33 -10
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/ripgrep.py +332 -9
- tunacode/utils/text_utils.py +18 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
- tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -182
- tunacode/prompts/system.md +0 -731
- tunacode/tools/read_file_async_poc.py +0 -196
- tunacode/tools/todo.py +0 -349
- tunacode_cli-0.0.55.dist-info/METADATA +0 -322
- tunacode_cli-0.0.55.dist-info/RECORD +0 -126
- tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/utils/ripgrep.py
CHANGED
|
@@ -1,17 +1,340 @@
|
|
|
1
|
+
"""Ripgrep binary management and execution utilities."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
1
8
|
import subprocess
|
|
2
|
-
from
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Tuple
|
|
3
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
|
|
15
|
+
@functools.lru_cache(maxsize=1)
|
|
16
|
+
def get_platform_identifier() -> Tuple[str, str]:
|
|
17
|
+
"""Get the current platform identifier.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tuple of (platform_key, system_name)
|
|
21
|
+
"""
|
|
22
|
+
system = platform.system().lower()
|
|
23
|
+
machine = platform.machine().lower()
|
|
24
|
+
|
|
25
|
+
if system == "linux":
|
|
26
|
+
if machine in ["x86_64", "amd64"]:
|
|
27
|
+
return "x64-linux", system
|
|
28
|
+
elif machine in ["aarch64", "arm64"]:
|
|
29
|
+
return "arm64-linux", system
|
|
30
|
+
elif system == "darwin":
|
|
31
|
+
if machine in ["x86_64", "amd64"]:
|
|
32
|
+
return "x64-darwin", system
|
|
33
|
+
elif machine in ["arm64", "aarch64"]:
|
|
34
|
+
return "arm64-darwin", system
|
|
35
|
+
elif system == "windows":
|
|
36
|
+
if machine in ["x86_64", "amd64"]:
|
|
37
|
+
return "x64-win32", system
|
|
38
|
+
|
|
39
|
+
raise ValueError(f"Unsupported platform: {system} {machine}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@functools.lru_cache(maxsize=1)
|
|
43
|
+
def get_ripgrep_binary_path() -> Optional[Path]:
|
|
44
|
+
"""Resolve the path to the ripgrep binary.
|
|
45
|
+
|
|
46
|
+
Resolution order:
|
|
47
|
+
1. Environment variable override (TUNACODE_RIPGREP_PATH)
|
|
48
|
+
2. System ripgrep (if newer or equal version)
|
|
49
|
+
3. Bundled ripgrep binary
|
|
50
|
+
4. None (fallback to Python-based search)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Path to ripgrep binary or None if not available
|
|
54
|
+
"""
|
|
55
|
+
# Check for environment variable override
|
|
56
|
+
env_path = os.environ.get("TUNACODE_RIPGREP_PATH")
|
|
57
|
+
if env_path:
|
|
58
|
+
path = Path(env_path)
|
|
59
|
+
if path.exists() and path.is_file():
|
|
60
|
+
logger.debug(f"Using ripgrep from environment variable: {path}")
|
|
61
|
+
return path
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(f"Invalid TUNACODE_RIPGREP_PATH: {env_path}")
|
|
64
|
+
|
|
65
|
+
# Check for system ripgrep
|
|
66
|
+
system_rg = shutil.which("rg")
|
|
67
|
+
if system_rg:
|
|
68
|
+
system_rg_path = Path(system_rg)
|
|
69
|
+
if _check_ripgrep_version(system_rg_path):
|
|
70
|
+
logger.debug(f"Using system ripgrep: {system_rg_path}")
|
|
71
|
+
return system_rg_path
|
|
72
|
+
|
|
73
|
+
# Check for bundled ripgrep
|
|
74
|
+
try:
|
|
75
|
+
platform_key, _ = get_platform_identifier()
|
|
76
|
+
binary_name = "rg.exe" if platform_key == "x64-win32" else "rg"
|
|
77
|
+
|
|
78
|
+
# Look for vendor directory relative to this file
|
|
79
|
+
vendor_dir = (
|
|
80
|
+
Path(__file__).parent.parent.parent.parent / "vendor" / "ripgrep" / platform_key
|
|
81
|
+
)
|
|
82
|
+
bundled_path = vendor_dir / binary_name
|
|
83
|
+
|
|
84
|
+
if bundled_path.exists():
|
|
85
|
+
logger.debug(f"Using bundled ripgrep: {bundled_path}")
|
|
86
|
+
return bundled_path
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.debug(f"Could not find bundled ripgrep: {e}")
|
|
89
|
+
|
|
90
|
+
logger.debug("No ripgrep binary found, will use Python fallback")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_ripgrep_version(rg_path: Path, min_version: str = "13.0.0") -> bool:
|
|
95
|
+
"""Check if ripgrep version meets minimum requirement.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
rg_path: Path to ripgrep binary
|
|
99
|
+
min_version: Minimum required version
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if version is sufficient, False otherwise
|
|
103
|
+
"""
|
|
7
104
|
try:
|
|
8
105
|
result = subprocess.run(
|
|
9
|
-
[
|
|
106
|
+
[str(rg_path), "--version"],
|
|
10
107
|
capture_output=True,
|
|
11
108
|
text=True,
|
|
12
|
-
|
|
13
|
-
timeout=5,
|
|
109
|
+
timeout=1,
|
|
14
110
|
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
111
|
+
if result.returncode == 0:
|
|
112
|
+
# Parse version from output like "ripgrep 14.1.1"
|
|
113
|
+
version_line = result.stdout.split("\n")[0]
|
|
114
|
+
version = version_line.split()[-1]
|
|
115
|
+
|
|
116
|
+
# Simple version comparison (works for x.y.z format)
|
|
117
|
+
current = tuple(map(int, version.split(".")))
|
|
118
|
+
required = tuple(map(int, min_version.split(".")))
|
|
119
|
+
|
|
120
|
+
return current >= required
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.debug(f"Could not check ripgrep version: {e}")
|
|
123
|
+
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RipgrepExecutor:
|
|
128
|
+
"""Wrapper for executing ripgrep commands with error handling."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, binary_path: Optional[Path] = None):
|
|
131
|
+
"""Initialize the executor.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
binary_path: Optional path to ripgrep binary
|
|
135
|
+
"""
|
|
136
|
+
self.binary_path = binary_path or get_ripgrep_binary_path()
|
|
137
|
+
self._use_python_fallback = self.binary_path is None
|
|
138
|
+
|
|
139
|
+
if self._use_python_fallback:
|
|
140
|
+
logger.info("Ripgrep binary not available, using Python fallback")
|
|
141
|
+
|
|
142
|
+
def search(
|
|
143
|
+
self,
|
|
144
|
+
pattern: str,
|
|
145
|
+
path: str = ".",
|
|
146
|
+
*,
|
|
147
|
+
timeout: int = 10,
|
|
148
|
+
max_matches: Optional[int] = None,
|
|
149
|
+
file_pattern: Optional[str] = None,
|
|
150
|
+
case_insensitive: bool = False,
|
|
151
|
+
multiline: bool = False,
|
|
152
|
+
context_before: int = 0,
|
|
153
|
+
context_after: int = 0,
|
|
154
|
+
**kwargs,
|
|
155
|
+
) -> List[str]:
|
|
156
|
+
"""Execute a ripgrep search.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
pattern: Search pattern (regex)
|
|
160
|
+
path: Directory or file to search
|
|
161
|
+
timeout: Maximum execution time in seconds
|
|
162
|
+
max_matches: Maximum number of matches to return
|
|
163
|
+
file_pattern: Glob pattern for files to include
|
|
164
|
+
case_insensitive: Case-insensitive search
|
|
165
|
+
multiline: Enable multiline mode
|
|
166
|
+
context_before: Lines of context before match
|
|
167
|
+
context_after: Lines of context after match
|
|
168
|
+
**kwargs: Additional ripgrep arguments
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of matching lines or file paths
|
|
172
|
+
"""
|
|
173
|
+
if self._use_python_fallback:
|
|
174
|
+
return self._python_fallback_search(
|
|
175
|
+
pattern, path, file_pattern=file_pattern, case_insensitive=case_insensitive
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
cmd = [str(self.binary_path)]
|
|
180
|
+
|
|
181
|
+
# Add flags
|
|
182
|
+
if case_insensitive:
|
|
183
|
+
cmd.append("-i")
|
|
184
|
+
if multiline:
|
|
185
|
+
cmd.extend(["-U", "--multiline-dotall"])
|
|
186
|
+
if context_before > 0:
|
|
187
|
+
cmd.extend(["-B", str(context_before)])
|
|
188
|
+
if context_after > 0:
|
|
189
|
+
cmd.extend(["-A", str(context_after)])
|
|
190
|
+
if max_matches:
|
|
191
|
+
cmd.extend(["-m", str(max_matches)])
|
|
192
|
+
if file_pattern:
|
|
193
|
+
cmd.extend(["-g", file_pattern])
|
|
194
|
+
|
|
195
|
+
# Add pattern and path
|
|
196
|
+
cmd.extend([pattern, path])
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Executing ripgrep: {' '.join(cmd)}")
|
|
199
|
+
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
cmd,
|
|
202
|
+
capture_output=True,
|
|
203
|
+
text=True,
|
|
204
|
+
timeout=timeout,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if result.returncode in [0, 1]: # 0 = matches found, 1 = no matches
|
|
208
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
209
|
+
else:
|
|
210
|
+
logger.warning(f"Ripgrep error: {result.stderr}")
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
except subprocess.TimeoutExpired:
|
|
214
|
+
logger.warning(f"Ripgrep search timed out after {timeout} seconds")
|
|
215
|
+
return []
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Ripgrep execution failed: {e}")
|
|
218
|
+
return self._python_fallback_search(pattern, path, file_pattern=file_pattern)
|
|
219
|
+
|
|
220
|
+
def list_files(self, pattern: str, directory: str = ".") -> List[str]:
|
|
221
|
+
"""List files matching a glob pattern using ripgrep.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
pattern: Glob pattern for files
|
|
225
|
+
directory: Directory to search
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of file paths
|
|
229
|
+
"""
|
|
230
|
+
if self._use_python_fallback:
|
|
231
|
+
return self._python_fallback_list_files(pattern, directory)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
result = subprocess.run(
|
|
235
|
+
[str(self.binary_path), "--files", "-g", pattern, directory],
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
timeout=5,
|
|
239
|
+
)
|
|
240
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
241
|
+
except Exception:
|
|
242
|
+
return self._python_fallback_list_files(pattern, directory)
|
|
243
|
+
|
|
244
|
+
def _python_fallback_search(
|
|
245
|
+
self,
|
|
246
|
+
pattern: str,
|
|
247
|
+
path: str,
|
|
248
|
+
file_pattern: Optional[str] = None,
|
|
249
|
+
case_insensitive: bool = False,
|
|
250
|
+
) -> List[str]:
|
|
251
|
+
"""Python-based fallback search implementation."""
|
|
252
|
+
import re
|
|
253
|
+
from pathlib import Path
|
|
254
|
+
|
|
255
|
+
results = []
|
|
256
|
+
path_obj = Path(path)
|
|
257
|
+
|
|
258
|
+
# Compile regex pattern
|
|
259
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
260
|
+
try:
|
|
261
|
+
regex = re.compile(pattern, flags)
|
|
262
|
+
except re.error:
|
|
263
|
+
logger.error(f"Invalid regex pattern: {pattern}")
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
# Search files
|
|
267
|
+
if path_obj.is_file():
|
|
268
|
+
files = [path_obj]
|
|
269
|
+
else:
|
|
270
|
+
glob_pattern = file_pattern or "**/*"
|
|
271
|
+
files = list(path_obj.glob(glob_pattern))
|
|
272
|
+
|
|
273
|
+
for file_path in files:
|
|
274
|
+
if not file_path.is_file():
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
with file_path.open("r", encoding="utf-8", errors="ignore") as f:
|
|
279
|
+
for line_num, line in enumerate(f, 1):
|
|
280
|
+
if regex.search(line):
|
|
281
|
+
results.append(f"{file_path}:{line_num}:{line.strip()}")
|
|
282
|
+
except Exception: # nosec B112 - continue on file read errors is appropriate
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
return results
|
|
286
|
+
|
|
287
|
+
def _python_fallback_list_files(self, pattern: str, directory: str) -> List[str]:
|
|
288
|
+
"""Python-based fallback for listing files."""
|
|
289
|
+
from pathlib import Path
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
base_path = Path(directory)
|
|
293
|
+
return [str(p) for p in base_path.glob(pattern) if p.is_file()]
|
|
294
|
+
except Exception:
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# Maintain backward compatibility
|
|
299
|
+
def ripgrep(pattern: str, directory: str = ".") -> List[str]:
|
|
300
|
+
"""Return a list of file paths matching a pattern using ripgrep.
|
|
301
|
+
|
|
302
|
+
This function maintains backward compatibility with the original implementation.
|
|
303
|
+
"""
|
|
304
|
+
executor = RipgrepExecutor()
|
|
305
|
+
return executor.list_files(pattern, directory)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# Performance metrics collection
|
|
309
|
+
class RipgrepMetrics:
|
|
310
|
+
"""Collect performance metrics for ripgrep operations."""
|
|
311
|
+
|
|
312
|
+
def __init__(self):
|
|
313
|
+
self.search_count = 0
|
|
314
|
+
self.total_search_time = 0.0
|
|
315
|
+
self.fallback_count = 0
|
|
316
|
+
|
|
317
|
+
def record_search(self, duration: float, used_fallback: bool = False):
|
|
318
|
+
"""Record a search operation."""
|
|
319
|
+
self.search_count += 1
|
|
320
|
+
self.total_search_time += duration
|
|
321
|
+
if used_fallback:
|
|
322
|
+
self.fallback_count += 1
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def average_search_time(self) -> float:
|
|
326
|
+
"""Get average search time."""
|
|
327
|
+
if self.search_count == 0:
|
|
328
|
+
return 0.0
|
|
329
|
+
return self.total_search_time / self.search_count
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def fallback_rate(self) -> float:
|
|
333
|
+
"""Get fallback usage rate."""
|
|
334
|
+
if self.search_count == 0:
|
|
335
|
+
return 0.0
|
|
336
|
+
return self.fallback_count / self.search_count
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# Global metrics instance
|
|
340
|
+
metrics = RipgrepMetrics()
|
tunacode/utils/text_utils.py
CHANGED
|
@@ -66,10 +66,12 @@ def expand_file_refs(text: str) -> Tuple[str, List[str]]:
|
|
|
66
66
|
- List of absolute paths of files that were successfully expanded.
|
|
67
67
|
|
|
68
68
|
Raises:
|
|
69
|
-
ValueError: If a referenced path does not exist
|
|
69
|
+
ValueError: If a referenced path does not exist or if attempting to expand
|
|
70
|
+
TunaCode's own source directory.
|
|
70
71
|
"""
|
|
71
72
|
import os
|
|
72
73
|
import re
|
|
74
|
+
from pathlib import Path
|
|
73
75
|
|
|
74
76
|
from tunacode.constants import (
|
|
75
77
|
ERROR_DIR_TOO_LARGE,
|
|
@@ -86,6 +88,21 @@ def expand_file_refs(text: str) -> Tuple[str, List[str]]:
|
|
|
86
88
|
def replacer(match: re.Match) -> str:
|
|
87
89
|
path_spec = match.group(1)
|
|
88
90
|
|
|
91
|
+
# Detect if we're expanding TunaCode's own src/ from TunaCode's repo
|
|
92
|
+
# This prevents accidentally expanding TunaCode's source instead of user's project
|
|
93
|
+
if path_spec.startswith("src/") or path_spec.startswith("src/**"):
|
|
94
|
+
cwd = Path.cwd()
|
|
95
|
+
tunacode_marker = cwd / "src" / "tunacode" / "__init__.py"
|
|
96
|
+
if tunacode_marker.exists():
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Error: TunaCode cannot expand file references when running "
|
|
99
|
+
"from its own source directory.\n"
|
|
100
|
+
"Please run TunaCode from your project directory, not from "
|
|
101
|
+
"the TunaCode repository itself.\n"
|
|
102
|
+
f"Current directory: {cwd}\n"
|
|
103
|
+
"Expected: Your project's root directory"
|
|
104
|
+
)
|
|
105
|
+
|
|
89
106
|
is_recursive = path_spec.endswith("/**")
|
|
90
107
|
is_dir = path_spec.endswith("/")
|
|
91
108
|
|
|
@@ -45,6 +45,10 @@ def load_config() -> Optional[UserConfig]:
|
|
|
45
45
|
# else, update fast path
|
|
46
46
|
_config_fingerprint = new_fp
|
|
47
47
|
_config_cache = loaded
|
|
48
|
+
|
|
49
|
+
# Initialize onboarding defaults for new configurations
|
|
50
|
+
_ensure_onboarding_defaults(loaded)
|
|
51
|
+
|
|
48
52
|
return loaded
|
|
49
53
|
except FileNotFoundError:
|
|
50
54
|
return None
|
|
@@ -91,3 +95,44 @@ def set_default_model(model_name: ModelName, state_manager: "StateManager") -> b
|
|
|
91
95
|
except ConfigurationError:
|
|
92
96
|
# Re-raise ConfigurationError to be handled by caller
|
|
93
97
|
raise
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ensure_onboarding_defaults(config: UserConfig) -> None:
|
|
101
|
+
"""Ensure onboarding-related default settings are present in config."""
|
|
102
|
+
from datetime import datetime
|
|
103
|
+
|
|
104
|
+
if "settings" not in config:
|
|
105
|
+
config["settings"] = {}
|
|
106
|
+
|
|
107
|
+
settings = config["settings"]
|
|
108
|
+
|
|
109
|
+
# Set tutorial enabled by default for new users
|
|
110
|
+
if "enable_tutorial" not in settings:
|
|
111
|
+
settings["enable_tutorial"] = True
|
|
112
|
+
|
|
113
|
+
# Set first installation date if not present (for new installs)
|
|
114
|
+
if "first_installation_date" not in settings:
|
|
115
|
+
settings["first_installation_date"] = datetime.now().isoformat()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def initialize_first_time_user(state_manager: "StateManager") -> None:
|
|
119
|
+
"""Initialize first-time user settings and save configuration."""
|
|
120
|
+
from datetime import datetime
|
|
121
|
+
|
|
122
|
+
# Ensure settings section exists
|
|
123
|
+
if "settings" not in state_manager.session.user_config:
|
|
124
|
+
state_manager.session.user_config["settings"] = {}
|
|
125
|
+
|
|
126
|
+
settings = state_manager.session.user_config["settings"]
|
|
127
|
+
|
|
128
|
+
# Only set installation date if it doesn't exist (true first-time)
|
|
129
|
+
if "first_installation_date" not in settings:
|
|
130
|
+
settings["first_installation_date"] = datetime.now().isoformat()
|
|
131
|
+
settings["enable_tutorial"] = True
|
|
132
|
+
|
|
133
|
+
# Save the updated configuration
|
|
134
|
+
try:
|
|
135
|
+
save_config(state_manager)
|
|
136
|
+
except ConfigurationError:
|
|
137
|
+
# Non-critical error, continue without failing
|
|
138
|
+
pass
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunacode-cli
|
|
3
|
+
Version: 0.0.78.6
|
|
4
|
+
Summary: Your agentic CLI developer.
|
|
5
|
+
Project-URL: Homepage, https://tunacode.xyz/
|
|
6
|
+
Project-URL: Repository, https://github.com/alchemiststudiosDOTai/tunacode
|
|
7
|
+
Project-URL: Issues, https://github.com/alchemiststudiosDOTai/tunacode/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/alchemiststudiosDOTai/tunacode#readme
|
|
9
|
+
Author-email: larock22 <noreply@github.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,automation,cli,development
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: <3.14,>=3.10
|
|
23
|
+
Requires-Dist: click<9.0.0,>=8.3.1
|
|
24
|
+
Requires-Dist: defusedxml
|
|
25
|
+
Requires-Dist: prompt-toolkit<4.0.0,>=3.0.52
|
|
26
|
+
Requires-Dist: pydantic-ai<2.0.0,>=1.18.0
|
|
27
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.4
|
|
28
|
+
Requires-Dist: pygments<3.0.0,>=2.19.2
|
|
29
|
+
Requires-Dist: rich<15.0.0,>=14.2.0
|
|
30
|
+
Requires-Dist: textual
|
|
31
|
+
Requires-Dist: tiktoken<1.0.0,>=0.12.0
|
|
32
|
+
Requires-Dist: typer<0.10.0,>=0.9.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: autoflake>=2.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: bandit; extra == 'dev'
|
|
36
|
+
Requires-Dist: build; extra == 'dev'
|
|
37
|
+
Requires-Dist: dead>=1.5.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: hatch>=1.6.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
40
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
44
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
45
|
+
Requires-Dist: textual-dev; extra == 'dev'
|
|
46
|
+
Requires-Dist: twine; extra == 'dev'
|
|
47
|
+
Requires-Dist: unimport>=1.0.0; extra == 'dev'
|
|
48
|
+
Requires-Dist: vulture>=2.7; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# TunaCode CLI
|
|
52
|
+
|
|
53
|
+
<div align="center">
|
|
54
|
+
|
|
55
|
+
[](https://badge.fury.io/py/tunacode-cli)
|
|
56
|
+
[](https://pepy.tech/project/tunacode-cli)
|
|
57
|
+
[](https://www.python.org/downloads/)
|
|
58
|
+
[](https://opensource.org/licenses/MIT)
|
|
59
|
+
|
|
60
|
+
**AI-powered CLI coding assistant**
|
|
61
|
+
|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Option 1: One-line install (Linux/macOS)
|
|
72
|
+
wget -qO- https://raw.githubusercontent.com/alchemiststudiosDOTai/tunacode/master/scripts/install_linux.sh | bash
|
|
73
|
+
|
|
74
|
+
# Option 2: UV install (recommended)
|
|
75
|
+
uv tool install tunacode-cli
|
|
76
|
+
|
|
77
|
+
# Option 3: pip install
|
|
78
|
+
pip install tunacode-cli
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For detailed installation and configuration instructions, see the [**Getting Started Guide**](documentation/user/getting-started.md).
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 1) Install (choose one)
|
|
87
|
+
uv tool install tunacode-cli # recommended
|
|
88
|
+
# or: pip install tunacode-cli
|
|
89
|
+
|
|
90
|
+
# 2) Launch the CLI
|
|
91
|
+
tunacode --wizard # guided setup (enter an API key, pick a model)
|
|
92
|
+
|
|
93
|
+
# 3) Try common commands in the REPL
|
|
94
|
+
/help # see commands
|
|
95
|
+
/model # explore models and set a default
|
|
96
|
+
/plan # enter read-only Plan Mode
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Tip: You can also skip the wizard and set everything via flags:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
tunacode --model openai:gpt-4.1 --key sk-your-key
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development Installation
|
|
106
|
+
|
|
107
|
+
For contributors and developers who want to work on TunaCode:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Clone the repository
|
|
111
|
+
git clone https://github.com/alchemiststudiosDOTai/tunacode.git
|
|
112
|
+
cd tunacode
|
|
113
|
+
|
|
114
|
+
# Quick setup (recommended) - uses UV automatically if available
|
|
115
|
+
./scripts/setup_dev_env.sh
|
|
116
|
+
|
|
117
|
+
# Or manual setup with UV (recommended)
|
|
118
|
+
uv venv
|
|
119
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
120
|
+
uv pip install -e ".[dev]"
|
|
121
|
+
|
|
122
|
+
# Alternative: traditional setup
|
|
123
|
+
python3 -m venv venv
|
|
124
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
|
|
127
|
+
# Verify installation
|
|
128
|
+
tunacode --version
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
See the [Hatch Build System Guide](documentation/development/hatch-build-system.md) for detailed instructions on the development environment.
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
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).
|
|
136
|
+
|
|
137
|
+
### New: Enhanced Model Selection
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
**If you encounter API key errors**, you can manually create a configuration file that matches the current schema:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Create the config file
|
|
145
|
+
cat > ~/.config/tunacode.json << 'EOF'
|
|
146
|
+
{
|
|
147
|
+
"default_model": "openai:gpt-4.1",
|
|
148
|
+
"env": {
|
|
149
|
+
"OPENAI_API_KEY": "your-openai-api-key-here",
|
|
150
|
+
"ANTHROPIC_API_KEY": "",
|
|
151
|
+
"GEMINI_API_KEY": "",
|
|
152
|
+
"OPENROUTER_API_KEY": ""
|
|
153
|
+
},
|
|
154
|
+
"settings": {
|
|
155
|
+
"enable_streaming": true,
|
|
156
|
+
"max_iterations": 40,
|
|
157
|
+
"context_window_size": 200000
|
|
158
|
+
},
|
|
159
|
+
"mcpServers": {}
|
|
160
|
+
}
|
|
161
|
+
EOF
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Replace the model and API key with your preferred provider and credentials. Examples:
|
|
165
|
+
- `openai:gpt-4.1` (requires OPENAI_API_KEY)
|
|
166
|
+
- `anthropic:claude-4-sonnet-20250522` (requires ANTHROPIC_API_KEY)
|
|
167
|
+
- `google:gemini-2.5-pro` (requires GEMINI_API_KEY)
|
|
168
|
+
|
|
169
|
+
### ⚠️ Important Notice
|
|
170
|
+
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
### Recommended Models
|
|
174
|
+
|
|
175
|
+
Based on extensive testing, these models provide the best performance:
|
|
176
|
+
|
|
177
|
+
- `google/gemini-2.5-pro` - Excellent for complex reasoning
|
|
178
|
+
- `openai/gpt-4.1` - Strong general-purpose model
|
|
179
|
+
- `deepseek/deepseek-r1-0528` - Great for code generation
|
|
180
|
+
- `openai/gpt-4.1-mini` - Fast and cost-effective
|
|
181
|
+
- `anthropic/claude-4-sonnet-20250522` - Superior context handling
|
|
182
|
+
|
|
183
|
+
_Note: Formal evaluations coming soon. Any model can work, but these have shown the best results in practice._
|
|
184
|
+
|
|
185
|
+
## Start Coding
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
tunacode
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Basic Commands
|
|
192
|
+
|
|
193
|
+
| Command | Description |
|
|
194
|
+
| ------------------------ | ---------------------- |
|
|
195
|
+
| `/help` | Show all commands |
|
|
196
|
+
| `/model <provider:name>` | Switch model |
|
|
197
|
+
| `/clear` | Clear message history |
|
|
198
|
+
| `/compact` | Summarize conversation |
|
|
199
|
+
| `/branch <name>` | Create Git branch |
|
|
200
|
+
| `/yolo` | Skip confirmations |
|
|
201
|
+
| `!<command>` | Run shell command |
|
|
202
|
+
| `exit` | Exit TunaCode |
|
|
203
|
+
|
|
204
|
+
## Performance
|
|
205
|
+
|
|
206
|
+
TunaCode leverages parallel execution for read-only operations, achieving **3x faster** file operations:
|
|
207
|
+
|
|
208
|
+

|
|
209
|
+
|
|
210
|
+
Multiple file reads, directory listings, and searches execute concurrently using async I/O, making code exploration significantly faster.
|
|
211
|
+
|
|
212
|
+
## Features in Development
|
|
213
|
+
|
|
214
|
+
- **Bug Fixes**: Actively addressing issues - please report any bugs you encounter!
|
|
215
|
+
|
|
216
|
+
_Note: While the tool is fully functional, we're focusing on stability and core features before optimizing for speed._
|
|
217
|
+
|
|
218
|
+
## Safety First
|
|
219
|
+
|
|
220
|
+
⚠️ **Important**: TunaCode can modify your codebase. Always:
|
|
221
|
+
|
|
222
|
+
- Use Git branches before making changes
|
|
223
|
+
- Review file modifications before confirming
|
|
224
|
+
- Keep backups of important work
|
|
225
|
+
|
|
226
|
+
## Documentation
|
|
227
|
+
|
|
228
|
+
For a complete overview of the documentation, see the [**Documentation Hub**](documentation/README.md).
|
|
229
|
+
|
|
230
|
+
### User Documentation
|
|
231
|
+
|
|
232
|
+
- [**Getting Started**](documentation/user/getting-started.md) - How to install, configure, and use TunaCode.
|
|
233
|
+
- [**Commands**](documentation/user/commands.md) - A complete list of all available commands.
|
|
234
|
+
|
|
235
|
+
### Developer Documentation
|
|
236
|
+
|
|
237
|
+
- **Architecture** (planned) - The overall architecture of the TunaCode application.
|
|
238
|
+
- **Contributing** (planned) - Guidelines for contributing to the project.
|
|
239
|
+
- **Tools** (planned) - How to create and use custom tools.
|
|
240
|
+
- **Testing** (planned) - Information on the testing philosophy and how to run tests.
|
|
241
|
+
|
|
242
|
+
### Guides
|
|
243
|
+
|
|
244
|
+
- [**Advanced Configuration**](documentation/configuration/config-file-example.md) - An example of an advanced configuration file.
|
|
245
|
+
|
|
246
|
+
### Reference
|
|
247
|
+
|
|
248
|
+
- **Changelog** (planned) - A history of changes to the application.
|
|
249
|
+
- **Roadmap** (planned) - The future direction of the project.
|
|
250
|
+
- **Security** (planned) - Information about the security of the application.
|
|
251
|
+
|
|
252
|
+
## Links
|
|
253
|
+
|
|
254
|
+
- [PyPI Package](https://pypi.org/project/tunacode-cli/)
|
|
255
|
+
- [GitHub Repository](https://github.com/alchemiststudiosDOTai/tunacode)
|
|
256
|
+
- [Report Issues](https://github.com/alchemiststudiosDOTai/tunacode/issues)
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
MIT License - see [LICENSE](LICENSE) file
|