tunacode-cli 0.0.47__py3-none-any.whl → 0.0.49__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.
- api/auth.py +13 -0
- api/users.py +8 -0
- tunacode/__init__.py +4 -0
- tunacode/cli/repl.py +28 -2
- tunacode/constants.py +2 -1
- tunacode/core/agents/main.py +33 -84
- tunacode/core/logging/__init__.py +29 -0
- tunacode/core/logging/config.py +28 -0
- tunacode/core/logging/formatters.py +48 -0
- tunacode/core/logging/handlers.py +83 -0
- tunacode/core/logging/logger.py +8 -0
- tunacode/core/recursive/hierarchy.py +2 -1
- tunacode/core/state.py +4 -0
- tunacode/tools/base.py +7 -1
- tunacode/types.py +1 -1
- tunacode/ui/completers.py +2 -2
- tunacode/ui/console.py +30 -9
- tunacode/ui/input.py +1 -1
- tunacode/ui/keybindings.py +47 -6
- tunacode/ui/logging_compat.py +44 -0
- tunacode/ui/output.py +7 -6
- tunacode/ui/panels.py +28 -10
- tunacode/utils/security.py +3 -2
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/METADATA +2 -2
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/RECORD +29 -21
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/top_level.txt +1 -0
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.47.dist-info → tunacode_cli-0.0.49.dist-info}/licenses/LICENSE +0 -0
api/auth.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def authenticate(username, password):
|
|
5
|
+
# TODO: Add password hashing
|
|
6
|
+
if username == "admin" and password == "admin":
|
|
7
|
+
return generate_token(username)
|
|
8
|
+
return None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_token(username):
|
|
12
|
+
# TODO: Add expiration
|
|
13
|
+
return jwt.encode({"user": username}, "secret")
|
api/users.py
ADDED
tunacode/__init__.py
CHANGED
tunacode/cli/repl.py
CHANGED
|
@@ -90,6 +90,12 @@ def _parse_args(args) -> ToolArgs:
|
|
|
90
90
|
|
|
91
91
|
async def _tool_handler(part, state_manager: StateManager):
|
|
92
92
|
"""Handle tool execution with separated business logic and UI."""
|
|
93
|
+
# Check for cancellation before tool execution (only if explicitly set to True)
|
|
94
|
+
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
95
|
+
if operation_cancelled is True:
|
|
96
|
+
logger.debug("Tool execution cancelled")
|
|
97
|
+
raise CancelledError("Operation was cancelled")
|
|
98
|
+
|
|
93
99
|
tool_handler = ToolHandler(state_manager)
|
|
94
100
|
|
|
95
101
|
if tool_handler.should_confirm(part.tool_name):
|
|
@@ -249,6 +255,12 @@ async def _display_agent_output(res, enable_streaming: bool) -> None:
|
|
|
249
255
|
async def process_request(text: str, state_manager: StateManager, output: bool = True):
|
|
250
256
|
"""Process input using the agent, handling cancellation safely."""
|
|
251
257
|
|
|
258
|
+
# Check for cancellation before starting (only if explicitly set to True)
|
|
259
|
+
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
260
|
+
if operation_cancelled is True:
|
|
261
|
+
logger.debug("Operation cancelled before processing started")
|
|
262
|
+
raise CancelledError("Operation was cancelled")
|
|
263
|
+
|
|
252
264
|
state_manager.session.spinner = await ui.spinner(
|
|
253
265
|
True, state_manager.session.spinner, state_manager
|
|
254
266
|
)
|
|
@@ -275,6 +287,12 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
275
287
|
await ui.error(str(e))
|
|
276
288
|
return
|
|
277
289
|
|
|
290
|
+
# Check for cancellation before proceeding with agent call (only if explicitly set to True)
|
|
291
|
+
operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
|
|
292
|
+
if operation_cancelled is True:
|
|
293
|
+
logger.debug("Operation cancelled before agent processing")
|
|
294
|
+
raise CancelledError("Operation was cancelled")
|
|
295
|
+
|
|
278
296
|
enable_streaming = state_manager.session.user_config.get("settings", {}).get(
|
|
279
297
|
"enable_streaming", True
|
|
280
298
|
)
|
|
@@ -338,13 +356,13 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
338
356
|
# Always show files in context after agent response
|
|
339
357
|
if state_manager.session.files_in_context:
|
|
340
358
|
filenames = [Path(f).name for f in sorted(state_manager.session.files_in_context)]
|
|
341
|
-
await ui.muted(f"
|
|
359
|
+
await ui.muted(f"Files in context: {', '.join(filenames)}")
|
|
342
360
|
|
|
343
361
|
# --- ERROR HANDLING ---
|
|
344
362
|
except CancelledError:
|
|
345
363
|
await ui.muted(MSG_REQUEST_CANCELLED)
|
|
346
364
|
except UserAbortError:
|
|
347
|
-
await ui.muted(
|
|
365
|
+
await ui.muted(MSG_OPERATION_ABORTED)
|
|
348
366
|
except UnexpectedModelBehavior as e:
|
|
349
367
|
error_message = str(e)
|
|
350
368
|
await ui.muted(error_message)
|
|
@@ -360,6 +378,9 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
|
|
|
360
378
|
finally:
|
|
361
379
|
await ui.spinner(False, state_manager.session.spinner, state_manager)
|
|
362
380
|
state_manager.session.current_task = None
|
|
381
|
+
# Reset cancellation flag when task completes (if attribute exists)
|
|
382
|
+
if hasattr(state_manager.session, "operation_cancelled"):
|
|
383
|
+
state_manager.session.operation_cancelled = False
|
|
363
384
|
|
|
364
385
|
if "multiline" in state_manager.session.input_sessions:
|
|
365
386
|
await run_in_terminal(
|
|
@@ -460,9 +481,14 @@ async def repl(state_manager: StateManager):
|
|
|
460
481
|
await ui.muted(MSG_AGENT_BUSY)
|
|
461
482
|
continue
|
|
462
483
|
|
|
484
|
+
# Reset cancellation flag for new operations (if attribute exists)
|
|
485
|
+
if hasattr(state_manager.session, "operation_cancelled"):
|
|
486
|
+
state_manager.session.operation_cancelled = False
|
|
487
|
+
|
|
463
488
|
state_manager.session.current_task = get_app().create_background_task(
|
|
464
489
|
process_request(line, state_manager)
|
|
465
490
|
)
|
|
491
|
+
await state_manager.session.current_task
|
|
466
492
|
|
|
467
493
|
state_manager.session.update_token_count()
|
|
468
494
|
context_display = get_context_window_display(
|
tunacode/constants.py
CHANGED
tunacode/core/agents/main.py
CHANGED
|
@@ -6,7 +6,6 @@ Handles agent creation, configuration, and request processing.
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
|
-
import logging
|
|
10
9
|
import os
|
|
11
10
|
import re
|
|
12
11
|
from datetime import datetime, timezone
|
|
@@ -15,6 +14,8 @@ from typing import Any, Iterator, List, Optional, Tuple
|
|
|
15
14
|
|
|
16
15
|
from pydantic_ai import Agent
|
|
17
16
|
|
|
17
|
+
from tunacode.core.logging.logger import get_logger
|
|
18
|
+
|
|
18
19
|
# Import streaming types with fallback for older versions
|
|
19
20
|
try:
|
|
20
21
|
from pydantic_ai.messages import (
|
|
@@ -30,7 +31,6 @@ except ImportError:
|
|
|
30
31
|
STREAMING_AVAILABLE = False
|
|
31
32
|
|
|
32
33
|
from tunacode.constants import READ_ONLY_TOOLS
|
|
33
|
-
from tunacode.core.recursive import RecursiveTaskExecutor
|
|
34
34
|
from tunacode.core.state import StateManager
|
|
35
35
|
from tunacode.core.token_usage.api_response_parser import ApiResponseParser
|
|
36
36
|
from tunacode.core.token_usage.cost_calculator import CostCalculator
|
|
@@ -60,7 +60,7 @@ from tunacode.types import (
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
# Configure logging
|
|
63
|
-
logger =
|
|
63
|
+
logger = get_logger(__name__)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class ToolBuffer:
|
|
@@ -528,12 +528,12 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
528
528
|
tunacode_content = tunacode_path.read_text(encoding="utf-8")
|
|
529
529
|
if tunacode_content.strip():
|
|
530
530
|
# Log that we found TUNACODE.md
|
|
531
|
-
|
|
531
|
+
logger.info("📄 TUNACODE.md located: Loading context...")
|
|
532
532
|
|
|
533
533
|
system_prompt += "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
|
|
534
534
|
else:
|
|
535
535
|
# Log that TUNACODE.md was not found
|
|
536
|
-
|
|
536
|
+
logger.info("📄 TUNACODE.md not found: Using default context")
|
|
537
537
|
except Exception as e:
|
|
538
538
|
# Log errors loading TUNACODE.md at debug level
|
|
539
539
|
logger.debug(f"Error loading TUNACODE.md: {e}")
|
|
@@ -547,9 +547,8 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
|
|
|
547
547
|
system_prompt += f'\n\n# Current Todo List\n\nYou have existing todos that need attention:\n\n{current_todos}\n\nRemember to check progress on these todos and update them as you work. Use todo("list") to see current status anytime.'
|
|
548
548
|
except Exception as e:
|
|
549
549
|
# Log error but don't fail agent creation
|
|
550
|
-
import sys
|
|
551
550
|
|
|
552
|
-
|
|
551
|
+
logger.warning(f"Warning: Failed to load todos: {e}")
|
|
553
552
|
|
|
554
553
|
state_manager.session.agents[model] = Agent(
|
|
555
554
|
model=model,
|
|
@@ -763,55 +762,6 @@ async def process_request(
|
|
|
763
762
|
"fallback_response", True
|
|
764
763
|
)
|
|
765
764
|
|
|
766
|
-
# Check if recursive execution is enabled
|
|
767
|
-
use_recursive = state_manager.session.user_config.get("settings", {}).get(
|
|
768
|
-
"use_recursive_execution", True
|
|
769
|
-
)
|
|
770
|
-
recursive_threshold = state_manager.session.user_config.get("settings", {}).get(
|
|
771
|
-
"recursive_complexity_threshold", 0.7
|
|
772
|
-
)
|
|
773
|
-
|
|
774
|
-
# Check if recursive execution should be used
|
|
775
|
-
if use_recursive and state_manager.session.current_recursion_depth == 0:
|
|
776
|
-
try:
|
|
777
|
-
# Initialize recursive executor
|
|
778
|
-
recursive_executor = RecursiveTaskExecutor(
|
|
779
|
-
state_manager=state_manager,
|
|
780
|
-
max_depth=state_manager.session.max_recursion_depth,
|
|
781
|
-
min_complexity_threshold=recursive_threshold,
|
|
782
|
-
default_iteration_budget=max_iterations,
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
# Analyze task complexity
|
|
786
|
-
complexity_result = await recursive_executor.decomposer.analyze_and_decompose(
|
|
787
|
-
message
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
complexity_result.should_decompose
|
|
792
|
-
and complexity_result.total_complexity >= recursive_threshold
|
|
793
|
-
):
|
|
794
|
-
if state_manager.session.show_thoughts:
|
|
795
|
-
from tunacode.ui import console as ui
|
|
796
|
-
|
|
797
|
-
await ui.muted(
|
|
798
|
-
f"\n🔄 RECURSIVE EXECUTION: Task complexity {complexity_result.total_complexity:.2f} >= {recursive_threshold}"
|
|
799
|
-
)
|
|
800
|
-
await ui.muted(f"Reasoning: {complexity_result.reasoning}")
|
|
801
|
-
await ui.muted(f"Subtasks: {len(complexity_result.subtasks)}")
|
|
802
|
-
|
|
803
|
-
# Execute recursively
|
|
804
|
-
success, result, error = await recursive_executor.execute_task(
|
|
805
|
-
request=message, parent_task_id=None, depth=0
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
# For now, fall back to normal execution
|
|
809
|
-
# TODO: Properly integrate recursive execution results
|
|
810
|
-
pass
|
|
811
|
-
except Exception as e:
|
|
812
|
-
logger.warning(f"Recursive execution failed, falling back to normal: {e}")
|
|
813
|
-
# Continue with normal execution
|
|
814
|
-
|
|
815
765
|
from tunacode.configuration.models import ModelRegistry
|
|
816
766
|
from tunacode.core.token_usage.usage_tracker import UsageTracker
|
|
817
767
|
|
|
@@ -910,28 +860,27 @@ async def process_request(
|
|
|
910
860
|
buffered_tasks = tool_buffer.flush()
|
|
911
861
|
start_time = time.time()
|
|
912
862
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
await ui.muted("=" * 60)
|
|
863
|
+
await ui.muted("\n" + "=" * 60)
|
|
864
|
+
await ui.muted(
|
|
865
|
+
f"🚀 FINAL BATCH: Executing {len(buffered_tasks)} buffered read-only tools"
|
|
866
|
+
)
|
|
867
|
+
await ui.muted("=" * 60)
|
|
919
868
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
869
|
+
for idx, (part, node) in enumerate(buffered_tasks, 1):
|
|
870
|
+
tool_desc = f" [{idx}] {part.tool_name}"
|
|
871
|
+
if hasattr(part, "args") and isinstance(part.args, dict):
|
|
872
|
+
if part.tool_name == "read_file" and "file_path" in part.args:
|
|
873
|
+
tool_desc += f" → {part.args['file_path']}"
|
|
874
|
+
elif part.tool_name == "grep" and "pattern" in part.args:
|
|
875
|
+
tool_desc += f" → pattern: '{part.args['pattern']}'"
|
|
876
|
+
if "include_files" in part.args:
|
|
877
|
+
tool_desc += f", files: '{part.args['include_files']}'"
|
|
878
|
+
elif part.tool_name == "list_dir" and "directory" in part.args:
|
|
879
|
+
tool_desc += f" → {part.args['directory']}"
|
|
880
|
+
elif part.tool_name == "glob" and "pattern" in part.args:
|
|
881
|
+
tool_desc += f" → pattern: '{part.args['pattern']}'"
|
|
882
|
+
await ui.muted(tool_desc)
|
|
883
|
+
await ui.muted("=" * 60)
|
|
935
884
|
|
|
936
885
|
await execute_tools_parallel(buffered_tasks, tool_callback)
|
|
937
886
|
|
|
@@ -939,11 +888,10 @@ async def process_request(
|
|
|
939
888
|
sequential_estimate = len(buffered_tasks) * 100
|
|
940
889
|
speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
|
|
941
890
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
)
|
|
891
|
+
await ui.muted(
|
|
892
|
+
f"✅ Final batch completed in {elapsed_time:.0f}ms "
|
|
893
|
+
f"(~{speedup:.1f}x faster than sequential)\n"
|
|
894
|
+
)
|
|
947
895
|
|
|
948
896
|
# If we need to add a fallback response, create a wrapper
|
|
949
897
|
if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
|
|
@@ -1089,6 +1037,7 @@ async def process_request(
|
|
|
1089
1037
|
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
1090
1038
|
)
|
|
1091
1039
|
|
|
1092
|
-
|
|
1040
|
+
return AgentRunWithState(agent_run)
|
|
1093
1041
|
except asyncio.CancelledError:
|
|
1094
|
-
raise UserAbortError
|
|
1042
|
+
# When task is cancelled, raise UserAbortError instead
|
|
1043
|
+
raise UserAbortError("Operation was cancelled by user")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# Custom log level: THOUGHT
|
|
4
|
+
THOUGHT = 25
|
|
5
|
+
logging.addLevelName(THOUGHT, "THOUGHT")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def thought(self, message, *args, **kwargs):
|
|
9
|
+
if self.isEnabledFor(THOUGHT):
|
|
10
|
+
self._log(THOUGHT, message, args, **kwargs)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logging.Logger.thought = thought
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# RichHandler for UI output (stub, real implementation in handlers.py)
|
|
17
|
+
class RichHandler(logging.Handler):
|
|
18
|
+
def emit(self, record):
|
|
19
|
+
# Actual implementation in handlers.py
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def setup_logging(config_path=None):
|
|
24
|
+
"""
|
|
25
|
+
Set up logging configuration from YAML file.
|
|
26
|
+
"""
|
|
27
|
+
from .config import LogConfig
|
|
28
|
+
|
|
29
|
+
LogConfig.load(config_path)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONFIG_PATH = os.path.join(
|
|
8
|
+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "config", "logging.yaml"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogConfig:
|
|
13
|
+
@staticmethod
|
|
14
|
+
def load(config_path=None):
|
|
15
|
+
"""
|
|
16
|
+
Load logging configuration from YAML file and apply it.
|
|
17
|
+
"""
|
|
18
|
+
path = config_path or DEFAULT_CONFIG_PATH
|
|
19
|
+
if not os.path.exists(path):
|
|
20
|
+
raise FileNotFoundError(f"Logging config file not found: {path}")
|
|
21
|
+
with open(path, "r") as f:
|
|
22
|
+
config = yaml.safe_load(f)
|
|
23
|
+
logging_config = config.get("logging", config)
|
|
24
|
+
try:
|
|
25
|
+
logging.config.dictConfig(logging_config)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"Failed to configure logging: {e}")
|
|
28
|
+
logging.basicConfig(level=logging.INFO)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SimpleFormatter(logging.Formatter):
|
|
5
|
+
"""
|
|
6
|
+
Simple formatter for UI output.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__("[%(levelname)s] %(message)s")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DetailedFormatter(logging.Formatter):
|
|
14
|
+
"""
|
|
15
|
+
Detailed formatter for backend text logs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__("[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] - %(message)s")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from pythonjsonlogger import jsonlogger
|
|
24
|
+
|
|
25
|
+
class JSONFormatter(jsonlogger.JsonFormatter):
|
|
26
|
+
"""
|
|
27
|
+
JSON formatter for structured logs.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__("%(asctime)s %(name)s %(levelname)s %(message)s")
|
|
32
|
+
except ImportError:
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
class JSONFormatter(logging.Formatter):
|
|
36
|
+
"""
|
|
37
|
+
Fallback JSON formatter if pythonjsonlogger is not installed.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def format(self, record):
|
|
41
|
+
log_entry = {
|
|
42
|
+
"timestamp": self.formatTime(record),
|
|
43
|
+
"level": record.levelname,
|
|
44
|
+
"name": record.name,
|
|
45
|
+
"line": record.lineno,
|
|
46
|
+
"message": record.getMessage(),
|
|
47
|
+
}
|
|
48
|
+
return json.dumps(log_entry)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
# Global context for streaming state
|
|
8
|
+
_streaming_context = {"just_finished": False}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RichHandler(logging.Handler):
|
|
12
|
+
"""
|
|
13
|
+
Handler that outputs logs to the console using rich formatting.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
level_icons = {
|
|
17
|
+
"INFO": "",
|
|
18
|
+
"WARNING": "⚠️",
|
|
19
|
+
"ERROR": "❌",
|
|
20
|
+
"CRITICAL": "🚨",
|
|
21
|
+
"THOUGHT": "🤔",
|
|
22
|
+
"DEBUG": "",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __init__(self, level=logging.NOTSET):
|
|
26
|
+
super().__init__(level)
|
|
27
|
+
self.console = Console()
|
|
28
|
+
|
|
29
|
+
def emit(self, record):
|
|
30
|
+
try:
|
|
31
|
+
icon = self.level_icons.get(record.levelname, "")
|
|
32
|
+
timestamp = self.formatTime(record)
|
|
33
|
+
msg = self.format(record)
|
|
34
|
+
if icon:
|
|
35
|
+
output = f"[{timestamp}] {icon} {msg}"
|
|
36
|
+
else:
|
|
37
|
+
output = f"[{timestamp}] {msg}"
|
|
38
|
+
|
|
39
|
+
# Check if we just finished streaming to avoid extra newlines
|
|
40
|
+
just_finished_streaming = _streaming_context.get("just_finished", False)
|
|
41
|
+
if just_finished_streaming:
|
|
42
|
+
_streaming_context["just_finished"] = False # Reset after use
|
|
43
|
+
# Don't add extra newline when transitioning from streaming
|
|
44
|
+
self.console.print(Text(output), end="\n")
|
|
45
|
+
else:
|
|
46
|
+
self.console.print(Text(output))
|
|
47
|
+
except Exception:
|
|
48
|
+
self.handleError(record)
|
|
49
|
+
|
|
50
|
+
def formatTime(self, record, datefmt=None):
|
|
51
|
+
from datetime import datetime
|
|
52
|
+
|
|
53
|
+
ct = datetime.fromtimestamp(record.created)
|
|
54
|
+
if datefmt:
|
|
55
|
+
return ct.strftime(datefmt)
|
|
56
|
+
return ct.strftime("%Y-%m-%d %H:%M:%S")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StructuredFileHandler(logging.FileHandler):
|
|
60
|
+
"""
|
|
61
|
+
Handler that outputs logs as structured JSON lines.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def emit(self, record):
|
|
65
|
+
try:
|
|
66
|
+
log_entry = {
|
|
67
|
+
"timestamp": self.formatTime(record),
|
|
68
|
+
"level": record.levelname,
|
|
69
|
+
"name": record.name,
|
|
70
|
+
"line": record.lineno,
|
|
71
|
+
"message": record.getMessage(),
|
|
72
|
+
"extra_data": getattr(record, "extra", {}),
|
|
73
|
+
}
|
|
74
|
+
self.stream.write(json.dumps(log_entry) + "\n")
|
|
75
|
+
self.flush()
|
|
76
|
+
except Exception:
|
|
77
|
+
self.handleError(record)
|
|
78
|
+
|
|
79
|
+
def formatTime(self, record, datefmt=None):
|
|
80
|
+
from datetime import datetime, timezone
|
|
81
|
+
|
|
82
|
+
ct = datetime.fromtimestamp(record.created, tz=timezone.utc)
|
|
83
|
+
return ct.isoformat()
|
|
@@ -332,10 +332,11 @@ class TaskHierarchy:
|
|
|
332
332
|
"""Propagate context from one task to another.
|
|
333
333
|
|
|
334
334
|
Args:
|
|
335
|
-
from_task: Source task ID
|
|
335
|
+
from_task: Source task ID (unused, kept for API consistency)
|
|
336
336
|
to_task: Target task ID
|
|
337
337
|
context_update: Context to propagate
|
|
338
338
|
"""
|
|
339
|
+
_ = from_task # Unused but kept for API consistency
|
|
339
340
|
if to_task in self._execution_contexts:
|
|
340
341
|
self._execution_contexts[to_task].inherited_context.update(context_update)
|
|
341
342
|
|
tunacode/core/state.py
CHANGED
|
@@ -41,6 +41,10 @@ class SessionState:
|
|
|
41
41
|
input_sessions: InputSessions = field(default_factory=dict)
|
|
42
42
|
current_task: Optional[Any] = None
|
|
43
43
|
todos: list[TodoItem] = field(default_factory=list)
|
|
44
|
+
# ESC key tracking for double-press functionality
|
|
45
|
+
esc_press_count: int = 0
|
|
46
|
+
last_esc_time: Optional[float] = None
|
|
47
|
+
operation_cancelled: bool = False
|
|
44
48
|
# Enhanced tracking for thoughts display
|
|
45
49
|
files_in_context: set[str] = field(default_factory=set)
|
|
46
50
|
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
tunacode/tools/base.py
CHANGED
|
@@ -8,6 +8,7 @@ from abc import ABC, abstractmethod
|
|
|
8
8
|
|
|
9
9
|
from pydantic_ai.exceptions import ModelRetry
|
|
10
10
|
|
|
11
|
+
from tunacode.core.logging.logger import get_logger
|
|
11
12
|
from tunacode.exceptions import FileOperationError, ToolExecutionError
|
|
12
13
|
from tunacode.types import FilePath, ToolName, ToolResult, UILogger
|
|
13
14
|
|
|
@@ -22,6 +23,7 @@ class BaseTool(ABC):
|
|
|
22
23
|
ui_logger: UI logger instance for displaying messages
|
|
23
24
|
"""
|
|
24
25
|
self.ui = ui_logger
|
|
26
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
25
27
|
|
|
26
28
|
async def execute(self, *args, **kwargs) -> ToolResult:
|
|
27
29
|
"""Execute the tool with error handling and logging.
|
|
@@ -39,14 +41,17 @@ class BaseTool(ABC):
|
|
|
39
41
|
ToolExecutionError: Raised for all other errors with structured information
|
|
40
42
|
"""
|
|
41
43
|
try:
|
|
44
|
+
msg = f"{self.tool_name}({self._format_args(*args, **kwargs)})"
|
|
42
45
|
if self.ui:
|
|
43
|
-
await self.ui.info(
|
|
46
|
+
await self.ui.info(msg)
|
|
47
|
+
self.logger.info(msg)
|
|
44
48
|
result = await self._execute(*args, **kwargs)
|
|
45
49
|
return result
|
|
46
50
|
except ModelRetry as e:
|
|
47
51
|
# Log as warning and re-raise for pydantic-ai
|
|
48
52
|
if self.ui:
|
|
49
53
|
await self.ui.warning(str(e))
|
|
54
|
+
self.logger.warning(f"ModelRetry: {e}")
|
|
50
55
|
raise
|
|
51
56
|
except ToolExecutionError:
|
|
52
57
|
# Already properly formatted, just re-raise
|
|
@@ -90,6 +95,7 @@ class BaseTool(ABC):
|
|
|
90
95
|
err_msg = f"Error {self._get_error_context(*args, **kwargs)}: {error}"
|
|
91
96
|
if self.ui:
|
|
92
97
|
await self.ui.error(err_msg)
|
|
98
|
+
self.logger.error(err_msg)
|
|
93
99
|
|
|
94
100
|
# Raise proper exception instead of returning string
|
|
95
101
|
raise ToolExecutionError(tool_name=self.tool_name, message=str(error), original_error=error)
|
tunacode/types.py
CHANGED
|
@@ -13,7 +13,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Prot
|
|
|
13
13
|
# Try to import pydantic-ai types if available
|
|
14
14
|
try:
|
|
15
15
|
from pydantic_ai import Agent
|
|
16
|
-
from pydantic_ai.messages import ModelRequest,
|
|
16
|
+
from pydantic_ai.messages import ModelRequest, ToolReturnPart
|
|
17
17
|
|
|
18
18
|
PydanticAgent = Agent
|
|
19
19
|
MessagePart = Union[ToolReturnPart, Any]
|
tunacode/ui/completers.py
CHANGED
|
@@ -17,7 +17,7 @@ class CommandCompleter(Completer):
|
|
|
17
17
|
self.command_registry = command_registry
|
|
18
18
|
|
|
19
19
|
def get_completions(
|
|
20
|
-
self, document: Document,
|
|
20
|
+
self, document: Document, _complete_event: CompleteEvent
|
|
21
21
|
) -> Iterable[Completion]:
|
|
22
22
|
"""Get completions for slash commands."""
|
|
23
23
|
# Get the text before cursor
|
|
@@ -65,7 +65,7 @@ class FileReferenceCompleter(Completer):
|
|
|
65
65
|
"""Completer for @file references that provides file path suggestions."""
|
|
66
66
|
|
|
67
67
|
def get_completions(
|
|
68
|
-
self, document: Document,
|
|
68
|
+
self, document: Document, _complete_event: CompleteEvent
|
|
69
69
|
) -> Iterable[Completion]:
|
|
70
70
|
"""Get completions for @file references."""
|
|
71
71
|
# Get the word before cursor
|
tunacode/ui/console.py
CHANGED
|
@@ -9,29 +9,26 @@ from rich.markdown import Markdown
|
|
|
9
9
|
# Import and re-export all functions from specialized modules
|
|
10
10
|
from .input import formatted_text, input, multiline_input
|
|
11
11
|
from .keybindings import create_key_bindings
|
|
12
|
+
|
|
13
|
+
# Unified UI logger compatibility layer
|
|
14
|
+
from .logging_compat import ui_logger
|
|
12
15
|
from .output import (
|
|
13
16
|
banner,
|
|
14
17
|
clear,
|
|
15
|
-
info,
|
|
16
18
|
line,
|
|
17
19
|
muted,
|
|
18
20
|
print,
|
|
19
21
|
spinner,
|
|
20
|
-
success,
|
|
21
22
|
sync_print,
|
|
22
23
|
update_available,
|
|
23
24
|
usage,
|
|
24
25
|
version,
|
|
25
|
-
warning,
|
|
26
26
|
)
|
|
27
|
-
|
|
28
|
-
# Patch banner to use sync fast version
|
|
29
27
|
from .panels import (
|
|
30
28
|
StreamingAgentPanel,
|
|
31
29
|
agent,
|
|
32
30
|
agent_streaming,
|
|
33
31
|
dump_messages,
|
|
34
|
-
error,
|
|
35
32
|
help,
|
|
36
33
|
models,
|
|
37
34
|
panel,
|
|
@@ -42,6 +39,28 @@ from .panels import (
|
|
|
42
39
|
from .prompt_manager import PromptConfig, PromptManager
|
|
43
40
|
from .validators import ModelValidator
|
|
44
41
|
|
|
42
|
+
|
|
43
|
+
# Async wrappers for UI logging
|
|
44
|
+
async def info(message: str) -> None:
|
|
45
|
+
await ui_logger.info(message)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def warning(message: str) -> None:
|
|
49
|
+
await ui_logger.warning(message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def error(message: str) -> None:
|
|
53
|
+
await ui_logger.error(message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def debug(message: str) -> None:
|
|
57
|
+
await ui_logger.debug(message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def success(message: str) -> None:
|
|
61
|
+
await ui_logger.success(message)
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
# Create console object for backward compatibility
|
|
46
65
|
console = RichConsole(force_terminal=True, legacy_windows=False)
|
|
47
66
|
|
|
@@ -68,22 +87,24 @@ __all__ = [
|
|
|
68
87
|
"banner",
|
|
69
88
|
"clear",
|
|
70
89
|
"console",
|
|
71
|
-
"info",
|
|
72
90
|
"line",
|
|
73
91
|
"muted",
|
|
74
92
|
"print",
|
|
75
93
|
"spinner",
|
|
76
|
-
"success",
|
|
77
94
|
"sync_print",
|
|
78
95
|
"update_available",
|
|
79
96
|
"usage",
|
|
80
97
|
"version",
|
|
98
|
+
# Unified logging wrappers
|
|
99
|
+
"info",
|
|
81
100
|
"warning",
|
|
101
|
+
"error",
|
|
102
|
+
"debug",
|
|
103
|
+
"success",
|
|
82
104
|
# From panels module
|
|
83
105
|
"agent",
|
|
84
106
|
"agent_streaming",
|
|
85
107
|
"dump_messages",
|
|
86
|
-
"error",
|
|
87
108
|
"help",
|
|
88
109
|
"models",
|
|
89
110
|
"panel",
|
tunacode/ui/input.py
CHANGED
|
@@ -81,7 +81,7 @@ async def multiline_input(
|
|
|
81
81
|
"<darkgrey>"
|
|
82
82
|
"<bold>Enter</bold> to submit • "
|
|
83
83
|
"<bold>Esc + Enter</bold> for new line • "
|
|
84
|
-
"<bold>Esc</bold> to cancel • "
|
|
84
|
+
"<bold>Esc twice</bold> to cancel • "
|
|
85
85
|
"<bold>/help</bold> for commands"
|
|
86
86
|
"</darkgrey>"
|
|
87
87
|
)
|
tunacode/ui/keybindings.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Key binding handlers for TunaCode UI."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import time
|
|
4
5
|
|
|
6
|
+
from prompt_toolkit.application import run_in_terminal
|
|
5
7
|
from prompt_toolkit.key_binding import KeyBindings
|
|
6
8
|
|
|
7
9
|
from ..core.state import StateManager
|
|
@@ -30,12 +32,51 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
|
|
|
30
32
|
|
|
31
33
|
@kb.add("escape")
|
|
32
34
|
def _escape(event):
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
"""Handle ESC key with double-press logic: first press warns, second cancels."""
|
|
36
|
+
if not state_manager:
|
|
37
|
+
logger.debug("Escape key pressed without state manager")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
current_time = time.time()
|
|
41
|
+
session = state_manager.session
|
|
42
|
+
|
|
43
|
+
# Reset counter if too much time has passed (3 seconds timeout)
|
|
44
|
+
if session.last_esc_time and (current_time - session.last_esc_time) > 3.0:
|
|
45
|
+
session.esc_press_count = 0
|
|
46
|
+
|
|
47
|
+
session.esc_press_count += 1
|
|
48
|
+
session.last_esc_time = current_time
|
|
49
|
+
|
|
50
|
+
logger.debug(f"ESC key pressed: count={session.esc_press_count}, time={current_time}")
|
|
51
|
+
|
|
52
|
+
if session.esc_press_count == 1:
|
|
53
|
+
# First ESC press - show warning message
|
|
54
|
+
from ..ui.output import warning
|
|
55
|
+
|
|
56
|
+
run_in_terminal(lambda: warning("Hit ESC again within 3 seconds to cancel operation"))
|
|
57
|
+
logger.debug("First ESC press - showing warning")
|
|
38
58
|
else:
|
|
39
|
-
|
|
59
|
+
# Second ESC press - cancel operation
|
|
60
|
+
session.esc_press_count = 0 # Reset counter
|
|
61
|
+
logger.debug("Second ESC press - initiating cancellation")
|
|
62
|
+
|
|
63
|
+
# Mark the session as being cancelled to prevent new operations
|
|
64
|
+
session.operation_cancelled = True
|
|
65
|
+
|
|
66
|
+
current_task = session.current_task
|
|
67
|
+
if current_task and not current_task.done():
|
|
68
|
+
logger.debug(f"Cancelling current task: {current_task}")
|
|
69
|
+
try:
|
|
70
|
+
current_task.cancel()
|
|
71
|
+
logger.debug("Task cancellation initiated successfully")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Failed to cancel task: {e}")
|
|
74
|
+
else:
|
|
75
|
+
logger.debug(f"No active task to cancel: current_task={current_task}")
|
|
76
|
+
|
|
77
|
+
# Force exit the current input by raising KeyboardInterrupt
|
|
78
|
+
# This will be caught by the prompt manager and converted to UserAbortError
|
|
79
|
+
logger.debug("Raising KeyboardInterrupt to abort current operation")
|
|
80
|
+
raise KeyboardInterrupt()
|
|
40
81
|
|
|
41
82
|
return kb
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UILogger compatibility layer for unified logging.
|
|
3
|
+
|
|
4
|
+
Implements the UILogger protocol using the unified logging system,
|
|
5
|
+
preserving UI formatting and behavior for all log levels.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tunacode.core.logging.logger import get_logger
|
|
9
|
+
from tunacode.types import UILogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnifiedUILogger(UILogger):
|
|
13
|
+
"""
|
|
14
|
+
UILogger implementation that routes all UI log calls through the unified logging system.
|
|
15
|
+
Preserves UI conventions for info, error, warning, debug, and success.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str = "ui"):
|
|
19
|
+
self.logger = get_logger(name)
|
|
20
|
+
|
|
21
|
+
async def info(self, message: str) -> None:
|
|
22
|
+
# Standard info log
|
|
23
|
+
self.logger.info(message)
|
|
24
|
+
|
|
25
|
+
async def error(self, message: str) -> None:
|
|
26
|
+
# Standard error log
|
|
27
|
+
self.logger.error(message)
|
|
28
|
+
|
|
29
|
+
async def warning(self, message: str) -> None:
|
|
30
|
+
# Standard warning log
|
|
31
|
+
self.logger.warning(message)
|
|
32
|
+
|
|
33
|
+
async def debug(self, message: str) -> None:
|
|
34
|
+
# Standard debug log
|
|
35
|
+
self.logger.debug(message)
|
|
36
|
+
|
|
37
|
+
async def success(self, message: str) -> None:
|
|
38
|
+
# "Success" is a UI convention; log as info with a marker for UI formatting
|
|
39
|
+
# Add a special prefix or extra field for downstream handlers/formatters
|
|
40
|
+
self.logger.info(f"[SUCCESS] {message}", extra={"ui_success": True})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Singleton instance for convenience
|
|
44
|
+
ui_logger: UILogger = UnifiedUILogger()
|
tunacode/ui/output.py
CHANGED
|
@@ -18,6 +18,7 @@ from tunacode.utils.token_counter import format_token_count
|
|
|
18
18
|
|
|
19
19
|
from .constants import SPINNER_TYPE
|
|
20
20
|
from .decorators import create_sync_wrapper
|
|
21
|
+
from .logging_compat import ui_logger
|
|
21
22
|
|
|
22
23
|
# Create console with explicit settings to ensure ANSI codes work properly
|
|
23
24
|
console = Console(force_terminal=True, legacy_windows=False)
|
|
@@ -54,18 +55,18 @@ async def line() -> None:
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
async def info(text: str) -> None:
|
|
57
|
-
"""
|
|
58
|
-
await
|
|
58
|
+
"""Unified logging: informational message."""
|
|
59
|
+
await ui_logger.info(text)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
async def success(message: str) -> None:
|
|
62
|
-
"""
|
|
63
|
-
await
|
|
63
|
+
"""Unified logging: success message."""
|
|
64
|
+
await ui_logger.success(message)
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
async def warning(text: str) -> None:
|
|
67
|
-
"""
|
|
68
|
-
await
|
|
68
|
+
"""Unified logging: warning message."""
|
|
69
|
+
await ui_logger.warning(text)
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
async def muted(text: str) -> None:
|
tunacode/ui/panels.py
CHANGED
|
@@ -9,7 +9,6 @@ from rich.padding import Padding
|
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
from rich.pretty import Pretty
|
|
11
11
|
from rich.table import Table
|
|
12
|
-
from rich.text import Text
|
|
13
12
|
|
|
14
13
|
from tunacode.configuration.models import ModelRegistry
|
|
15
14
|
from tunacode.constants import (
|
|
@@ -31,7 +30,6 @@ from tunacode.constants import (
|
|
|
31
30
|
DESC_MODEL_SWITCH,
|
|
32
31
|
DESC_YOLO,
|
|
33
32
|
PANEL_AVAILABLE_COMMANDS,
|
|
34
|
-
PANEL_ERROR,
|
|
35
33
|
PANEL_MESSAGE_HISTORY,
|
|
36
34
|
PANEL_MODELS,
|
|
37
35
|
UI_COLORS,
|
|
@@ -88,17 +86,17 @@ class StreamingAgentPanel:
|
|
|
88
86
|
def _create_panel(self) -> Panel:
|
|
89
87
|
"""Create a Rich panel with current content."""
|
|
90
88
|
# Use the UI_THINKING_MESSAGE constant instead of hardcoded text
|
|
89
|
+
from rich.text import Text
|
|
90
|
+
|
|
91
91
|
from tunacode.constants import UI_THINKING_MESSAGE
|
|
92
92
|
|
|
93
|
-
#
|
|
93
|
+
# Handle the default thinking message with Rich markup
|
|
94
94
|
if not self.content:
|
|
95
|
-
|
|
95
|
+
content_renderable = Text.from_markup(UI_THINKING_MESSAGE)
|
|
96
96
|
else:
|
|
97
|
-
|
|
98
|
-
content_to_display = Markdown(self.content)
|
|
99
|
-
|
|
97
|
+
content_renderable = Markdown(self.content)
|
|
100
98
|
panel_obj = Panel(
|
|
101
|
-
Padding(
|
|
99
|
+
Padding(content_renderable, (0, 1, 0, 1)),
|
|
102
100
|
title=f"[bold]{self.title}[/bold]",
|
|
103
101
|
title_align="left",
|
|
104
102
|
border_style=colors.primary,
|
|
@@ -137,7 +135,25 @@ class StreamingAgentPanel:
|
|
|
137
135
|
async def stop(self):
|
|
138
136
|
"""Stop the live streaming display."""
|
|
139
137
|
if self.live:
|
|
138
|
+
# Get the console before stopping the live display
|
|
139
|
+
from .output import console
|
|
140
|
+
|
|
141
|
+
# Stop the live display
|
|
140
142
|
self.live.stop()
|
|
143
|
+
|
|
144
|
+
# Comprehensive cleanup to prevent extra lines
|
|
145
|
+
console.print("", end="") # Reset the current line without newline
|
|
146
|
+
if hasattr(console, "file") and hasattr(console.file, "flush"):
|
|
147
|
+
console.file.flush() # Ensure output is flushed
|
|
148
|
+
|
|
149
|
+
# Mark that we just finished streaming (for output control)
|
|
150
|
+
try:
|
|
151
|
+
from tunacode.core.logging.handlers import _streaming_context
|
|
152
|
+
|
|
153
|
+
_streaming_context["just_finished"] = True
|
|
154
|
+
except ImportError:
|
|
155
|
+
pass # If we can't import, continue without the optimization
|
|
156
|
+
|
|
141
157
|
self.live = None
|
|
142
158
|
|
|
143
159
|
|
|
@@ -159,8 +175,10 @@ async def agent_streaming(content_stream, bottom: int = 1):
|
|
|
159
175
|
|
|
160
176
|
|
|
161
177
|
async def error(text: str) -> None:
|
|
162
|
-
"""
|
|
163
|
-
|
|
178
|
+
"""Unified logging: error message."""
|
|
179
|
+
from .logging_compat import ui_logger
|
|
180
|
+
|
|
181
|
+
await ui_logger.error(text)
|
|
164
182
|
|
|
165
183
|
|
|
166
184
|
async def dump_messages(messages_list=None, state_manager: StateManager = None) -> None:
|
tunacode/utils/security.py
CHANGED
|
@@ -3,13 +3,14 @@ Security utilities for safe command execution and input validation.
|
|
|
3
3
|
Provides defensive measures against command injection attacks.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import logging
|
|
7
6
|
import re
|
|
8
7
|
import shlex
|
|
9
8
|
import subprocess
|
|
10
9
|
from typing import List, Optional
|
|
11
10
|
|
|
12
|
-
logger
|
|
11
|
+
from tunacode.core.logging.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
13
14
|
|
|
14
15
|
# Dangerous shell metacharacters that indicate potential injection
|
|
15
16
|
DANGEROUS_CHARS = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunacode-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.49
|
|
4
4
|
Summary: Your agentic CLI developer.
|
|
5
5
|
Author-email: larock22 <noreply@github.com>
|
|
6
6
|
License: MIT
|
|
@@ -37,7 +37,7 @@ Requires-Dist: textual-dev; extra == "dev"
|
|
|
37
37
|
Requires-Dist: pre-commit; extra == "dev"
|
|
38
38
|
Dynamic: license-file
|
|
39
39
|
|
|
40
|
-
# TunaCode
|
|
40
|
+
# TunaCode CLI
|
|
41
41
|
|
|
42
42
|
<div align="center">
|
|
43
43
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
api/auth.py,sha256=_ysF1RCXvtJR1S35lbYQZexES1lif4J6VVzEyqNK74Q,303
|
|
2
|
+
api/users.py,sha256=WRcy1Vsr4cEC8CW2qeN3XrA9EMyIk47ufpMyvQ4nLuw,193
|
|
3
|
+
tunacode/__init__.py,sha256=yUul8igNYMfUrHnYfioIGAqvrH8b5BKiO_pt1wVnmd0,119
|
|
4
|
+
tunacode/constants.py,sha256=oxUB7zL0wk998eBXu9xV85PKp18FtYSbHlqxRy8TtbI,5169
|
|
3
5
|
tunacode/context.py,sha256=_gXVCyjU052jlyRAl9tklZSwl5U_zI_EIX8XN87VVWE,2786
|
|
4
6
|
tunacode/exceptions.py,sha256=oDO1SVKOgjcKIylwqdbqh_g5my4roU5mB9Nv4n_Vb0s,3877
|
|
5
7
|
tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
8
|
tunacode/setup.py,sha256=XPt4eAK-qcIZQv64jGZ_ryxcImDwps9OmXjJfIS1xcs,1899
|
|
7
|
-
tunacode/types.py,sha256=
|
|
9
|
+
tunacode/types.py,sha256=y8jqLvdSDdi2Y4YECVG2uctpBjjBh9MJ4w1EDEZdYf0,8173
|
|
8
10
|
tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
|
|
9
11
|
tunacode/cli/main.py,sha256=erP6jNXcxVQOVn8sm6uNaLEAYevniVXl6Sem872mW68,2755
|
|
10
|
-
tunacode/cli/repl.py,sha256=
|
|
12
|
+
tunacode/cli/repl.py,sha256=jg2lnr-ZuYUKiSIJBDR9uFsIP7b_gBA6EoxMDgTSwCw,20385
|
|
11
13
|
tunacode/cli/commands/__init__.py,sha256=zmE9JcJ9Qd2xJhgdS4jMDJOoZsrAZmL5MAFxbKkk7F8,1670
|
|
12
14
|
tunacode/cli/commands/base.py,sha256=GxUuDsDSpz0iXryy8MrEw88UM3C3yxL__kDK1QhshoA,2517
|
|
13
15
|
tunacode/cli/commands/registry.py,sha256=XVuLpp5S4Fw7GfIZfLrVZFo4jMLMNmYNpYN7xWgXyOk,8223
|
|
@@ -24,20 +26,25 @@ tunacode/configuration/models.py,sha256=buH8ZquvcYI3OQBDIZeJ08cu00rSCeNABtUwl3VQ
|
|
|
24
26
|
tunacode/configuration/settings.py,sha256=KoN0u6GG3Hh_TWt02D_wpRfbACYri3gCDTXHtJfHl2w,994
|
|
25
27
|
tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
28
|
tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,15661
|
|
27
|
-
tunacode/core/state.py,sha256=
|
|
29
|
+
tunacode/core/state.py,sha256=gYFP6MIwlfYLxT6NFvqkJ3tEHkdxGdexunwXZwmRv0E,5762
|
|
28
30
|
tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
|
|
29
31
|
tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
|
|
30
|
-
tunacode/core/agents/main.py,sha256=
|
|
32
|
+
tunacode/core/agents/main.py,sha256=htxekWzljg0nrbY3xf6PApxsBHUMOSQxVaDGxP0GwUs,44968
|
|
31
33
|
tunacode/core/agents/utils.py,sha256=7kJAiUlkyWO3-b4T07XsGgycVrcNhv3NEPLdaktBnP4,12847
|
|
32
34
|
tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
35
|
tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
|
|
34
36
|
tunacode/core/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
tunacode/core/logging/__init__.py,sha256=pGr3EXxS4Yyz6Gasqg-m9fUENWnnbyaQqMaxR56WW4c,635
|
|
38
|
+
tunacode/core/logging/config.py,sha256=EC5JLPq6JcF-zYNxJ6ZTizrxttY3Jp56zb8TGTWaUjQ,845
|
|
39
|
+
tunacode/core/logging/formatters.py,sha256=lDex7P5eFzu-r9VEMshohj1zxWqANWSS581CJoMp-HA,1210
|
|
40
|
+
tunacode/core/logging/handlers.py,sha256=SxmgH7yWc8bbCKcDeBtkbkHJy5saRqbIoDoTQh4jiYQ,2538
|
|
41
|
+
tunacode/core/logging/logger.py,sha256=9RjRuX0GoUojRJ8WnJGQPFdXiluiJMCoFmvc8xEioB8,142
|
|
35
42
|
tunacode/core/recursive/__init__.py,sha256=S9_dN0faJqam3Pnaum9PRC8Hd90bpES8syFgAD8-QbI,446
|
|
36
43
|
tunacode/core/recursive/aggregator.py,sha256=KlWajEyT8OokWeY6ZwQ4EsVu05V4KeH3o2cYUz-Ce1Y,15725
|
|
37
44
|
tunacode/core/recursive/budget.py,sha256=0wY6xSrZKmudUwthwX1mlcF5yat2_y3fNu5shX_IzvA,14131
|
|
38
45
|
tunacode/core/recursive/decomposer.py,sha256=k8rgaPEMMb2mZ9W0m_Yg0oEBqbYpqDnK0zyTkx6eTPs,14550
|
|
39
46
|
tunacode/core/recursive/executor.py,sha256=7taFsjYjSfL8OUQLM1ZVn9Xix7YoputFBAZSo9tDtDM,16646
|
|
40
|
-
tunacode/core/recursive/hierarchy.py,sha256=
|
|
47
|
+
tunacode/core/recursive/hierarchy.py,sha256=YKYIt1APsZviVmhjxfQ9p441rVkfEHRcmuh3cFV-vqU,15744
|
|
41
48
|
tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
|
|
42
49
|
tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
|
|
43
50
|
tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
|
|
@@ -52,7 +59,7 @@ tunacode/prompts/system.md,sha256=hXpjZ8Yiv2Acr2_6EmC2uOklP8FbmvyYR9oais-1KLk,16
|
|
|
52
59
|
tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
|
|
53
60
|
tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
|
|
54
61
|
tunacode/tools/__init__.py,sha256=ECBuUWWF1JjHW42CCceaPKgVTQyuljbz3RlhuA2fe2s,314
|
|
55
|
-
tunacode/tools/base.py,sha256=
|
|
62
|
+
tunacode/tools/base.py,sha256=DhlanZZZxU2JJaBOwwyGFKMUcoCWR_CzLuwVeSXC0Go,7297
|
|
56
63
|
tunacode/tools/bash.py,sha256=mgZqugfDFevZ4BETuUv90pzXvtq7qKGUGFiuDxzmytk,8766
|
|
57
64
|
tunacode/tools/glob.py,sha256=TSgVK79ewZgGw8ucYkkiHgVqRgkw-wZrhP8j52nm_gQ,10334
|
|
58
65
|
tunacode/tools/grep.py,sha256=jboEVA2ATv0YI8zg9dF89emZ_HWy2vVtsQ_-hDhlr7g,26337
|
|
@@ -64,15 +71,16 @@ tunacode/tools/todo.py,sha256=bVbohgwKqvvTe8efxXrMZDQU8vdk4E3jF9Cj38dRq7k,12727
|
|
|
64
71
|
tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
|
|
65
72
|
tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
|
|
66
73
|
tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
|
|
67
|
-
tunacode/ui/completers.py,sha256=
|
|
68
|
-
tunacode/ui/console.py,sha256=
|
|
74
|
+
tunacode/ui/completers.py,sha256=40wkF1nqG9HNVmP8MRWEPya0zV6GbyFXAuT-k7uXmxg,4880
|
|
75
|
+
tunacode/ui/console.py,sha256=wLiJ9cVrWHS0zNadqbcXnsg0T2wj4xLACg17YAYLmU4,2574
|
|
69
76
|
tunacode/ui/constants.py,sha256=A76B_KpM8jCuBYRg4cPmhi8_j6LLyWttO7_jjv47r3w,421
|
|
70
77
|
tunacode/ui/decorators.py,sha256=e2KM-_pI5EKHa2M045IjUe4rPkTboxaKHXJT0K3461g,1914
|
|
71
|
-
tunacode/ui/input.py,sha256=
|
|
72
|
-
tunacode/ui/keybindings.py,sha256=
|
|
78
|
+
tunacode/ui/input.py,sha256=NCZlj5qzNPy0gsSeGKeDNdAOMKZVGph8Z-UBXhX-Sbk,3020
|
|
79
|
+
tunacode/ui/keybindings.py,sha256=ACUofGGs3xFvrNWi1cQ_Tkyy0lhIrZAEIOPlK6vZFVY,3014
|
|
73
80
|
tunacode/ui/lexers.py,sha256=tmg4ic1enyTRLzanN5QPP7D_0n12YjX_8ZhsffzhXA4,1340
|
|
74
|
-
tunacode/ui/
|
|
75
|
-
tunacode/ui/
|
|
81
|
+
tunacode/ui/logging_compat.py,sha256=5v6lcjVaG1CxdY1Zm9FAGr9H7Sy-tP6ihGfhP-5YvAY,1406
|
|
82
|
+
tunacode/ui/output.py,sha256=vXfkPfQWmQzkmfGkY6snAk7auLgN8E7XEGUc-YVjTlM,5604
|
|
83
|
+
tunacode/ui/panels.py,sha256=usGbzBDZdBY2pS2zHdlY0s5NKf9AUw7llOHKWoaNIlU,9307
|
|
76
84
|
tunacode/ui/prompt_manager.py,sha256=U2cntB34vm-YwOj3gzFRUK362zccrz8pigQfpxr5sv8,4650
|
|
77
85
|
tunacode/ui/recursive_progress.py,sha256=V0dGpJWt19TVArOYcQ3Lki8cR3ZepFT6iGwnChSFhFI,12906
|
|
78
86
|
tunacode/ui/tool_ui.py,sha256=qp1aZUpLO5UOdJziY8tw0URC8gjoWoSKdGu5y2wuTUU,7013
|
|
@@ -86,14 +94,14 @@ tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDg
|
|
|
86
94
|
tunacode/utils/message_utils.py,sha256=kM6VSS2Dudjplie009khHgmIRjDoBUzv6tvHcYNDAAE,586
|
|
87
95
|
tunacode/utils/retry.py,sha256=AHdUzY6m-mwlT4OPXdtWWMAafL_NeS7JAMORGyM8c5k,4931
|
|
88
96
|
tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
|
|
89
|
-
tunacode/utils/security.py,sha256=
|
|
97
|
+
tunacode/utils/security.py,sha256=i3eGKg4o-qY2S_ObTlEaHO93q14iBfiPXR5O7srHn58,6579
|
|
90
98
|
tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
|
|
91
99
|
tunacode/utils/text_utils.py,sha256=6YBD9QfkDO44-6jxnwRWIpmfIifPG-NqMzy_O2NAouc,7277
|
|
92
100
|
tunacode/utils/token_counter.py,sha256=lLbkrNUraRQn5RMhwnGurqq1RHFDyn4AaFhruONWIxo,2690
|
|
93
101
|
tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
|
|
94
|
-
tunacode_cli-0.0.
|
|
95
|
-
tunacode_cli-0.0.
|
|
96
|
-
tunacode_cli-0.0.
|
|
97
|
-
tunacode_cli-0.0.
|
|
98
|
-
tunacode_cli-0.0.
|
|
99
|
-
tunacode_cli-0.0.
|
|
102
|
+
tunacode_cli-0.0.49.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
|
|
103
|
+
tunacode_cli-0.0.49.dist-info/METADATA,sha256=YdmUijOV5ybOmyMWEYJHEtAWpVur1dhi8ezMKKS81BM,5906
|
|
104
|
+
tunacode_cli-0.0.49.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
105
|
+
tunacode_cli-0.0.49.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
|
|
106
|
+
tunacode_cli-0.0.49.dist-info/top_level.txt,sha256=GuU751acRvOhM5yLKFW0-gBg62JGh5zycDSq4tRFOYE,13
|
|
107
|
+
tunacode_cli-0.0.49.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|