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,294 @@
1
+ """Events streaming API client with batching support."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from datetime import UTC, datetime
6
+ from typing import Any, Self
7
+
8
+ import httpx
9
+ from loguru import logger
10
+ from pydantic import BaseModel, Field
11
+
12
+ from steerdev_agent.api.client import get_api_endpoint, get_api_key
13
+
14
+
15
+ class EventData(BaseModel):
16
+ """Model for a single event to be sent to the API."""
17
+
18
+ event_type: str = Field(description="Type of event (e.g., assistant, user, system, tool_use)")
19
+ event_timestamp: str = Field(description="ISO timestamp of when the event occurred")
20
+ data: dict[str, Any] = Field(description="Structured event data")
21
+ raw_json: str = Field(description="Original raw JSON from the agent")
22
+
23
+
24
+ class EventBatchRequest(BaseModel):
25
+ """Request model for batch inserting events."""
26
+
27
+ events: list[EventData] = Field(description="List of events to insert")
28
+
29
+
30
+ class EventResponse(BaseModel):
31
+ """Response model for an event."""
32
+
33
+ id: str
34
+ session_id: str
35
+ event_type: str
36
+ event_timestamp: str
37
+ data: dict[str, Any]
38
+ raw_json: str
39
+ created_at: str
40
+
41
+
42
+ class EventListResponse(BaseModel):
43
+ """Response model for listing events."""
44
+
45
+ events: list[EventResponse]
46
+ total: int
47
+
48
+
49
+ class EventsClient:
50
+ """Async HTTP client for Events API with batching support.
51
+
52
+ Buffers events and sends them in batches to reduce API calls.
53
+ Configurable batch size and flush interval.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ session_id: str,
59
+ api_key: str | None = None,
60
+ timeout: float = 30.0,
61
+ batch_size: int = 10,
62
+ flush_interval_seconds: float = 5.0,
63
+ ) -> None:
64
+ """Initialize the client.
65
+
66
+ Args:
67
+ session_id: Session ID to associate events with.
68
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
69
+ timeout: Request timeout in seconds.
70
+ batch_size: Number of events to buffer before auto-flushing.
71
+ flush_interval_seconds: Maximum time to wait before flushing events.
72
+ """
73
+ self.session_id = session_id
74
+ self.api_key = api_key or get_api_key()
75
+ self.api_base = get_api_endpoint()
76
+ self.timeout = timeout
77
+ self.batch_size = batch_size
78
+ self.flush_interval_seconds = flush_interval_seconds
79
+
80
+ self._client: httpx.AsyncClient | None = None
81
+ self._buffer: list[EventData] = []
82
+ self._buffer_lock = asyncio.Lock()
83
+ self._flush_task: asyncio.Task[None] | None = None
84
+ self._running = False
85
+
86
+ @property
87
+ def headers(self) -> dict[str, str]:
88
+ """Get request headers with authentication."""
89
+ return {
90
+ "Authorization": f"Bearer {self.api_key}",
91
+ "Content-Type": "application/json",
92
+ }
93
+
94
+ async def _get_client(self) -> httpx.AsyncClient:
95
+ """Get or create async HTTP client."""
96
+ if self._client is None:
97
+ self._client = httpx.AsyncClient(timeout=self.timeout)
98
+ return self._client
99
+
100
+ async def start(self) -> None:
101
+ """Start the background flush task."""
102
+ if self._running:
103
+ return
104
+
105
+ self._running = True
106
+ self._flush_task = asyncio.create_task(self._flush_loop())
107
+ logger.debug(f"EventsClient started for session {self.session_id}")
108
+
109
+ async def stop(self) -> None:
110
+ """Stop the background flush task and flush remaining events."""
111
+ self._running = False
112
+
113
+ if self._flush_task:
114
+ self._flush_task.cancel()
115
+ with contextlib.suppress(asyncio.CancelledError):
116
+ await self._flush_task
117
+ self._flush_task = None
118
+
119
+ # Final flush
120
+ await self.flush()
121
+ logger.debug(f"EventsClient stopped for session {self.session_id}")
122
+
123
+ async def close(self) -> None:
124
+ """Stop and close the HTTP client."""
125
+ await self.stop()
126
+ if self._client is not None:
127
+ await self._client.aclose()
128
+ self._client = None
129
+
130
+ async def __aenter__(self) -> Self:
131
+ """Enter async context manager."""
132
+ await self.start()
133
+ return self
134
+
135
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
136
+ """Exit async context manager."""
137
+ await self.close()
138
+
139
+ async def _flush_loop(self) -> None:
140
+ """Background task that periodically flushes events."""
141
+ while self._running:
142
+ try:
143
+ await asyncio.sleep(self.flush_interval_seconds)
144
+ await self.flush()
145
+ except asyncio.CancelledError:
146
+ break
147
+ except Exception as e:
148
+ logger.error(f"Error in flush loop: {e}")
149
+
150
+ async def add_event(
151
+ self,
152
+ event_type: str,
153
+ data: dict[str, Any],
154
+ raw_json: str,
155
+ timestamp: datetime | None = None,
156
+ ) -> None:
157
+ """Add an event to the buffer.
158
+
159
+ Events are automatically flushed when the batch size is reached
160
+ or when the flush interval expires.
161
+
162
+ Args:
163
+ event_type: Type of event (e.g., assistant, user, system).
164
+ data: Structured event data.
165
+ raw_json: Original raw JSON string.
166
+ timestamp: Event timestamp. Defaults to now.
167
+ """
168
+ if timestamp is None:
169
+ timestamp = datetime.now(UTC)
170
+
171
+ event = EventData(
172
+ event_type=event_type,
173
+ event_timestamp=timestamp.isoformat(),
174
+ data=data,
175
+ raw_json=raw_json,
176
+ )
177
+
178
+ async with self._buffer_lock:
179
+ self._buffer.append(event)
180
+ buffer_size = len(self._buffer)
181
+
182
+ # Auto-flush if batch size reached
183
+ if buffer_size >= self.batch_size:
184
+ await self.flush()
185
+
186
+ async def flush(self) -> bool:
187
+ """Flush buffered events to the API.
188
+
189
+ Returns:
190
+ True if flush succeeded or no events to flush, False on error.
191
+ """
192
+ async with self._buffer_lock:
193
+ if not self._buffer:
194
+ return True
195
+
196
+ events_to_send = self._buffer.copy()
197
+ self._buffer.clear()
198
+
199
+ logger.debug(f"Flushing {len(events_to_send)} events for session {self.session_id}")
200
+
201
+ client = await self._get_client()
202
+ request = EventBatchRequest(events=events_to_send)
203
+ url = f"{self.api_base}/sessions/{self.session_id}/events"
204
+
205
+ try:
206
+ logger.debug(f"POST {url}")
207
+ response = await client.post(
208
+ url,
209
+ headers=self.headers,
210
+ json=request.model_dump(),
211
+ )
212
+
213
+ if response.status_code in (200, 201):
214
+ logger.debug(f"Successfully flushed {len(events_to_send)} events")
215
+ return True
216
+
217
+ logger.error(f"Failed to flush events to {url}: {response.status_code} - {response.text[:200]}")
218
+ # Re-add events to buffer on failure
219
+ async with self._buffer_lock:
220
+ self._buffer = events_to_send + self._buffer
221
+ return False
222
+
223
+ except httpx.RequestError as e:
224
+ logger.error(f"Request error flushing events to {url}: {e}")
225
+ # Re-add events to buffer on failure
226
+ async with self._buffer_lock:
227
+ self._buffer = events_to_send + self._buffer
228
+ return False
229
+
230
+ async def get_events(
231
+ self,
232
+ limit: int = 100,
233
+ offset: int = 0,
234
+ ) -> EventListResponse | None:
235
+ """Get events for the session (for debugging).
236
+
237
+ Args:
238
+ limit: Maximum number of events to return.
239
+ offset: Offset for pagination.
240
+
241
+ Returns:
242
+ Event list response or None on failure.
243
+ """
244
+ client = await self._get_client()
245
+ params: dict[str, int] = {"limit": limit, "offset": offset}
246
+
247
+ logger.debug(f"Getting events for session {self.session_id}")
248
+
249
+ try:
250
+ response = await client.get(
251
+ f"{self.api_base}/sessions/{self.session_id}/events",
252
+ headers=self.headers,
253
+ params=params,
254
+ )
255
+
256
+ if response.status_code == 200:
257
+ return EventListResponse(**response.json())
258
+
259
+ logger.error(f"Failed to get events: {response.status_code} - {response.text}")
260
+ return None
261
+
262
+ except httpx.RequestError as e:
263
+ logger.error(f"Request error getting events: {e}")
264
+ return None
265
+
266
+ @property
267
+ def pending_events(self) -> int:
268
+ """Return the number of events pending in the buffer."""
269
+ return len(self._buffer)
270
+
271
+
272
+ def create_event_from_stream(
273
+ event_type: str,
274
+ message_data: dict[str, Any],
275
+ raw_json: str,
276
+ ) -> EventData:
277
+ """Create an EventData from stream output.
278
+
279
+ Helper function to create events from Claude's stream-json output.
280
+
281
+ Args:
282
+ event_type: The type from Claude's stream (assistant, user, system, etc.).
283
+ message_data: The parsed message data.
284
+ raw_json: The original raw JSON line.
285
+
286
+ Returns:
287
+ EventData ready to be added to the client.
288
+ """
289
+ return EventData(
290
+ event_type=event_type,
291
+ event_timestamp=datetime.now(UTC).isoformat(),
292
+ data=message_data,
293
+ raw_json=raw_json,
294
+ )
@@ -0,0 +1,178 @@
1
+ """Claude Code hooks API client for activity reporting."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from datetime import UTC, datetime
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from steerdev_agent.api.client import get_agent_name, get_api_key
12
+
13
+ # Default activity API endpoint
14
+ DEFAULT_ACTIVITY_ENDPOINT = "https://steerdev.com/api/v1"
15
+
16
+
17
+ def get_activity_endpoint() -> str:
18
+ """Get activity API endpoint from environment or use default."""
19
+ return os.environ.get("STEERDEV_API_ENDPOINT", DEFAULT_ACTIVITY_ENDPOINT)
20
+
21
+
22
+ class HooksClient:
23
+ """Client for reporting hook events to SteerDev activity API.
24
+
25
+ Hooks read JSON input from stdin (provided by Claude Code) and
26
+ report activity events to the SteerDev platform.
27
+ """
28
+
29
+ def __init__(self, timeout: float = 5.0) -> None:
30
+ """Initialize the hooks client.
31
+
32
+ Args:
33
+ timeout: Request timeout in seconds (short for hooks).
34
+ """
35
+ self.api_key = get_api_key()
36
+ self.api_endpoint = get_activity_endpoint()
37
+ self.timeout = timeout
38
+ self.agent_name = get_agent_name()
39
+
40
+ def read_stdin_input(self) -> dict[str, Any] | None:
41
+ """Read JSON input from stdin.
42
+
43
+ Returns:
44
+ Parsed JSON dict or None on failure.
45
+ """
46
+ try:
47
+ return json.load(sys.stdin)
48
+ except json.JSONDecodeError:
49
+ return None
50
+
51
+ def report_event(
52
+ self,
53
+ event_type: str,
54
+ input_data: dict[str, Any],
55
+ extra_metadata: dict[str, Any] | None = None,
56
+ ) -> bool:
57
+ """Report an activity event to the SteerDev API.
58
+
59
+ Args:
60
+ event_type: Type of event (e.g., "session_start", "agent_stopped").
61
+ input_data: Input data from Claude Code hook.
62
+ extra_metadata: Additional metadata to include.
63
+
64
+ Returns:
65
+ True if the event was reported successfully.
66
+ """
67
+ if not self.api_key:
68
+ return False
69
+
70
+ # Build base metadata
71
+ metadata: dict[str, Any] = {
72
+ "cwd": input_data.get("cwd"),
73
+ "transcript_path": input_data.get("transcript_path"),
74
+ "permission_mode": input_data.get("permission_mode"),
75
+ }
76
+
77
+ # Add extra metadata
78
+ if extra_metadata:
79
+ metadata.update(extra_metadata)
80
+
81
+ # Build event payload
82
+ event = {
83
+ "event_type": event_type,
84
+ "timestamp": datetime.now(UTC).isoformat(),
85
+ "session_name": input_data.get("session_id"),
86
+ "metadata": metadata,
87
+ }
88
+
89
+ # Build request body
90
+ request_body = {
91
+ "agent_name": self.agent_name,
92
+ "application": "claude_code",
93
+ "events": [event],
94
+ }
95
+
96
+ # Send to API (non-blocking)
97
+ try:
98
+ httpx.post(
99
+ f"{self.api_endpoint}/activity/report",
100
+ json=request_body,
101
+ headers={"Authorization": f"Bearer {self.api_key}"},
102
+ timeout=self.timeout,
103
+ )
104
+ return True
105
+ except Exception:
106
+ return False
107
+
108
+ def session_start(self) -> None:
109
+ """Process SessionStart hook event.
110
+
111
+ Reads JSON from stdin and reports session_start event.
112
+ """
113
+ input_data = self.read_stdin_input()
114
+ if not input_data:
115
+ sys.exit(0)
116
+
117
+ self.report_event(
118
+ event_type="session_start",
119
+ input_data=input_data,
120
+ extra_metadata={
121
+ "source": input_data.get("source"), # startup, resume, clear, compact
122
+ },
123
+ )
124
+ sys.exit(0)
125
+
126
+ def session_end(self) -> None:
127
+ """Process SessionEnd hook event.
128
+
129
+ Reads JSON from stdin and reports session_end event.
130
+ """
131
+ input_data = self.read_stdin_input()
132
+ if not input_data:
133
+ sys.exit(0)
134
+
135
+ self.report_event(
136
+ event_type="session_end",
137
+ input_data=input_data,
138
+ extra_metadata={
139
+ "reason": input_data.get("reason"), # clear, logout, prompt_input_exit, other
140
+ },
141
+ )
142
+ sys.exit(0)
143
+
144
+ def agent_stop(self) -> None:
145
+ """Process Stop hook event (main agent).
146
+
147
+ Reads JSON from stdin and reports agent_stopped event.
148
+ """
149
+ input_data = self.read_stdin_input()
150
+ if not input_data:
151
+ sys.exit(0)
152
+
153
+ self.report_event(
154
+ event_type="agent_stopped",
155
+ input_data=input_data,
156
+ extra_metadata={
157
+ "stop_hook_active": input_data.get("stop_hook_active"),
158
+ },
159
+ )
160
+ sys.exit(0)
161
+
162
+ def subagent_stop(self) -> None:
163
+ """Process SubagentStop hook event.
164
+
165
+ Reads JSON from stdin and reports subagent_stopped event.
166
+ """
167
+ input_data = self.read_stdin_input()
168
+ if not input_data:
169
+ sys.exit(0)
170
+
171
+ self.report_event(
172
+ event_type="subagent_stopped",
173
+ input_data=input_data,
174
+ extra_metadata={
175
+ "stop_hook_active": input_data.get("stop_hook_active"),
176
+ },
177
+ )
178
+ sys.exit(0)