mdan-cli 2.5.1 → 2.7.0

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 (57) hide show
  1. package/AGENTS.md +76 -1
  2. package/README.md +274 -4
  3. package/agents/auto-orchestrator.md +343 -0
  4. package/agents/devops.md +511 -94
  5. package/cli/mdan.py +111 -6
  6. package/cli/mdan_crewai.py +539 -0
  7. package/core/crewai_orchestrator.md +419 -0
  8. package/core/debate-protocol.md +454 -0
  9. package/core/universal-envelope.md +113 -0
  10. package/integrations/__init__.py +33 -0
  11. package/integrations/crewai/__init__.py +27 -0
  12. package/integrations/crewai/agents/__init__.py +21 -0
  13. package/integrations/crewai/agents/architect_agent.py +264 -0
  14. package/integrations/crewai/agents/dev_agent.py +271 -0
  15. package/integrations/crewai/agents/devops_agent.py +421 -0
  16. package/integrations/crewai/agents/doc_agent.py +388 -0
  17. package/integrations/crewai/agents/product_agent.py +203 -0
  18. package/integrations/crewai/agents/security_agent.py +386 -0
  19. package/integrations/crewai/agents/test_agent.py +358 -0
  20. package/integrations/crewai/agents/ux_agent.py +257 -0
  21. package/integrations/crewai/flows/__init__.py +13 -0
  22. package/integrations/crewai/flows/auto_flow.py +451 -0
  23. package/integrations/crewai/flows/build_flow.py +297 -0
  24. package/integrations/crewai/flows/debate_flow.py +422 -0
  25. package/integrations/crewai/flows/discovery_flow.py +267 -0
  26. package/integrations/crewai/orchestrator.py +558 -0
  27. package/integrations/crewai/skills/__init__.py +8 -0
  28. package/integrations/crewai/skills/skill_router.py +534 -0
  29. package/integrations/crewai/tools/__init__.py +11 -0
  30. package/integrations/crewai/tools/file_tool.py +355 -0
  31. package/integrations/crewai/tools/serper_tool.py +169 -0
  32. package/integrations/crewai/tools/sql_tool.py +435 -0
  33. package/memory/CONTEXT-SAVE-FORMAT.md +328 -0
  34. package/memory/MEMORY-AUTO.json +66 -0
  35. package/memory/RESUME-PROTOCOL.md +379 -0
  36. package/package.json +1 -1
  37. package/phases/auto-01-load.md +165 -0
  38. package/phases/auto-02-discover.md +207 -0
  39. package/phases/auto-03-plan.md +509 -0
  40. package/phases/auto-04-architect.md +567 -0
  41. package/phases/auto-05-implement.md +713 -0
  42. package/phases/auto-06-test.md +559 -0
  43. package/phases/auto-07-deploy.md +510 -0
  44. package/phases/auto-08-doc.md +970 -0
  45. package/skills/azure-devops/skill.md +1757 -0
  46. package/templates/dotnet-blazor/README.md +415 -0
  47. package/templates/external-services/ExampleService.cs +361 -0
  48. package/templates/external-services/IService.cs +113 -0
  49. package/templates/external-services/README.md +325 -0
  50. package/templates/external-services/ServiceBase.cs +492 -0
  51. package/templates/external-services/ServiceProvider.cs +243 -0
  52. package/templates/prompts/devops-agent.yaml +327 -0
  53. package/templates/prompts.json +15 -1
  54. package/templates/sql-server/README.md +37 -0
  55. package/templates/sql-server/functions.sql +158 -0
  56. package/templates/sql-server/schema.sql +188 -0
  57. package/templates/sql-server/stored-procedures.sql +284 -0
@@ -0,0 +1,435 @@
1
+ """Async SQL Database Connector Tool"""
2
+
3
+ import os
4
+ import asyncio
5
+ from typing import Optional, List, Dict, Any, Union
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DatabaseType(Enum):
14
+ """Supported database types"""
15
+
16
+ POSTGRESQL = "postgresql"
17
+ MYSQL = "mysql"
18
+ SQLSERVER = "sqlserver"
19
+ SQLITE = "sqlite"
20
+
21
+
22
+ @dataclass
23
+ class QueryResult:
24
+ """Represents a query result"""
25
+
26
+ columns: List[str]
27
+ rows: List[Dict[str, Any]]
28
+ row_count: int
29
+ execution_time: float
30
+
31
+
32
+ class SQLTool:
33
+ """Async SQL database connector with support for multiple database types"""
34
+
35
+ def __init__(
36
+ self,
37
+ connection_string: Optional[str] = None,
38
+ db_type: DatabaseType = DatabaseType.POSTGRESQL,
39
+ **kwargs,
40
+ ):
41
+ """
42
+ Initialize SQLTool
43
+
44
+ Args:
45
+ connection_string: Database connection string. If None, reads from DATABASE_URL
46
+ db_type: Type of database (POSTGRESQL, MYSQL, SQLSERVER, SQLITE)
47
+ **kwargs: Additional connection parameters
48
+ """
49
+ self.connection_string = connection_string or os.getenv("DATABASE_URL")
50
+ self.db_type = db_type
51
+ self.connection_params = kwargs
52
+ self._pool = None
53
+ self._driver = None
54
+
55
+ if not self.connection_string and db_type != DatabaseType.SQLLITE:
56
+ raise ValueError(
57
+ "DATABASE_URL not found. Set it as environment variable "
58
+ "or pass connection_string to the constructor."
59
+ )
60
+
61
+ self._initialize_driver()
62
+
63
+ def _initialize_driver(self):
64
+ """Initialize the appropriate database driver"""
65
+ if self.db_type == DatabaseType.POSTGRESQL:
66
+ try:
67
+ import asyncpg
68
+
69
+ self._driver = asyncpg
70
+ except ImportError:
71
+ raise ImportError(
72
+ "asyncpg is not installed. Install it with: pip install asyncpg"
73
+ )
74
+
75
+ elif self.db_type == DatabaseType.MYSQL:
76
+ try:
77
+ import aiomysql
78
+
79
+ self._driver = aiomysql
80
+ except ImportError:
81
+ raise ImportError(
82
+ "aiomysql is not installed. Install it with: pip install aiomysql"
83
+ )
84
+
85
+ elif self.db_type == DatabaseType.SQLSERVER:
86
+ try:
87
+ import aioodbc
88
+
89
+ self._driver = aioodbc
90
+ except ImportError:
91
+ raise ImportError(
92
+ "aioodbc is not installed. Install it with: pip install aioodbc"
93
+ )
94
+
95
+ elif self.db_type == DatabaseType.SQLLITE:
96
+ try:
97
+ import aiosqlite
98
+
99
+ self._driver = aiosqlite
100
+ except ImportError:
101
+ raise ImportError(
102
+ "aiosqlite is not installed. Install it with: pip install aiosqlite"
103
+ )
104
+
105
+ async def connect(self):
106
+ """Establish database connection pool"""
107
+ if self._pool:
108
+ return
109
+
110
+ if self.db_type == DatabaseType.POSTGRESQL:
111
+ self._pool = await self._driver.create_pool(
112
+ self.connection_string, **self.connection_params
113
+ )
114
+
115
+ elif self.db_type == DatabaseType.MYSQL:
116
+ self._pool = await self._driver.create_pool(
117
+ host=self.connection_params.get("host", "localhost"),
118
+ port=self.connection_params.get("port", 3306),
119
+ user=self.connection_params.get("user"),
120
+ password=self.connection_params.get("password"),
121
+ db=self.connection_params.get("database"),
122
+ **self.connection_params,
123
+ )
124
+
125
+ elif self.db_type == DatabaseType.SQLSERVER:
126
+ self._pool = await self._driver.create_pool(
127
+ dsn=self.connection_string, **self.connection_params
128
+ )
129
+
130
+ elif self.db_type == DatabaseType.SQLLITE:
131
+ self._pool = await self._driver.connect(
132
+ self.connection_string or ":memory:"
133
+ )
134
+
135
+ async def disconnect(self):
136
+ """Close database connection pool"""
137
+ if self._pool:
138
+ if self.db_type == DatabaseType.SQLLITE:
139
+ await self._pool.close()
140
+ else:
141
+ self._pool.close()
142
+ await self._pool.wait_closed()
143
+ self._pool = None
144
+
145
+ async def execute_query(
146
+ self, query: str, params: Optional[tuple] = None
147
+ ) -> QueryResult:
148
+ """
149
+ Execute a SELECT query and return results
150
+
151
+ Args:
152
+ query: SQL query string
153
+ params: Query parameters
154
+
155
+ Returns:
156
+ QueryResult with columns, rows, and metadata
157
+ """
158
+ import time
159
+
160
+ start_time = time.time()
161
+
162
+ if not self._pool:
163
+ await self.connect()
164
+
165
+ try:
166
+ if self.db_type == DatabaseType.POSTGRESQL:
167
+ async with self._pool.acquire() as conn:
168
+ rows = (
169
+ await conn.fetch(query, *params)
170
+ if params
171
+ else await conn.fetch(query)
172
+ )
173
+ columns = list(rows[0].keys()) if rows else []
174
+ row_dicts = [dict(row) for row in rows]
175
+
176
+ elif self.db_type == DatabaseType.MYSQL:
177
+ async with self._pool.acquire() as conn:
178
+ async with conn.cursor() as cursor:
179
+ await cursor.execute(query, params or ())
180
+ rows = await cursor.fetchall()
181
+ columns = (
182
+ [desc[0] for desc in cursor.description]
183
+ if cursor.description
184
+ else []
185
+ )
186
+ row_dicts = [dict(zip(columns, row)) for row in rows]
187
+
188
+ elif self.db_type == DatabaseType.SQLSERVER:
189
+ async with self._pool.acquire() as conn:
190
+ async with conn.cursor() as cursor:
191
+ await cursor.execute(query, params or ())
192
+ rows = await cursor.fetchall()
193
+ columns = (
194
+ [column[0] for column in cursor.description]
195
+ if cursor.description
196
+ else []
197
+ )
198
+ row_dicts = [dict(zip(columns, row)) for row in rows]
199
+
200
+ elif self.db_type == DatabaseType.SQLLITE:
201
+ rows = await self._pool.execute_fetchall(query, params or ())
202
+ columns = (
203
+ [desc[0] for desc in self._pool.description]
204
+ if self._pool.description
205
+ else []
206
+ )
207
+ row_dicts = [dict(zip(columns, row)) for row in rows]
208
+
209
+ execution_time = time.time() - start_time
210
+
211
+ return QueryResult(
212
+ columns=columns,
213
+ rows=row_dicts,
214
+ row_count=len(row_dicts),
215
+ execution_time=execution_time,
216
+ )
217
+
218
+ except Exception as e:
219
+ logger.error(f"Query execution failed: {str(e)}")
220
+ raise RuntimeError(f"Query execution failed: {str(e)}") from e
221
+
222
+ async def execute_command(
223
+ self, command: str, params: Optional[tuple] = None
224
+ ) -> int:
225
+ """
226
+ Execute an INSERT, UPDATE, or DELETE command
227
+
228
+ Args:
229
+ command: SQL command string
230
+ params: Command parameters
231
+
232
+ Returns:
233
+ Number of affected rows
234
+ """
235
+ if not self._pool:
236
+ await self.connect()
237
+
238
+ try:
239
+ if self.db_type == DatabaseType.POSTGRESQL:
240
+ async with self._pool.acquire() as conn:
241
+ result = (
242
+ await conn.execute(command, *params)
243
+ if params
244
+ else await conn.execute(command)
245
+ )
246
+ return result.split()[-1] if isinstance(result, str) else result
247
+
248
+ elif self.db_type == DatabaseType.MYSQL:
249
+ async with self._pool.acquire() as conn:
250
+ async with conn.cursor() as cursor:
251
+ await cursor.execute(command, params or ())
252
+ await conn.commit()
253
+ return cursor.rowcount
254
+
255
+ elif self.db_type == DatabaseType.SQLSERVER:
256
+ async with self._pool.acquire() as conn:
257
+ async with conn.cursor() as cursor:
258
+ await cursor.execute(command, params or ())
259
+ return cursor.rowcount
260
+
261
+ elif self.db_type == DatabaseType.SQLLITE:
262
+ await self._pool.execute(command, params or ())
263
+ await self._pool.commit()
264
+ return self._pool.total_changes
265
+
266
+ except Exception as e:
267
+ logger.error(f"Command execution failed: {str(e)}")
268
+ raise RuntimeError(f"Command execution failed: {str(e)}") from e
269
+
270
+ async def execute_transaction(self, commands: List[tuple]) -> List[int]:
271
+ """
272
+ Execute multiple commands in a transaction
273
+
274
+ Args:
275
+ commands: List of (command, params) tuples
276
+
277
+ Returns:
278
+ List of affected row counts
279
+ """
280
+ if not self._pool:
281
+ await self.connect()
282
+
283
+ results = []
284
+
285
+ try:
286
+ if self.db_type == DatabaseType.POSTGRESQL:
287
+ async with self._pool.acquire() as conn:
288
+ async with conn.transaction():
289
+ for command, params in commands:
290
+ result = (
291
+ await conn.execute(command, *params)
292
+ if params
293
+ else await conn.execute(command)
294
+ )
295
+ results.append(
296
+ int(result.split()[-1])
297
+ if isinstance(result, str)
298
+ else result
299
+ )
300
+
301
+ elif self.db_type in [DatabaseType.MYSQL, DatabaseType.SQLSERVER]:
302
+ async with self._pool.acquire() as conn:
303
+ async with conn.cursor() as cursor:
304
+ for command, params in commands:
305
+ await cursor.execute(command, params or ())
306
+ results.append(cursor.rowcount)
307
+ await conn.commit()
308
+
309
+ elif self.db_type == DatabaseType.SQLLITE:
310
+ for command, params in commands:
311
+ await self._pool.execute(command, params or ())
312
+ results.append(self._pool.total_changes)
313
+ await self._pool.commit()
314
+
315
+ return results
316
+
317
+ except Exception as e:
318
+ logger.error(f"Transaction failed: {str(e)}")
319
+ raise RuntimeError(f"Transaction failed: {str(e)}") from e
320
+
321
+ async def get_table_schema(self, table_name: str) -> List[Dict[str, Any]]:
322
+ """Get schema information for a table"""
323
+ if self.db_type == DatabaseType.POSTGRESQL:
324
+ query = """
325
+ SELECT column_name, data_type, is_nullable, column_default
326
+ FROM information_schema.columns
327
+ WHERE table_name = $1
328
+ ORDER BY ordinal_position
329
+ """
330
+ result = await self.execute_query(query, (table_name,))
331
+ return result.rows
332
+
333
+ elif self.db_type == DatabaseType.MYSQL:
334
+ query = """
335
+ SELECT column_name, data_type, is_nullable, column_default
336
+ FROM information_schema.columns
337
+ WHERE table_name = %s
338
+ ORDER BY ordinal_position
339
+ """
340
+ result = await self.execute_query(query, (table_name,))
341
+ return result.rows
342
+
343
+ elif self.db_type == DatabaseType.SQLSERVER:
344
+ query = """
345
+ SELECT column_name, data_type, is_nullable, column_default
346
+ FROM information_schema.columns
347
+ WHERE table_name = ?
348
+ ORDER BY ordinal_position
349
+ """
350
+ result = await self.execute_query(query, (table_name,))
351
+ return result.rows
352
+
353
+ elif self.db_type == DatabaseType.SQLLITE:
354
+ query = f"PRAGMA table_info({table_name})"
355
+ result = await self.execute_query(query)
356
+ return result.rows
357
+
358
+ async def list_tables(self) -> List[str]:
359
+ """List all tables in the database"""
360
+ if self.db_type == DatabaseType.POSTGRESQL:
361
+ query = """
362
+ SELECT table_name
363
+ FROM information_schema.tables
364
+ WHERE table_schema = 'public'
365
+ ORDER BY table_name
366
+ """
367
+ elif self.db_type == DatabaseType.MYSQL:
368
+ query = """
369
+ SELECT table_name
370
+ FROM information_schema.tables
371
+ WHERE table_schema = DATABASE()
372
+ ORDER BY table_name
373
+ """
374
+ elif self.db_type == DatabaseType.SQLSERVER:
375
+ query = """
376
+ SELECT table_name
377
+ FROM information_schema.tables
378
+ WHERE table_type = 'BASE TABLE'
379
+ ORDER BY table_name
380
+ """
381
+ elif self.db_type == DatabaseType.SQLLITE:
382
+ query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
383
+
384
+ result = await self.execute_query(query)
385
+ return [
386
+ row["table_name"] if "table_name" in row else row["name"]
387
+ for row in result.rows
388
+ ]
389
+
390
+ def format_results(self, result: QueryResult) -> str:
391
+ """Format query results as readable text"""
392
+ lines = [
393
+ f"Query returned {result.row_count} rows in {result.execution_time:.3f}s",
394
+ "",
395
+ ]
396
+
397
+ if result.columns:
398
+ header = " | ".join(result.columns)
399
+ lines.append(header)
400
+ lines.append("-" * len(header))
401
+
402
+ for row in result.rows:
403
+ values = " | ".join(str(v) for v in row.values())
404
+ lines.append(values)
405
+
406
+ return "\n".join(lines)
407
+
408
+ async def __aenter__(self):
409
+ """Async context manager entry"""
410
+ await self.connect()
411
+ return self
412
+
413
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
414
+ """Async context manager exit"""
415
+ await self.disconnect()
416
+
417
+
418
+ def run_query_sync(query: str, params: Optional[tuple] = None, **kwargs) -> QueryResult:
419
+ """
420
+ Synchronous wrapper for async query execution
421
+
422
+ Args:
423
+ query: SQL query string
424
+ params: Query parameters
425
+ **kwargs: Additional SQLTool parameters
426
+
427
+ Returns:
428
+ QueryResult with columns, rows, and metadata
429
+ """
430
+
431
+ async def _run():
432
+ async with SQLTool(**kwargs) as db:
433
+ return await db.execute_query(query, params)
434
+
435
+ return asyncio.run(_run())