todo-agent 0.1.0__py3-none-any.whl → 0.2.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.
- todo_agent/_version.py +2 -2
- todo_agent/core/__init__.py +3 -3
- todo_agent/core/conversation_manager.py +39 -17
- todo_agent/core/todo_manager.py +40 -30
- todo_agent/infrastructure/__init__.py +1 -1
- todo_agent/infrastructure/config.py +7 -5
- todo_agent/infrastructure/inference.py +109 -54
- todo_agent/infrastructure/llm_client_factory.py +13 -9
- todo_agent/infrastructure/logger.py +38 -41
- todo_agent/infrastructure/ollama_client.py +22 -15
- todo_agent/infrastructure/openrouter_client.py +37 -26
- todo_agent/infrastructure/todo_shell.py +12 -10
- todo_agent/infrastructure/token_counter.py +39 -38
- todo_agent/interface/cli.py +51 -37
- todo_agent/interface/tools.py +47 -40
- todo_agent/main.py +1 -1
- {todo_agent-0.1.0.dist-info → todo_agent-0.2.1.dist-info}/METADATA +76 -38
- todo_agent-0.2.1.dist-info/RECORD +27 -0
- todo_agent-0.1.0.dist-info/RECORD +0 -27
- {todo_agent-0.1.0.dist-info → todo_agent-0.2.1.dist-info}/WHEEL +0 -0
- {todo_agent-0.1.0.dist-info → todo_agent-0.2.1.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.1.0.dist-info → todo_agent-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.1.0.dist-info → todo_agent-0.2.1.dist-info}/top_level.txt +0 -0
@@ -7,15 +7,17 @@ from typing import Optional
|
|
7
7
|
try:
|
8
8
|
from todo_agent.infrastructure.config import Config
|
9
9
|
from todo_agent.infrastructure.llm_client import LLMClient
|
10
|
-
from todo_agent.infrastructure.openrouter_client import OpenRouterClient
|
11
|
-
from todo_agent.infrastructure.ollama_client import OllamaClient
|
12
10
|
from todo_agent.infrastructure.logger import Logger
|
11
|
+
from todo_agent.infrastructure.ollama_client import OllamaClient
|
12
|
+
from todo_agent.infrastructure.openrouter_client import OpenRouterClient
|
13
13
|
except ImportError:
|
14
|
-
from infrastructure.config import Config
|
15
|
-
from infrastructure.llm_client import LLMClient
|
16
|
-
from infrastructure.
|
17
|
-
from infrastructure.ollama_client import OllamaClient
|
18
|
-
from infrastructure.
|
14
|
+
from infrastructure.config import Config # type: ignore[no-redef]
|
15
|
+
from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
|
16
|
+
from infrastructure.logger import Logger # type: ignore[no-redef]
|
17
|
+
from infrastructure.ollama_client import OllamaClient # type: ignore[no-redef]
|
18
|
+
from infrastructure.openrouter_client import ( # type: ignore[no-redef]
|
19
|
+
OpenRouterClient,
|
20
|
+
)
|
19
21
|
|
20
22
|
|
21
23
|
class LLMClientFactory:
|
@@ -37,9 +39,11 @@ class LLMClientFactory:
|
|
37
39
|
ValueError: If provider is not supported
|
38
40
|
"""
|
39
41
|
logger = logger or Logger("llm_client_factory")
|
40
|
-
|
42
|
+
|
41
43
|
if config.provider == "openrouter":
|
42
|
-
logger.info(
|
44
|
+
logger.info(
|
45
|
+
f"Creating OpenRouter client with model: {config.openrouter_model}"
|
46
|
+
)
|
43
47
|
return OpenRouterClient(config)
|
44
48
|
elif config.provider == "ollama":
|
45
49
|
logger.info(f"Creating Ollama client with model: {config.ollama_model}")
|
@@ -6,40 +6,39 @@ import logging
|
|
6
6
|
import os
|
7
7
|
from datetime import datetime
|
8
8
|
from pathlib import Path
|
9
|
-
from typing import Optional
|
10
9
|
|
11
10
|
|
12
11
|
class Logger:
|
13
12
|
"""Custom logger that respects LOG_LEVEL environment variable and logs to screen and file."""
|
14
|
-
|
13
|
+
|
15
14
|
def __init__(self, name: str = "todo_agent"):
|
16
15
|
"""
|
17
16
|
Initialize the logger.
|
18
|
-
|
17
|
+
|
19
18
|
Args:
|
20
19
|
name: Logger name, defaults to "todo_agent"
|
21
20
|
"""
|
22
21
|
self.name = name
|
23
22
|
self.logger = logging.getLogger(name)
|
24
23
|
self.logger.setLevel(logging.DEBUG)
|
25
|
-
|
24
|
+
|
26
25
|
# Clear any existing handlers
|
27
26
|
self.logger.handlers.clear()
|
28
|
-
|
27
|
+
|
29
28
|
# Create logs directory if it doesn't exist
|
30
29
|
self._ensure_logs_directory()
|
31
|
-
|
30
|
+
|
32
31
|
# Set up file handler (always active)
|
33
32
|
self._setup_file_handler()
|
34
|
-
|
33
|
+
|
35
34
|
# Set up console handler with appropriate log level
|
36
35
|
self._setup_console_handler()
|
37
|
-
|
38
|
-
def _ensure_logs_directory(self):
|
36
|
+
|
37
|
+
def _ensure_logs_directory(self) -> None:
|
39
38
|
"""Ensure the logs directory exists in TODO_DIR."""
|
40
39
|
logs_dir = self._get_logs_directory()
|
41
40
|
logs_dir.mkdir(exist_ok=True)
|
42
|
-
|
41
|
+
|
43
42
|
def _get_logs_directory(self) -> Path:
|
44
43
|
"""Get the logs directory path from TODO_DIR environment variable."""
|
45
44
|
todo_dir = os.getenv("TODO_DIR")
|
@@ -48,81 +47,79 @@ class Logger:
|
|
48
47
|
else:
|
49
48
|
# Fallback to local logs directory if TODO_DIR is not set
|
50
49
|
return Path("logs")
|
51
|
-
|
50
|
+
|
52
51
|
def _get_log_level(self) -> int:
|
53
52
|
"""Get log level from LOG_LEVEL environment variable."""
|
54
53
|
log_level_str = os.getenv("LOG_LEVEL", "INFO").upper()
|
55
|
-
|
54
|
+
|
56
55
|
# Map string values to logging constants
|
57
56
|
level_map = {
|
58
57
|
"DEBUG": logging.DEBUG,
|
59
58
|
"INFO": logging.INFO,
|
60
59
|
"WARNING": logging.WARNING,
|
61
60
|
"ERROR": logging.ERROR,
|
62
|
-
"CRITICAL": logging.CRITICAL
|
61
|
+
"CRITICAL": logging.CRITICAL,
|
63
62
|
}
|
64
|
-
|
63
|
+
|
65
64
|
return level_map.get(log_level_str, logging.INFO)
|
66
|
-
|
65
|
+
|
67
66
|
def _should_log_to_console(self) -> bool:
|
68
67
|
"""Check if we should log to console based on DEBUG environment variable."""
|
69
68
|
return os.getenv("DEBUG") is not None
|
70
|
-
|
71
|
-
def _setup_file_handler(self):
|
69
|
+
|
70
|
+
def _setup_file_handler(self) -> None:
|
72
71
|
"""Set up file handler for logging to file."""
|
73
72
|
# Create log filename with timestamp
|
74
73
|
timestamp = datetime.now().strftime("%Y%m%d")
|
75
74
|
logs_dir = self._get_logs_directory()
|
76
75
|
log_file = logs_dir / f"todo_agent_{timestamp}.log"
|
77
|
-
|
76
|
+
|
78
77
|
file_handler = logging.FileHandler(log_file)
|
79
78
|
file_handler.setLevel(logging.DEBUG)
|
80
|
-
|
79
|
+
|
81
80
|
# Create formatter for file logging
|
82
81
|
file_formatter = logging.Formatter(
|
83
|
-
|
82
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
84
83
|
)
|
85
84
|
file_handler.setFormatter(file_formatter)
|
86
|
-
|
85
|
+
|
87
86
|
self.logger.addHandler(file_handler)
|
88
|
-
|
89
|
-
def _setup_console_handler(self):
|
87
|
+
|
88
|
+
def _setup_console_handler(self) -> None:
|
90
89
|
"""Set up console handler for logging to screen with appropriate log level."""
|
91
90
|
# Only add console handler if DEBUG environment variable is set
|
92
91
|
if not self._should_log_to_console():
|
93
92
|
return
|
94
|
-
|
93
|
+
|
95
94
|
console_handler = logging.StreamHandler()
|
96
95
|
console_handler.setLevel(self._get_log_level())
|
97
|
-
|
96
|
+
|
98
97
|
# Create formatter for console logging (more concise)
|
99
|
-
console_formatter = logging.Formatter(
|
100
|
-
'%(levelname)s - %(message)s'
|
101
|
-
)
|
98
|
+
console_formatter = logging.Formatter("%(levelname)s - %(message)s")
|
102
99
|
console_handler.setFormatter(console_formatter)
|
103
|
-
|
100
|
+
|
104
101
|
self.logger.addHandler(console_handler)
|
105
|
-
|
106
|
-
def debug(self, message: str):
|
102
|
+
|
103
|
+
def debug(self, message: str) -> None:
|
107
104
|
"""Log a debug message."""
|
108
105
|
self.logger.debug(message)
|
109
|
-
|
110
|
-
def info(self, message: str):
|
106
|
+
|
107
|
+
def info(self, message: str) -> None:
|
111
108
|
"""Log an info message."""
|
112
109
|
self.logger.info(message)
|
113
|
-
|
114
|
-
def warning(self, message: str):
|
110
|
+
|
111
|
+
def warning(self, message: str) -> None:
|
115
112
|
"""Log a warning message."""
|
116
113
|
self.logger.warning(message)
|
117
|
-
|
118
|
-
def error(self, message: str):
|
114
|
+
|
115
|
+
def error(self, message: str) -> None:
|
119
116
|
"""Log an error message."""
|
120
117
|
self.logger.error(message)
|
121
|
-
|
122
|
-
def critical(self, message: str):
|
118
|
+
|
119
|
+
def critical(self, message: str) -> None:
|
123
120
|
"""Log a critical message."""
|
124
121
|
self.logger.critical(message)
|
125
|
-
|
126
|
-
def exception(self, message: str):
|
122
|
+
|
123
|
+
def exception(self, message: str) -> None:
|
127
124
|
"""Log an exception message with traceback."""
|
128
125
|
self.logger.exception(message)
|
@@ -10,14 +10,14 @@ import requests
|
|
10
10
|
|
11
11
|
try:
|
12
12
|
from todo_agent.infrastructure.config import Config
|
13
|
+
from todo_agent.infrastructure.llm_client import LLMClient
|
13
14
|
from todo_agent.infrastructure.logger import Logger
|
14
15
|
from todo_agent.infrastructure.token_counter import get_token_counter
|
15
|
-
from todo_agent.infrastructure.llm_client import LLMClient
|
16
16
|
except ImportError:
|
17
|
-
from infrastructure.config import Config
|
18
|
-
from infrastructure.
|
19
|
-
from infrastructure.
|
20
|
-
from infrastructure.
|
17
|
+
from infrastructure.config import Config # type: ignore[no-redef]
|
18
|
+
from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
|
19
|
+
from infrastructure.logger import Logger # type: ignore[no-redef]
|
20
|
+
from infrastructure.token_counter import get_token_counter # type: ignore[no-redef]
|
21
21
|
|
22
22
|
|
23
23
|
class OllamaClient(LLMClient):
|
@@ -48,7 +48,7 @@ class OllamaClient(LLMClient):
|
|
48
48
|
"""
|
49
49
|
return self.token_counter.count_tokens(text)
|
50
50
|
|
51
|
-
def _log_request_details(self, payload: Dict[str, Any], start_time: float):
|
51
|
+
def _log_request_details(self, payload: Dict[str, Any], start_time: float) -> None:
|
52
52
|
"""Log request details including accurate token count."""
|
53
53
|
# Count tokens for messages
|
54
54
|
messages = payload.get("messages", [])
|
@@ -59,7 +59,9 @@ class OllamaClient(LLMClient):
|
|
59
59
|
self.logger.info(f"Request sent - Token count: {total_tokens}")
|
60
60
|
# self.logger.debug(f"Raw request payload: {json.dumps(payload, indent=2)}")
|
61
61
|
|
62
|
-
def _log_response_details(
|
62
|
+
def _log_response_details(
|
63
|
+
self, response: Dict[str, Any], start_time: float
|
64
|
+
) -> None:
|
63
65
|
"""Log response details including latency."""
|
64
66
|
end_time = time.time()
|
65
67
|
latency_ms = (end_time - start_time) * 1000
|
@@ -72,10 +74,12 @@ class OllamaClient(LLMClient):
|
|
72
74
|
self.logger.info(f"Response contains {len(tool_calls)} tool calls")
|
73
75
|
for i, tool_call in enumerate(tool_calls):
|
74
76
|
tool_name = tool_call.get("function", {}).get("name", "unknown")
|
75
|
-
self.logger.info(f" Tool call {i+1}: {tool_name}")
|
77
|
+
self.logger.info(f" Tool call {i + 1}: {tool_name}")
|
76
78
|
elif "message" in response and "content" in response["message"]:
|
77
79
|
content = response["message"]["content"]
|
78
|
-
self.logger.debug(
|
80
|
+
self.logger.debug(
|
81
|
+
f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}"
|
82
|
+
)
|
79
83
|
|
80
84
|
self.logger.debug(f"Raw response: {json.dumps(response, indent=2)}")
|
81
85
|
|
@@ -106,7 +110,7 @@ class OllamaClient(LLMClient):
|
|
106
110
|
start_time = time.time()
|
107
111
|
self._log_request_details(payload, start_time)
|
108
112
|
|
109
|
-
response = requests.post(
|
113
|
+
response = requests.post( # nosec B113
|
110
114
|
f"{self.base_url}/api/chat", headers=headers, json=payload
|
111
115
|
)
|
112
116
|
|
@@ -114,7 +118,7 @@ class OllamaClient(LLMClient):
|
|
114
118
|
self.logger.error(f"Ollama API error: {response.text}")
|
115
119
|
raise Exception(f"Ollama API error: {response.text}")
|
116
120
|
|
117
|
-
response_data = response.json()
|
121
|
+
response_data: Dict[str, Any] = response.json()
|
118
122
|
self._log_response_details(response_data, start_time)
|
119
123
|
|
120
124
|
return response_data
|
@@ -122,7 +126,7 @@ class OllamaClient(LLMClient):
|
|
122
126
|
def extract_tool_calls(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
123
127
|
"""Extract tool calls from API response."""
|
124
128
|
tool_calls = []
|
125
|
-
|
129
|
+
|
126
130
|
# Ollama response format is different from OpenRouter
|
127
131
|
if "message" in response and "tool_calls" in response["message"]:
|
128
132
|
tool_calls = response["message"]["tool_calls"]
|
@@ -130,16 +134,19 @@ class OllamaClient(LLMClient):
|
|
130
134
|
for i, tool_call in enumerate(tool_calls):
|
131
135
|
tool_name = tool_call.get("function", {}).get("name", "unknown")
|
132
136
|
tool_call_id = tool_call.get("id", "unknown")
|
133
|
-
self.logger.debug(
|
137
|
+
self.logger.debug(
|
138
|
+
f"Tool call {i + 1}: {tool_name} (ID: {tool_call_id})"
|
139
|
+
)
|
134
140
|
else:
|
135
141
|
self.logger.debug("No tool calls found in response")
|
136
|
-
|
142
|
+
|
137
143
|
return tool_calls
|
138
144
|
|
139
145
|
def extract_content(self, response: Dict[str, Any]) -> str:
|
140
146
|
"""Extract content from API response."""
|
141
147
|
if "message" in response and "content" in response["message"]:
|
142
|
-
|
148
|
+
content = response["message"]["content"]
|
149
|
+
return content if isinstance(content, str) else str(content)
|
143
150
|
return ""
|
144
151
|
|
145
152
|
def get_model_name(self) -> str:
|
@@ -10,14 +10,14 @@ import requests
|
|
10
10
|
|
11
11
|
try:
|
12
12
|
from todo_agent.infrastructure.config import Config
|
13
|
+
from todo_agent.infrastructure.llm_client import LLMClient
|
13
14
|
from todo_agent.infrastructure.logger import Logger
|
14
15
|
from todo_agent.infrastructure.token_counter import get_token_counter
|
15
|
-
from todo_agent.infrastructure.llm_client import LLMClient
|
16
16
|
except ImportError:
|
17
|
-
from infrastructure.config import Config
|
18
|
-
from infrastructure.
|
19
|
-
from infrastructure.
|
20
|
-
from infrastructure.
|
17
|
+
from infrastructure.config import Config # type: ignore[no-redef]
|
18
|
+
from infrastructure.llm_client import LLMClient # type: ignore[no-redef]
|
19
|
+
from infrastructure.logger import Logger # type: ignore[no-redef]
|
20
|
+
from infrastructure.token_counter import get_token_counter # type: ignore[no-redef]
|
21
21
|
|
22
22
|
|
23
23
|
class OpenRouterClient(LLMClient):
|
@@ -34,53 +34,59 @@ class OpenRouterClient(LLMClient):
|
|
34
34
|
def _estimate_tokens(self, text: str) -> int:
|
35
35
|
"""
|
36
36
|
Estimate token count for text using accurate tokenization.
|
37
|
-
|
37
|
+
|
38
38
|
Args:
|
39
39
|
text: Text to count tokens for
|
40
|
-
|
40
|
+
|
41
41
|
Returns:
|
42
42
|
Number of tokens
|
43
43
|
"""
|
44
44
|
return self.token_counter.count_tokens(text)
|
45
45
|
|
46
|
-
def _log_request_details(self, payload: Dict[str, Any], start_time: float):
|
46
|
+
def _log_request_details(self, payload: Dict[str, Any], start_time: float) -> None:
|
47
47
|
"""Log request details including accurate token count."""
|
48
48
|
# Count tokens for messages
|
49
49
|
messages = payload.get("messages", [])
|
50
50
|
tools = payload.get("tools", [])
|
51
|
-
|
51
|
+
|
52
52
|
total_tokens = self.token_counter.count_request_tokens(messages, tools)
|
53
|
-
|
53
|
+
|
54
54
|
self.logger.info(f"Request sent - Token count: {total_tokens}")
|
55
55
|
# self.logger.debug(f"Raw request payload: {json.dumps(payload, indent=2)}")
|
56
56
|
|
57
|
-
def _log_response_details(
|
57
|
+
def _log_response_details(
|
58
|
+
self, response: Dict[str, Any], start_time: float
|
59
|
+
) -> None:
|
58
60
|
"""Log response details including token count and latency."""
|
59
61
|
end_time = time.time()
|
60
62
|
latency_ms = (end_time - start_time) * 1000
|
61
|
-
|
63
|
+
|
62
64
|
# Extract token usage from response if available
|
63
65
|
usage = response.get("usage", {})
|
64
66
|
prompt_tokens = usage.get("prompt_tokens", "unknown")
|
65
67
|
completion_tokens = usage.get("completion_tokens", "unknown")
|
66
68
|
total_tokens = usage.get("total_tokens", "unknown")
|
67
|
-
|
69
|
+
|
68
70
|
self.logger.info(f"Response received - Latency: {latency_ms:.2f}ms")
|
69
|
-
self.logger.info(
|
70
|
-
|
71
|
+
self.logger.info(
|
72
|
+
f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}"
|
73
|
+
)
|
74
|
+
|
71
75
|
# Log tool call details if present
|
72
|
-
if
|
76
|
+
if response.get("choices"):
|
73
77
|
choice = response["choices"][0]
|
74
78
|
if "message" in choice and "tool_calls" in choice["message"]:
|
75
79
|
tool_calls = choice["message"]["tool_calls"]
|
76
80
|
self.logger.info(f"Response contains {len(tool_calls)} tool calls")
|
77
81
|
for i, tool_call in enumerate(tool_calls):
|
78
82
|
tool_name = tool_call.get("function", {}).get("name", "unknown")
|
79
|
-
self.logger.info(f" Tool call {i+1}: {tool_name}")
|
83
|
+
self.logger.info(f" Tool call {i + 1}: {tool_name}")
|
80
84
|
elif "message" in choice and "content" in choice["message"]:
|
81
85
|
content = choice["message"]["content"]
|
82
|
-
self.logger.debug(
|
83
|
-
|
86
|
+
self.logger.debug(
|
87
|
+
f"Response contains content: {content[:100]}{'...' if len(content) > 100 else ''}"
|
88
|
+
)
|
89
|
+
|
84
90
|
self.logger.debug(f"Raw response: {json.dumps(response, indent=2)}")
|
85
91
|
|
86
92
|
def chat_with_tools(
|
@@ -111,7 +117,7 @@ class OpenRouterClient(LLMClient):
|
|
111
117
|
start_time = time.time()
|
112
118
|
self._log_request_details(payload, start_time)
|
113
119
|
|
114
|
-
response = requests.post(
|
120
|
+
response = requests.post( # nosec B113
|
115
121
|
f"{self.base_url}/chat/completions", headers=headers, json=payload
|
116
122
|
)
|
117
123
|
|
@@ -119,7 +125,7 @@ class OpenRouterClient(LLMClient):
|
|
119
125
|
self.logger.error(f"OpenRouter API error: {response.text}")
|
120
126
|
raise Exception(f"OpenRouter API error: {response.text}")
|
121
127
|
|
122
|
-
response_data = response.json()
|
128
|
+
response_data: Dict[str, Any] = response.json()
|
123
129
|
self._log_response_details(response_data, start_time)
|
124
130
|
|
125
131
|
return response_data
|
@@ -140,15 +146,19 @@ class OpenRouterClient(LLMClient):
|
|
140
146
|
def extract_tool_calls(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
141
147
|
"""Extract tool calls from API response."""
|
142
148
|
tool_calls = []
|
143
|
-
if
|
149
|
+
if response.get("choices"):
|
144
150
|
choice = response["choices"][0]
|
145
151
|
if "message" in choice and "tool_calls" in choice["message"]:
|
146
152
|
tool_calls = choice["message"]["tool_calls"]
|
147
|
-
self.logger.debug(
|
153
|
+
self.logger.debug(
|
154
|
+
f"Extracted {len(tool_calls)} tool calls from response"
|
155
|
+
)
|
148
156
|
for i, tool_call in enumerate(tool_calls):
|
149
157
|
tool_name = tool_call.get("function", {}).get("name", "unknown")
|
150
158
|
tool_call_id = tool_call.get("id", "unknown")
|
151
|
-
self.logger.debug(
|
159
|
+
self.logger.debug(
|
160
|
+
f"Tool call {i + 1}: {tool_name} (ID: {tool_call_id})"
|
161
|
+
)
|
152
162
|
else:
|
153
163
|
self.logger.debug("No tool calls found in response")
|
154
164
|
else:
|
@@ -157,10 +167,11 @@ class OpenRouterClient(LLMClient):
|
|
157
167
|
|
158
168
|
def extract_content(self, response: Dict[str, Any]) -> str:
|
159
169
|
"""Extract content from API response."""
|
160
|
-
if
|
170
|
+
if response.get("choices"):
|
161
171
|
choice = response["choices"][0]
|
162
172
|
if "message" in choice and "content" in choice["message"]:
|
163
|
-
|
173
|
+
content = choice["message"]["content"]
|
174
|
+
return content if isinstance(content, str) else str(content)
|
164
175
|
return ""
|
165
176
|
|
166
177
|
def get_model_name(self) -> str:
|
@@ -3,19 +3,19 @@ Subprocess wrapper for todo.sh operations.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import os
|
6
|
-
import subprocess
|
7
|
-
from typing import List, Optional
|
6
|
+
import subprocess # nosec B404
|
7
|
+
from typing import Any, List, Optional
|
8
8
|
|
9
9
|
try:
|
10
10
|
from todo_agent.core.exceptions import TodoShellError
|
11
11
|
except ImportError:
|
12
|
-
from core.exceptions import TodoShellError
|
12
|
+
from core.exceptions import TodoShellError # type: ignore[no-redef]
|
13
13
|
|
14
14
|
|
15
15
|
class TodoShell:
|
16
16
|
"""Subprocess execution wrapper with error management."""
|
17
17
|
|
18
|
-
def __init__(self, todo_file_path: str, logger=None):
|
18
|
+
def __init__(self, todo_file_path: str, logger: Optional[Any] = None) -> None:
|
19
19
|
self.todo_file_path = todo_file_path
|
20
20
|
self.todo_dir = os.path.dirname(todo_file_path) or os.getcwd()
|
21
21
|
self.logger = logger
|
@@ -40,13 +40,13 @@ class TodoShell:
|
|
40
40
|
self.logger.debug(f"=== RAW COMMAND EXECUTION ===")
|
41
41
|
self.logger.debug(f"Raw command: {raw_command}")
|
42
42
|
self.logger.debug(f"Working directory: {cwd or self.todo_dir}")
|
43
|
-
|
43
|
+
|
44
44
|
try:
|
45
45
|
working_dir = cwd or self.todo_dir
|
46
|
-
result = subprocess.run(
|
46
|
+
result = subprocess.run( # nosec B603
|
47
47
|
command, cwd=working_dir, capture_output=True, text=True, check=True
|
48
48
|
)
|
49
|
-
|
49
|
+
|
50
50
|
# Log the raw output
|
51
51
|
if self.logger:
|
52
52
|
self.logger.debug(f"=== RAW COMMAND OUTPUT ===")
|
@@ -54,7 +54,7 @@ class TodoShell:
|
|
54
54
|
self.logger.debug(f"Raw stdout: {result.stdout}")
|
55
55
|
self.logger.debug(f"Raw stderr: {result.stderr}")
|
56
56
|
self.logger.debug(f"Return code: {result.returncode}")
|
57
|
-
|
57
|
+
|
58
58
|
return result.stdout.strip()
|
59
59
|
except subprocess.CalledProcessError as e:
|
60
60
|
# Log error details
|
@@ -69,7 +69,7 @@ class TodoShell:
|
|
69
69
|
if self.logger:
|
70
70
|
self.logger.error(f"=== COMMAND EXECUTION EXCEPTION ===")
|
71
71
|
self.logger.error(f"Raw command: {' '.join(command)}")
|
72
|
-
self.logger.error(f"Exception: {
|
72
|
+
self.logger.error(f"Exception: {e!s}")
|
73
73
|
raise TodoShellError(f"Todo.sh command failed: {e}")
|
74
74
|
|
75
75
|
def add(self, description: str) -> str:
|
@@ -106,7 +106,9 @@ class TodoShell:
|
|
106
106
|
command.append(term)
|
107
107
|
return self.execute(command)
|
108
108
|
|
109
|
-
def move(
|
109
|
+
def move(
|
110
|
+
self, task_number: int, destination: str, source: Optional[str] = None
|
111
|
+
) -> str:
|
110
112
|
"""Move task from source to destination file."""
|
111
113
|
command = ["todo.sh", "-f", "move", str(task_number), destination]
|
112
114
|
if source:
|