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.

Files changed (174) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/textual_repl.tcss +283 -0
  3. tunacode/configuration/__init__.py +1 -0
  4. tunacode/configuration/defaults.py +45 -0
  5. tunacode/configuration/models.py +147 -0
  6. tunacode/configuration/models_registry.json +1 -0
  7. tunacode/configuration/pricing.py +74 -0
  8. tunacode/configuration/settings.py +35 -0
  9. tunacode/constants.py +227 -0
  10. tunacode/core/__init__.py +6 -0
  11. tunacode/core/agents/__init__.py +39 -0
  12. tunacode/core/agents/agent_components/__init__.py +48 -0
  13. tunacode/core/agents/agent_components/agent_config.py +441 -0
  14. tunacode/core/agents/agent_components/agent_helpers.py +290 -0
  15. tunacode/core/agents/agent_components/message_handler.py +99 -0
  16. tunacode/core/agents/agent_components/node_processor.py +477 -0
  17. tunacode/core/agents/agent_components/response_state.py +129 -0
  18. tunacode/core/agents/agent_components/result_wrapper.py +51 -0
  19. tunacode/core/agents/agent_components/state_transition.py +112 -0
  20. tunacode/core/agents/agent_components/streaming.py +271 -0
  21. tunacode/core/agents/agent_components/task_completion.py +40 -0
  22. tunacode/core/agents/agent_components/tool_buffer.py +44 -0
  23. tunacode/core/agents/agent_components/tool_executor.py +101 -0
  24. tunacode/core/agents/agent_components/truncation_checker.py +37 -0
  25. tunacode/core/agents/delegation_tools.py +109 -0
  26. tunacode/core/agents/main.py +545 -0
  27. tunacode/core/agents/prompts.py +66 -0
  28. tunacode/core/agents/research_agent.py +231 -0
  29. tunacode/core/compaction.py +218 -0
  30. tunacode/core/prompting/__init__.py +27 -0
  31. tunacode/core/prompting/loader.py +66 -0
  32. tunacode/core/prompting/prompting_engine.py +98 -0
  33. tunacode/core/prompting/sections.py +50 -0
  34. tunacode/core/prompting/templates.py +69 -0
  35. tunacode/core/state.py +409 -0
  36. tunacode/exceptions.py +313 -0
  37. tunacode/indexing/__init__.py +5 -0
  38. tunacode/indexing/code_index.py +432 -0
  39. tunacode/indexing/constants.py +86 -0
  40. tunacode/lsp/__init__.py +112 -0
  41. tunacode/lsp/client.py +351 -0
  42. tunacode/lsp/diagnostics.py +19 -0
  43. tunacode/lsp/servers.py +101 -0
  44. tunacode/prompts/default_prompt.md +952 -0
  45. tunacode/prompts/research/sections/agent_role.xml +5 -0
  46. tunacode/prompts/research/sections/constraints.xml +14 -0
  47. tunacode/prompts/research/sections/output_format.xml +57 -0
  48. tunacode/prompts/research/sections/tool_use.xml +23 -0
  49. tunacode/prompts/sections/advanced_patterns.xml +255 -0
  50. tunacode/prompts/sections/agent_role.xml +8 -0
  51. tunacode/prompts/sections/completion.xml +10 -0
  52. tunacode/prompts/sections/critical_rules.xml +37 -0
  53. tunacode/prompts/sections/examples.xml +220 -0
  54. tunacode/prompts/sections/output_style.xml +94 -0
  55. tunacode/prompts/sections/parallel_exec.xml +105 -0
  56. tunacode/prompts/sections/search_pattern.xml +100 -0
  57. tunacode/prompts/sections/system_info.xml +6 -0
  58. tunacode/prompts/sections/tool_use.xml +84 -0
  59. tunacode/prompts/sections/user_instructions.xml +3 -0
  60. tunacode/py.typed +0 -0
  61. tunacode/templates/__init__.py +5 -0
  62. tunacode/templates/loader.py +15 -0
  63. tunacode/tools/__init__.py +10 -0
  64. tunacode/tools/authorization/__init__.py +29 -0
  65. tunacode/tools/authorization/context.py +32 -0
  66. tunacode/tools/authorization/factory.py +20 -0
  67. tunacode/tools/authorization/handler.py +58 -0
  68. tunacode/tools/authorization/notifier.py +35 -0
  69. tunacode/tools/authorization/policy.py +19 -0
  70. tunacode/tools/authorization/requests.py +119 -0
  71. tunacode/tools/authorization/rules.py +72 -0
  72. tunacode/tools/bash.py +222 -0
  73. tunacode/tools/decorators.py +213 -0
  74. tunacode/tools/glob.py +353 -0
  75. tunacode/tools/grep.py +468 -0
  76. tunacode/tools/grep_components/__init__.py +9 -0
  77. tunacode/tools/grep_components/file_filter.py +93 -0
  78. tunacode/tools/grep_components/pattern_matcher.py +158 -0
  79. tunacode/tools/grep_components/result_formatter.py +87 -0
  80. tunacode/tools/grep_components/search_result.py +34 -0
  81. tunacode/tools/list_dir.py +205 -0
  82. tunacode/tools/prompts/bash_prompt.xml +10 -0
  83. tunacode/tools/prompts/glob_prompt.xml +7 -0
  84. tunacode/tools/prompts/grep_prompt.xml +10 -0
  85. tunacode/tools/prompts/list_dir_prompt.xml +7 -0
  86. tunacode/tools/prompts/read_file_prompt.xml +9 -0
  87. tunacode/tools/prompts/todoclear_prompt.xml +12 -0
  88. tunacode/tools/prompts/todoread_prompt.xml +16 -0
  89. tunacode/tools/prompts/todowrite_prompt.xml +28 -0
  90. tunacode/tools/prompts/update_file_prompt.xml +9 -0
  91. tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
  92. tunacode/tools/prompts/write_file_prompt.xml +7 -0
  93. tunacode/tools/react.py +111 -0
  94. tunacode/tools/read_file.py +68 -0
  95. tunacode/tools/todo.py +222 -0
  96. tunacode/tools/update_file.py +62 -0
  97. tunacode/tools/utils/__init__.py +1 -0
  98. tunacode/tools/utils/ripgrep.py +311 -0
  99. tunacode/tools/utils/text_match.py +352 -0
  100. tunacode/tools/web_fetch.py +245 -0
  101. tunacode/tools/write_file.py +34 -0
  102. tunacode/tools/xml_helper.py +34 -0
  103. tunacode/types/__init__.py +166 -0
  104. tunacode/types/base.py +94 -0
  105. tunacode/types/callbacks.py +53 -0
  106. tunacode/types/dataclasses.py +121 -0
  107. tunacode/types/pydantic_ai.py +31 -0
  108. tunacode/types/state.py +122 -0
  109. tunacode/ui/__init__.py +6 -0
  110. tunacode/ui/app.py +542 -0
  111. tunacode/ui/commands/__init__.py +430 -0
  112. tunacode/ui/components/__init__.py +1 -0
  113. tunacode/ui/headless/__init__.py +5 -0
  114. tunacode/ui/headless/output.py +72 -0
  115. tunacode/ui/main.py +252 -0
  116. tunacode/ui/renderers/__init__.py +41 -0
  117. tunacode/ui/renderers/errors.py +197 -0
  118. tunacode/ui/renderers/panels.py +550 -0
  119. tunacode/ui/renderers/search.py +314 -0
  120. tunacode/ui/renderers/tools/__init__.py +21 -0
  121. tunacode/ui/renderers/tools/bash.py +247 -0
  122. tunacode/ui/renderers/tools/diagnostics.py +186 -0
  123. tunacode/ui/renderers/tools/glob.py +226 -0
  124. tunacode/ui/renderers/tools/grep.py +228 -0
  125. tunacode/ui/renderers/tools/list_dir.py +198 -0
  126. tunacode/ui/renderers/tools/read_file.py +226 -0
  127. tunacode/ui/renderers/tools/research.py +294 -0
  128. tunacode/ui/renderers/tools/update_file.py +237 -0
  129. tunacode/ui/renderers/tools/web_fetch.py +182 -0
  130. tunacode/ui/repl_support.py +226 -0
  131. tunacode/ui/screens/__init__.py +16 -0
  132. tunacode/ui/screens/model_picker.py +303 -0
  133. tunacode/ui/screens/session_picker.py +181 -0
  134. tunacode/ui/screens/setup.py +218 -0
  135. tunacode/ui/screens/theme_picker.py +90 -0
  136. tunacode/ui/screens/update_confirm.py +69 -0
  137. tunacode/ui/shell_runner.py +129 -0
  138. tunacode/ui/styles/layout.tcss +98 -0
  139. tunacode/ui/styles/modals.tcss +38 -0
  140. tunacode/ui/styles/panels.tcss +81 -0
  141. tunacode/ui/styles/theme-nextstep.tcss +303 -0
  142. tunacode/ui/styles/widgets.tcss +33 -0
  143. tunacode/ui/styles.py +18 -0
  144. tunacode/ui/widgets/__init__.py +23 -0
  145. tunacode/ui/widgets/command_autocomplete.py +62 -0
  146. tunacode/ui/widgets/editor.py +402 -0
  147. tunacode/ui/widgets/file_autocomplete.py +47 -0
  148. tunacode/ui/widgets/messages.py +46 -0
  149. tunacode/ui/widgets/resource_bar.py +182 -0
  150. tunacode/ui/widgets/status_bar.py +98 -0
  151. tunacode/utils/__init__.py +0 -0
  152. tunacode/utils/config/__init__.py +13 -0
  153. tunacode/utils/config/user_configuration.py +91 -0
  154. tunacode/utils/messaging/__init__.py +10 -0
  155. tunacode/utils/messaging/message_utils.py +34 -0
  156. tunacode/utils/messaging/token_counter.py +77 -0
  157. tunacode/utils/parsing/__init__.py +13 -0
  158. tunacode/utils/parsing/command_parser.py +55 -0
  159. tunacode/utils/parsing/json_utils.py +188 -0
  160. tunacode/utils/parsing/retry.py +146 -0
  161. tunacode/utils/parsing/tool_parser.py +267 -0
  162. tunacode/utils/security/__init__.py +15 -0
  163. tunacode/utils/security/command.py +106 -0
  164. tunacode/utils/system/__init__.py +25 -0
  165. tunacode/utils/system/gitignore.py +155 -0
  166. tunacode/utils/system/paths.py +190 -0
  167. tunacode/utils/ui/__init__.py +9 -0
  168. tunacode/utils/ui/file_filter.py +135 -0
  169. tunacode/utils/ui/helpers.py +24 -0
  170. tunacode_cli-0.1.21.dist-info/METADATA +170 -0
  171. tunacode_cli-0.1.21.dist-info/RECORD +174 -0
  172. tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
  173. tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
  174. 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,9 @@
1
+ """UI utilities: completion helpers and data structures."""
2
+
3
+ from tunacode.utils.ui.file_filter import FileFilter
4
+ from tunacode.utils.ui.helpers import DotDict
5
+
6
+ __all__ = [
7
+ "DotDict",
8
+ "FileFilter",
9
+ ]
@@ -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
+ [![PyPI version](https://badge.fury.io/py/tunacode-cli.svg)](https://badge.fury.io/py/tunacode-cli)
57
+ [![Downloads](https://pepy.tech/badge/tunacode-cli)](https://pepy.tech/project/tunacode-cli)
58
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
59
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
60
+ [![Discord Shield](https://discord.com/api/guilds/1447688577126367346/widget.png?style=shield)](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
+ ![TUI Interface](docs/images/tui.png)
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
+ ![Theme](docs/images/theme.png)
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
+ ![TUI Model Setup](docs/images/tui-model-setup.png)
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