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.
- devloop/__init__.py +3 -0
- devloop/agents/__init__.py +33 -0
- devloop/agents/agent_health_monitor.py +105 -0
- devloop/agents/ci_monitor.py +237 -0
- devloop/agents/code_rabbit.py +248 -0
- devloop/agents/doc_lifecycle.py +374 -0
- devloop/agents/echo.py +24 -0
- devloop/agents/file_logger.py +46 -0
- devloop/agents/formatter.py +511 -0
- devloop/agents/git_commit_assistant.py +421 -0
- devloop/agents/linter.py +399 -0
- devloop/agents/performance_profiler.py +284 -0
- devloop/agents/security_scanner.py +322 -0
- devloop/agents/snyk.py +292 -0
- devloop/agents/test_runner.py +484 -0
- devloop/agents/type_checker.py +242 -0
- devloop/cli/__init__.py +1 -0
- devloop/cli/commands/__init__.py +1 -0
- devloop/cli/commands/custom_agents.py +144 -0
- devloop/cli/commands/feedback.py +161 -0
- devloop/cli/commands/summary.py +50 -0
- devloop/cli/main.py +430 -0
- devloop/cli/main_v1.py +144 -0
- devloop/collectors/__init__.py +17 -0
- devloop/collectors/base.py +55 -0
- devloop/collectors/filesystem.py +126 -0
- devloop/collectors/git.py +171 -0
- devloop/collectors/manager.py +159 -0
- devloop/collectors/process.py +221 -0
- devloop/collectors/system.py +195 -0
- devloop/core/__init__.py +21 -0
- devloop/core/agent.py +206 -0
- devloop/core/agent_template.py +498 -0
- devloop/core/amp_integration.py +166 -0
- devloop/core/auto_fix.py +224 -0
- devloop/core/config.py +272 -0
- devloop/core/context.py +0 -0
- devloop/core/context_store.py +530 -0
- devloop/core/contextual_feedback.py +311 -0
- devloop/core/custom_agent.py +439 -0
- devloop/core/debug_trace.py +289 -0
- devloop/core/event.py +105 -0
- devloop/core/event_store.py +316 -0
- devloop/core/feedback.py +311 -0
- devloop/core/learning.py +351 -0
- devloop/core/manager.py +219 -0
- devloop/core/performance.py +433 -0
- devloop/core/proactive_feedback.py +302 -0
- devloop/core/summary_formatter.py +159 -0
- devloop/core/summary_generator.py +275 -0
- devloop-0.2.0.dist-info/METADATA +705 -0
- devloop-0.2.0.dist-info/RECORD +55 -0
- devloop-0.2.0.dist-info/WHEEL +4 -0
- devloop-0.2.0.dist-info/entry_points.txt +3 -0
- 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)
|