tunacode-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
tunacode/cli/repl.py ADDED
@@ -0,0 +1,251 @@
1
+ """
2
+ Module: sidekick.cli.repl
3
+
4
+ Interactive REPL (Read-Eval-Print Loop) implementation for Sidekick.
5
+ Handles user input, command processing, and agent interaction in an interactive shell.
6
+ """
7
+
8
+ import json
9
+ from asyncio.exceptions import CancelledError
10
+
11
+ from prompt_toolkit.application import run_in_terminal
12
+ from prompt_toolkit.application.current import get_app
13
+ from pydantic_ai.exceptions import UnexpectedModelBehavior
14
+
15
+ from tunacode.configuration.settings import ApplicationSettings
16
+ from tunacode.core.agents import main as agent
17
+ from tunacode.core.agents.main import patch_tool_messages
18
+ from tunacode.core.tool_handler import ToolHandler
19
+ from tunacode.exceptions import AgentError, UserAbortError, ValidationError
20
+ from tunacode.ui import console as ui
21
+ from tunacode.ui.tool_ui import ToolUI
22
+
23
+ from ..types import CommandContext, CommandResult, StateManager, ToolArgs
24
+ from .commands import CommandRegistry
25
+
26
+ # Tool UI instance
27
+ _tool_ui = ToolUI()
28
+
29
+
30
+ def _parse_args(args) -> ToolArgs:
31
+ """
32
+ Parse tool arguments from a JSON string or dictionary.
33
+
34
+ Args:
35
+ args (str or dict): A JSON-formatted string or a dictionary containing tool arguments.
36
+
37
+ Returns:
38
+ dict: The parsed arguments.
39
+
40
+ Raises:
41
+ ValueError: If 'args' is not a string or dictionary, or if the string is not valid JSON.
42
+ """
43
+ if isinstance(args, str):
44
+ try:
45
+ return json.loads(args)
46
+ except json.JSONDecodeError:
47
+ raise ValidationError(f"Invalid JSON: {args}")
48
+ elif isinstance(args, dict):
49
+ return args
50
+ else:
51
+ raise ValidationError(f"Invalid args type: {type(args)}")
52
+
53
+
54
+ async def _tool_confirm(tool_call, node, state_manager: StateManager):
55
+ """Confirm tool execution with separated business logic and UI."""
56
+ # Create tool handler with state
57
+ tool_handler = ToolHandler(state_manager)
58
+ args = _parse_args(tool_call.args)
59
+
60
+ # Check if confirmation is needed
61
+ if not tool_handler.should_confirm(tool_call.tool_name):
62
+ # Log MCP tools when skipping confirmation
63
+ app_settings = ApplicationSettings()
64
+ if tool_call.tool_name not in app_settings.internal_tools:
65
+ title = _tool_ui._get_tool_title(tool_call.tool_name)
66
+ await _tool_ui.log_mcp(title, args)
67
+ return
68
+
69
+ # Stop spinner during user interaction
70
+ state_manager.session.spinner.stop()
71
+
72
+ # Create confirmation request
73
+ request = tool_handler.create_confirmation_request(tool_call.tool_name, args)
74
+
75
+ # Show UI and get response
76
+ response = await _tool_ui.show_confirmation(request, state_manager)
77
+
78
+ # Process the response
79
+ if not tool_handler.process_confirmation(response, tool_call.tool_name):
80
+ raise UserAbortError("User aborted.")
81
+
82
+ await ui.line() # Add line after user input
83
+ state_manager.session.spinner.start()
84
+
85
+
86
+ async def _tool_handler(part, node, state_manager: StateManager):
87
+ """Handle tool execution with separated business logic and UI."""
88
+ await ui.info(f"Tool({part.tool_name})")
89
+ state_manager.session.spinner.stop()
90
+
91
+ try:
92
+ # Create tool handler with state
93
+ tool_handler = ToolHandler(state_manager)
94
+ args = _parse_args(part.args)
95
+
96
+ # Use a synchronous function in run_in_terminal to avoid async deadlocks
97
+ def confirm_func():
98
+ # Skip confirmation if not needed
99
+ if not tool_handler.should_confirm(part.tool_name):
100
+ return False
101
+
102
+ # Create confirmation request
103
+ request = tool_handler.create_confirmation_request(part.tool_name, args)
104
+
105
+ # Show sync UI and get response
106
+ response = _tool_ui.show_sync_confirmation(request)
107
+
108
+ # Process the response
109
+ if not tool_handler.process_confirmation(response, part.tool_name):
110
+ return True # Abort
111
+ return False # Continue
112
+
113
+ # Run the confirmation in the terminal
114
+ should_abort = await run_in_terminal(confirm_func)
115
+
116
+ if should_abort:
117
+ raise UserAbortError("User aborted.")
118
+
119
+ except UserAbortError:
120
+ patch_tool_messages("Operation aborted by user.", state_manager)
121
+ raise
122
+ finally:
123
+ state_manager.session.spinner.start()
124
+
125
+
126
+ # Initialize command registry
127
+ _command_registry = CommandRegistry()
128
+ _command_registry.register_all_default_commands()
129
+
130
+
131
+ async def _handle_command(command: str, state_manager: StateManager) -> CommandResult:
132
+ """
133
+ Handles a command string using the command registry.
134
+
135
+ Args:
136
+ command: The command string entered by the user.
137
+ state_manager: The state manager instance.
138
+
139
+ Returns:
140
+ Command result (varies by command).
141
+ """
142
+ # Create command context
143
+ context = CommandContext(state_manager=state_manager, process_request=process_request)
144
+
145
+ try:
146
+ # Set the process_request callback for commands that need it
147
+ _command_registry.set_process_request_callback(process_request)
148
+
149
+ # Execute the command
150
+ return await _command_registry.execute(command, context)
151
+ except ValidationError as e:
152
+ await ui.error(str(e))
153
+
154
+
155
+ async def process_request(text: str, state_manager: StateManager, output: bool = True):
156
+ """Process input using the agent, handling cancellation safely."""
157
+ state_manager.session.spinner = await ui.spinner(
158
+ True, state_manager.session.spinner, state_manager
159
+ )
160
+ try:
161
+ # Expand @file references before sending to the agent
162
+ try:
163
+ from tunacode.utils.text_utils import expand_file_refs
164
+
165
+ text = expand_file_refs(text)
166
+ except ValueError as e:
167
+ await ui.error(str(e))
168
+ return
169
+
170
+ # Create a partial function that includes state_manager
171
+ def tool_callback_with_state(part, node):
172
+ return _tool_handler(part, node, state_manager)
173
+
174
+ res = await agent.process_request(
175
+ state_manager.session.current_model,
176
+ text,
177
+ state_manager,
178
+ tool_callback=tool_callback_with_state,
179
+ )
180
+ if output:
181
+ await ui.agent(res.result.output)
182
+ except CancelledError:
183
+ await ui.muted("Request cancelled")
184
+ except UserAbortError:
185
+ await ui.muted("Operation aborted.")
186
+ except UnexpectedModelBehavior as e:
187
+ error_message = str(e)
188
+ await ui.muted(error_message)
189
+ patch_tool_messages(error_message, state_manager)
190
+ except Exception as e:
191
+ # Wrap unexpected exceptions in AgentError for better tracking
192
+ agent_error = AgentError(f"Agent processing failed: {str(e)}")
193
+ agent_error.__cause__ = e # Preserve the original exception chain
194
+ await ui.error(str(e))
195
+ finally:
196
+ await ui.spinner(False, state_manager.session.spinner, state_manager)
197
+ state_manager.session.current_task = None
198
+
199
+ # Force refresh of the multiline input prompt to restore placeholder
200
+ if "multiline" in state_manager.session.input_sessions:
201
+ await run_in_terminal(
202
+ lambda: state_manager.session.input_sessions["multiline"].app.invalidate()
203
+ )
204
+
205
+
206
+ async def repl(state_manager: StateManager):
207
+ action = None
208
+
209
+ # Hacky startup message
210
+ await ui.warning("⚠️ tunaCode v0.1 - BETA SOFTWARE")
211
+ await ui.muted("→ All changes will be made on a new branch for safety")
212
+ await ui.muted("→ Use with caution! This tool can modify your codebase")
213
+ await ui.muted(f"→ Model loaded: {state_manager.session.current_model}")
214
+ await ui.line()
215
+ await ui.success("ready to hack...")
216
+ await ui.line()
217
+
218
+ instance = agent.get_or_create_agent(state_manager.session.current_model, state_manager)
219
+
220
+ async with instance.run_mcp_servers():
221
+ while True:
222
+ try:
223
+ line = await ui.multiline_input(state_manager, _command_registry)
224
+ except (EOFError, KeyboardInterrupt):
225
+ break
226
+
227
+ if not line:
228
+ continue
229
+
230
+ if line.lower() in ["exit", "quit"]:
231
+ break
232
+
233
+ if line.startswith("/"):
234
+ action = await _handle_command(line, state_manager)
235
+ if action == "restart":
236
+ break
237
+ continue
238
+
239
+ # Check if another task is already running
240
+ if state_manager.session.current_task and not state_manager.session.current_task.done():
241
+ await ui.muted("Agent is busy, press Ctrl+C to interrupt.")
242
+ continue
243
+
244
+ state_manager.session.current_task = get_app().create_background_task(
245
+ process_request(line, state_manager)
246
+ )
247
+
248
+ if action == "restart":
249
+ await repl(state_manager)
250
+ else:
251
+ await ui.info("Thanks for all the fish.")
@@ -0,0 +1 @@
1
+ # Config package
@@ -0,0 +1,26 @@
1
+ """
2
+ Module: sidekick.configuration.defaults
3
+
4
+ Default configuration values for the Sidekick CLI.
5
+ Provides baseline settings for user configuration including API keys,
6
+ tool settings, and MCP servers.
7
+ """
8
+
9
+ from tunacode.constants import GUIDE_FILE_NAME, TOOL_READ_FILE
10
+ from tunacode.types import UserConfig
11
+
12
+ DEFAULT_USER_CONFIG: UserConfig = {
13
+ "default_model": "openrouter:openai/gpt-4.1",
14
+ "env": {
15
+ "ANTHROPIC_API_KEY": "",
16
+ "GEMINI_API_KEY": "",
17
+ "OPENAI_API_KEY": "",
18
+ "OPENROUTER_API_KEY": "",
19
+ },
20
+ "settings": {
21
+ "max_retries": 10,
22
+ "tool_ignore": [TOOL_READ_FILE],
23
+ "guide_file": GUIDE_FILE_NAME,
24
+ },
25
+ "mcpServers": {},
26
+ }
@@ -0,0 +1,69 @@
1
+ """
2
+ Module: sidekick.configuration.models
3
+
4
+ Configuration model definitions and model registry for AI models.
5
+ Manages available AI models, their configurations, and pricing information.
6
+ """
7
+
8
+ from tunacode.types import ModelConfig, ModelName, ModelPricing
9
+ from tunacode.types import ModelRegistry as ModelRegistryType
10
+
11
+
12
+ class ModelRegistry:
13
+ def __init__(self):
14
+ self._models = self._load_default_models()
15
+
16
+ def _load_default_models(self) -> ModelRegistryType:
17
+ return {
18
+ "anthropic:claude-opus-4-20250514": ModelConfig(
19
+ pricing=ModelPricing(input=3.00, cached_input=1.50, output=15.00)
20
+ ),
21
+ "anthropic:claude-sonnet-4-20250514": ModelConfig(
22
+ pricing=ModelPricing(input=3.00, cached_input=1.50, output=15.00)
23
+ ),
24
+ "anthropic:claude-3-7-sonnet-latest": ModelConfig(
25
+ pricing=ModelPricing(input=3.00, cached_input=1.50, output=15.00)
26
+ ),
27
+ "google-gla:gemini-2.0-flash": ModelConfig(
28
+ pricing=ModelPricing(input=0.10, cached_input=0.025, output=0.40)
29
+ ),
30
+ "google-gla:gemini-2.5-flash-preview-05-20": ModelConfig(
31
+ pricing=ModelPricing(input=0.15, cached_input=0.025, output=0.60)
32
+ ),
33
+ "google-gla:gemini-2.5-pro-preview-05-06": ModelConfig(
34
+ pricing=ModelPricing(input=1.25, cached_input=0.025, output=10.00)
35
+ ),
36
+ "openai:gpt-4.1": ModelConfig(
37
+ pricing=ModelPricing(input=2.00, cached_input=0.50, output=8.00)
38
+ ),
39
+ "openai:gpt-4.1-mini": ModelConfig(
40
+ pricing=ModelPricing(input=0.40, cached_input=0.10, output=1.60)
41
+ ),
42
+ "openai:gpt-4.1-nano": ModelConfig(
43
+ pricing=ModelPricing(input=0.10, cached_input=0.025, output=0.40)
44
+ ),
45
+ "openai:gpt-4o": ModelConfig(
46
+ pricing=ModelPricing(input=2.50, cached_input=1.25, output=10.00)
47
+ ),
48
+ "openai:o3": ModelConfig(
49
+ pricing=ModelPricing(input=10.00, cached_input=2.50, output=40.00)
50
+ ),
51
+ "openai:o3-mini": ModelConfig(
52
+ pricing=ModelPricing(input=1.10, cached_input=0.55, output=4.40)
53
+ ),
54
+ "openrouter:mistralai/devstral-small": ModelConfig(
55
+ pricing=ModelPricing(input=0.0, cached_input=0.0, output=0.0)
56
+ ),
57
+ "openrouter:openai/gpt-4.1": ModelConfig(
58
+ pricing=ModelPricing(input=2.00, cached_input=0.50, output=8.00)
59
+ ),
60
+ }
61
+
62
+ def get_model(self, name: ModelName) -> ModelConfig:
63
+ return self._models.get(name)
64
+
65
+ def list_models(self) -> ModelRegistryType:
66
+ return self._models.copy()
67
+
68
+ def list_model_ids(self) -> list[ModelName]:
69
+ return list(self._models.keys())
@@ -0,0 +1,32 @@
1
+ """
2
+ Module: sidekick.configuration.settings
3
+
4
+ Application settings management for the Sidekick CLI.
5
+ Manages application paths, tool configurations, and runtime settings.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from tunacode.constants import (APP_NAME, APP_VERSION, CONFIG_FILE_NAME, TOOL_READ_FILE,
11
+ TOOL_RUN_COMMAND, TOOL_UPDATE_FILE, TOOL_WRITE_FILE)
12
+ from tunacode.types import ConfigFile, ConfigPath, ToolName
13
+
14
+
15
+ class PathConfig:
16
+ def __init__(self):
17
+ self.config_dir: ConfigPath = Path.home() / ".config"
18
+ self.config_file: ConfigFile = self.config_dir / CONFIG_FILE_NAME
19
+
20
+
21
+ class ApplicationSettings:
22
+ def __init__(self):
23
+ self.version = APP_VERSION
24
+ self.name = APP_NAME
25
+ self.guide_file = f"{self.name.upper()}.md"
26
+ self.paths = PathConfig()
27
+ self.internal_tools: list[ToolName] = [
28
+ TOOL_READ_FILE,
29
+ TOOL_RUN_COMMAND,
30
+ TOOL_UPDATE_FILE,
31
+ TOOL_WRITE_FILE,
32
+ ]
tunacode/constants.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ Module: tunacode.constants
3
+
4
+ Global constants and configuration values for the TunaCode CLI application.
5
+ Centralizes all magic strings, UI text, error messages, and application constants.
6
+ """
7
+
8
+ # Application info
9
+ APP_NAME = "TunaCode"
10
+ APP_VERSION = "0.5.1"
11
+
12
+ # File patterns
13
+ GUIDE_FILE_PATTERN = "{name}.md"
14
+ GUIDE_FILE_NAME = "TUNACODE.md"
15
+ ENV_FILE = ".env"
16
+ CONFIG_FILE_NAME = "tunacode.json"
17
+
18
+ # Default limits
19
+ MAX_FILE_SIZE = 100 * 1024 # 100KB
20
+ MAX_COMMAND_OUTPUT = 5000 # 5000 chars
21
+
22
+ # Command output processing
23
+ COMMAND_OUTPUT_THRESHOLD = 3500 # Length threshold for truncation
24
+ COMMAND_OUTPUT_START_INDEX = 2500 # Where to start showing content
25
+ COMMAND_OUTPUT_END_SIZE = 1000 # How much to show from the end
26
+
27
+ # Tool names
28
+ TOOL_READ_FILE = "read_file"
29
+ TOOL_WRITE_FILE = "write_file"
30
+ TOOL_UPDATE_FILE = "update_file"
31
+ TOOL_RUN_COMMAND = "run_command"
32
+
33
+ # Commands
34
+ CMD_HELP = "/help"
35
+ CMD_CLEAR = "/clear"
36
+ CMD_DUMP = "/dump"
37
+ CMD_YOLO = "/yolo"
38
+ CMD_UNDO = "/undo"
39
+ CMD_COMPACT = "/compact"
40
+ CMD_MODEL = "/model"
41
+ CMD_EXIT = "exit"
42
+ CMD_QUIT = "quit"
43
+
44
+ # Command descriptions
45
+ DESC_HELP = "Show this help message"
46
+ DESC_CLEAR = "Clear the conversation history"
47
+ DESC_DUMP = "Show the current conversation history"
48
+ DESC_YOLO = "Toggle confirmation prompts on/off"
49
+ DESC_UNDO = "Undo the last file change"
50
+ DESC_COMPACT = "Summarize the conversation context"
51
+ DESC_MODEL = "List available models"
52
+ DESC_MODEL_SWITCH = "Switch to a specific model"
53
+ DESC_MODEL_DEFAULT = "Set a model as the default"
54
+ DESC_EXIT = "Exit the application"
55
+
56
+ # Command Configuration
57
+ COMMAND_PREFIX = "/"
58
+ COMMAND_CATEGORIES = {
59
+ "state": ["yolo", "undo"],
60
+ "debug": ["dump", "compact"],
61
+ "ui": ["clear", "help"],
62
+ "config": ["model"],
63
+ }
64
+
65
+ # System paths
66
+ SIDEKICK_HOME_DIR = ".tunacode"
67
+ SESSIONS_SUBDIR = "sessions"
68
+ DEVICE_ID_FILE = "device_id"
69
+
70
+ # UI colors - Modern sleek color scheme
71
+ UI_COLORS = {
72
+ "primary": "#00d7ff", # Bright cyan
73
+ "secondary": "#64748b", # Slate gray
74
+ "accent": "#7c3aed", # Purple accent
75
+ "success": "#10b981", # Emerald green
76
+ "warning": "#f59e0b", # Amber
77
+ "error": "#ef4444", # Red
78
+ "muted": "#94a3b8", # Light slate
79
+ "file_ref": "#00d7ff", # Bright cyan
80
+ "background": "#0f172a", # Dark slate
81
+ "border": "#334155", # Slate border
82
+ }
83
+
84
+ # UI text and formatting
85
+ UI_PROMPT_PREFIX = "❯ "
86
+ UI_THINKING_MESSAGE = "[bold #00d7ff]● Thinking...[/bold #00d7ff]"
87
+ UI_DARKGREY_OPEN = "<darkgrey>"
88
+ UI_DARKGREY_CLOSE = "</darkgrey>"
89
+ UI_BOLD_OPEN = "<bold>"
90
+ UI_BOLD_CLOSE = "</bold>"
91
+ UI_KEY_ENTER = "Enter"
92
+ UI_KEY_ESC_ENTER = "Esc + Enter"
93
+
94
+ # Panel titles
95
+ PANEL_ERROR = "Error"
96
+ PANEL_MESSAGE_HISTORY = "Message History"
97
+ PANEL_MODELS = "Models"
98
+ PANEL_AVAILABLE_COMMANDS = "Available Commands"
99
+
100
+ # Error messages
101
+ ERROR_PROVIDER_EMPTY = "Provider number cannot be empty"
102
+ ERROR_INVALID_PROVIDER = "Invalid provider number"
103
+ ERROR_FILE_NOT_FOUND = "Error: File not found at '{filepath}'."
104
+ ERROR_FILE_TOO_LARGE = "Error: File '{filepath}' is too large (> 100KB)."
105
+ ERROR_FILE_DECODE = "Error reading file '{filepath}': Could not decode using UTF-8."
106
+ ERROR_FILE_DECODE_DETAILS = "It might be a binary file or use a different encoding. {error}"
107
+ ERROR_COMMAND_NOT_FOUND = "Error: Command not found or failed to execute:"
108
+ ERROR_COMMAND_EXECUTION = (
109
+ "Error: Command not found or failed to execute: {command}. Details: {error}"
110
+ )
111
+ ERROR_UNDO_INIT = "Error initializing undo system: {e}"
112
+
113
+ # Command output messages
114
+ CMD_OUTPUT_NO_OUTPUT = "No output."
115
+ CMD_OUTPUT_NO_ERRORS = "No errors."
116
+ CMD_OUTPUT_FORMAT = "STDOUT:\n{output}\n\nSTDERR:\n{error}"
117
+ CMD_OUTPUT_TRUNCATED = "\n...\n[truncated]\n...\n"
118
+
119
+ # Undo system messages
120
+ UNDO_DISABLED_HOME = "Undo system disabled, running from home directory"
121
+ UNDO_DISABLED_NO_GIT = "⚠️ Not in a git repository - undo functionality will be limited"
122
+ UNDO_INITIAL_COMMIT = "Initial commit for tunacode undo history"
123
+ UNDO_GIT_TIMEOUT = "Git initialization timed out"
124
+
125
+ # Log/status messages
126
+ MSG_UPDATE_AVAILABLE = "Update available: v{latest_version}"
127
+ MSG_UPDATE_INSTRUCTION = "Exit, and run: [bold]pip install --upgrade tunacode-cli"
128
+ MSG_VERSION_DISPLAY = "TunaCode CLI {version}"
129
+ MSG_FILE_SIZE_LIMIT = " Please specify a smaller file or use other tools to process it."
tunacode/context.py ADDED
@@ -0,0 +1,83 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ from tunacode.utils.system import list_cwd
8
+ from tunacode.utils.ripgrep import ripgrep
9
+
10
+
11
+ async def get_git_status() -> Dict[str, object]:
12
+ """Return git branch and dirty state information."""
13
+ try:
14
+ result = subprocess.run(
15
+ ["git", "status", "--porcelain", "--branch"],
16
+ capture_output=True,
17
+ text=True,
18
+ check=True,
19
+ timeout=5,
20
+ )
21
+ lines = result.stdout.splitlines()
22
+ branch_line = lines[0][2:] if lines else ""
23
+ branch = branch_line.split("...")[0]
24
+ ahead = behind = 0
25
+ if "[" in branch_line and "]" in branch_line:
26
+ bracket = branch_line.split("[", 1)[1].split("]", 1)[0]
27
+ for part in bracket.split(","):
28
+ if "ahead" in part:
29
+ ahead = int(part.split("ahead")[1].strip().strip(" ]"))
30
+ if "behind" in part:
31
+ behind = int(part.split("behind")[1].strip().strip(" ]"))
32
+ dirty = any(line for line in lines[1:])
33
+ return {"branch": branch, "ahead": ahead, "behind": behind, "dirty": dirty}
34
+ except Exception:
35
+ return {}
36
+
37
+
38
+ async def get_directory_structure(max_depth: int = 3) -> str:
39
+ """Return a simple directory tree string."""
40
+ files = list_cwd(max_depth=max_depth)
41
+ lines: List[str] = []
42
+ for path in files:
43
+ depth = path.count("/")
44
+ indent = " " * depth
45
+ name = path.split("/")[-1]
46
+ lines.append(f"{indent}{name}")
47
+ return "\n".join(lines)
48
+
49
+
50
+ async def get_code_style() -> str:
51
+ """Concatenate contents of all TUNACODE.md files up the directory tree."""
52
+ parts: List[str] = []
53
+ current = Path.cwd()
54
+ while True:
55
+ file = current / "TUNACODE.md"
56
+ if file.exists():
57
+ try:
58
+ parts.append(file.read_text(encoding="utf-8"))
59
+ except Exception:
60
+ pass
61
+ if current == current.parent:
62
+ break
63
+ current = current.parent
64
+ return "\n".join(parts)
65
+
66
+
67
+ async def get_claude_files() -> List[str]:
68
+ """Return a list of additional TUNACODE.md files in the repo."""
69
+ return ripgrep("TUNACODE.md", ".")
70
+
71
+
72
+ async def get_context() -> Dict[str, object]:
73
+ """Gather repository context."""
74
+ git = await get_git_status()
75
+ directory = await get_directory_structure()
76
+ style = await get_code_style()
77
+ claude_files = await get_claude_files()
78
+ return {
79
+ "git": git,
80
+ "directory": directory,
81
+ "codeStyle": style,
82
+ "claudeFiles": claude_files,
83
+ }
File without changes
File without changes