devloop 0.2.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.
Files changed (55) hide show
  1. devloop/__init__.py +3 -0
  2. devloop/agents/__init__.py +33 -0
  3. devloop/agents/agent_health_monitor.py +105 -0
  4. devloop/agents/ci_monitor.py +237 -0
  5. devloop/agents/code_rabbit.py +248 -0
  6. devloop/agents/doc_lifecycle.py +374 -0
  7. devloop/agents/echo.py +24 -0
  8. devloop/agents/file_logger.py +46 -0
  9. devloop/agents/formatter.py +511 -0
  10. devloop/agents/git_commit_assistant.py +421 -0
  11. devloop/agents/linter.py +399 -0
  12. devloop/agents/performance_profiler.py +284 -0
  13. devloop/agents/security_scanner.py +322 -0
  14. devloop/agents/snyk.py +292 -0
  15. devloop/agents/test_runner.py +484 -0
  16. devloop/agents/type_checker.py +242 -0
  17. devloop/cli/__init__.py +1 -0
  18. devloop/cli/commands/__init__.py +1 -0
  19. devloop/cli/commands/custom_agents.py +144 -0
  20. devloop/cli/commands/feedback.py +161 -0
  21. devloop/cli/commands/summary.py +50 -0
  22. devloop/cli/main.py +430 -0
  23. devloop/cli/main_v1.py +144 -0
  24. devloop/collectors/__init__.py +17 -0
  25. devloop/collectors/base.py +55 -0
  26. devloop/collectors/filesystem.py +126 -0
  27. devloop/collectors/git.py +171 -0
  28. devloop/collectors/manager.py +159 -0
  29. devloop/collectors/process.py +221 -0
  30. devloop/collectors/system.py +195 -0
  31. devloop/core/__init__.py +21 -0
  32. devloop/core/agent.py +206 -0
  33. devloop/core/agent_template.py +498 -0
  34. devloop/core/amp_integration.py +166 -0
  35. devloop/core/auto_fix.py +224 -0
  36. devloop/core/config.py +272 -0
  37. devloop/core/context.py +0 -0
  38. devloop/core/context_store.py +530 -0
  39. devloop/core/contextual_feedback.py +311 -0
  40. devloop/core/custom_agent.py +439 -0
  41. devloop/core/debug_trace.py +289 -0
  42. devloop/core/event.py +105 -0
  43. devloop/core/event_store.py +316 -0
  44. devloop/core/feedback.py +311 -0
  45. devloop/core/learning.py +351 -0
  46. devloop/core/manager.py +219 -0
  47. devloop/core/performance.py +433 -0
  48. devloop/core/proactive_feedback.py +302 -0
  49. devloop/core/summary_formatter.py +159 -0
  50. devloop/core/summary_generator.py +275 -0
  51. devloop-0.2.0.dist-info/METADATA +705 -0
  52. devloop-0.2.0.dist-info/RECORD +55 -0
  53. devloop-0.2.0.dist-info/WHEEL +4 -0
  54. devloop-0.2.0.dist-info/entry_points.txt +3 -0
  55. devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
devloop/cli/main.py ADDED
@@ -0,0 +1,430 @@
1
+ """CLI entry point - v2 with real agents."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import signal
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.logging import RichHandler
12
+ from rich.table import Table
13
+
14
+ from .commands import custom_agents as custom_agents_cmd
15
+ from .commands import feedback as feedback_cmd
16
+ from .commands import summary as summary_cmd
17
+ from devloop.agents import (
18
+ AgentHealthMonitorAgent,
19
+ FormatterAgent,
20
+ GitCommitAssistantAgent,
21
+ LinterAgent,
22
+ PerformanceProfilerAgent,
23
+ SecurityScannerAgent,
24
+ TestRunnerAgent,
25
+ TypeCheckerAgent,
26
+ )
27
+ from devloop.collectors import FileSystemCollector
28
+ from devloop.core import (
29
+ AgentManager,
30
+ Config,
31
+ ConfigWrapper,
32
+ EventBus,
33
+ context_store,
34
+ event_store,
35
+ )
36
+ from devloop.core.amp_integration import check_agent_findings, show_agent_status
37
+
38
+ app = typer.Typer(
39
+ help="DevLoop - Development workflow automation", add_completion=False
40
+ )
41
+ console = Console()
42
+
43
+ app.add_typer(summary_cmd.app, name="summary")
44
+ app.add_typer(custom_agents_cmd.app, name="custom")
45
+ app.add_typer(feedback_cmd.app, name="feedback")
46
+
47
+
48
+ @app.command()
49
+ def amp_status():
50
+ """Show current agent status for Amp."""
51
+ import asyncio
52
+
53
+ result = asyncio.run(show_agent_status())
54
+ console.print_json(data=result)
55
+
56
+
57
+ @app.command()
58
+ def amp_findings():
59
+ """Show agent findings for Amp."""
60
+ import asyncio
61
+
62
+ result = asyncio.run(check_agent_findings())
63
+ console.print_json(data=result)
64
+
65
+
66
+ @app.command()
67
+ def amp_context():
68
+ """Show context store index for Amp."""
69
+ from pathlib import Path
70
+
71
+ # Try to read the context index
72
+ context_dir = Path(".devloop/context")
73
+ index_file = context_dir / "index.json"
74
+
75
+ if index_file.exists():
76
+ try:
77
+ import json
78
+
79
+ with open(index_file) as f:
80
+ data = json.load(f)
81
+ console.print_json(data=data)
82
+ except Exception as e:
83
+ console.print(f"[red]Error reading context: {e}[/red]")
84
+ else:
85
+ console.print(
86
+ "[yellow]No context index found. Start agents with 'devloop watch .' first.[/yellow]"
87
+ )
88
+
89
+
90
+ def setup_logging(verbose: bool = False):
91
+ """Setup logging configuration."""
92
+ level = logging.DEBUG if verbose else logging.INFO
93
+
94
+ logging.basicConfig(
95
+ level=level,
96
+ format="%(message)s",
97
+ handlers=[RichHandler(console=console, rich_tracebacks=True)],
98
+ )
99
+
100
+
101
+ def run_daemon(path: Path, config_path: Path | None, verbose: bool):
102
+ """Run devloop in daemon/background mode."""
103
+ import os
104
+ import sys
105
+
106
+ # Fork to background
107
+ try:
108
+ pid = os.fork()
109
+ if pid > 0:
110
+ # Parent process - exit
111
+ console.print(
112
+ f"[green]✓[/green] DevLoop started in background (PID: {pid})"
113
+ )
114
+ console.print("[dim]Run 'devloop stop' to stop the daemon[/dim]")
115
+ sys.exit(0)
116
+ except OSError as e:
117
+ console.print(f"[red]✗[/red] Failed to start daemon: {e}")
118
+ sys.exit(1)
119
+
120
+ # Child process continues
121
+ # Convert path to absolute before changing directory
122
+ project_dir = path.resolve()
123
+
124
+ os.chdir("/")
125
+ os.setsid()
126
+ os.umask(0)
127
+
128
+ # Redirect stdout/stderr to log file
129
+ log_file = project_dir / ".devloop" / "devloop.log"
130
+ log_file.parent.mkdir(parents=True, exist_ok=True)
131
+
132
+ with open(log_file, "a") as f:
133
+ os.dup2(f.fileno(), sys.stdout.fileno())
134
+ os.dup2(f.fileno(), sys.stderr.fileno())
135
+
136
+ # Setup logging for daemon
137
+ setup_logging(verbose)
138
+
139
+ # Write PID file
140
+ pid_file = project_dir / ".devloop" / "devloop.pid"
141
+ with open(pid_file, "w") as f:
142
+ f.write(str(os.getpid()))
143
+
144
+ print(f"DevLoop v2 daemon started (PID: {os.getpid()})")
145
+ print(f"Watching: {project_dir}")
146
+
147
+ # Run the async main loop (will run indefinitely)
148
+ # Ensure config_path is also absolute if specified
149
+ abs_config_path = (
150
+ config_path.resolve()
151
+ if config_path
152
+ else project_dir / ".devloop" / "agents.json"
153
+ )
154
+ try:
155
+ asyncio.run(watch_async(project_dir, abs_config_path))
156
+ except Exception as e:
157
+ import traceback
158
+
159
+ print(f"Daemon error: {e}")
160
+ traceback.print_exc()
161
+ finally:
162
+ # Clean up PID file
163
+ if pid_file.exists():
164
+ pid_file.unlink()
165
+
166
+
167
+ @app.command()
168
+ def watch(
169
+ path: Path = typer.Argument(Path.cwd(), help="Path to watch for changes"),
170
+ config_path: Optional[Path] = typer.Option(
171
+ None, "--config", help="Path to configuration file"
172
+ ),
173
+ verbose: bool = typer.Option(False, "--verbose", help="Verbose logging"),
174
+ foreground: bool = typer.Option(
175
+ False, "--foreground", help="Run in foreground (blocking mode) for debugging"
176
+ ),
177
+ ):
178
+ """
179
+ Watch a directory for file changes and run agents.
180
+
181
+ Agents will automatically lint, format, and test your code as you work.
182
+
183
+ Runs in background by default for coding agent integration.
184
+ Use --foreground for debugging/interactive mode.
185
+ """
186
+ if foreground:
187
+ # Run in foreground for debugging
188
+ setup_logging(verbose)
189
+ console.print("[bold green]DevLoop v2[/bold green]")
190
+ console.print(f"Watching: [cyan]{path.absolute()}[/cyan] (foreground mode)\\n")
191
+ else:
192
+ # Run in background (default)
193
+ run_daemon(path, config_path, verbose)
194
+ return
195
+
196
+ # Run the async main loop
197
+ try:
198
+ asyncio.run(watch_async(path, config_path))
199
+ except KeyboardInterrupt:
200
+ console.print("\n[yellow]Shutting down...[/yellow]")
201
+
202
+
203
+ async def watch_async(path: Path, config_path: Path | None):
204
+ """Async watch implementation."""
205
+ # Load configuration
206
+ if config_path:
207
+ # Ensure it's a Path object and convert to string
208
+ config_manager = Config(str(Path(config_path).resolve()))
209
+ else:
210
+ # Default to project .devloop/agents.json
211
+ config_manager = Config(str((path / ".devloop" / "agents.json").resolve()))
212
+ config_dict = config_manager.load()
213
+ config = ConfigWrapper(config_dict)
214
+
215
+ # Create event bus
216
+ event_bus = EventBus()
217
+
218
+ # Initialize context store
219
+ context_store.context_dir = path / ".devloop" / "context"
220
+ await context_store.initialize()
221
+
222
+ # Initialize event store
223
+ event_store.db_path = path / ".devloop" / "events.db"
224
+ await event_store.initialize()
225
+ console.print(f"[dim]Context store: {context_store.context_dir}[/dim]")
226
+ console.print(f"[dim]Event store: {event_store.db_path}[/dim]")
227
+
228
+ # Create agent manager with project directory
229
+ agent_manager = AgentManager(event_bus, project_dir=path)
230
+
231
+ # Create filesystem collector
232
+ fs_config = {"watch_paths": [str(path)]}
233
+ fs_collector = FileSystemCollector(event_bus=event_bus, config=fs_config)
234
+
235
+ # Create and register agents based on configuration
236
+ if config.is_agent_enabled("linter"):
237
+ linter_config = config.get_agent_config("linter") or {}
238
+ linter = LinterAgent(
239
+ name="linter",
240
+ triggers=linter_config.get("triggers", ["file:modified"]),
241
+ event_bus=event_bus,
242
+ config=linter_config.get("config", {}),
243
+ )
244
+ agent_manager.register(linter)
245
+
246
+ if config.is_agent_enabled("formatter"):
247
+ formatter_config = config.get_agent_config("formatter") or {}
248
+ formatter = FormatterAgent(
249
+ name="formatter",
250
+ triggers=formatter_config.get("triggers", ["file:modified"]),
251
+ event_bus=event_bus,
252
+ config=formatter_config.get("config", {}),
253
+ )
254
+ agent_manager.register(formatter)
255
+
256
+ if config.is_agent_enabled("test-runner"):
257
+ test_config = config.get_agent_config("test-runner") or {}
258
+ test_runner = TestRunnerAgent(
259
+ name="test-runner",
260
+ triggers=test_config.get("triggers", ["file:modified"]),
261
+ event_bus=event_bus,
262
+ config=test_config.get("config", {}),
263
+ )
264
+ agent_manager.register(test_runner)
265
+
266
+ if config.is_agent_enabled("agent-health-monitor"):
267
+ monitor_config = config.get_agent_config("agent-health-monitor") or {}
268
+ health_monitor = AgentHealthMonitorAgent(
269
+ name="agent-health-monitor",
270
+ triggers=monitor_config.get("triggers", ["agent:*:completed"]),
271
+ event_bus=event_bus,
272
+ config=monitor_config.get("config", {}),
273
+ )
274
+ agent_manager.register(health_monitor)
275
+
276
+ if config.is_agent_enabled("type-checker"):
277
+ type_config = config.get_agent_config("type-checker") or {}
278
+ type_checker = TypeCheckerAgent(
279
+ config=type_config.get("config", {}), event_bus=event_bus
280
+ )
281
+ agent_manager.register(type_checker)
282
+
283
+ if config.is_agent_enabled("security-scanner"):
284
+ security_config = config.get_agent_config("security-scanner") or {}
285
+ security_scanner = SecurityScannerAgent(
286
+ config=security_config.get("config", {}), event_bus=event_bus
287
+ )
288
+ agent_manager.register(security_scanner)
289
+
290
+ if config.is_agent_enabled("git-commit-assistant"):
291
+ commit_config = config.get_agent_config("git-commit-assistant") or {}
292
+ commit_assistant = GitCommitAssistantAgent(
293
+ config=commit_config.get("config", {}), event_bus=event_bus
294
+ )
295
+ agent_manager.register(commit_assistant)
296
+
297
+ if config.is_agent_enabled("performance-profiler"):
298
+ perf_config = config.get_agent_config("performance-profiler") or {}
299
+ performance_profiler = PerformanceProfilerAgent(
300
+ config=perf_config.get("config", {}), event_bus=event_bus
301
+ )
302
+ agent_manager.register(performance_profiler)
303
+
304
+ # Start everything
305
+ await fs_collector.start()
306
+ await agent_manager.start_all()
307
+
308
+ console.print("[green]✓[/green] Started agents:")
309
+ for agent_name in agent_manager.list_agents():
310
+ console.print(f" • [cyan]{agent_name}[/cyan]")
311
+
312
+ console.print("\n[dim]Waiting for file changes... (Ctrl+C to stop)[/dim]\n")
313
+
314
+ # Wait for shutdown signal
315
+ shutdown_event = asyncio.Event()
316
+
317
+ def signal_handler(sig, frame):
318
+ shutdown_event.set()
319
+
320
+ signal.signal(signal.SIGINT, signal_handler)
321
+ signal.signal(signal.SIGTERM, signal_handler)
322
+
323
+ # Keep running until shutdown
324
+ await shutdown_event.wait()
325
+
326
+ # Stop everything
327
+ await agent_manager.stop_all()
328
+ await fs_collector.stop()
329
+
330
+
331
+ @app.command()
332
+ def init(
333
+ path: Path = typer.Argument(Path.cwd(), help="Project directory"),
334
+ skip_config: bool = typer.Option(
335
+ False, "--skip-config", help="Skip creating configuration file"
336
+ ),
337
+ ):
338
+ """Initialize devloop in a project."""
339
+ claude_dir = path / ".devloop"
340
+
341
+ if claude_dir.exists():
342
+ console.print(f"[yellow]Directory already exists: {claude_dir}[/yellow]")
343
+ else:
344
+ claude_dir.mkdir(exist_ok=True)
345
+ console.print(f"[green]✓[/green] Created: {claude_dir}")
346
+ # Create default configuration
347
+ if not skip_config:
348
+ config_file = claude_dir / "agents.json"
349
+ if config_file.exists():
350
+ console.print(
351
+ f"[yellow]Configuration already exists: {config_file}[/yellow]"
352
+ )
353
+ else:
354
+ config = Config.default_config()
355
+ config.save(config_file)
356
+ console.print(f"[green]✓[/green] Created: {config_file}")
357
+
358
+ console.print("\n[green]✓[/green] Initialized!")
359
+ console.print("\nNext steps:")
360
+ console.print(f" 1. Review/edit: [cyan]{claude_dir / 'agents.json'}[/cyan]")
361
+ console.print(f" 2. Run: [cyan]devloop watch {path}[/cyan]")
362
+
363
+
364
+ @app.command()
365
+ def status():
366
+ """Show configuration and agent status."""
367
+ # Load configuration
368
+ config_manager = Config()
369
+ config_dict = config_manager.load()
370
+ config = ConfigWrapper(config_dict)
371
+
372
+ table = Table(title="Agent Configuration")
373
+
374
+ table.add_column("Agent", style="cyan")
375
+ table.add_column("Enabled", style="green")
376
+ table.add_column("Triggers", style="yellow")
377
+
378
+ for agent_name, agent_config in config.agents().items():
379
+ enabled = "✓" if agent_config.get("enabled") else "✗"
380
+ triggers = ", ".join(agent_config.get("triggers", []))
381
+ table.add_row(agent_name, enabled, triggers)
382
+
383
+ console.print(table)
384
+
385
+
386
+ @app.command()
387
+ def stop(path: Path = typer.Argument(Path.cwd(), help="Project directory")):
388
+ """Stop the background devloop daemon."""
389
+ import os
390
+ import signal
391
+
392
+ pid_file = path / ".devloop" / "devloop.pid"
393
+
394
+ if not pid_file.exists():
395
+ console.print(f"[yellow]No daemon running in {path}[/yellow]")
396
+ return
397
+
398
+ try:
399
+ with open(pid_file) as f:
400
+ pid = int(f.read().strip())
401
+
402
+ # Check if process is still running
403
+ os.kill(pid, 0) # Signal 0 just checks if process exists
404
+
405
+ # Send SIGTERM to gracefully stop
406
+ os.kill(pid, signal.SIGTERM)
407
+ console.print(f"[green]✓[/green] Stopped devloop daemon (PID: {pid})")
408
+
409
+ # Clean up files
410
+ pid_file.unlink()
411
+ log_file = path / ".devloop" / "devloop.log"
412
+ if log_file.exists():
413
+ console.print(f"[dim]Logs available at: {log_file}[/dim]")
414
+
415
+ except (ValueError, OSError) as e:
416
+ console.print(f"[red]✗[/red] Failed to stop daemon: {e}")
417
+ if pid_file.exists():
418
+ pid_file.unlink()
419
+
420
+
421
+ @app.command()
422
+ def version():
423
+ """Show version information."""
424
+ from devloop import __version__
425
+
426
+ console.print(f"DevLoop v{__version__}")
427
+
428
+
429
+ if __name__ == "__main__":
430
+ app()
devloop/cli/main_v1.py ADDED
@@ -0,0 +1,144 @@
1
+ """CLI entry point - prototype version."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import signal
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.logging import RichHandler
11
+
12
+ from devloop.agents import EchoAgent, FileLoggerAgent
13
+ from devloop.collectors import FileSystemCollector
14
+ from devloop.core import EventBus
15
+
16
+ app = typer.Typer(
17
+ help="Claude Agents - Development workflow automation (PROTOTYPE)",
18
+ add_completion=False,
19
+ )
20
+ console = Console()
21
+
22
+
23
+ def setup_logging(verbose: bool = False):
24
+ """Setup logging configuration."""
25
+ level = logging.DEBUG if verbose else logging.INFO
26
+
27
+ logging.basicConfig(
28
+ level=level,
29
+ format="%(message)s",
30
+ handlers=[RichHandler(console=console, rich_tracebacks=True)],
31
+ )
32
+
33
+
34
+ @app.command()
35
+ def watch(
36
+ path: Path = typer.Argument(Path.cwd(), help="Path to watch for changes"),
37
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging"),
38
+ ):
39
+ """
40
+ Watch a directory for file changes and run agents.
41
+
42
+ This is a prototype that demonstrates the core architecture:
43
+ - FileSystem collector watches for file changes
44
+ - Events are emitted to the EventBus
45
+ - Agents subscribe to events and process them
46
+
47
+ Press Ctrl+C to stop.
48
+ """
49
+ setup_logging(verbose)
50
+
51
+ console.print("[bold green]Claude Agents Prototype[/bold green]")
52
+ console.print(f"Watching: [cyan]{path.absolute()}[/cyan]\n")
53
+
54
+ # Run the async main loop
55
+ try:
56
+ asyncio.run(watch_async(path))
57
+ except KeyboardInterrupt:
58
+ console.print("\n[yellow]Shutting down...[/yellow]")
59
+
60
+
61
+ async def watch_async(path: Path):
62
+ """Async watch implementation."""
63
+ # Create event bus
64
+ event_bus = EventBus()
65
+
66
+ # Create filesystem collector
67
+ fs_collector = FileSystemCollector(event_bus=event_bus, watch_paths=[str(path)])
68
+
69
+ # Create agents
70
+ echo_agent = EchoAgent(
71
+ name="echo",
72
+ triggers=["file:*"], # Listen to all file events
73
+ event_bus=event_bus,
74
+ )
75
+
76
+ logger_agent = FileLoggerAgent(
77
+ name="file-logger",
78
+ triggers=["file:modified", "file:created"],
79
+ event_bus=event_bus,
80
+ )
81
+
82
+ # Start everything
83
+ await fs_collector.start()
84
+ await echo_agent.start()
85
+ await logger_agent.start()
86
+
87
+ console.print("[green]✓[/green] Agents started:")
88
+ console.print(" • [cyan]echo[/cyan] - logs all file events")
89
+ console.print(
90
+ " • [cyan]file-logger[/cyan] - writes changes to .devloop/file-changes.log"
91
+ )
92
+ console.print("\n[dim]Waiting for file changes... (Ctrl+C to stop)[/dim]\n")
93
+
94
+ # Wait for shutdown signal
95
+ shutdown_event = asyncio.Event()
96
+
97
+ def signal_handler(sig, frame):
98
+ shutdown_event.set()
99
+
100
+ signal.signal(signal.SIGINT, signal_handler)
101
+ signal.signal(signal.SIGTERM, signal_handler)
102
+
103
+ # Keep running until shutdown
104
+ await shutdown_event.wait()
105
+
106
+ # Stop everything
107
+ await echo_agent.stop()
108
+ await logger_agent.stop()
109
+ await fs_collector.stop()
110
+
111
+
112
+ @app.command()
113
+ def events(
114
+ count: int = typer.Option(10, "--count", "-n", help="Number of events to show")
115
+ ):
116
+ """Show recent events (from last watch session)."""
117
+ console.print("[yellow]Event history not yet implemented in prototype[/yellow]")
118
+ console.print("Events are logged to .devloop/file-changes.log")
119
+
120
+
121
+ @app.command()
122
+ def init(path: Path = typer.Argument(Path.cwd(), help="Project directory")):
123
+ """Initialize devloop in a project."""
124
+ claude_dir = path / ".devloop"
125
+
126
+ if claude_dir.exists():
127
+ console.print(f"[yellow]Directory already exists: {claude_dir}[/yellow]")
128
+ return
129
+
130
+ claude_dir.mkdir(exist_ok=True)
131
+ console.print(f"[green]✓[/green] Created: {claude_dir}")
132
+ console.print(f"\nRun [cyan]devloop watch {path}[/cyan] to start watching!")
133
+
134
+
135
+ @app.command()
136
+ def version():
137
+ """Show version information."""
138
+ from devloop import __version__
139
+
140
+ console.print(f"Claude Agents v{__version__} (PROTOTYPE)")
141
+
142
+
143
+ if __name__ == "__main__":
144
+ app()
@@ -0,0 +1,17 @@
1
+ """Event collectors."""
2
+
3
+ from .base import BaseCollector
4
+ from .filesystem import FileSystemCollector
5
+ from .git import GitCollector
6
+ from .manager import CollectorManager
7
+ from .process import ProcessCollector
8
+ from .system import SystemCollector
9
+
10
+ __all__ = [
11
+ "BaseCollector",
12
+ "FileSystemCollector",
13
+ "GitCollector",
14
+ "ProcessCollector",
15
+ "SystemCollector",
16
+ "CollectorManager",
17
+ ]
@@ -0,0 +1,55 @@
1
+ """Base collector class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, Optional
8
+
9
+ from devloop.core.event import Event, EventBus, Priority
10
+
11
+
12
+ class BaseCollector(ABC):
13
+ """Base class for all event collectors."""
14
+
15
+ def __init__(
16
+ self, name: str, event_bus: EventBus, config: Optional[Dict[str, Any]] = None
17
+ ):
18
+ self.name = name
19
+ self.event_bus = event_bus
20
+ self.config = config or {}
21
+ self.logger = logging.getLogger(f"collector.{name}")
22
+ self._running = False
23
+
24
+ @abstractmethod
25
+ async def start(self) -> None:
26
+ """Start the collector."""
27
+ pass
28
+
29
+ @abstractmethod
30
+ async def stop(self) -> None:
31
+ """Stop the collector."""
32
+ pass
33
+
34
+ @property
35
+ def is_running(self) -> bool:
36
+ """Check if collector is running."""
37
+ return self._running
38
+
39
+ def _set_running(self, running: bool) -> None:
40
+ """Set running state."""
41
+ self._running = running
42
+
43
+ async def _emit_event(
44
+ self,
45
+ event_type: str,
46
+ payload: Dict[str, Any],
47
+ priority: str = "normal",
48
+ source: Optional[str] = None,
49
+ ) -> None:
50
+ """Emit an event to the event bus."""
51
+ prio = Priority.NORMAL if priority == "normal" else Priority.HIGH
52
+ event = Event(
53
+ type=event_type, payload=payload, source=source or self.name, priority=prio
54
+ )
55
+ await self.event_bus.emit(event)