steerdev 0.4.27__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 (57) hide show
  1. steerdev-0.4.27.dist-info/METADATA +224 -0
  2. steerdev-0.4.27.dist-info/RECORD +57 -0
  3. steerdev-0.4.27.dist-info/WHEEL +4 -0
  4. steerdev-0.4.27.dist-info/entry_points.txt +2 -0
  5. steerdev_agent/__init__.py +10 -0
  6. steerdev_agent/api/__init__.py +32 -0
  7. steerdev_agent/api/activity.py +278 -0
  8. steerdev_agent/api/agents.py +145 -0
  9. steerdev_agent/api/client.py +158 -0
  10. steerdev_agent/api/commands.py +399 -0
  11. steerdev_agent/api/configs.py +238 -0
  12. steerdev_agent/api/context.py +306 -0
  13. steerdev_agent/api/events.py +294 -0
  14. steerdev_agent/api/hooks.py +178 -0
  15. steerdev_agent/api/implementation_plan.py +408 -0
  16. steerdev_agent/api/messages.py +231 -0
  17. steerdev_agent/api/prd.py +281 -0
  18. steerdev_agent/api/runs.py +526 -0
  19. steerdev_agent/api/sessions.py +403 -0
  20. steerdev_agent/api/specs.py +321 -0
  21. steerdev_agent/api/tasks.py +659 -0
  22. steerdev_agent/api/workflow_runs.py +351 -0
  23. steerdev_agent/api/workflows.py +191 -0
  24. steerdev_agent/cli.py +2254 -0
  25. steerdev_agent/config/__init__.py +19 -0
  26. steerdev_agent/config/models.py +236 -0
  27. steerdev_agent/config/platform.py +272 -0
  28. steerdev_agent/config/settings.py +62 -0
  29. steerdev_agent/daemon.py +675 -0
  30. steerdev_agent/executor/__init__.py +64 -0
  31. steerdev_agent/executor/base.py +121 -0
  32. steerdev_agent/executor/claude.py +328 -0
  33. steerdev_agent/executor/stream.py +163 -0
  34. steerdev_agent/git/__init__.py +1 -0
  35. steerdev_agent/handlers/__init__.py +5 -0
  36. steerdev_agent/handlers/prd.py +533 -0
  37. steerdev_agent/integration.py +334 -0
  38. steerdev_agent/prompt/__init__.py +10 -0
  39. steerdev_agent/prompt/builder.py +263 -0
  40. steerdev_agent/prompt/templates.py +422 -0
  41. steerdev_agent/py.typed +0 -0
  42. steerdev_agent/runner.py +829 -0
  43. steerdev_agent/setup/__init__.py +5 -0
  44. steerdev_agent/setup/claude_setup.py +560 -0
  45. steerdev_agent/setup/templates/claude_md_section.md +140 -0
  46. steerdev_agent/setup/templates/settings.json +69 -0
  47. steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
  48. steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
  49. steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
  50. steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
  51. steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
  52. steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
  53. steerdev_agent/setup/templates/steerdev.yaml +51 -0
  54. steerdev_agent/version.py +149 -0
  55. steerdev_agent/workflow/__init__.py +10 -0
  56. steerdev_agent/workflow/executor.py +494 -0
  57. steerdev_agent/workflow/memory.py +185 -0
@@ -0,0 +1,238 @@
1
+ """Agent configuration sync client for platform configs and workflows."""
2
+
3
+ from typing import Any, Self
4
+
5
+ import httpx
6
+ from loguru import logger
7
+
8
+ from steerdev_agent.api.client import get_api_endpoint, get_api_key, get_project_id
9
+ from steerdev_agent.config.platform import PlatformConfig, WorkflowConfig
10
+
11
+
12
+ class ConfigsClient:
13
+ """Async HTTP client for syncing agent configurations from platform.
14
+
15
+ Manages fetching platform configs (system prompts, skills, MCPs, env vars)
16
+ and workflow definitions for agent startup and execution.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ api_key: str | None = None,
22
+ project_id: str | None = None,
23
+ timeout: float = 30.0,
24
+ ) -> None:
25
+ """Initialize the client.
26
+
27
+ Args:
28
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
29
+ project_id: Project ID to sync configs for. If not provided, reads from STEERDEV_PROJECT_ID.
30
+ timeout: Request timeout in seconds.
31
+ """
32
+ self.api_key = api_key or get_api_key()
33
+ self.project_id = project_id or get_project_id()
34
+ self.api_base = get_api_endpoint()
35
+ self.timeout = timeout
36
+ self._client: httpx.AsyncClient | None = None
37
+
38
+ @property
39
+ def headers(self) -> dict[str, str]:
40
+ """Get request headers with authentication."""
41
+ return {
42
+ "Authorization": f"Bearer {self.api_key}",
43
+ "Content-Type": "application/json",
44
+ }
45
+
46
+ async def _get_client(self) -> httpx.AsyncClient:
47
+ """Get or create async HTTP client."""
48
+ if self._client is None:
49
+ self._client = httpx.AsyncClient(timeout=self.timeout)
50
+ return self._client
51
+
52
+ async def close(self) -> None:
53
+ """Close the HTTP client."""
54
+ if self._client is not None:
55
+ await self._client.aclose()
56
+ self._client = None
57
+
58
+ async def __aenter__(self) -> Self:
59
+ """Enter async context manager."""
60
+ return self
61
+
62
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
63
+ """Exit async context manager."""
64
+ await self.close()
65
+
66
+ async def sync_configs(self, project_id: str | None = None) -> PlatformConfig | None:
67
+ """Sync all active configurations from platform.
68
+
69
+ Args:
70
+ project_id: Override project ID for this request.
71
+
72
+ Returns:
73
+ PlatformConfig with all synced configs, or None on failure.
74
+ """
75
+ project_id = project_id or self.project_id
76
+ if not project_id:
77
+ logger.error("No project_id provided for config sync")
78
+ return None
79
+
80
+ client = await self._get_client()
81
+ logger.debug(f"Syncing configs for project {project_id}")
82
+
83
+ try:
84
+ response = await client.get(
85
+ f"{self.api_base}/configs/sync",
86
+ headers=self.headers,
87
+ params={"project_id": project_id},
88
+ )
89
+
90
+ if response.status_code == 200:
91
+ data = response.json()
92
+ config = PlatformConfig.from_api_response(data)
93
+ logger.info(
94
+ f"Synced configs: {len(config.system_prompts)} prompts, "
95
+ f"{len(config.skills)} skills, {len(config.mcps)} MCPs, "
96
+ f"{len(config.env_vars)} env var sets"
97
+ )
98
+ return config
99
+
100
+ logger.error(f"Failed to sync configs: {response.status_code} - {response.text}")
101
+ return None
102
+
103
+ except httpx.RequestError as e:
104
+ logger.error(f"Request error syncing configs: {e}")
105
+ return None
106
+
107
+ async def get_workflow(
108
+ self,
109
+ workflow_id: str,
110
+ ) -> WorkflowConfig | None:
111
+ """Get a specific workflow by ID.
112
+
113
+ Args:
114
+ workflow_id: Workflow ID to fetch.
115
+
116
+ Returns:
117
+ WorkflowConfig or None if not found.
118
+ """
119
+ client = await self._get_client()
120
+ logger.debug(f"Fetching workflow {workflow_id}")
121
+
122
+ try:
123
+ response = await client.get(
124
+ f"{self.api_base}/workflows/{workflow_id}",
125
+ headers=self.headers,
126
+ )
127
+
128
+ if response.status_code == 200:
129
+ data = response.json()
130
+ return WorkflowConfig.from_api_response(data)
131
+
132
+ if response.status_code == 404:
133
+ logger.warning(f"Workflow {workflow_id} not found")
134
+ return None
135
+
136
+ logger.error(f"Failed to get workflow: {response.status_code} - {response.text}")
137
+ return None
138
+
139
+ except httpx.RequestError as e:
140
+ logger.error(f"Request error getting workflow: {e}")
141
+ return None
142
+
143
+ async def get_default_workflow(
144
+ self,
145
+ project_id: str | None = None,
146
+ ) -> WorkflowConfig | None:
147
+ """Get the default workflow for a project.
148
+
149
+ Args:
150
+ project_id: Override project ID for this request.
151
+
152
+ Returns:
153
+ WorkflowConfig or None if no default workflow exists.
154
+ """
155
+ project_id = project_id or self.project_id
156
+ if not project_id:
157
+ logger.error("No project_id provided for getting default workflow")
158
+ return None
159
+
160
+ client = await self._get_client()
161
+ logger.debug(f"Fetching default workflow for project {project_id}")
162
+
163
+ try:
164
+ response = await client.get(
165
+ f"{self.api_base}/workflows",
166
+ headers=self.headers,
167
+ params={"project_id": project_id},
168
+ )
169
+
170
+ if response.status_code == 200:
171
+ data = response.json()
172
+ workflows = data.get("workflows", [])
173
+
174
+ # Find the default workflow
175
+ for wf in workflows:
176
+ if wf.get("is_default"):
177
+ return WorkflowConfig.from_api_response(wf)
178
+
179
+ # If no default, return the first workflow if any
180
+ if workflows:
181
+ logger.debug("No default workflow found, using first available")
182
+ return WorkflowConfig.from_api_response(workflows[0])
183
+
184
+ logger.debug("No workflows found for project")
185
+ return None
186
+
187
+ logger.error(f"Failed to list workflows: {response.status_code} - {response.text}")
188
+ return None
189
+
190
+ except httpx.RequestError as e:
191
+ logger.error(f"Request error listing workflows: {e}")
192
+ return None
193
+
194
+ async def list_workflows(
195
+ self,
196
+ project_id: str | None = None,
197
+ limit: int = 50,
198
+ offset: int = 0,
199
+ ) -> list[WorkflowConfig]:
200
+ """List all workflows for a project.
201
+
202
+ Args:
203
+ project_id: Override project ID for this request.
204
+ limit: Maximum number of workflows to return.
205
+ offset: Offset for pagination.
206
+
207
+ Returns:
208
+ List of WorkflowConfig objects.
209
+ """
210
+ project_id = project_id or self.project_id
211
+ if not project_id:
212
+ logger.error("No project_id provided for listing workflows")
213
+ return []
214
+
215
+ client = await self._get_client()
216
+ logger.debug(f"Listing workflows for project {project_id}")
217
+
218
+ try:
219
+ response = await client.get(
220
+ f"{self.api_base}/workflows",
221
+ headers=self.headers,
222
+ params={"project_id": project_id, "limit": limit, "offset": offset},
223
+ )
224
+
225
+ if response.status_code == 200:
226
+ data = response.json()
227
+ workflows = [
228
+ WorkflowConfig.from_api_response(wf) for wf in data.get("workflows", [])
229
+ ]
230
+ logger.debug(f"Found {len(workflows)} workflows")
231
+ return workflows
232
+
233
+ logger.error(f"Failed to list workflows: {response.status_code} - {response.text}")
234
+ return []
235
+
236
+ except httpx.RequestError as e:
237
+ logger.error(f"Request error listing workflows: {e}")
238
+ return []
@@ -0,0 +1,306 @@
1
+ """Context API client for SteerDev Agent.
2
+
3
+ Provides methods for retrieving and managing project context
4
+ (tech stack, patterns, structure, etc.) for AI agents.
5
+
6
+ The Codebase Awareness feature provides:
7
+ - Tech stack detection (frameworks, libraries, tools)
8
+ - Directory structure analysis
9
+ - Code patterns and conventions
10
+ - Dependencies mapping
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from pydantic import BaseModel, Field
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+
21
+ from steerdev_agent.api.client import SteerDevClient, get_project_id
22
+
23
+ console = Console()
24
+
25
+
26
+ class TechStackInfo(BaseModel):
27
+ """Tech stack detection result."""
28
+
29
+ framework: dict[str, Any] | None = None
30
+ language: dict[str, Any] | None = None
31
+ database: dict[str, Any] | None = None
32
+ auth: dict[str, Any] | None = None
33
+ styling: dict[str, Any] | None = None
34
+ testing: dict[str, Any] | None = None
35
+ package_manager: str | None = Field(default=None, alias="packageManager")
36
+ build_tool: str | None = Field(default=None, alias="buildTool")
37
+
38
+
39
+ class StructureInfo(BaseModel):
40
+ """Directory structure analysis."""
41
+
42
+ type: str = "single"
43
+ directories: list[dict[str, Any]] = Field(default_factory=list)
44
+ entry_points: list[str] = Field(default_factory=list, alias="entryPoints")
45
+ config_files: list[str] = Field(default_factory=list, alias="configFiles")
46
+
47
+
48
+ class PatternsInfo(BaseModel):
49
+ """Code patterns and conventions."""
50
+
51
+ naming: dict[str, str] = Field(default_factory=dict)
52
+ architecture: list[str] = Field(default_factory=list)
53
+ conventions: list[dict[str, Any]] = Field(default_factory=list)
54
+ abstractions: dict[str, list[str]] = Field(default_factory=dict)
55
+
56
+
57
+ class CodebaseContext(BaseModel):
58
+ """Codebase context model - full analysis result."""
59
+
60
+ project_id: str
61
+ repositories: list[dict[str, Any]] = Field(default_factory=list)
62
+ context: dict[str, Any] | None = None
63
+ status: str
64
+ last_analyzed_at: str | None = None
65
+ error: str | None = None
66
+
67
+
68
+ class ContextClient(SteerDevClient):
69
+ """Client for project context operations.
70
+
71
+ Provides methods to retrieve and refresh project context
72
+ that helps AI agents understand the codebase.
73
+ """
74
+
75
+ def get_context(
76
+ self, project_id: str | None = None, format: str = "json"
77
+ ) -> dict[str, Any] | str | None:
78
+ """Get project context.
79
+
80
+ Args:
81
+ project_id: Project ID. Falls back to STEERDEV_PROJECT_ID env var.
82
+ format: Response format - "json" or "markdown".
83
+
84
+ Returns:
85
+ Project context (dict for JSON, str for markdown) or None if not found.
86
+ """
87
+ effective_project_id = project_id or get_project_id()
88
+ if not effective_project_id:
89
+ console.print(
90
+ "[red]Error: project_id is required. "
91
+ "Use --project-id or set STEERDEV_PROJECT_ID environment variable[/red]"
92
+ )
93
+ return None
94
+
95
+ console.print(
96
+ f"Fetching context for project {effective_project_id} "
97
+ f"from {self.api_base}/context?project_id={effective_project_id}"
98
+ )
99
+ response = self.get(f"/context?project_id={effective_project_id}&format={format}")
100
+
101
+ if response.status_code == 404:
102
+ console.print(f"[yellow]No context found for project {effective_project_id}[/yellow]")
103
+ return None
104
+
105
+ if response.status_code != 200:
106
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
107
+ return None
108
+
109
+ # Return raw text for markdown format
110
+ if format == "markdown":
111
+ return response.text
112
+
113
+ return response.json()
114
+
115
+ def refresh_context(
116
+ self, project_id: str | None = None, force: bool = False
117
+ ) -> dict[str, Any] | None:
118
+ """Force refresh of project context.
119
+
120
+ Triggers a re-analysis of the project repositories to update cached context.
121
+
122
+ Args:
123
+ project_id: Project ID. Falls back to STEERDEV_PROJECT_ID env var.
124
+ force: Force re-analysis even if context is up to date.
125
+
126
+ Returns:
127
+ Analysis result dict or None on failure.
128
+ """
129
+ effective_project_id = project_id or get_project_id()
130
+ if not effective_project_id:
131
+ console.print(
132
+ "[red]Error: project_id is required. "
133
+ "Use --project-id or set STEERDEV_PROJECT_ID environment variable[/red]"
134
+ )
135
+ return None
136
+
137
+ console.print(
138
+ f"Refreshing context for project {effective_project_id} "
139
+ f"at {self.api_base}/context/analyze"
140
+ )
141
+ response = self.post(
142
+ "/context/analyze",
143
+ json={"project_id": effective_project_id, "force": force},
144
+ )
145
+
146
+ if response.status_code == 404:
147
+ console.print(f"[red]Error: Project '{effective_project_id}' not found[/red]")
148
+ return None
149
+
150
+ if response.status_code == 400:
151
+ error_data = response.json()
152
+ console.print(f"[red]Error: {error_data.get('error', 'Unknown error')}[/red]")
153
+ return None
154
+
155
+ if response.status_code not in (200, 201, 202):
156
+ console.print(f"[red]API Error: {response.status_code} - {response.text}[/red]")
157
+ return None
158
+
159
+ return response.json()
160
+
161
+
162
+ def display_context(context: dict[str, Any]) -> None:
163
+ """Display project context in a formatted panel.
164
+
165
+ Args:
166
+ context: Project context dict from the API.
167
+ """
168
+ project_id = context.get("project_id", "Unknown")
169
+ status = context.get("status", "unknown")
170
+ ctx = context.get("context") or {}
171
+ repos = context.get("repositories", [])
172
+
173
+ # Build context info
174
+ info_lines = []
175
+
176
+ # Repositories
177
+ if repos:
178
+ info_lines.append("[bold cyan]Repositories:[/bold cyan]")
179
+ for repo in repos:
180
+ info_lines.append(f" • {repo.get('full_name', 'unknown')}")
181
+ info_lines.append("")
182
+
183
+ # Summary (markdown)
184
+ if ctx.get("summary"):
185
+ info_lines.append("[bold cyan]Summary:[/bold cyan]")
186
+ # Truncate long summaries
187
+ summary = ctx["summary"]
188
+ lines = summary.split("\n")[:15]
189
+ for line in lines:
190
+ info_lines.append(f" {line}")
191
+ if len(summary.split("\n")) > 15:
192
+ info_lines.append(" ...")
193
+ info_lines.append("")
194
+
195
+ # Tech stack
196
+ tech_stack = ctx.get("tech_stack", {})
197
+ if tech_stack:
198
+ info_lines.append("[bold cyan]Tech Stack:[/bold cyan]")
199
+ if tech_stack.get("framework"):
200
+ fw = tech_stack["framework"]
201
+ fw_str = fw.get("name", "")
202
+ if fw.get("version"):
203
+ fw_str += f" {fw['version']}"
204
+ if fw.get("router"):
205
+ fw_str += f" ({fw['router']})"
206
+ info_lines.append(f" • Framework: {fw_str}")
207
+ if tech_stack.get("language"):
208
+ info_lines.append(f" • Language: {tech_stack['language'].get('primary', 'unknown')}")
209
+ if tech_stack.get("database"):
210
+ db = tech_stack["database"]
211
+ db_str = db.get("name", "")
212
+ if db.get("client"):
213
+ db_str += f" ({db['client']})"
214
+ info_lines.append(f" • Database: {db_str}")
215
+ if tech_stack.get("auth"):
216
+ info_lines.append(f" • Auth: {tech_stack['auth'].get('provider', 'unknown')}")
217
+ if tech_stack.get("styling"):
218
+ style = tech_stack["styling"]
219
+ style_str = style.get("framework", "")
220
+ if style.get("ui"):
221
+ style_str += f" + {style['ui']}"
222
+ info_lines.append(f" • Styling: {style_str}")
223
+ if tech_stack.get("testing"):
224
+ test = tech_stack["testing"]
225
+ test_parts = []
226
+ if test.get("framework"):
227
+ test_parts.append(test["framework"])
228
+ if test.get("e2e"):
229
+ test_parts.append(test["e2e"])
230
+ if test_parts:
231
+ info_lines.append(f" • Testing: {' + '.join(test_parts)}")
232
+ if tech_stack.get("packageManager"):
233
+ info_lines.append(f" • Package Manager: {tech_stack['packageManager']}")
234
+ info_lines.append("")
235
+
236
+ # Patterns
237
+ patterns = ctx.get("patterns", {})
238
+ if patterns:
239
+ arch = patterns.get("architecture", [])
240
+ if arch:
241
+ info_lines.append("[bold cyan]Architecture:[/bold cyan]")
242
+ for a in arch[:5]:
243
+ info_lines.append(f" • {a}")
244
+ info_lines.append("")
245
+
246
+ conventions = patterns.get("conventions", [])
247
+ if conventions:
248
+ info_lines.append("[bold cyan]Conventions:[/bold cyan]")
249
+ for conv in conventions[:5]:
250
+ info_lines.append(f" • {conv.get('pattern', '')}")
251
+ info_lines.append("")
252
+
253
+ # Structure (directories)
254
+ structure = ctx.get("structure", {})
255
+ if structure:
256
+ dirs = structure.get("directories", [])
257
+ if dirs:
258
+ info_lines.append("[bold cyan]Key Directories:[/bold cyan]")
259
+ for d in dirs[:6]:
260
+ path = d.get("path", "")
261
+ purpose = d.get("purpose", "")
262
+ info_lines.append(f" • {path}: {purpose}")
263
+ if len(dirs) > 6:
264
+ info_lines.append(f" ... and {len(dirs) - 6} more")
265
+ info_lines.append("")
266
+
267
+ # Status and metadata
268
+ status_color = {
269
+ "completed": "green",
270
+ "analyzing": "blue",
271
+ "pending": "yellow",
272
+ "failed": "red",
273
+ }.get(status, "white")
274
+ info_lines.append(f"[dim]Status: [{status_color}]{status}[/{status_color}][/dim]")
275
+
276
+ if context.get("last_analyzed_at"):
277
+ info_lines.append(f"[dim]Last analyzed: {context['last_analyzed_at']}[/dim]")
278
+
279
+ if context.get("error"):
280
+ info_lines.append(f"[red]Error: {context['error']}[/red]")
281
+
282
+ console.print(
283
+ Panel(
284
+ "\n".join(info_lines),
285
+ title=f"Codebase Context: {project_id[:8]}...",
286
+ border_style="blue",
287
+ )
288
+ )
289
+
290
+
291
+ def display_context_refresh_success(project_id: str) -> None:
292
+ """Display context refresh success message.
293
+
294
+ Args:
295
+ project_id: Project ID that was refreshed.
296
+ """
297
+ console.print(
298
+ Panel(
299
+ f"[bold green]Context refresh initiated[/bold green]\n\n"
300
+ f"Project: {project_id}\n\n"
301
+ "The context will be updated in the background. "
302
+ "Run `steerdev context get` to retrieve the updated context.",
303
+ title="Success",
304
+ border_style="green",
305
+ )
306
+ )