tunacode-cli 0.0.56__py3-none-any.whl → 0.0.60__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 (45) hide show
  1. tunacode/cli/commands/implementations/plan.py +8 -8
  2. tunacode/cli/commands/registry.py +2 -2
  3. tunacode/cli/repl.py +214 -407
  4. tunacode/cli/repl_components/command_parser.py +37 -4
  5. tunacode/cli/repl_components/error_recovery.py +79 -1
  6. tunacode/cli/repl_components/output_display.py +14 -11
  7. tunacode/cli/repl_components/tool_executor.py +7 -4
  8. tunacode/configuration/defaults.py +8 -0
  9. tunacode/constants.py +8 -2
  10. tunacode/core/agents/agent_components/agent_config.py +128 -65
  11. tunacode/core/agents/agent_components/node_processor.py +6 -2
  12. tunacode/core/code_index.py +83 -29
  13. tunacode/core/state.py +1 -1
  14. tunacode/core/token_usage/usage_tracker.py +2 -2
  15. tunacode/core/tool_handler.py +3 -3
  16. tunacode/prompts/system.md +117 -490
  17. tunacode/services/mcp.py +29 -7
  18. tunacode/tools/base.py +110 -0
  19. tunacode/tools/bash.py +96 -1
  20. tunacode/tools/exit_plan_mode.py +114 -32
  21. tunacode/tools/glob.py +366 -33
  22. tunacode/tools/grep.py +226 -77
  23. tunacode/tools/grep_components/result_formatter.py +98 -4
  24. tunacode/tools/list_dir.py +132 -2
  25. tunacode/tools/present_plan.py +111 -31
  26. tunacode/tools/read_file.py +91 -0
  27. tunacode/tools/run_command.py +99 -0
  28. tunacode/tools/schema_assembler.py +167 -0
  29. tunacode/tools/todo.py +108 -1
  30. tunacode/tools/update_file.py +94 -0
  31. tunacode/tools/write_file.py +86 -0
  32. tunacode/types.py +10 -9
  33. tunacode/ui/input.py +1 -0
  34. tunacode/ui/keybindings.py +1 -0
  35. tunacode/ui/panels.py +49 -27
  36. tunacode/ui/prompt_manager.py +13 -7
  37. tunacode/utils/json_utils.py +206 -0
  38. tunacode/utils/ripgrep.py +332 -9
  39. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/METADATA +7 -2
  40. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/RECORD +44 -43
  41. tunacode/tools/read_file_async_poc.py +0 -196
  42. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/WHEEL +0 -0
  43. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/entry_points.txt +0 -0
  44. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/licenses/LICENSE +0 -0
  45. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@
3
3
  import logging
4
4
  import os
5
5
  import threading
6
+ import time
6
7
  from collections import defaultdict
7
8
  from pathlib import Path
8
9
  from typing import Dict, List, Optional, Set
@@ -17,6 +18,10 @@ class CodeIndex:
17
18
  grep searches that can timeout in large repositories.
18
19
  """
19
20
 
21
+ # Singleton instance
22
+ _instance: Optional["CodeIndex"] = None
23
+ _instance_lock = threading.RLock()
24
+
20
25
  # Directories to ignore during indexing
21
26
  IGNORE_DIRS = {
22
27
  ".git",
@@ -121,8 +126,83 @@ class CodeIndex:
121
126
  # Cache for directory contents
122
127
  self._dir_cache: Dict[Path, List[Path]] = {}
123
128
 
129
+ # Cache freshness tracking
130
+ self._cache_timestamps: Dict[Path, float] = {}
131
+ self._cache_ttl = 5.0 # 5 seconds TTL for directory cache
132
+
124
133
  self._indexed = False
125
134
 
135
+ @classmethod
136
+ def get_instance(cls, root_dir: Optional[str] = None) -> "CodeIndex":
137
+ """Get the singleton CodeIndex instance.
138
+
139
+ Args:
140
+ root_dir: Root directory to index. Only used on first call.
141
+
142
+ Returns:
143
+ The singleton CodeIndex instance.
144
+ """
145
+ if cls._instance is None:
146
+ with cls._instance_lock:
147
+ if cls._instance is None:
148
+ cls._instance = cls(root_dir)
149
+ return cls._instance
150
+
151
+ @classmethod
152
+ def reset_instance(cls) -> None:
153
+ """Reset the singleton instance (for testing)."""
154
+ with cls._instance_lock:
155
+ cls._instance = None
156
+
157
+ def get_directory_contents(self, path: Path) -> List[str]:
158
+ """Get cached directory contents if available and fresh.
159
+
160
+ Args:
161
+ path: Directory path to check
162
+
163
+ Returns:
164
+ List of filenames in directory, empty list if not cached/stale
165
+ """
166
+ with self._lock:
167
+ if path not in self._dir_cache:
168
+ return []
169
+
170
+ if not self.is_cache_fresh(path):
171
+ # Remove stale entry
172
+ self._dir_cache.pop(path, None)
173
+ self._cache_timestamps.pop(path, None)
174
+ return []
175
+
176
+ # Return just the filenames, not Path objects
177
+ return [p.name for p in self._dir_cache[path]]
178
+
179
+ def is_cache_fresh(self, path: Path) -> bool:
180
+ """Check if cached directory data is still fresh.
181
+
182
+ Args:
183
+ path: Directory path to check
184
+
185
+ Returns:
186
+ True if cache is fresh, False if stale or missing
187
+ """
188
+ if path not in self._cache_timestamps:
189
+ return False
190
+
191
+ age = time.time() - self._cache_timestamps[path]
192
+ return age < self._cache_ttl
193
+
194
+ def update_directory_cache(self, path: Path, entries: List[str]) -> None:
195
+ """Update the directory cache with fresh data.
196
+
197
+ Args:
198
+ path: Directory path
199
+ entries: List of filenames in the directory
200
+ """
201
+ with self._lock:
202
+ # Convert filenames back to Path objects for internal storage
203
+ self._dir_cache[path] = [Path(path) / entry for entry in entries]
204
+ self._cache_timestamps[path] = time.time()
205
+
126
206
  def build_index(self, force: bool = False) -> None:
127
207
  """Build the file index for the repository.
128
208
 
@@ -152,6 +232,7 @@ class CodeIndex:
152
232
  self._class_definitions.clear()
153
233
  self._function_definitions.clear()
154
234
  self._dir_cache.clear()
235
+ self._cache_timestamps.clear()
155
236
 
156
237
  def _should_ignore_path(self, path: Path) -> bool:
157
238
  """Check if a path should be ignored during indexing."""
@@ -183,8 +264,9 @@ class CodeIndex:
183
264
  self._index_file(entry)
184
265
  file_list.append(entry)
185
266
 
186
- # Cache directory contents
267
+ # Cache directory contents with timestamp
187
268
  self._dir_cache[directory] = file_list
269
+ self._cache_timestamps[directory] = time.time()
188
270
 
189
271
  except PermissionError:
190
272
  logger.debug(f"Permission denied: {directory}")
@@ -352,34 +434,6 @@ class CodeIndex:
352
434
 
353
435
  return sorted(self._all_files)
354
436
 
355
- def get_directory_contents(self, directory: str) -> List[Path]:
356
- """Get cached contents of a directory.
357
-
358
- Args:
359
- directory: Directory path relative to root
360
-
361
- Returns:
362
- List of file paths in the directory.
363
- """
364
- with self._lock:
365
- if not self._indexed:
366
- self.build_index()
367
-
368
- dir_path = self.root_dir / directory
369
- if dir_path in self._dir_cache:
370
- return [p.relative_to(self.root_dir) for p in self._dir_cache[dir_path]]
371
-
372
- # Fallback to scanning if not in cache
373
- results = []
374
- for file_path in self._all_files:
375
- if str(file_path).startswith(directory + os.sep):
376
- # Only include direct children
377
- relative = str(file_path)[len(directory) + 1 :]
378
- if os.sep not in relative:
379
- results.append(file_path)
380
-
381
- return sorted(results)
382
-
383
437
  def find_imports(self, module_name: str) -> List[Path]:
384
438
  """Find files that import a specific module.
385
439
 
tunacode/core/state.py CHANGED
@@ -86,7 +86,7 @@ class SessionState:
86
86
  task_hierarchy: dict[str, Any] = field(default_factory=dict)
87
87
  iteration_budgets: dict[str, int] = field(default_factory=dict)
88
88
  recursive_context_stack: list[dict[str, Any]] = field(default_factory=list)
89
-
89
+
90
90
  # Plan Mode state tracking
91
91
  plan_mode: bool = False
92
92
  plan_phase: Optional[PlanPhase] = None
@@ -37,10 +37,10 @@ class UsageTracker(UsageTrackerProtocol):
37
37
  # 2. Calculate the cost
38
38
  cost = self._calculate_cost(parsed_data)
39
39
 
40
- # 3. Update the session state
40
+ # 3. Update the session state (always done to track totals for session cost display)
41
41
  self._update_state(parsed_data, cost)
42
42
 
43
- # 4. Display the summary if enabled
43
+ # 4. Display detailed per-call summary only if debugging enabled
44
44
  if self.state_manager.session.show_thoughts:
45
45
  await self._display_summary()
46
46
 
@@ -44,7 +44,7 @@ class ToolHandler:
44
44
  # Never confirm present_plan - it has its own approval flow
45
45
  if tool_name == "present_plan":
46
46
  return False
47
-
47
+
48
48
  # Block write tools in plan mode
49
49
  if self.is_tool_blocked_in_plan_mode(tool_name):
50
50
  return True # Force confirmation for blocked tools
@@ -64,11 +64,11 @@ class ToolHandler:
64
64
  """Check if tool is blocked in plan mode."""
65
65
  if not self.state.is_plan_mode():
66
66
  return False
67
-
67
+
68
68
  # Allow present_plan tool to end planning phase
69
69
  if tool_name == "present_plan":
70
70
  return False
71
-
71
+
72
72
  # Allow read-only tools
73
73
  return not is_read_only_tool(tool_name)
74
74