tunacode-cli 0.1.21__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/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.utils.system.gitignore
|
|
3
|
+
|
|
4
|
+
Provides gitignore pattern matching and file listing with ignore support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import fnmatch
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from tunacode.constants import ENV_FILE
|
|
11
|
+
|
|
12
|
+
# Default ignore patterns if .gitignore is not found
|
|
13
|
+
DEFAULT_IGNORE_PATTERNS = {
|
|
14
|
+
"node_modules/",
|
|
15
|
+
"env/",
|
|
16
|
+
"venv/",
|
|
17
|
+
".git/",
|
|
18
|
+
"build/",
|
|
19
|
+
"dist/",
|
|
20
|
+
"__pycache__/",
|
|
21
|
+
"*.pyc",
|
|
22
|
+
"*.pyo",
|
|
23
|
+
"*.pyd",
|
|
24
|
+
".DS_Store",
|
|
25
|
+
"Thumbs.db",
|
|
26
|
+
ENV_FILE,
|
|
27
|
+
".venv",
|
|
28
|
+
"*.egg-info",
|
|
29
|
+
".pytest_cache/",
|
|
30
|
+
".coverage",
|
|
31
|
+
"htmlcov/",
|
|
32
|
+
".tox/",
|
|
33
|
+
"coverage.xml",
|
|
34
|
+
"*.cover",
|
|
35
|
+
".idea/",
|
|
36
|
+
".vscode/",
|
|
37
|
+
"*.swp",
|
|
38
|
+
"*.swo",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_gitignore_patterns(filepath=".gitignore"):
|
|
43
|
+
"""Loads patterns from a .gitignore file."""
|
|
44
|
+
patterns = set()
|
|
45
|
+
try:
|
|
46
|
+
with open(filepath, encoding="utf-8") as f:
|
|
47
|
+
for line in f:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if line and not line.startswith("#"):
|
|
50
|
+
patterns.add(line)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
return None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Error reading {filepath}: {e}")
|
|
55
|
+
return None
|
|
56
|
+
patterns.add(".git/")
|
|
57
|
+
return patterns
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_ignored(rel_path, name, patterns):
|
|
61
|
+
"""
|
|
62
|
+
Checks if a given relative path or name matches any ignore patterns.
|
|
63
|
+
Mimics basic .gitignore behavior using fnmatch.
|
|
64
|
+
"""
|
|
65
|
+
if not patterns:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
if name == ".git" or rel_path.startswith(".git/") or "/.git/" in rel_path:
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
path_parts = rel_path.split(os.sep)
|
|
72
|
+
|
|
73
|
+
for pattern in patterns:
|
|
74
|
+
is_dir_pattern = pattern.endswith("/")
|
|
75
|
+
match_pattern = pattern.rstrip("/") if is_dir_pattern else pattern
|
|
76
|
+
|
|
77
|
+
if match_pattern.startswith("/"):
|
|
78
|
+
match_pattern = match_pattern.lstrip("/")
|
|
79
|
+
if fnmatch.fnmatch(rel_path, match_pattern) or fnmatch.fnmatch(
|
|
80
|
+
rel_path, match_pattern + "/*"
|
|
81
|
+
):
|
|
82
|
+
if is_dir_pattern:
|
|
83
|
+
if rel_path == match_pattern or rel_path.startswith(match_pattern + os.sep):
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
return True
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if fnmatch.fnmatch(name, match_pattern):
|
|
90
|
+
if is_dir_pattern:
|
|
91
|
+
pass
|
|
92
|
+
else:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
if fnmatch.fnmatch(rel_path, match_pattern):
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
if is_dir_pattern or "/" not in pattern:
|
|
99
|
+
limit = len(path_parts) if is_dir_pattern else len(path_parts) - 1
|
|
100
|
+
for i in range(limit):
|
|
101
|
+
if fnmatch.fnmatch(path_parts[i], match_pattern):
|
|
102
|
+
return True
|
|
103
|
+
if name == path_parts[-1] and fnmatch.fnmatch(name, match_pattern):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def list_cwd(max_depth=3):
|
|
110
|
+
"""
|
|
111
|
+
Lists files in the current working directory up to a specified depth,
|
|
112
|
+
respecting .gitignore rules or a default ignore list.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
max_depth (int): Maximum directory depth to traverse.
|
|
116
|
+
0: only files in the current directory.
|
|
117
|
+
1: includes files in immediate subdirectories.
|
|
118
|
+
... Default is 3.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
list: A sorted list of relative file paths.
|
|
122
|
+
"""
|
|
123
|
+
ignore_patterns = _load_gitignore_patterns()
|
|
124
|
+
if ignore_patterns is None:
|
|
125
|
+
ignore_patterns = DEFAULT_IGNORE_PATTERNS
|
|
126
|
+
|
|
127
|
+
file_list = []
|
|
128
|
+
start_path = "."
|
|
129
|
+
max_depth = max(0, max_depth)
|
|
130
|
+
|
|
131
|
+
for root, dirs, files in os.walk(start_path, topdown=True):
|
|
132
|
+
rel_root = os.path.relpath(root, start_path)
|
|
133
|
+
if rel_root == ".":
|
|
134
|
+
rel_root = ""
|
|
135
|
+
current_depth = 0
|
|
136
|
+
else:
|
|
137
|
+
current_depth = rel_root.count(os.sep) + 1
|
|
138
|
+
|
|
139
|
+
if current_depth >= max_depth:
|
|
140
|
+
dirs[:] = []
|
|
141
|
+
|
|
142
|
+
original_dirs = list(dirs)
|
|
143
|
+
dirs[:] = []
|
|
144
|
+
for d in original_dirs:
|
|
145
|
+
dir_rel_path = os.path.join(rel_root, d) if rel_root else d
|
|
146
|
+
if not _is_ignored(dir_rel_path, d, ignore_patterns):
|
|
147
|
+
dirs.append(d)
|
|
148
|
+
|
|
149
|
+
if current_depth <= max_depth:
|
|
150
|
+
for f in files:
|
|
151
|
+
file_rel_path = os.path.join(rel_root, f) if rel_root else f
|
|
152
|
+
if not _is_ignored(file_rel_path, f, ignore_patterns):
|
|
153
|
+
file_list.append(file_rel_path.replace(os.sep, "/"))
|
|
154
|
+
|
|
155
|
+
return sorted(file_list)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.utils.system.paths
|
|
3
|
+
|
|
4
|
+
Provides path utilities, session management, device identification, and update checking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
14
|
+
from tunacode.constants import DEVICE_ID_FILE, SESSIONS_SUBDIR, TUNACODE_HOME_DIR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_tunacode_home():
|
|
18
|
+
"""
|
|
19
|
+
Get the path to the TunaCode home directory (~/.tunacode).
|
|
20
|
+
Creates it if it doesn't exist.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path: The path to the TunaCode home directory.
|
|
24
|
+
"""
|
|
25
|
+
home = Path.home() / TUNACODE_HOME_DIR
|
|
26
|
+
home.mkdir(exist_ok=True)
|
|
27
|
+
return home
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_session_dir(state_manager):
|
|
31
|
+
"""
|
|
32
|
+
Get the path to the current session directory.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
state_manager: The StateManager instance containing session info.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Path: The path to the current session directory.
|
|
39
|
+
"""
|
|
40
|
+
session_dir = get_tunacode_home() / SESSIONS_SUBDIR / state_manager.session.session_id
|
|
41
|
+
session_dir.mkdir(exist_ok=True, parents=True)
|
|
42
|
+
return session_dir
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_cwd():
|
|
46
|
+
"""Returns the current working directory."""
|
|
47
|
+
return os.getcwd()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_project_id() -> str:
|
|
51
|
+
"""
|
|
52
|
+
Get a project identifier based on the git repository root or cwd.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: A 16-character hash identifying the current project.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
timeout=2,
|
|
63
|
+
)
|
|
64
|
+
if result.returncode == 0:
|
|
65
|
+
repo_root = result.stdout.strip()
|
|
66
|
+
return hashlib.sha256(repo_root.encode()).hexdigest()[:16]
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return hashlib.sha256(os.getcwd().encode()).hexdigest()[:16]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_session_storage_dir() -> Path:
|
|
73
|
+
"""
|
|
74
|
+
Get the XDG-compliant session storage directory.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Path: The directory where session files are stored.
|
|
78
|
+
"""
|
|
79
|
+
xdg_data = os.environ.get("XDG_DATA_HOME", str(Path.home() / ".local" / "share"))
|
|
80
|
+
storage_dir = Path(xdg_data) / "tunacode" / "sessions"
|
|
81
|
+
storage_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
82
|
+
return storage_dir
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_device_id():
|
|
86
|
+
"""
|
|
87
|
+
Get the device ID from the ~/.tunacode/device_id file.
|
|
88
|
+
If the file doesn't exist, generate a new UUID and save it.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: The device ID as a string.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
tunacode_home = get_tunacode_home()
|
|
95
|
+
device_id_file = tunacode_home / DEVICE_ID_FILE
|
|
96
|
+
|
|
97
|
+
if device_id_file.exists():
|
|
98
|
+
device_id = device_id_file.read_text().strip()
|
|
99
|
+
if device_id:
|
|
100
|
+
return device_id
|
|
101
|
+
|
|
102
|
+
device_id = str(uuid.uuid4())
|
|
103
|
+
device_id_file.write_text(device_id)
|
|
104
|
+
|
|
105
|
+
return device_id
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error getting device ID: {e}")
|
|
108
|
+
return str(uuid.uuid4())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cleanup_session(state_manager):
|
|
112
|
+
"""
|
|
113
|
+
Clean up temporary session runtime files after the CLI exits.
|
|
114
|
+
Session data is preserved in XDG_DATA_HOME for persistence.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
state_manager: The StateManager instance containing session info.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
bool: True if cleanup was successful, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
if state_manager.session.session_id is None:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
session_dir = get_session_dir(state_manager)
|
|
127
|
+
|
|
128
|
+
if session_dir.exists():
|
|
129
|
+
import shutil
|
|
130
|
+
|
|
131
|
+
shutil.rmtree(session_dir)
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"Error cleaning up session: {e}")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def delete_session_file(project_id: str, session_id: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Delete a persisted session file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
project_id: The project identifier.
|
|
145
|
+
session_id: The session identifier.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
bool: True if deletion was successful, False otherwise.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
storage_dir = get_session_storage_dir()
|
|
152
|
+
session_file = storage_dir / f"{project_id}_{session_id}.json"
|
|
153
|
+
if session_file.exists():
|
|
154
|
+
session_file.unlink()
|
|
155
|
+
return True
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"Error deleting session file: {e}")
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_for_updates():
|
|
162
|
+
"""
|
|
163
|
+
Check if there's a newer version of tunacode-cli available on PyPI.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
tuple: (has_update, latest_version)
|
|
167
|
+
- has_update (bool): True if a newer version is available
|
|
168
|
+
- latest_version (str): The latest version available
|
|
169
|
+
"""
|
|
170
|
+
app_settings = ApplicationSettings()
|
|
171
|
+
current_version = app_settings.version
|
|
172
|
+
try:
|
|
173
|
+
result = subprocess.run(
|
|
174
|
+
["pip", "index", "versions", "tunacode-cli"], capture_output=True, text=True, check=True
|
|
175
|
+
)
|
|
176
|
+
output = result.stdout
|
|
177
|
+
|
|
178
|
+
if "Available versions:" in output:
|
|
179
|
+
versions_line = output.split("Available versions:")[1].strip()
|
|
180
|
+
versions = versions_line.split(", ")
|
|
181
|
+
latest_version = versions[0]
|
|
182
|
+
|
|
183
|
+
latest_version = latest_version.strip()
|
|
184
|
+
|
|
185
|
+
if latest_version > current_version:
|
|
186
|
+
return True, latest_version
|
|
187
|
+
|
|
188
|
+
return False, current_version
|
|
189
|
+
except Exception:
|
|
190
|
+
return False, current_version
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pathspec
|
|
7
|
+
|
|
8
|
+
from tunacode.constants import AUTOCOMPLETE_MAX_DEPTH, AUTOCOMPLETE_RESULT_LIMIT
|
|
9
|
+
|
|
10
|
+
DEFAULT_IGNORES = [
|
|
11
|
+
".git/",
|
|
12
|
+
".venv/",
|
|
13
|
+
"venv/",
|
|
14
|
+
"env/",
|
|
15
|
+
"node_modules/",
|
|
16
|
+
"__pycache__/",
|
|
17
|
+
"*.pyc",
|
|
18
|
+
"*.pyo",
|
|
19
|
+
"*.egg-info/",
|
|
20
|
+
".DS_Store",
|
|
21
|
+
"Thumbs.db",
|
|
22
|
+
".idea/",
|
|
23
|
+
".vscode/",
|
|
24
|
+
"build/",
|
|
25
|
+
"dist/",
|
|
26
|
+
"target/",
|
|
27
|
+
".env",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileFilter:
|
|
32
|
+
"""Gitignore-aware file filtering."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
35
|
+
self.root = root or Path(".")
|
|
36
|
+
self._spec = self._build_spec()
|
|
37
|
+
|
|
38
|
+
def _build_spec(self) -> pathspec.PathSpec:
|
|
39
|
+
patterns = list(DEFAULT_IGNORES)
|
|
40
|
+
gitignore = self.root / ".gitignore"
|
|
41
|
+
if gitignore.exists():
|
|
42
|
+
patterns.extend(gitignore.read_text().splitlines())
|
|
43
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
44
|
+
|
|
45
|
+
def is_ignored(self, path: Path) -> bool:
|
|
46
|
+
try:
|
|
47
|
+
rel = path.relative_to(self.root)
|
|
48
|
+
return self._spec.match_file(str(rel))
|
|
49
|
+
except ValueError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _parse_prefix(self, prefix: str) -> tuple[Path, str]:
|
|
53
|
+
"""Parse prefix into search path and name filter."""
|
|
54
|
+
if not prefix:
|
|
55
|
+
return self.root, ""
|
|
56
|
+
|
|
57
|
+
candidate = self.root / prefix
|
|
58
|
+
|
|
59
|
+
if candidate.exists() and candidate.is_dir():
|
|
60
|
+
return candidate, ""
|
|
61
|
+
|
|
62
|
+
search_path = candidate.parent
|
|
63
|
+
name_prefix = Path(prefix).name
|
|
64
|
+
|
|
65
|
+
if not search_path.exists():
|
|
66
|
+
return self.root, ""
|
|
67
|
+
|
|
68
|
+
return search_path, name_prefix
|
|
69
|
+
|
|
70
|
+
def _matches_prefix(self, path: Path, name_prefix: str, search_path: Path) -> bool:
|
|
71
|
+
"""Check if path matches name prefix filter."""
|
|
72
|
+
if not name_prefix:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
prefix_lower = name_prefix.lower()
|
|
76
|
+
|
|
77
|
+
if path.parent == search_path:
|
|
78
|
+
return path.name.lower().startswith(prefix_lower)
|
|
79
|
+
|
|
80
|
+
rel_path = path.relative_to(search_path)
|
|
81
|
+
return any(prefix_lower in part.lower() for part in rel_path.parts)
|
|
82
|
+
|
|
83
|
+
def complete(
|
|
84
|
+
self,
|
|
85
|
+
prefix: str = "",
|
|
86
|
+
limit: int = AUTOCOMPLETE_RESULT_LIMIT,
|
|
87
|
+
max_depth: int = AUTOCOMPLETE_MAX_DEPTH,
|
|
88
|
+
) -> list[str]:
|
|
89
|
+
"""Return filtered file paths matching prefix."""
|
|
90
|
+
search_path, name_prefix = self._parse_prefix(prefix)
|
|
91
|
+
|
|
92
|
+
if not search_path.exists():
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
results_with_depth: list[tuple[int, str]] = []
|
|
96
|
+
|
|
97
|
+
for root_dir, dirs, files in os.walk(str(search_path), topdown=True):
|
|
98
|
+
root_path = Path(root_dir)
|
|
99
|
+
rel_root = root_path.relative_to(search_path)
|
|
100
|
+
current_depth = len(rel_root.parts)
|
|
101
|
+
|
|
102
|
+
if current_depth >= max_depth:
|
|
103
|
+
dirs[:] = []
|
|
104
|
+
|
|
105
|
+
dirs[:] = sorted(d for d in dirs if not self.is_ignored(root_path / d))
|
|
106
|
+
|
|
107
|
+
for d in dirs:
|
|
108
|
+
dir_path = root_path / d
|
|
109
|
+
if not self._matches_prefix(dir_path, name_prefix, search_path):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
rel = dir_path.relative_to(self.root)
|
|
113
|
+
results_with_depth.append((current_depth, f"{rel}/"))
|
|
114
|
+
|
|
115
|
+
if len(results_with_depth) >= limit:
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
for f in sorted(files):
|
|
119
|
+
file_path = root_path / f
|
|
120
|
+
if self.is_ignored(file_path):
|
|
121
|
+
continue
|
|
122
|
+
if not self._matches_prefix(file_path, name_prefix, search_path):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
rel = file_path.relative_to(self.root)
|
|
126
|
+
results_with_depth.append((current_depth, str(rel)))
|
|
127
|
+
|
|
128
|
+
if len(results_with_depth) >= limit:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
if len(results_with_depth) >= limit:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
results_with_depth.sort(key=lambda x: (x[0], x[1]))
|
|
135
|
+
return [path for _, path in results_with_depth[:limit]]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: tunacode.utils.ui.helpers
|
|
3
|
+
|
|
4
|
+
Provides DotDict for dot notation access to dictionary attributes
|
|
5
|
+
and shared UI utility functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DotDict(dict):
|
|
12
|
+
"""dot.notation access to dictionary attributes"""
|
|
13
|
+
|
|
14
|
+
__getattr__ = dict.get
|
|
15
|
+
__setattr__ = dict.__setitem__ # type: ignore[assignment]
|
|
16
|
+
__delattr__ = dict.__delitem__ # type: ignore[assignment]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def truncate(value: Any, max_length: int = 50) -> str:
|
|
20
|
+
"""Truncate a value to max_length with ellipsis."""
|
|
21
|
+
s = str(value)
|
|
22
|
+
if len(s) <= max_length:
|
|
23
|
+
return s
|
|
24
|
+
return s[: max_length - 3] + "..."
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunacode-cli
|
|
3
|
+
Version: 0.1.21
|
|
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.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: <3.14,>=3.11
|
|
22
|
+
Requires-Dist: click<8.2.0,>=8.1.0
|
|
23
|
+
Requires-Dist: defusedxml
|
|
24
|
+
Requires-Dist: html2text>=2024.2.26
|
|
25
|
+
Requires-Dist: pathspec>=0.12.1
|
|
26
|
+
Requires-Dist: prompt-toolkit<4.0.0,>=3.0.52
|
|
27
|
+
Requires-Dist: pydantic-ai<2.0.0,>=1.18.0
|
|
28
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.4
|
|
29
|
+
Requires-Dist: pygments<3.0.0,>=2.19.2
|
|
30
|
+
Requires-Dist: python-levenshtein>=0.21.0
|
|
31
|
+
Requires-Dist: rich<15.0.0,>=14.2.0
|
|
32
|
+
Requires-Dist: ruff>=0.14.0
|
|
33
|
+
Requires-Dist: textual-autocomplete>=4.0.6
|
|
34
|
+
Requires-Dist: textual<5.0.0,>=4.0.0
|
|
35
|
+
Requires-Dist: tiktoken<1.0.0,>=0.12.0
|
|
36
|
+
Requires-Dist: typer>=0.15.0
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: autoflake>=2.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: bandit; extra == 'dev'
|
|
40
|
+
Requires-Dist: build; extra == 'dev'
|
|
41
|
+
Requires-Dist: dead>=1.5.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
43
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
47
|
+
Requires-Dist: ruff==0.14.9; extra == 'dev'
|
|
48
|
+
Requires-Dist: textual-dev; extra == 'dev'
|
|
49
|
+
Requires-Dist: twine; extra == 'dev'
|
|
50
|
+
Requires-Dist: unimport>=1.0.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: vulture>=2.7; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# tunacode-cli
|
|
55
|
+
|
|
56
|
+
[](https://badge.fury.io/py/tunacode-cli)
|
|
57
|
+
[](https://pepy.tech/project/tunacode-cli)
|
|
58
|
+
[](https://www.python.org/downloads/)
|
|
59
|
+
[](https://opensource.org/licenses/MIT)
|
|
60
|
+
[](https://discord.gg/TN7Fpynv6H)
|
|
61
|
+
|
|
62
|
+
A TUI code agent.
|
|
63
|
+
|
|
64
|
+
> **Note:** Under active development - expect bugs.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Interface
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
The Textual-based terminal user interface provides a clean, interactive environment for AI-assisted coding, with a design heavily inspired by the classic NeXTSTEP user interface.
|
|
73
|
+
|
|
74
|
+
## Theme Support
|
|
75
|
+
|
|
76
|
+
The interface supports multiple themes for different preferences and environments.
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
Customize the appearance with built-in themes or create your own color schemes.
|
|
81
|
+
|
|
82
|
+
## Model Setup
|
|
83
|
+
|
|
84
|
+
Configure your AI models and settings through the provided setup interface.
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+
**Note:** TunaCode has full bash shell access. This tool assumes you know what you're doing. If you're concerned, run it in a sandboxed environment.
|
|
89
|
+
|
|
90
|
+
## v0.1.1 - Major Rewrite
|
|
91
|
+
|
|
92
|
+
This release is a complete rewrite with a new Textual-based TUI.
|
|
93
|
+
|
|
94
|
+
**Upgrading from v1?** The legacy v1 codebase is preserved in the `legacy-v1` branch and will only receive security updates.
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
- Python 3.11+
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv tool install tunacode-cli
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
1. Run the setup wizard to configure your API key:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
tunacode --setup
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
2. Start coding:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
tunacode
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
Set your API key as an environment variable or use the setup wizard:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export OPENAI_API_KEY="your-key"
|
|
126
|
+
# or
|
|
127
|
+
export ANTHROPIC_API_KEY="your-key"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Config file location: `~/.config/tunacode.json`
|
|
131
|
+
|
|
132
|
+
## Commands
|
|
133
|
+
|
|
134
|
+
| Command | Description |
|
|
135
|
+
| -------- | ---------------------------- |
|
|
136
|
+
| /help | Show available commands |
|
|
137
|
+
| /model | Change AI model |
|
|
138
|
+
| /clear | Clear conversation history |
|
|
139
|
+
| /yolo | Toggle auto-confirm mode |
|
|
140
|
+
| /branch | Create and switch git branch |
|
|
141
|
+
| /plan | Toggle read-only planning |
|
|
142
|
+
| /theme | Change UI theme |
|
|
143
|
+
| /resume | Load/delete saved sessions |
|
|
144
|
+
| !<cmd> | Run shell command |
|
|
145
|
+
| exit | Quit tunacode |
|
|
146
|
+
|
|
147
|
+
## LSP Integration (Beta)
|
|
148
|
+
|
|
149
|
+
TunaCode includes experimental Language Server Protocol support for real-time diagnostics. When an LSP server is detected in your PATH, it activates automatically.
|
|
150
|
+
|
|
151
|
+
**Supported languages:**
|
|
152
|
+
| Language | LSP Server |
|
|
153
|
+
| ---------- | ----------------------------- |
|
|
154
|
+
| Python | `ruff server` |
|
|
155
|
+
| TypeScript | `typescript-language-server` |
|
|
156
|
+
| JavaScript | `typescript-language-server` |
|
|
157
|
+
| Go | `gopls` |
|
|
158
|
+
| Rust | `rust-analyzer` |
|
|
159
|
+
|
|
160
|
+
Diagnostics appear in the UI when editing files. This feature is beta - expect rough edges.
|
|
161
|
+
|
|
162
|
+
## Discord Server
|
|
163
|
+
|
|
164
|
+
Join our official discord server to receive help, show us how you're using tunacode, and chat about anything LLM.
|
|
165
|
+
|
|
166
|
+
[<img src="https://discord.com/api/guilds/1447688577126367346/widget.png?style=banner3" alt="Discord Banner 3"/>](https://discord.gg/TN7Fpynv6H)
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|