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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
  115. {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
- logger.warning("watchdog library not available, file watching will use polling fallback")
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: 'FileFreshnessService'):
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(f"Todo file modified: {self.file_path} for agent {self.agent_id}")
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('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
-
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 {'mtime': 0, 'size': 0}
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__(self, agent_id: str, file_path: str, service: 'FileFreshnessService', interval: float = 1.0):
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(self.file_path)
152
+
153
+ reminder = self.service.generate_file_modification_reminder(
154
+ self.file_path
155
+ )
138
156
  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
-
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('session:startup', self._handle_session_startup)
188
-
209
+ add_event_listener("session:startup", self._handle_session_startup)
210
+
189
211
  # Listen for todo change events
190
- add_event_listener('todo:changed', self._handle_todo_changed)
191
-
212
+ add_event_listener("todo:changed", self._handle_todo_changed)
213
+
192
214
  # Listen for file access events
193
- add_event_listener('file:read', self._handle_file_read)
194
-
215
+ add_event_listener("file:read", self._handle_file_read)
216
+
195
217
  # Listen for file edit events
196
- add_event_listener('file:edited', self._handle_file_edited)
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('file:read', {
241
- 'file_path': path_str,
242
- 'timestamp': timestamp.last_read,
243
- 'size': timestamp.size,
244
- 'modified': timestamp.last_modified
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('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
-
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(self, file_path: Union[str, Path], content: Optional[str] = None) -> None:
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('file:edited', {
326
- 'file_path': path_str,
327
- 'timestamp': now,
328
- 'content_length': len(content) if content else 0,
329
- 'source': 'agent'
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(self, file_path: Union[str, Path]) -> Optional[str]:
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 (recorded.last_agent_edit and
359
- recorded.last_agent_edit >= recorded.last_modified - TIME_TOLERANCE_MS):
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(self, max_files: int = 5) -> List[Dict[str, Union[str, float, int]]]:
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
- 'path': path,
418
- 'timestamp': info.last_read,
419
- 'size': info.size
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['timestamp'], reverse=True)
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
- '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
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(self, agent_id: str, file_path: Optional[str] = None) -> None:
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(f"Error starting todo file watch for agent {agent_id}: {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(f"Error stopping todo file watch for agent {agent_id}: {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
- def record_file_edit(file_path: Union[str, Path], content: Optional[str] = None) -> None:
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)