tapps-agents 3.5.38__py3-none-any.whl → 3.5.39__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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/cleanup/__init__.py +7 -0
- tapps_agents/agents/cleanup/agent.py +445 -0
- tapps_agents/agents/enhancer/agent.py +2728 -2728
- tapps_agents/cli/commands/cleanup_agent.py +92 -0
- tapps_agents/cli/main.py +651 -645
- tapps_agents/cli/parsers/cleanup_agent.py +228 -0
- tapps_agents/core/config.py +1622 -1579
- tapps_agents/core/init_project.py +11 -10
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/workflow/enforcer.py +36 -23
- tapps_agents/workflow/message_formatter.py +187 -0
- tapps_agents/workflow/rules_generator.py +7 -1
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/METADATA +5 -5
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/RECORD +19 -14
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/licenses/LICENSE +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.39.dist-info}/top_level.txt +0 -0
tapps_agents/__init__.py
CHANGED
|
@@ -24,8 +24,8 @@ Example:
|
|
|
24
24
|
```
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
__version__: str = "3.5.
|
|
27
|
+
__version__: str = "3.5.39"
|
|
28
28
|
|
|
29
29
|
# Also expose as _version_ for compatibility with some import mechanisms
|
|
30
30
|
# This helps with editable installs where __version__ might not be importable
|
|
31
|
-
_version_: str = "3.5.
|
|
31
|
+
_version_: str = "3.5.39"
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cleanup Agent - Project structure analysis and intelligent cleanup
|
|
3
|
+
|
|
4
|
+
This agent helps keep projects clean by:
|
|
5
|
+
- Analyzing project structure for cleanup opportunities
|
|
6
|
+
- Detecting duplicate files, outdated docs, and naming inconsistencies
|
|
7
|
+
- Generating cleanup plans with rationale for each action
|
|
8
|
+
- Executing cleanup operations safely with backups and rollback
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ...core.agent_base import BaseAgent
|
|
15
|
+
from ...core.config import ProjectConfig, load_config
|
|
16
|
+
from ...utils.project_cleanup_agent import (
|
|
17
|
+
AnalysisReport,
|
|
18
|
+
CleanupAgent as CleanupAgentUtil,
|
|
19
|
+
CleanupPlan,
|
|
20
|
+
ExecutionReport,
|
|
21
|
+
ProjectAnalyzer,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CleanupAgent(BaseAgent):
|
|
26
|
+
"""
|
|
27
|
+
Cleanup Agent - Project structure analysis and cleanup.
|
|
28
|
+
|
|
29
|
+
Permissions: Read, Write, Edit, Glob, Bash
|
|
30
|
+
|
|
31
|
+
This agent provides guided project cleanup capabilities:
|
|
32
|
+
- Analyze project structure (duplicates, outdated files, naming issues)
|
|
33
|
+
- Generate cleanup plans with user confirmation
|
|
34
|
+
- Execute cleanup operations safely with backups
|
|
35
|
+
- Support dry-run mode for previewing changes
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: ProjectConfig | None = None):
|
|
39
|
+
super().__init__(
|
|
40
|
+
agent_id="cleanup-agent",
|
|
41
|
+
agent_name="Cleanup Agent",
|
|
42
|
+
config=config,
|
|
43
|
+
)
|
|
44
|
+
# Use config if provided, otherwise load defaults
|
|
45
|
+
if config is None:
|
|
46
|
+
config = load_config()
|
|
47
|
+
self.config = config
|
|
48
|
+
|
|
49
|
+
# Get cleanup agent config
|
|
50
|
+
cleanup_config = config.agents.cleanup_agent if config and config.agents else None
|
|
51
|
+
self.dry_run_default = cleanup_config.dry_run_default if cleanup_config else True
|
|
52
|
+
self.backup_enabled = cleanup_config.backup_enabled if cleanup_config else True
|
|
53
|
+
self.interactive_mode = cleanup_config.interactive_mode if cleanup_config else True
|
|
54
|
+
|
|
55
|
+
# Utility components (lazily initialized)
|
|
56
|
+
self._util: CleanupAgentUtil | None = None
|
|
57
|
+
self._analyzer: ProjectAnalyzer | None = None
|
|
58
|
+
|
|
59
|
+
def _get_util(self, project_root: Path | None = None) -> CleanupAgentUtil:
|
|
60
|
+
"""Get or create the cleanup utility instance."""
|
|
61
|
+
root = project_root or self._project_root or Path.cwd()
|
|
62
|
+
if self._util is None or self._util.project_root != root:
|
|
63
|
+
self._util = CleanupAgentUtil(root)
|
|
64
|
+
return self._util
|
|
65
|
+
|
|
66
|
+
def _get_analyzer(self, project_root: Path | None = None) -> ProjectAnalyzer:
|
|
67
|
+
"""Get or create the analyzer instance."""
|
|
68
|
+
root = project_root or self._project_root or Path.cwd()
|
|
69
|
+
if self._analyzer is None or self._analyzer.project_root != root:
|
|
70
|
+
self._analyzer = ProjectAnalyzer(root)
|
|
71
|
+
return self._analyzer
|
|
72
|
+
|
|
73
|
+
def get_commands(self) -> list[dict[str, str]]:
|
|
74
|
+
"""Return list of available commands."""
|
|
75
|
+
commands = super().get_commands()
|
|
76
|
+
commands.extend(
|
|
77
|
+
[
|
|
78
|
+
{
|
|
79
|
+
"command": "*analyze",
|
|
80
|
+
"description": "Analyze project structure for cleanup opportunities",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"command": "*plan",
|
|
84
|
+
"description": "Generate cleanup plan from analysis",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"command": "*execute",
|
|
88
|
+
"description": "Execute cleanup plan (dry-run by default)",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"command": "*run",
|
|
92
|
+
"description": "Run full cleanup workflow (analyze, plan, execute)",
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
return commands
|
|
97
|
+
|
|
98
|
+
async def run(self, command: str, **kwargs) -> dict[str, Any]:
|
|
99
|
+
"""Execute a command."""
|
|
100
|
+
if command == "analyze":
|
|
101
|
+
return await self.analyze_command(**kwargs)
|
|
102
|
+
elif command == "plan":
|
|
103
|
+
return await self.plan_command(**kwargs)
|
|
104
|
+
elif command == "execute":
|
|
105
|
+
return await self.execute_command(**kwargs)
|
|
106
|
+
elif command == "run":
|
|
107
|
+
return await self.run_full_cleanup_command(**kwargs)
|
|
108
|
+
elif command == "help":
|
|
109
|
+
return self._help()
|
|
110
|
+
else:
|
|
111
|
+
return {"error": f"Unknown command: {command}"}
|
|
112
|
+
|
|
113
|
+
async def analyze_command(
|
|
114
|
+
self,
|
|
115
|
+
path: str | Path | None = None,
|
|
116
|
+
pattern: str = "*.md",
|
|
117
|
+
output: str | Path | None = None,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""
|
|
120
|
+
Analyze project structure for cleanup opportunities.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
path: Path to analyze (defaults to project docs/)
|
|
124
|
+
pattern: File pattern to match (default: *.md)
|
|
125
|
+
output: Optional output file for analysis report
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Analysis report with duplicates, outdated files, naming issues
|
|
129
|
+
"""
|
|
130
|
+
project_root = self._project_root or Path.cwd()
|
|
131
|
+
scan_path = Path(path) if path else project_root / "docs"
|
|
132
|
+
|
|
133
|
+
if not scan_path.exists():
|
|
134
|
+
return {
|
|
135
|
+
"error": f"Path does not exist: {scan_path}",
|
|
136
|
+
"type": "analyze",
|
|
137
|
+
"success": False,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
util = self._get_util(project_root)
|
|
142
|
+
report = await util.run_analysis(scan_path, pattern)
|
|
143
|
+
|
|
144
|
+
result = {
|
|
145
|
+
"type": "analyze",
|
|
146
|
+
"success": True,
|
|
147
|
+
"report": {
|
|
148
|
+
"total_files": report.total_files,
|
|
149
|
+
"total_size_mb": report.total_size / 1024 / 1024,
|
|
150
|
+
"duplicate_groups": len(report.duplicates),
|
|
151
|
+
"duplicate_files": report.duplicate_count,
|
|
152
|
+
"potential_savings_kb": report.potential_savings / 1024,
|
|
153
|
+
"outdated_files": len(report.outdated_files),
|
|
154
|
+
"obsolete_files": report.obsolete_file_count,
|
|
155
|
+
"naming_issues": len(report.naming_issues),
|
|
156
|
+
"timestamp": report.timestamp.isoformat(),
|
|
157
|
+
"scan_path": str(report.scan_path),
|
|
158
|
+
},
|
|
159
|
+
"summary": report.to_markdown(),
|
|
160
|
+
"message": f"Analysis complete: {report.total_files} files analyzed",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Save report if output specified
|
|
164
|
+
if output:
|
|
165
|
+
output_path = Path(output)
|
|
166
|
+
output_path.write_text(report.model_dump_json(indent=2))
|
|
167
|
+
result["output_file"] = str(output_path)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return {
|
|
173
|
+
"type": "analyze",
|
|
174
|
+
"success": False,
|
|
175
|
+
"error": str(e),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async def plan_command(
|
|
179
|
+
self,
|
|
180
|
+
analysis_file: str | Path | None = None,
|
|
181
|
+
path: str | Path | None = None,
|
|
182
|
+
pattern: str = "*.md",
|
|
183
|
+
output: str | Path | None = None,
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Generate cleanup plan from analysis.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
analysis_file: Path to analysis report JSON (optional)
|
|
190
|
+
path: Path to analyze if no analysis file (defaults to docs/)
|
|
191
|
+
pattern: File pattern if running fresh analysis
|
|
192
|
+
output: Optional output file for cleanup plan
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Cleanup plan with prioritized actions
|
|
196
|
+
"""
|
|
197
|
+
project_root = self._project_root or Path.cwd()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Load analysis or run fresh
|
|
201
|
+
if analysis_file:
|
|
202
|
+
analysis_path = Path(analysis_file)
|
|
203
|
+
if not analysis_path.exists():
|
|
204
|
+
return {
|
|
205
|
+
"error": f"Analysis file not found: {analysis_path}",
|
|
206
|
+
"type": "plan",
|
|
207
|
+
"success": False,
|
|
208
|
+
}
|
|
209
|
+
analysis = AnalysisReport.model_validate_json(analysis_path.read_text())
|
|
210
|
+
else:
|
|
211
|
+
scan_path = Path(path) if path else project_root / "docs"
|
|
212
|
+
util = self._get_util(project_root)
|
|
213
|
+
analysis = await util.run_analysis(scan_path, pattern)
|
|
214
|
+
|
|
215
|
+
# Generate plan
|
|
216
|
+
util = self._get_util(project_root)
|
|
217
|
+
plan = util.run_planning(analysis)
|
|
218
|
+
|
|
219
|
+
result = {
|
|
220
|
+
"type": "plan",
|
|
221
|
+
"success": True,
|
|
222
|
+
"plan": {
|
|
223
|
+
"total_actions": len(plan.actions),
|
|
224
|
+
"high_priority": plan.high_priority_count,
|
|
225
|
+
"medium_priority": plan.medium_priority_count,
|
|
226
|
+
"low_priority": plan.low_priority_count,
|
|
227
|
+
"estimated_savings_mb": plan.estimated_savings / 1024 / 1024,
|
|
228
|
+
"estimated_file_reduction": f"{plan.estimated_file_reduction:.1f}%",
|
|
229
|
+
"created_at": plan.created_at.isoformat(),
|
|
230
|
+
},
|
|
231
|
+
"actions_preview": [
|
|
232
|
+
{
|
|
233
|
+
"type": str(a.action_type),
|
|
234
|
+
"files": [str(f) for f in a.source_files],
|
|
235
|
+
"target": str(a.target_path) if a.target_path else None,
|
|
236
|
+
"rationale": a.rationale,
|
|
237
|
+
"priority": a.priority,
|
|
238
|
+
"safety": str(a.safety_level),
|
|
239
|
+
"requires_confirmation": a.requires_confirmation,
|
|
240
|
+
}
|
|
241
|
+
for a in plan.actions[:10] # Preview first 10
|
|
242
|
+
],
|
|
243
|
+
"summary": plan.to_markdown(),
|
|
244
|
+
"message": f"Plan generated: {len(plan.actions)} actions",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Save plan if output specified
|
|
248
|
+
if output:
|
|
249
|
+
output_path = Path(output)
|
|
250
|
+
output_path.write_text(plan.model_dump_json(indent=2))
|
|
251
|
+
result["output_file"] = str(output_path)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
return {
|
|
257
|
+
"type": "plan",
|
|
258
|
+
"success": False,
|
|
259
|
+
"error": str(e),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async def execute_command(
|
|
263
|
+
self,
|
|
264
|
+
plan_file: str | Path | None = None,
|
|
265
|
+
path: str | Path | None = None,
|
|
266
|
+
pattern: str = "*.md",
|
|
267
|
+
dry_run: bool | None = None,
|
|
268
|
+
backup: bool | None = None,
|
|
269
|
+
) -> dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Execute cleanup plan.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
plan_file: Path to cleanup plan JSON (optional)
|
|
275
|
+
path: Path to analyze if no plan file
|
|
276
|
+
pattern: File pattern if running fresh
|
|
277
|
+
dry_run: Preview changes without executing (default: True)
|
|
278
|
+
backup: Create backup before execution (default: True)
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Execution report with results
|
|
282
|
+
"""
|
|
283
|
+
project_root = self._project_root or Path.cwd()
|
|
284
|
+
|
|
285
|
+
# Use defaults if not specified
|
|
286
|
+
if dry_run is None:
|
|
287
|
+
dry_run = self.dry_run_default
|
|
288
|
+
if backup is None:
|
|
289
|
+
backup = self.backup_enabled
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
# Load plan or generate fresh
|
|
293
|
+
if plan_file:
|
|
294
|
+
plan_path = Path(plan_file)
|
|
295
|
+
if not plan_path.exists():
|
|
296
|
+
return {
|
|
297
|
+
"error": f"Plan file not found: {plan_path}",
|
|
298
|
+
"type": "execute",
|
|
299
|
+
"success": False,
|
|
300
|
+
}
|
|
301
|
+
plan = CleanupPlan.model_validate_json(plan_path.read_text())
|
|
302
|
+
else:
|
|
303
|
+
# Run analysis and planning first
|
|
304
|
+
scan_path = Path(path) if path else project_root / "docs"
|
|
305
|
+
util = self._get_util(project_root)
|
|
306
|
+
analysis = await util.run_analysis(scan_path, pattern)
|
|
307
|
+
plan = util.run_planning(analysis)
|
|
308
|
+
|
|
309
|
+
# Execute plan
|
|
310
|
+
util = self._get_util(project_root)
|
|
311
|
+
report = await util.run_execution(plan, dry_run=dry_run, create_backup=backup)
|
|
312
|
+
|
|
313
|
+
result = {
|
|
314
|
+
"type": "execute",
|
|
315
|
+
"success": True,
|
|
316
|
+
"dry_run": report.dry_run,
|
|
317
|
+
"report": {
|
|
318
|
+
"total_operations": len(report.operations),
|
|
319
|
+
"successful": report.success_count,
|
|
320
|
+
"failed": report.failure_count,
|
|
321
|
+
"files_deleted": report.files_deleted,
|
|
322
|
+
"files_moved": report.files_moved,
|
|
323
|
+
"files_renamed": report.files_renamed,
|
|
324
|
+
"files_modified": report.files_modified,
|
|
325
|
+
"duration_seconds": report.duration_seconds,
|
|
326
|
+
"backup_location": str(report.backup_location) if report.backup_location else None,
|
|
327
|
+
},
|
|
328
|
+
"summary": report.to_markdown(),
|
|
329
|
+
"message": (
|
|
330
|
+
f"{'Dry run' if dry_run else 'Execution'} complete: "
|
|
331
|
+
f"{report.success_count} successful, {report.failure_count} failed"
|
|
332
|
+
),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
return {
|
|
339
|
+
"type": "execute",
|
|
340
|
+
"success": False,
|
|
341
|
+
"error": str(e),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async def run_full_cleanup_command(
|
|
345
|
+
self,
|
|
346
|
+
path: str | Path | None = None,
|
|
347
|
+
pattern: str = "*.md",
|
|
348
|
+
dry_run: bool | None = None,
|
|
349
|
+
backup: bool | None = None,
|
|
350
|
+
) -> dict[str, Any]:
|
|
351
|
+
"""
|
|
352
|
+
Run full cleanup workflow (analyze, plan, execute).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
path: Path to analyze (defaults to docs/)
|
|
356
|
+
pattern: File pattern to match
|
|
357
|
+
dry_run: Preview changes without executing (default: True)
|
|
358
|
+
backup: Create backup before execution (default: True)
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Combined report with analysis, plan, and execution results
|
|
362
|
+
"""
|
|
363
|
+
project_root = self._project_root or Path.cwd()
|
|
364
|
+
scan_path = Path(path) if path else project_root / "docs"
|
|
365
|
+
|
|
366
|
+
# Use defaults if not specified
|
|
367
|
+
if dry_run is None:
|
|
368
|
+
dry_run = self.dry_run_default
|
|
369
|
+
if backup is None:
|
|
370
|
+
backup = self.backup_enabled
|
|
371
|
+
|
|
372
|
+
if not scan_path.exists():
|
|
373
|
+
return {
|
|
374
|
+
"error": f"Path does not exist: {scan_path}",
|
|
375
|
+
"type": "run",
|
|
376
|
+
"success": False,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
util = self._get_util(project_root)
|
|
381
|
+
analysis, plan, execution = await util.run_full_cleanup(
|
|
382
|
+
scan_path,
|
|
383
|
+
pattern,
|
|
384
|
+
dry_run=dry_run,
|
|
385
|
+
create_backup=backup,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"type": "run",
|
|
390
|
+
"success": True,
|
|
391
|
+
"dry_run": execution.dry_run,
|
|
392
|
+
"analysis": {
|
|
393
|
+
"total_files": analysis.total_files,
|
|
394
|
+
"duplicates": analysis.duplicate_count,
|
|
395
|
+
"outdated": len(analysis.outdated_files),
|
|
396
|
+
"naming_issues": len(analysis.naming_issues),
|
|
397
|
+
},
|
|
398
|
+
"plan": {
|
|
399
|
+
"total_actions": len(plan.actions),
|
|
400
|
+
"estimated_savings_mb": plan.estimated_savings / 1024 / 1024,
|
|
401
|
+
},
|
|
402
|
+
"execution": {
|
|
403
|
+
"successful": execution.success_count,
|
|
404
|
+
"failed": execution.failure_count,
|
|
405
|
+
"files_modified": execution.files_modified,
|
|
406
|
+
"backup_location": str(execution.backup_location) if execution.backup_location else None,
|
|
407
|
+
},
|
|
408
|
+
"summary": "\n".join([
|
|
409
|
+
"=" * 60,
|
|
410
|
+
analysis.to_markdown(),
|
|
411
|
+
"=" * 60,
|
|
412
|
+
plan.to_markdown(),
|
|
413
|
+
"=" * 60,
|
|
414
|
+
execution.to_markdown(),
|
|
415
|
+
]),
|
|
416
|
+
"message": (
|
|
417
|
+
f"{'Dry run' if dry_run else 'Cleanup'} complete: "
|
|
418
|
+
f"{analysis.total_files} files analyzed, "
|
|
419
|
+
f"{len(plan.actions)} actions, "
|
|
420
|
+
f"{execution.success_count} successful"
|
|
421
|
+
),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return {
|
|
426
|
+
"type": "run",
|
|
427
|
+
"success": False,
|
|
428
|
+
"error": str(e),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
def _help(self) -> dict[str, Any]:
|
|
432
|
+
"""Return help information for Cleanup Agent."""
|
|
433
|
+
examples = [
|
|
434
|
+
" *analyze --path ./docs --pattern '*.md'",
|
|
435
|
+
" *plan --analysis-file analysis.json --output cleanup-plan.json",
|
|
436
|
+
" *execute --plan-file cleanup-plan.json --dry-run",
|
|
437
|
+
" *run --path ./docs --dry-run --backup",
|
|
438
|
+
]
|
|
439
|
+
help_text = "\n".join([self.format_help(), "\nExamples:", *examples])
|
|
440
|
+
return {"type": "help", "content": help_text}
|
|
441
|
+
|
|
442
|
+
async def close(self):
|
|
443
|
+
"""Close agent and clean up resources."""
|
|
444
|
+
self._util = None
|
|
445
|
+
self._analyzer = None
|