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.
- tunacode/__init__.py +0 -0
- tunacode/cli/__init__.py +4 -0
- tunacode/cli/commands.py +632 -0
- tunacode/cli/main.py +47 -0
- tunacode/cli/repl.py +251 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +26 -0
- tunacode/configuration/models.py +69 -0
- tunacode/configuration/settings.py +32 -0
- tunacode/constants.py +129 -0
- tunacode/context.py +83 -0
- tunacode/core/__init__.py +0 -0
- tunacode/core/agents/__init__.py +0 -0
- tunacode/core/agents/main.py +119 -0
- tunacode/core/setup/__init__.py +17 -0
- tunacode/core/setup/agent_setup.py +41 -0
- tunacode/core/setup/base.py +37 -0
- tunacode/core/setup/config_setup.py +179 -0
- tunacode/core/setup/coordinator.py +45 -0
- tunacode/core/setup/environment_setup.py +62 -0
- tunacode/core/setup/git_safety_setup.py +188 -0
- tunacode/core/setup/undo_setup.py +32 -0
- tunacode/core/state.py +43 -0
- tunacode/core/tool_handler.py +57 -0
- tunacode/exceptions.py +105 -0
- tunacode/prompts/system.txt +71 -0
- tunacode/py.typed +0 -0
- tunacode/services/__init__.py +1 -0
- tunacode/services/mcp.py +86 -0
- tunacode/services/undo_service.py +244 -0
- tunacode/setup.py +50 -0
- tunacode/tools/__init__.py +0 -0
- tunacode/tools/base.py +244 -0
- tunacode/tools/read_file.py +89 -0
- tunacode/tools/run_command.py +107 -0
- tunacode/tools/update_file.py +117 -0
- tunacode/tools/write_file.py +82 -0
- tunacode/types.py +259 -0
- tunacode/ui/__init__.py +1 -0
- tunacode/ui/completers.py +129 -0
- tunacode/ui/console.py +74 -0
- tunacode/ui/constants.py +16 -0
- tunacode/ui/decorators.py +59 -0
- tunacode/ui/input.py +95 -0
- tunacode/ui/keybindings.py +27 -0
- tunacode/ui/lexers.py +46 -0
- tunacode/ui/output.py +109 -0
- tunacode/ui/panels.py +156 -0
- tunacode/ui/prompt_manager.py +117 -0
- tunacode/ui/tool_ui.py +187 -0
- tunacode/ui/validators.py +23 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/bm25.py +55 -0
- tunacode/utils/diff_utils.py +69 -0
- tunacode/utils/file_utils.py +41 -0
- tunacode/utils/ripgrep.py +17 -0
- tunacode/utils/system.py +336 -0
- tunacode/utils/text_utils.py +87 -0
- tunacode/utils/user_configuration.py +54 -0
- tunacode_cli-0.0.1.dist-info/METADATA +242 -0
- tunacode_cli-0.0.1.dist-info/RECORD +65 -0
- tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
- tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- 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
|