minion-code 0.1.0__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.
- examples/advance_tui.py +508 -0
- examples/agent_with_todos.py +165 -0
- examples/file_freshness_example.py +97 -0
- examples/file_watching_example.py +110 -0
- examples/interruptible_tui.py +5 -0
- examples/message_response_children_demo.py +226 -0
- examples/rich_example.py +4 -0
- examples/simple_file_watching.py +57 -0
- examples/simple_tui.py +267 -0
- examples/simple_usage.py +69 -0
- minion_code/__init__.py +16 -0
- minion_code/agents/__init__.py +11 -0
- minion_code/agents/code_agent.py +320 -0
- minion_code/cli.py +502 -0
- minion_code/commands/__init__.py +90 -0
- minion_code/commands/clear_command.py +70 -0
- minion_code/commands/help_command.py +90 -0
- minion_code/commands/history_command.py +104 -0
- minion_code/commands/quit_command.py +32 -0
- minion_code/commands/status_command.py +115 -0
- minion_code/commands/tools_command.py +86 -0
- minion_code/commands/version_command.py +104 -0
- minion_code/components/Message.py +304 -0
- minion_code/components/MessageResponse.py +188 -0
- minion_code/components/PromptInput.py +534 -0
- minion_code/components/__init__.py +29 -0
- minion_code/screens/REPL.py +925 -0
- minion_code/screens/__init__.py +4 -0
- minion_code/services/__init__.py +50 -0
- minion_code/services/event_system.py +108 -0
- minion_code/services/file_freshness_service.py +582 -0
- minion_code/tools/__init__.py +69 -0
- minion_code/tools/bash_tool.py +58 -0
- minion_code/tools/file_edit_tool.py +238 -0
- minion_code/tools/file_read_tool.py +73 -0
- minion_code/tools/file_write_tool.py +36 -0
- minion_code/tools/glob_tool.py +58 -0
- minion_code/tools/grep_tool.py +105 -0
- minion_code/tools/ls_tool.py +65 -0
- minion_code/tools/multi_edit_tool.py +271 -0
- minion_code/tools/python_interpreter_tool.py +105 -0
- minion_code/tools/todo_read_tool.py +100 -0
- minion_code/tools/todo_write_tool.py +234 -0
- minion_code/tools/user_input_tool.py +53 -0
- minion_code/types.py +88 -0
- minion_code/utils/__init__.py +44 -0
- minion_code/utils/mcp_loader.py +211 -0
- minion_code/utils/todo_file_utils.py +110 -0
- minion_code/utils/todo_storage.py +149 -0
- minion_code-0.1.0.dist-info/METADATA +350 -0
- minion_code-0.1.0.dist-info/RECORD +59 -0
- minion_code-0.1.0.dist-info/WHEEL +5 -0
- minion_code-0.1.0.dist-info/entry_points.txt +4 -0
- minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
- minion_code-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_basic.py +20 -0
- tests/test_readonly_tools.py +102 -0
- tests/test_tools.py +83 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""File freshness tracking service."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional, Union, Set, List, NamedTuple
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from .event_system import EventDispatcher, EventContext, emit_event, add_event_listener
|
|
12
|
+
from ..utils.todo_file_utils import get_todo_file_path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Try to import watchdog, fall back to polling if not available
|
|
17
|
+
try:
|
|
18
|
+
from watchdog.observers import Observer
|
|
19
|
+
from watchdog.events import FileSystemEventHandler, FileModifiedEvent
|
|
20
|
+
WATCHDOG_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
WATCHDOG_AVAILABLE = False
|
|
23
|
+
# Create dummy classes for when watchdog is not available
|
|
24
|
+
class FileSystemEventHandler:
|
|
25
|
+
pass
|
|
26
|
+
class Observer:
|
|
27
|
+
pass
|
|
28
|
+
logger.warning("watchdog library not available, file watching will use polling fallback")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FileTimestamp:
|
|
33
|
+
"""Information about a file's timestamp tracking."""
|
|
34
|
+
path: str
|
|
35
|
+
last_read: float
|
|
36
|
+
last_modified: float
|
|
37
|
+
size: int
|
|
38
|
+
last_agent_edit: Optional[float] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FreshnessResult(NamedTuple):
|
|
42
|
+
"""Result of file freshness check."""
|
|
43
|
+
is_fresh: bool
|
|
44
|
+
last_read: Optional[float] = None
|
|
45
|
+
current_modified: Optional[float] = None
|
|
46
|
+
conflict: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TodoFileWatcher(FileSystemEventHandler):
|
|
50
|
+
"""File system event handler for todo files."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, agent_id: str, file_path: str, service: 'FileFreshnessService'):
|
|
53
|
+
self.agent_id = agent_id
|
|
54
|
+
self.file_path = file_path
|
|
55
|
+
self.service = service
|
|
56
|
+
super().__init__()
|
|
57
|
+
|
|
58
|
+
def on_modified(self, event):
|
|
59
|
+
"""Handle file modification events."""
|
|
60
|
+
if event.is_directory:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Check if the modified file is our watched file
|
|
64
|
+
if os.path.abspath(event.src_path) == os.path.abspath(self.file_path):
|
|
65
|
+
logger.debug(f"Todo file modified: {self.file_path} for agent {self.agent_id}")
|
|
66
|
+
|
|
67
|
+
# Check if this was an external modification
|
|
68
|
+
reminder = self.service.generate_file_modification_reminder(self.file_path)
|
|
69
|
+
if reminder:
|
|
70
|
+
# File was modified externally, emit todo change reminder
|
|
71
|
+
emit_event('todo:file_changed', {
|
|
72
|
+
'agent_id': self.agent_id,
|
|
73
|
+
'file_path': self.file_path,
|
|
74
|
+
'reminder': reminder,
|
|
75
|
+
'timestamp': time.time(),
|
|
76
|
+
'current_stats': self._get_file_stats(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
def _get_file_stats(self) -> Dict[str, Union[float, int]]:
|
|
80
|
+
"""Get current file statistics."""
|
|
81
|
+
try:
|
|
82
|
+
if os.path.exists(self.file_path):
|
|
83
|
+
stats = os.stat(self.file_path)
|
|
84
|
+
return {
|
|
85
|
+
'mtime': stats.st_mtime,
|
|
86
|
+
'size': stats.st_size
|
|
87
|
+
}
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
return {'mtime': 0, 'size': 0}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PollingWatcher:
|
|
94
|
+
"""Fallback polling-based file watcher."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, agent_id: str, file_path: str, service: 'FileFreshnessService', interval: float = 1.0):
|
|
97
|
+
self.agent_id = agent_id
|
|
98
|
+
self.file_path = file_path
|
|
99
|
+
self.service = service
|
|
100
|
+
self.interval = interval
|
|
101
|
+
self.running = False
|
|
102
|
+
self.thread = None
|
|
103
|
+
self.last_mtime = None
|
|
104
|
+
|
|
105
|
+
# Get initial modification time
|
|
106
|
+
if os.path.exists(file_path):
|
|
107
|
+
self.last_mtime = os.path.getmtime(file_path)
|
|
108
|
+
|
|
109
|
+
def start(self):
|
|
110
|
+
"""Start polling for file changes."""
|
|
111
|
+
if self.running:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self.running = True
|
|
115
|
+
self.thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
116
|
+
self.thread.start()
|
|
117
|
+
logger.debug(f"Started polling watcher for {self.file_path}")
|
|
118
|
+
|
|
119
|
+
def stop(self):
|
|
120
|
+
"""Stop polling for file changes."""
|
|
121
|
+
self.running = False
|
|
122
|
+
if self.thread and self.thread.is_alive():
|
|
123
|
+
self.thread.join(timeout=2.0)
|
|
124
|
+
logger.debug(f"Stopped polling watcher for {self.file_path}")
|
|
125
|
+
|
|
126
|
+
def _poll_loop(self):
|
|
127
|
+
"""Main polling loop."""
|
|
128
|
+
while self.running:
|
|
129
|
+
try:
|
|
130
|
+
if os.path.exists(self.file_path):
|
|
131
|
+
current_mtime = os.path.getmtime(self.file_path)
|
|
132
|
+
|
|
133
|
+
if self.last_mtime is not None and current_mtime > self.last_mtime:
|
|
134
|
+
# File was modified
|
|
135
|
+
logger.debug(f"Polling detected modification: {self.file_path}")
|
|
136
|
+
|
|
137
|
+
reminder = self.service.generate_file_modification_reminder(self.file_path)
|
|
138
|
+
if reminder:
|
|
139
|
+
emit_event('todo:file_changed', {
|
|
140
|
+
'agent_id': self.agent_id,
|
|
141
|
+
'file_path': self.file_path,
|
|
142
|
+
'reminder': reminder,
|
|
143
|
+
'timestamp': time.time(),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
self.last_mtime = current_mtime
|
|
147
|
+
elif self.last_mtime is not None:
|
|
148
|
+
# File was deleted
|
|
149
|
+
logger.debug(f"Polling detected deletion: {self.file_path}")
|
|
150
|
+
self.last_mtime = None
|
|
151
|
+
|
|
152
|
+
time.sleep(self.interval)
|
|
153
|
+
|
|
154
|
+
except Exception as error:
|
|
155
|
+
logger.error(f"Error in polling loop for {self.file_path}: {error}")
|
|
156
|
+
time.sleep(self.interval)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class FileFreshnessState:
|
|
161
|
+
"""State container for file freshness tracking."""
|
|
162
|
+
read_timestamps: Dict[str, FileTimestamp]
|
|
163
|
+
edit_conflicts: Set[str]
|
|
164
|
+
session_files: Set[str]
|
|
165
|
+
watched_todo_files: Dict[str, str] # agent_id -> file_path
|
|
166
|
+
file_watchers: Dict[str, Union[Observer, PollingWatcher]] # agent_id -> watcher
|
|
167
|
+
todo_handlers: Dict[str, TodoFileWatcher] # agent_id -> handler
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class FileFreshnessService:
|
|
171
|
+
"""Service for tracking file freshness and changes."""
|
|
172
|
+
|
|
173
|
+
def __init__(self):
|
|
174
|
+
self.state = FileFreshnessState(
|
|
175
|
+
read_timestamps={},
|
|
176
|
+
edit_conflicts=set(),
|
|
177
|
+
session_files=set(),
|
|
178
|
+
watched_todo_files={},
|
|
179
|
+
file_watchers={},
|
|
180
|
+
todo_handlers={}
|
|
181
|
+
)
|
|
182
|
+
self.setup_event_listeners()
|
|
183
|
+
|
|
184
|
+
def setup_event_listeners(self) -> None:
|
|
185
|
+
"""Setup event listeners for session management."""
|
|
186
|
+
# Listen for session startup events
|
|
187
|
+
add_event_listener('session:startup', self._handle_session_startup)
|
|
188
|
+
|
|
189
|
+
# Listen for todo change events
|
|
190
|
+
add_event_listener('todo:changed', self._handle_todo_changed)
|
|
191
|
+
|
|
192
|
+
# Listen for file access events
|
|
193
|
+
add_event_listener('file:read', self._handle_file_read)
|
|
194
|
+
|
|
195
|
+
# Listen for file edit events
|
|
196
|
+
add_event_listener('file:edited', self._handle_file_edited)
|
|
197
|
+
|
|
198
|
+
def _handle_session_startup(self, context: EventContext) -> None:
|
|
199
|
+
"""Handle session startup event."""
|
|
200
|
+
self.reset_session()
|
|
201
|
+
logger.info("File freshness session reset on startup")
|
|
202
|
+
|
|
203
|
+
def _handle_todo_changed(self, context: EventContext) -> None:
|
|
204
|
+
"""Handle todo change event."""
|
|
205
|
+
# Update last todo update time if needed
|
|
206
|
+
logger.debug("Todo changed event received")
|
|
207
|
+
|
|
208
|
+
def _handle_file_read(self, context: EventContext) -> None:
|
|
209
|
+
"""Handle file read event."""
|
|
210
|
+
# This handler is for external events, not for self-generated events
|
|
211
|
+
# We don't need to do anything here as the event was already processed
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
def _handle_file_edited(self, context: EventContext) -> None:
|
|
215
|
+
"""Handle file edit event."""
|
|
216
|
+
# This handler is for external events, not for self-generated events
|
|
217
|
+
# We don't need to do anything here as the event was already processed
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
def record_file_read(self, file_path: Union[str, Path]) -> None:
|
|
221
|
+
"""Record file read operation with timestamp tracking."""
|
|
222
|
+
path_str = str(file_path)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
if not os.path.exists(path_str):
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
stats = os.stat(path_str)
|
|
229
|
+
timestamp = FileTimestamp(
|
|
230
|
+
path=path_str,
|
|
231
|
+
last_read=time.time(),
|
|
232
|
+
last_modified=stats.st_mtime,
|
|
233
|
+
size=stats.st_size
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.state.read_timestamps[path_str] = timestamp
|
|
237
|
+
self.state.session_files.add(path_str)
|
|
238
|
+
|
|
239
|
+
# Emit file read event
|
|
240
|
+
emit_event('file:read', {
|
|
241
|
+
'file_path': path_str,
|
|
242
|
+
'timestamp': timestamp.last_read,
|
|
243
|
+
'size': timestamp.size,
|
|
244
|
+
'modified': timestamp.last_modified
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
logger.debug(f"Recorded file read: {path_str}")
|
|
248
|
+
|
|
249
|
+
except Exception as error:
|
|
250
|
+
logger.error(f"Error recording file read for {path_str}: {error}")
|
|
251
|
+
|
|
252
|
+
def check_file_freshness(self, file_path: Union[str, Path]) -> FreshnessResult:
|
|
253
|
+
"""Check if file has been modified since last read."""
|
|
254
|
+
path_str = str(file_path)
|
|
255
|
+
recorded = self.state.read_timestamps.get(path_str)
|
|
256
|
+
|
|
257
|
+
if not recorded:
|
|
258
|
+
return FreshnessResult(is_fresh=True, conflict=False)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
if not os.path.exists(path_str):
|
|
262
|
+
return FreshnessResult(is_fresh=False, conflict=True)
|
|
263
|
+
|
|
264
|
+
current_stats = os.stat(path_str)
|
|
265
|
+
is_fresh = current_stats.st_mtime <= recorded.last_modified
|
|
266
|
+
conflict = not is_fresh
|
|
267
|
+
|
|
268
|
+
if conflict:
|
|
269
|
+
self.state.edit_conflicts.add(path_str)
|
|
270
|
+
|
|
271
|
+
# Emit file conflict event
|
|
272
|
+
emit_event('file:conflict', {
|
|
273
|
+
'file_path': path_str,
|
|
274
|
+
'last_read': recorded.last_read,
|
|
275
|
+
'last_modified': recorded.last_modified,
|
|
276
|
+
'current_modified': current_stats.st_mtime,
|
|
277
|
+
'size_diff': current_stats.st_size - recorded.size
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
logger.warning(f"File conflict detected: {path_str}")
|
|
281
|
+
|
|
282
|
+
return FreshnessResult(
|
|
283
|
+
is_fresh=is_fresh,
|
|
284
|
+
last_read=recorded.last_read,
|
|
285
|
+
current_modified=current_stats.st_mtime,
|
|
286
|
+
conflict=conflict
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
except Exception as error:
|
|
290
|
+
logger.error(f"Error checking freshness for {path_str}: {error}")
|
|
291
|
+
return FreshnessResult(is_fresh=False, conflict=True)
|
|
292
|
+
|
|
293
|
+
def record_file_edit(self, file_path: Union[str, Path], content: Optional[str] = None) -> None:
|
|
294
|
+
"""Record file edit operation by Agent."""
|
|
295
|
+
path_str = str(file_path)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
now = time.time()
|
|
299
|
+
|
|
300
|
+
# Update recorded timestamp after edit
|
|
301
|
+
if os.path.exists(path_str):
|
|
302
|
+
stats = os.stat(path_str)
|
|
303
|
+
existing = self.state.read_timestamps.get(path_str)
|
|
304
|
+
|
|
305
|
+
if existing:
|
|
306
|
+
existing.last_modified = stats.st_mtime
|
|
307
|
+
existing.size = stats.st_size
|
|
308
|
+
existing.last_agent_edit = now
|
|
309
|
+
self.state.read_timestamps[path_str] = existing
|
|
310
|
+
else:
|
|
311
|
+
# Create new record for Agent-edited file
|
|
312
|
+
timestamp = FileTimestamp(
|
|
313
|
+
path=path_str,
|
|
314
|
+
last_read=now,
|
|
315
|
+
last_modified=stats.st_mtime,
|
|
316
|
+
size=stats.st_size,
|
|
317
|
+
last_agent_edit=now
|
|
318
|
+
)
|
|
319
|
+
self.state.read_timestamps[path_str] = timestamp
|
|
320
|
+
|
|
321
|
+
# Remove from conflicts since we just edited it
|
|
322
|
+
self.state.edit_conflicts.discard(path_str)
|
|
323
|
+
|
|
324
|
+
# Emit file edit event
|
|
325
|
+
emit_event('file:edited', {
|
|
326
|
+
'file_path': path_str,
|
|
327
|
+
'timestamp': now,
|
|
328
|
+
'content_length': len(content) if content else 0,
|
|
329
|
+
'source': 'agent'
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
logger.debug(f"Recorded file edit: {path_str}")
|
|
333
|
+
|
|
334
|
+
except Exception as error:
|
|
335
|
+
logger.error(f"Error recording file edit for {path_str}: {error}")
|
|
336
|
+
|
|
337
|
+
def generate_file_modification_reminder(self, file_path: Union[str, Path]) -> Optional[str]:
|
|
338
|
+
"""Generate reminder message for externally modified files."""
|
|
339
|
+
path_str = str(file_path)
|
|
340
|
+
recorded = self.state.read_timestamps.get(path_str)
|
|
341
|
+
|
|
342
|
+
if not recorded:
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
if not os.path.exists(path_str):
|
|
347
|
+
return f"Note: {path_str} was deleted since last read."
|
|
348
|
+
|
|
349
|
+
current_stats = os.stat(path_str)
|
|
350
|
+
is_modified = current_stats.st_mtime > recorded.last_modified
|
|
351
|
+
|
|
352
|
+
if not is_modified:
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
# Check if this was an Agent-initiated change
|
|
356
|
+
# Use small time tolerance to handle filesystem timestamp precision issues
|
|
357
|
+
TIME_TOLERANCE_MS = 0.1 # 100ms in seconds
|
|
358
|
+
if (recorded.last_agent_edit and
|
|
359
|
+
recorded.last_agent_edit >= recorded.last_modified - TIME_TOLERANCE_MS):
|
|
360
|
+
# Agent modified this file recently, no reminder needed
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
# External modification detected - generate reminder
|
|
364
|
+
return f"Note: {path_str} was modified externally since last read. The file may have changed outside of this session."
|
|
365
|
+
|
|
366
|
+
except Exception as error:
|
|
367
|
+
logger.error(f"Error checking modification for {path_str}: {error}")
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
def get_conflicted_files(self) -> List[str]:
|
|
371
|
+
"""Get list of files with edit conflicts."""
|
|
372
|
+
return list(self.state.edit_conflicts)
|
|
373
|
+
|
|
374
|
+
def get_session_files(self) -> List[str]:
|
|
375
|
+
"""Get list of files accessed in current session."""
|
|
376
|
+
return list(self.state.session_files)
|
|
377
|
+
|
|
378
|
+
def reset_session(self) -> None:
|
|
379
|
+
"""Reset session state."""
|
|
380
|
+
# Clean up existing todo file watchers
|
|
381
|
+
for agent_id in list(self.state.watched_todo_files.keys()):
|
|
382
|
+
self.stop_watching_todo_file(agent_id)
|
|
383
|
+
|
|
384
|
+
self.state = FileFreshnessState(
|
|
385
|
+
read_timestamps={},
|
|
386
|
+
edit_conflicts=set(),
|
|
387
|
+
session_files=set(),
|
|
388
|
+
watched_todo_files={},
|
|
389
|
+
file_watchers={},
|
|
390
|
+
todo_handlers={}
|
|
391
|
+
)
|
|
392
|
+
logger.info("File freshness session reset")
|
|
393
|
+
|
|
394
|
+
def get_file_info(self, file_path: Union[str, Path]) -> Optional[FileTimestamp]:
|
|
395
|
+
"""Get file timestamp information."""
|
|
396
|
+
path_str = str(file_path)
|
|
397
|
+
return self.state.read_timestamps.get(path_str)
|
|
398
|
+
|
|
399
|
+
def is_file_tracked(self, file_path: Union[str, Path]) -> bool:
|
|
400
|
+
"""Check if file is being tracked."""
|
|
401
|
+
path_str = str(file_path)
|
|
402
|
+
return path_str in self.state.read_timestamps
|
|
403
|
+
|
|
404
|
+
def get_important_files(self, max_files: int = 5) -> List[Dict[str, Union[str, float, int]]]:
|
|
405
|
+
"""
|
|
406
|
+
Retrieves files prioritized for recovery during conversation compression.
|
|
407
|
+
|
|
408
|
+
Selects recently accessed files based on:
|
|
409
|
+
- File access recency (most recent first)
|
|
410
|
+
- File type relevance (excludes dependencies, build artifacts)
|
|
411
|
+
- Development workflow importance
|
|
412
|
+
"""
|
|
413
|
+
files = []
|
|
414
|
+
for path, info in self.state.read_timestamps.items():
|
|
415
|
+
if self._is_valid_for_recovery(path):
|
|
416
|
+
files.append({
|
|
417
|
+
'path': path,
|
|
418
|
+
'timestamp': info.last_read,
|
|
419
|
+
'size': info.size
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
# Sort by timestamp (newest first) and limit results
|
|
423
|
+
files.sort(key=lambda x: x['timestamp'], reverse=True)
|
|
424
|
+
return files[:max_files]
|
|
425
|
+
|
|
426
|
+
def _is_valid_for_recovery(self, file_path: str) -> bool:
|
|
427
|
+
"""
|
|
428
|
+
Determines which files are suitable for automatic recovery.
|
|
429
|
+
|
|
430
|
+
Excludes files that are typically not relevant for development context:
|
|
431
|
+
- Build artifacts and generated files
|
|
432
|
+
- Dependencies and cached files
|
|
433
|
+
- Temporary files and system directories
|
|
434
|
+
"""
|
|
435
|
+
return (
|
|
436
|
+
'node_modules' not in file_path and
|
|
437
|
+
'.git' not in file_path and
|
|
438
|
+
not file_path.startswith('/tmp') and
|
|
439
|
+
'.cache' not in file_path and
|
|
440
|
+
'dist/' not in file_path and
|
|
441
|
+
'build/' not in file_path and
|
|
442
|
+
'__pycache__' not in file_path and
|
|
443
|
+
'.pyc' not in file_path
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def start_watching_todo_file(self, agent_id: str, file_path: Optional[str] = None) -> None:
|
|
447
|
+
"""Start watching todo file for an agent."""
|
|
448
|
+
try:
|
|
449
|
+
# Use provided file_path or generate default path using todo_file_utils
|
|
450
|
+
if file_path is None:
|
|
451
|
+
file_path = get_todo_file_path(agent_id)
|
|
452
|
+
|
|
453
|
+
# Don't watch if already watching
|
|
454
|
+
if agent_id in self.state.watched_todo_files:
|
|
455
|
+
logger.debug(f"Already watching todo file for agent {agent_id}")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
self.state.watched_todo_files[agent_id] = file_path
|
|
459
|
+
|
|
460
|
+
# Record initial state if file exists
|
|
461
|
+
if os.path.exists(file_path):
|
|
462
|
+
self.record_file_read(file_path)
|
|
463
|
+
|
|
464
|
+
# Start watching for changes
|
|
465
|
+
if WATCHDOG_AVAILABLE:
|
|
466
|
+
self._start_watchdog_watcher(agent_id, file_path)
|
|
467
|
+
else:
|
|
468
|
+
self._start_polling_watcher(agent_id, file_path)
|
|
469
|
+
|
|
470
|
+
logger.info(f"Started watching todo file for agent {agent_id}: {file_path}")
|
|
471
|
+
|
|
472
|
+
except Exception as error:
|
|
473
|
+
logger.error(f"Error starting todo file watch for agent {agent_id}: {error}")
|
|
474
|
+
|
|
475
|
+
def stop_watching_todo_file(self, agent_id: str) -> None:
|
|
476
|
+
"""Stop watching todo file for an agent."""
|
|
477
|
+
try:
|
|
478
|
+
if agent_id not in self.state.watched_todo_files:
|
|
479
|
+
logger.debug(f"Not watching todo file for agent {agent_id}")
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Stop the appropriate watcher
|
|
483
|
+
if agent_id in self.state.file_watchers:
|
|
484
|
+
watcher = self.state.file_watchers[agent_id]
|
|
485
|
+
|
|
486
|
+
if isinstance(watcher, Observer):
|
|
487
|
+
watcher.stop()
|
|
488
|
+
watcher.join(timeout=2.0)
|
|
489
|
+
elif isinstance(watcher, PollingWatcher):
|
|
490
|
+
watcher.stop()
|
|
491
|
+
|
|
492
|
+
del self.state.file_watchers[agent_id]
|
|
493
|
+
|
|
494
|
+
# Clean up handler
|
|
495
|
+
if agent_id in self.state.todo_handlers:
|
|
496
|
+
del self.state.todo_handlers[agent_id]
|
|
497
|
+
|
|
498
|
+
# Remove from watched files
|
|
499
|
+
file_path = self.state.watched_todo_files.pop(agent_id, None)
|
|
500
|
+
|
|
501
|
+
logger.info(f"Stopped watching todo file for agent {agent_id}: {file_path}")
|
|
502
|
+
|
|
503
|
+
except Exception as error:
|
|
504
|
+
logger.error(f"Error stopping todo file watch for agent {agent_id}: {error}")
|
|
505
|
+
|
|
506
|
+
def _start_watchdog_watcher(self, agent_id: str, file_path: str) -> None:
|
|
507
|
+
"""Start watchdog-based file watcher."""
|
|
508
|
+
try:
|
|
509
|
+
# Create event handler
|
|
510
|
+
handler = TodoFileWatcher(agent_id, file_path, self)
|
|
511
|
+
self.state.todo_handlers[agent_id] = handler
|
|
512
|
+
|
|
513
|
+
# Create observer and watch the directory containing the file
|
|
514
|
+
observer = Observer()
|
|
515
|
+
watch_dir = os.path.dirname(os.path.abspath(file_path))
|
|
516
|
+
|
|
517
|
+
# Create directory if it doesn't exist
|
|
518
|
+
os.makedirs(watch_dir, exist_ok=True)
|
|
519
|
+
|
|
520
|
+
observer.schedule(handler, watch_dir, recursive=False)
|
|
521
|
+
observer.start()
|
|
522
|
+
|
|
523
|
+
self.state.file_watchers[agent_id] = observer
|
|
524
|
+
logger.debug(f"Started watchdog watcher for {file_path}")
|
|
525
|
+
|
|
526
|
+
except Exception as error:
|
|
527
|
+
logger.error(f"Error starting watchdog watcher for {file_path}: {error}")
|
|
528
|
+
# Fall back to polling
|
|
529
|
+
self._start_polling_watcher(agent_id, file_path)
|
|
530
|
+
|
|
531
|
+
def _start_polling_watcher(self, agent_id: str, file_path: str) -> None:
|
|
532
|
+
"""Start polling-based file watcher."""
|
|
533
|
+
try:
|
|
534
|
+
watcher = PollingWatcher(agent_id, file_path, self)
|
|
535
|
+
watcher.start()
|
|
536
|
+
|
|
537
|
+
self.state.file_watchers[agent_id] = watcher
|
|
538
|
+
logger.debug(f"Started polling watcher for {file_path}")
|
|
539
|
+
|
|
540
|
+
except Exception as error:
|
|
541
|
+
logger.error(f"Error starting polling watcher for {file_path}: {error}")
|
|
542
|
+
|
|
543
|
+
def get_watched_files(self) -> Dict[str, str]:
|
|
544
|
+
"""Get currently watched todo files."""
|
|
545
|
+
return self.state.watched_todo_files.copy()
|
|
546
|
+
|
|
547
|
+
def is_watching_agent(self, agent_id: str) -> bool:
|
|
548
|
+
"""Check if we're watching todo file for an agent."""
|
|
549
|
+
return agent_id in self.state.watched_todo_files
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# Global service instance
|
|
553
|
+
file_freshness_service = FileFreshnessService()
|
|
554
|
+
|
|
555
|
+
# Convenience functions for external use
|
|
556
|
+
def record_file_read(file_path: Union[str, Path]) -> None:
|
|
557
|
+
"""Record file read operation."""
|
|
558
|
+
file_freshness_service.record_file_read(file_path)
|
|
559
|
+
|
|
560
|
+
def record_file_edit(file_path: Union[str, Path], content: Optional[str] = None) -> None:
|
|
561
|
+
"""Record file edit operation."""
|
|
562
|
+
file_freshness_service.record_file_edit(file_path, content)
|
|
563
|
+
|
|
564
|
+
def check_file_freshness(file_path: Union[str, Path]) -> FreshnessResult:
|
|
565
|
+
"""Check file freshness."""
|
|
566
|
+
return file_freshness_service.check_file_freshness(file_path)
|
|
567
|
+
|
|
568
|
+
def generate_file_modification_reminder(file_path: Union[str, Path]) -> Optional[str]:
|
|
569
|
+
"""Generate file modification reminder."""
|
|
570
|
+
return file_freshness_service.generate_file_modification_reminder(file_path)
|
|
571
|
+
|
|
572
|
+
def reset_file_freshness_session() -> None:
|
|
573
|
+
"""Reset file freshness session."""
|
|
574
|
+
file_freshness_service.reset_session()
|
|
575
|
+
|
|
576
|
+
def start_watching_todo_file(agent_id: str, file_path: Optional[str] = None) -> None:
|
|
577
|
+
"""Start watching todo file for an agent."""
|
|
578
|
+
file_freshness_service.start_watching_todo_file(agent_id, file_path)
|
|
579
|
+
|
|
580
|
+
def stop_watching_todo_file(agent_id: str) -> None:
|
|
581
|
+
"""Stop watching todo file for an agent."""
|
|
582
|
+
file_freshness_service.stop_watching_todo_file(agent_id)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Minion Code Tools Package
|
|
5
|
+
A collection of development tools for code analysis and manipulation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import base classes from minion framework
|
|
9
|
+
from minion.tools import BaseTool, tool, ToolCollection
|
|
10
|
+
|
|
11
|
+
# Import individual tools
|
|
12
|
+
from .file_read_tool import FileReadTool
|
|
13
|
+
from .file_write_tool import FileWriteTool
|
|
14
|
+
from .file_edit_tool import FileEditTool
|
|
15
|
+
from .multi_edit_tool import MultiEditTool
|
|
16
|
+
from .bash_tool import BashTool
|
|
17
|
+
from .grep_tool import GrepTool
|
|
18
|
+
from .glob_tool import GlobTool
|
|
19
|
+
from .ls_tool import LsTool
|
|
20
|
+
from .python_interpreter_tool import PythonInterpreterTool
|
|
21
|
+
from .user_input_tool import UserInputTool
|
|
22
|
+
|
|
23
|
+
from .todo_write_tool import TodoWriteTool
|
|
24
|
+
from .todo_read_tool import TodoReadTool
|
|
25
|
+
|
|
26
|
+
# Tool mapping
|
|
27
|
+
TOOL_MAPPING = {
|
|
28
|
+
tool_class.name: tool_class
|
|
29
|
+
for tool_class in [
|
|
30
|
+
FileReadTool,
|
|
31
|
+
FileWriteTool,
|
|
32
|
+
FileEditTool,
|
|
33
|
+
MultiEditTool,
|
|
34
|
+
BashTool,
|
|
35
|
+
GrepTool,
|
|
36
|
+
GlobTool,
|
|
37
|
+
LsTool,
|
|
38
|
+
PythonInterpreterTool,
|
|
39
|
+
UserInputTool,
|
|
40
|
+
|
|
41
|
+
TodoWriteTool,
|
|
42
|
+
TodoReadTool,
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Base classes
|
|
48
|
+
# File system tools
|
|
49
|
+
"FileReadTool",
|
|
50
|
+
"FileWriteTool",
|
|
51
|
+
"FileEditTool",
|
|
52
|
+
"FileEditToolNew",
|
|
53
|
+
"MultiEditTool",
|
|
54
|
+
"BashTool",
|
|
55
|
+
"GrepTool",
|
|
56
|
+
"GlobTool",
|
|
57
|
+
"LsTool",
|
|
58
|
+
# Execution tools
|
|
59
|
+
"PythonInterpreterTool",
|
|
60
|
+
# Web tools
|
|
61
|
+
# Interactive tools
|
|
62
|
+
"UserInputTool",
|
|
63
|
+
|
|
64
|
+
# Todo tools
|
|
65
|
+
"TodoWriteTool",
|
|
66
|
+
"TodoReadTool",
|
|
67
|
+
# Utilities
|
|
68
|
+
"TOOL_MAPPING",
|
|
69
|
+
]
|