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