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,145 @@
1
+ """Agent registration API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Self
6
+
7
+ import httpx
8
+ from loguru import logger
9
+ from pydantic import BaseModel, Field
10
+
11
+ from steerdev_agent.api.client import get_api_endpoint, get_api_key
12
+
13
+
14
+ class AgentRegisterRequest(BaseModel):
15
+ """Request model for registering an agent."""
16
+
17
+ name: str = Field(description="Agent name for identification")
18
+ application: str | None = Field(
19
+ default=None, description="Application type (e.g., claude_code)"
20
+ )
21
+ hostname: str | None = Field(default=None, description="Hostname where agent is running")
22
+
23
+
24
+ class AgentResponse(BaseModel):
25
+ """Response model for agent data."""
26
+
27
+ id: str
28
+ project_id: str
29
+ name: str | None = None
30
+ application: str | None = None
31
+ hostname: str | None = None
32
+ status: str
33
+ last_heartbeat_at: str | None = None
34
+ created_at: str
35
+ updated_at: str
36
+
37
+
38
+ class AgentsClient:
39
+ """Async HTTP client for Agent registration API.
40
+
41
+ Allows agents to register themselves and get their agent_id.
42
+ """
43
+
44
+ def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
45
+ """Initialize the client.
46
+
47
+ Args:
48
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
49
+ timeout: Request timeout in seconds.
50
+ """
51
+ self.api_key = api_key or get_api_key()
52
+ self.api_base = get_api_endpoint()
53
+ self.timeout = timeout
54
+ self._client: httpx.AsyncClient | None = None
55
+
56
+ @property
57
+ def headers(self) -> dict[str, str]:
58
+ """Get request headers with authentication."""
59
+ return {
60
+ "Authorization": f"Bearer {self.api_key}",
61
+ "Content-Type": "application/json",
62
+ }
63
+
64
+ async def _get_client(self) -> httpx.AsyncClient:
65
+ """Get or create async HTTP client."""
66
+ if self._client is None:
67
+ self._client = httpx.AsyncClient(timeout=self.timeout)
68
+ return self._client
69
+
70
+ async def close(self) -> None:
71
+ """Close the HTTP client."""
72
+ if self._client is not None:
73
+ await self._client.aclose()
74
+ self._client = None
75
+
76
+ async def __aenter__(self) -> Self:
77
+ """Enter async context manager."""
78
+ return self
79
+
80
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
81
+ """Exit async context manager."""
82
+ await self.close()
83
+
84
+ async def register(self, request: AgentRegisterRequest) -> AgentResponse | None:
85
+ """Register an agent or get existing one.
86
+
87
+ This is idempotent - calling with the same name will return the existing agent.
88
+
89
+ Args:
90
+ request: Agent registration request with name and optional metadata.
91
+
92
+ Returns:
93
+ Agent data including the agent_id, or None on failure.
94
+ """
95
+ client = await self._get_client()
96
+ logger.debug(f"Registering agent '{request.name}' at {self.api_base}/agents")
97
+
98
+ try:
99
+ response = await client.post(
100
+ f"{self.api_base}/agents",
101
+ headers=self.headers,
102
+ json=request.model_dump(exclude_none=True),
103
+ )
104
+
105
+ if response.status_code in (200, 201):
106
+ data = response.json()
107
+ logger.info(f"Agent registered successfully with ID: {data.get('id')}")
108
+ return AgentResponse(**data)
109
+
110
+ logger.error(f"Failed to register agent: {response.status_code} - {response.text}")
111
+ return None
112
+
113
+ except httpx.RequestError as e:
114
+ logger.error(f"Request error registering agent: {e}")
115
+ return None
116
+
117
+ async def list_agents(self, limit: int = 50, offset: int = 0) -> list[AgentResponse]:
118
+ """List agents for the project.
119
+
120
+ Args:
121
+ limit: Maximum number of agents to return.
122
+ offset: Offset for pagination.
123
+
124
+ Returns:
125
+ List of agent data.
126
+ """
127
+ client = await self._get_client()
128
+
129
+ try:
130
+ response = await client.get(
131
+ f"{self.api_base}/agents",
132
+ headers=self.headers,
133
+ params={"limit": limit, "offset": offset},
134
+ )
135
+
136
+ if response.status_code == 200:
137
+ data = response.json()
138
+ return [AgentResponse(**agent) for agent in data.get("agents", [])]
139
+
140
+ logger.error(f"Failed to list agents: {response.status_code} - {response.text}")
141
+ return []
142
+
143
+ except httpx.RequestError as e:
144
+ logger.error(f"Request error listing agents: {e}")
145
+ return []
@@ -0,0 +1,158 @@
1
+ """Base API client for SteerDev API."""
2
+
3
+ import os
4
+ from typing import Any, Self
5
+
6
+ import httpx
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+ # Default API endpoint
12
+ DEFAULT_API_ENDPOINT = "https://steerdev.com/api/v1"
13
+
14
+
15
+ def get_api_key() -> str | None:
16
+ """Get API key from environment."""
17
+ return os.environ.get("STEERDEV_API_KEY")
18
+
19
+
20
+ def get_project_id() -> str | None:
21
+ """Get project ID from environment."""
22
+ return os.environ.get("STEERDEV_PROJECT_ID")
23
+
24
+
25
+ def get_agent_id() -> str | None:
26
+ """Get agent ID from environment."""
27
+ return os.environ.get("STEERDEV_AGENT_ID")
28
+
29
+
30
+ def get_agent_name() -> str | None:
31
+ """Get agent name from environment."""
32
+ return os.environ.get("STEERDEV_AGENT_NAME")
33
+
34
+
35
+ def get_api_endpoint() -> str:
36
+ """Get API endpoint from environment or use default."""
37
+ return os.environ.get("STEERDEV_API_ENDPOINT", DEFAULT_API_ENDPOINT)
38
+
39
+
40
+ class SteerDevClient:
41
+ """Base HTTP client for SteerDev API.
42
+
43
+ Handles authentication, headers, and common error handling.
44
+ """
45
+
46
+ def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
47
+ """Initialize the client.
48
+
49
+ Args:
50
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY env var.
51
+ timeout: Request timeout in seconds.
52
+ """
53
+ self.api_key = api_key or get_api_key()
54
+ self.api_base = get_api_endpoint()
55
+ self.timeout = timeout
56
+ self._client: httpx.Client | None = None
57
+
58
+ @property
59
+ def headers(self) -> dict[str, str]:
60
+ """Get request headers with authentication."""
61
+ return {
62
+ "Authorization": f"Bearer {self.api_key}",
63
+ "Content-Type": "application/json",
64
+ }
65
+
66
+ def _get_client(self) -> httpx.Client:
67
+ """Get or create HTTP client."""
68
+ if self._client is None:
69
+ self._client = httpx.Client(timeout=self.timeout, follow_redirects=True)
70
+ return self._client
71
+
72
+ def close(self) -> None:
73
+ """Close the HTTP client."""
74
+ if self._client is not None:
75
+ self._client.close()
76
+ self._client = None
77
+
78
+ def __enter__(self) -> Self:
79
+ """Enter context manager."""
80
+ return self
81
+
82
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
83
+ """Exit context manager."""
84
+ self.close()
85
+
86
+ def get(
87
+ self,
88
+ path: str,
89
+ params: dict[str, Any] | None = None,
90
+ ) -> httpx.Response:
91
+ """Make a GET request.
92
+
93
+ Args:
94
+ path: API path (e.g., "/tasks").
95
+ params: Query parameters.
96
+
97
+ Returns:
98
+ HTTP response.
99
+ """
100
+ client = self._get_client()
101
+ return client.get(
102
+ f"{self.api_base}{path}",
103
+ headers=self.headers,
104
+ params=params,
105
+ )
106
+
107
+ def post(
108
+ self,
109
+ path: str,
110
+ json: dict[str, Any] | None = None,
111
+ ) -> httpx.Response:
112
+ """Make a POST request.
113
+
114
+ Args:
115
+ path: API path (e.g., "/tasks").
116
+ json: Request body as JSON.
117
+
118
+ Returns:
119
+ HTTP response.
120
+ """
121
+ client = self._get_client()
122
+ return client.post(
123
+ f"{self.api_base}{path}",
124
+ headers=self.headers,
125
+ json=json,
126
+ )
127
+
128
+ def patch(
129
+ self,
130
+ path: str,
131
+ json: dict[str, Any] | None = None,
132
+ ) -> httpx.Response:
133
+ """Make a PATCH request.
134
+
135
+ Args:
136
+ path: API path (e.g., "/tasks/{id}").
137
+ json: Request body as JSON.
138
+
139
+ Returns:
140
+ HTTP response.
141
+ """
142
+ client = self._get_client()
143
+ return client.patch(
144
+ f"{self.api_base}{path}",
145
+ headers=self.headers,
146
+ json=json,
147
+ )
148
+
149
+ def check_api_key(self) -> bool:
150
+ """Check if API key is configured.
151
+
152
+ Returns:
153
+ True if API key is set.
154
+ """
155
+ if not self.api_key:
156
+ console.print("[red]Error: STEERDEV_API_KEY environment variable not set[/red]")
157
+ return False
158
+ return True
@@ -0,0 +1,399 @@
1
+ """Commands API client for agent daemon mode."""
2
+
3
+ from typing import Any, Literal, Self
4
+
5
+ import httpx
6
+ from loguru import logger
7
+ from pydantic import BaseModel, Field
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from steerdev_agent.api.client import get_agent_id, get_api_endpoint, get_api_key
13
+
14
+ console = Console()
15
+
16
+ CommandType = Literal["task", "prompt", "control"]
17
+ CommandStatus = Literal[
18
+ "pending", "claimed", "executing", "completed", "failed", "cancelled", "expired"
19
+ ]
20
+
21
+ STATUS_STYLES: dict[str, str] = {
22
+ "pending": "dim",
23
+ "claimed": "yellow",
24
+ "executing": "blue",
25
+ "completed": "green",
26
+ "failed": "red",
27
+ "cancelled": "dim",
28
+ "expired": "dim",
29
+ }
30
+
31
+
32
+ class CommandResponse(BaseModel):
33
+ """Response model for command data."""
34
+
35
+ id: str
36
+ agent_id: str
37
+ project_id: str
38
+ command_type: CommandType
39
+ task_id: str | None = None
40
+ prompt: str | None = None
41
+ control_action: str | None = None
42
+ working_directory: str | None = None
43
+ metadata: dict[str, Any] = Field(default_factory=dict)
44
+ status: CommandStatus
45
+ priority: int = 0
46
+ session_id: str | None = None
47
+ result: str | None = None
48
+ error: str | None = None
49
+ claimed_at: str | None = None
50
+ started_at: str | None = None
51
+ completed_at: str | None = None
52
+ expires_at: str | None = None
53
+ submitted_by: str | None = None
54
+ created_at: str
55
+ updated_at: str
56
+
57
+
58
+ class CommandListResponse(BaseModel):
59
+ """Response model for listing commands."""
60
+
61
+ commands: list[CommandResponse]
62
+ total: int
63
+ limit: int
64
+ offset: int
65
+
66
+
67
+ class CommandsClient:
68
+ """Async HTTP client for the agent commands API.
69
+
70
+ Used by the daemon to poll for commands, claim them, and report results.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ agent_id: str | None = None,
76
+ api_key: str | None = None,
77
+ timeout: float = 30.0,
78
+ ) -> None:
79
+ """Initialize the client.
80
+
81
+ Args:
82
+ agent_id: Agent ID for API calls. If not provided, reads from STEERDEV_AGENT_ID.
83
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
84
+ timeout: Request timeout in seconds.
85
+ """
86
+ self.agent_id = agent_id or get_agent_id()
87
+ self.api_key = api_key or get_api_key()
88
+ self.api_base = get_api_endpoint()
89
+ self.timeout = timeout
90
+ self._client: httpx.AsyncClient | None = None
91
+
92
+ @property
93
+ def _base_url(self) -> str:
94
+ """Get the base URL for commands endpoints."""
95
+ return f"{self.api_base}/agents/{self.agent_id}/commands"
96
+
97
+ @property
98
+ def headers(self) -> dict[str, str]:
99
+ """Get request headers with authentication."""
100
+ return {
101
+ "Authorization": f"Bearer {self.api_key}",
102
+ "Content-Type": "application/json",
103
+ }
104
+
105
+ async def _get_client(self) -> httpx.AsyncClient:
106
+ """Get or create async HTTP client."""
107
+ if self._client is None:
108
+ self._client = httpx.AsyncClient(timeout=self.timeout)
109
+ return self._client
110
+
111
+ async def close(self) -> None:
112
+ """Close the HTTP client."""
113
+ if self._client is not None:
114
+ await self._client.aclose()
115
+ self._client = None
116
+
117
+ async def __aenter__(self) -> Self:
118
+ """Enter async context manager."""
119
+ return self
120
+
121
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
122
+ """Exit async context manager."""
123
+ await self.close()
124
+
125
+ async def poll_next(self) -> CommandResponse | None:
126
+ """Poll for and atomically claim the next pending command.
127
+
128
+ Returns:
129
+ Claimed command or None if queue is empty.
130
+ """
131
+ client = await self._get_client()
132
+ logger.debug(f"Polling for next command at {self._base_url}/next")
133
+
134
+ try:
135
+ response = await client.get(
136
+ f"{self._base_url}/next",
137
+ headers=self.headers,
138
+ )
139
+
140
+ if response.status_code == 200:
141
+ return CommandResponse(**response.json())
142
+ if response.status_code == 204:
143
+ return None
144
+
145
+ logger.error(f"Failed to poll next command: {response.status_code} - {response.text}")
146
+ return None
147
+
148
+ except httpx.RequestError as e:
149
+ logger.error(f"Request error polling next command: {e}")
150
+ return None
151
+
152
+ async def update_command(
153
+ self,
154
+ command_id: str,
155
+ status: CommandStatus | None = None,
156
+ session_id: str | None = None,
157
+ result: str | None = None,
158
+ error: str | None = None,
159
+ started_at: str | None = None,
160
+ completed_at: str | None = None,
161
+ ) -> CommandResponse | None:
162
+ """Update a command's status and metadata.
163
+
164
+ Args:
165
+ command_id: Command ID to update.
166
+ status: New status.
167
+ session_id: Session ID created for this command.
168
+ result: Result summary.
169
+ error: Error message.
170
+ started_at: Execution start timestamp.
171
+ completed_at: Completion timestamp.
172
+
173
+ Returns:
174
+ Updated command or None on failure.
175
+ """
176
+ client = await self._get_client()
177
+
178
+ payload: dict[str, Any] = {}
179
+ if status is not None:
180
+ payload["status"] = status
181
+ if session_id is not None:
182
+ payload["session_id"] = session_id
183
+ if result is not None:
184
+ payload["result"] = result
185
+ if error is not None:
186
+ payload["error"] = error
187
+ if started_at is not None:
188
+ payload["started_at"] = started_at
189
+ if completed_at is not None:
190
+ payload["completed_at"] = completed_at
191
+
192
+ if not payload:
193
+ logger.warning("No fields to update")
194
+ return None
195
+
196
+ try:
197
+ response = await client.patch(
198
+ f"{self._base_url}/{command_id}",
199
+ headers=self.headers,
200
+ json=payload,
201
+ )
202
+
203
+ if response.status_code == 200:
204
+ return CommandResponse(**response.json())
205
+
206
+ logger.error(f"Failed to update command: {response.status_code} - {response.text}")
207
+ return None
208
+
209
+ except httpx.RequestError as e:
210
+ logger.error(f"Request error updating command: {e}")
211
+ return None
212
+
213
+ async def mark_executing(self, command_id: str, session_id: str) -> CommandResponse | None:
214
+ """Mark a command as executing with its session ID.
215
+
216
+ Args:
217
+ command_id: Command ID.
218
+ session_id: Session ID created for execution.
219
+
220
+ Returns:
221
+ Updated command or None on failure.
222
+ """
223
+ from datetime import UTC, datetime
224
+
225
+ return await self.update_command(
226
+ command_id,
227
+ status="executing",
228
+ session_id=session_id,
229
+ started_at=datetime.now(UTC).isoformat(),
230
+ )
231
+
232
+ async def mark_completed(
233
+ self, command_id: str, result: str | None = None
234
+ ) -> CommandResponse | None:
235
+ """Mark a command as completed.
236
+
237
+ Args:
238
+ command_id: Command ID.
239
+ result: Result summary.
240
+
241
+ Returns:
242
+ Updated command or None on failure.
243
+ """
244
+ from datetime import UTC, datetime
245
+
246
+ return await self.update_command(
247
+ command_id,
248
+ status="completed",
249
+ result=result,
250
+ completed_at=datetime.now(UTC).isoformat(),
251
+ )
252
+
253
+ async def mark_failed(
254
+ self, command_id: str, error: str | None = None
255
+ ) -> CommandResponse | None:
256
+ """Mark a command as failed.
257
+
258
+ Args:
259
+ command_id: Command ID.
260
+ error: Error message.
261
+
262
+ Returns:
263
+ Updated command or None on failure.
264
+ """
265
+ from datetime import UTC, datetime
266
+
267
+ return await self.update_command(
268
+ command_id,
269
+ status="failed",
270
+ error=error,
271
+ completed_at=datetime.now(UTC).isoformat(),
272
+ )
273
+
274
+ async def get_command(self, command_id: str) -> CommandResponse | None:
275
+ """Get a single command by ID.
276
+
277
+ Args:
278
+ command_id: Command ID.
279
+
280
+ Returns:
281
+ Command data or None if not found.
282
+ """
283
+ client = await self._get_client()
284
+
285
+ try:
286
+ response = await client.get(
287
+ f"{self._base_url}/{command_id}",
288
+ headers=self.headers,
289
+ )
290
+
291
+ if response.status_code == 200:
292
+ return CommandResponse(**response.json())
293
+ if response.status_code == 404:
294
+ return None
295
+
296
+ logger.error(f"Failed to get command: {response.status_code} - {response.text}")
297
+ return None
298
+
299
+ except httpx.RequestError as e:
300
+ logger.error(f"Request error getting command: {e}")
301
+ return None
302
+
303
+ async def list_commands(
304
+ self,
305
+ status: CommandStatus | None = None,
306
+ limit: int = 50,
307
+ offset: int = 0,
308
+ ) -> CommandListResponse | None:
309
+ """List commands for this agent.
310
+
311
+ Args:
312
+ status: Filter by status.
313
+ limit: Maximum results.
314
+ offset: Pagination offset.
315
+
316
+ Returns:
317
+ Command list response or None on failure.
318
+ """
319
+ client = await self._get_client()
320
+ params: dict[str, str | int] = {"limit": limit, "offset": offset}
321
+ if status:
322
+ params["status"] = status
323
+
324
+ try:
325
+ response = await client.get(
326
+ self._base_url,
327
+ headers=self.headers,
328
+ params=params,
329
+ )
330
+
331
+ if response.status_code == 200:
332
+ return CommandListResponse(**response.json())
333
+
334
+ logger.error(f"Failed to list commands: {response.status_code} - {response.text}")
335
+ return None
336
+
337
+ except httpx.RequestError as e:
338
+ logger.error(f"Request error listing commands: {e}")
339
+ return None
340
+
341
+
342
+ def display_command(command: CommandResponse, title: str = "Command") -> None:
343
+ """Display a command in a formatted panel."""
344
+ status_style = STATUS_STYLES.get(command.status, "white")
345
+
346
+ info = (
347
+ f"[bold cyan]ID:[/bold cyan] {command.id}\n"
348
+ f"[bold cyan]Type:[/bold cyan] {command.command_type}\n"
349
+ f"[bold cyan]Status:[/bold cyan] [{status_style}]{command.status}[/{status_style}]\n"
350
+ f"[bold cyan]Priority:[/bold cyan] {command.priority}\n"
351
+ f"[bold cyan]Created:[/bold cyan] {command.created_at}\n"
352
+ )
353
+
354
+ if command.task_id:
355
+ info += f"[bold cyan]Task ID:[/bold cyan] {command.task_id}\n"
356
+ if command.prompt:
357
+ prompt_display = (
358
+ command.prompt[:200] + "..." if len(command.prompt) > 200 else command.prompt
359
+ )
360
+ info += f"[bold cyan]Prompt:[/bold cyan] {prompt_display}\n"
361
+ if command.control_action:
362
+ info += f"[bold cyan]Control Action:[/bold cyan] {command.control_action}\n"
363
+ if command.session_id:
364
+ info += f"[bold cyan]Session ID:[/bold cyan] {command.session_id}\n"
365
+ if command.result:
366
+ info += f"[bold cyan]Result:[/bold cyan] {command.result}\n"
367
+ if command.error:
368
+ info += f"[bold cyan]Error:[/bold cyan] [red]{command.error}[/red]\n"
369
+
370
+ console.print(Panel(info, title=title, border_style="green"))
371
+
372
+
373
+ def display_command_list(commands: list[CommandResponse], full_ids: bool = True) -> None:
374
+ """Display a list of commands in a formatted table."""
375
+ if not commands:
376
+ console.print("[yellow]No commands found[/yellow]")
377
+ return
378
+
379
+ table = Table(title="Agent Commands")
380
+ table.add_column("ID", style="cyan", no_wrap=True)
381
+ table.add_column("Type", style="white")
382
+ table.add_column("Status", style="magenta")
383
+ table.add_column("Priority", style="yellow")
384
+ table.add_column("Created", style="green")
385
+
386
+ for cmd in commands:
387
+ status_style = STATUS_STYLES.get(cmd.status, "white")
388
+ cmd_id = cmd.id if full_ids else (cmd.id[:8] + "..." if len(cmd.id) > 8 else cmd.id)
389
+
390
+ table.add_row(
391
+ cmd_id,
392
+ cmd.command_type,
393
+ f"[{status_style}]{cmd.status}[/{status_style}]",
394
+ str(cmd.priority),
395
+ cmd.created_at[:19] if cmd.created_at else "-",
396
+ )
397
+
398
+ console.print(table)
399
+ console.print(f"\n[dim]Total: {len(commands)} commands[/dim]")