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.
- steerdev-0.4.27.dist-info/METADATA +224 -0
- steerdev-0.4.27.dist-info/RECORD +57 -0
- steerdev-0.4.27.dist-info/WHEEL +4 -0
- steerdev-0.4.27.dist-info/entry_points.txt +2 -0
- steerdev_agent/__init__.py +10 -0
- steerdev_agent/api/__init__.py +32 -0
- steerdev_agent/api/activity.py +278 -0
- steerdev_agent/api/agents.py +145 -0
- steerdev_agent/api/client.py +158 -0
- steerdev_agent/api/commands.py +399 -0
- steerdev_agent/api/configs.py +238 -0
- steerdev_agent/api/context.py +306 -0
- steerdev_agent/api/events.py +294 -0
- steerdev_agent/api/hooks.py +178 -0
- steerdev_agent/api/implementation_plan.py +408 -0
- steerdev_agent/api/messages.py +231 -0
- steerdev_agent/api/prd.py +281 -0
- steerdev_agent/api/runs.py +526 -0
- steerdev_agent/api/sessions.py +403 -0
- steerdev_agent/api/specs.py +321 -0
- steerdev_agent/api/tasks.py +659 -0
- steerdev_agent/api/workflow_runs.py +351 -0
- steerdev_agent/api/workflows.py +191 -0
- steerdev_agent/cli.py +2254 -0
- steerdev_agent/config/__init__.py +19 -0
- steerdev_agent/config/models.py +236 -0
- steerdev_agent/config/platform.py +272 -0
- steerdev_agent/config/settings.py +62 -0
- steerdev_agent/daemon.py +675 -0
- steerdev_agent/executor/__init__.py +64 -0
- steerdev_agent/executor/base.py +121 -0
- steerdev_agent/executor/claude.py +328 -0
- steerdev_agent/executor/stream.py +163 -0
- steerdev_agent/git/__init__.py +1 -0
- steerdev_agent/handlers/__init__.py +5 -0
- steerdev_agent/handlers/prd.py +533 -0
- steerdev_agent/integration.py +334 -0
- steerdev_agent/prompt/__init__.py +10 -0
- steerdev_agent/prompt/builder.py +263 -0
- steerdev_agent/prompt/templates.py +422 -0
- steerdev_agent/py.typed +0 -0
- steerdev_agent/runner.py +829 -0
- steerdev_agent/setup/__init__.py +5 -0
- steerdev_agent/setup/claude_setup.py +560 -0
- steerdev_agent/setup/templates/claude_md_section.md +140 -0
- steerdev_agent/setup/templates/settings.json +69 -0
- steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
- steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
- steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
- steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
- steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
- steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
- steerdev_agent/setup/templates/steerdev.yaml +51 -0
- steerdev_agent/version.py +149 -0
- steerdev_agent/workflow/__init__.py +10 -0
- steerdev_agent/workflow/executor.py +494 -0
- steerdev_agent/workflow/memory.py +185 -0
steerdev_agent/cli.py
ADDED
|
@@ -0,0 +1,2254 @@
|
|
|
1
|
+
"""CLI interface for steerdev."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import typer
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from steerdev_agent.config.models import SteerDevConfig
|
|
15
|
+
from steerdev_agent.version import get_version
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
# Load environment variables from .env file in current working directory
|
|
20
|
+
load_dotenv(Path.cwd() / ".env")
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="steerdev",
|
|
24
|
+
help="Backend task runner for steerdev.com - orchestrates CLI coding agents",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# Tasks Command Group
|
|
30
|
+
# ============================================================================
|
|
31
|
+
tasks_app = typer.Typer(
|
|
32
|
+
name="tasks",
|
|
33
|
+
help="Task management commands for steerdev.com",
|
|
34
|
+
no_args_is_help=True,
|
|
35
|
+
)
|
|
36
|
+
app.add_typer(tasks_app)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@tasks_app.command("next")
|
|
40
|
+
def tasks_next(
|
|
41
|
+
project_id: Annotated[
|
|
42
|
+
str | None,
|
|
43
|
+
typer.Option("--project-id", "-p", help="Filter by project ID (UUID)"),
|
|
44
|
+
] = None,
|
|
45
|
+
waves: Annotated[
|
|
46
|
+
bool,
|
|
47
|
+
typer.Option(
|
|
48
|
+
"--waves/--no-waves", help="Enable wave-aware task scheduling (default: enabled)"
|
|
49
|
+
),
|
|
50
|
+
] = True,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Get the next task to work on.
|
|
53
|
+
|
|
54
|
+
By default, returns the next task with full wave context (if waves exist).
|
|
55
|
+
Use --no-waves to get a single task without wave context.
|
|
56
|
+
|
|
57
|
+
Returns the highest priority task in order:
|
|
58
|
+
1. In Progress tasks (to continue work)
|
|
59
|
+
2. Todo tasks (to start new work)
|
|
60
|
+
3. Backlog tasks (if nothing else available)
|
|
61
|
+
"""
|
|
62
|
+
from steerdev_agent.api.tasks import TasksClient, display_task, display_wave_context
|
|
63
|
+
|
|
64
|
+
with TasksClient() as client:
|
|
65
|
+
if not client.check_api_key():
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Try wave-aware fetch first (if enabled)
|
|
70
|
+
if waves:
|
|
71
|
+
wave_response = client.get_next_wave(project_id=project_id)
|
|
72
|
+
if wave_response is not None:
|
|
73
|
+
display_wave_context(wave_response)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Fall back to single-task
|
|
77
|
+
task = client.get_next_task(project_id=project_id)
|
|
78
|
+
if task is None:
|
|
79
|
+
console.print("[yellow]No tasks available[/yellow]")
|
|
80
|
+
raise typer.Exit(0)
|
|
81
|
+
|
|
82
|
+
display_task(task, title="Next Task")
|
|
83
|
+
|
|
84
|
+
except httpx.TimeoutException:
|
|
85
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
86
|
+
raise typer.Exit(1) from None
|
|
87
|
+
except httpx.HTTPError as e:
|
|
88
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
89
|
+
raise typer.Exit(1) from None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@tasks_app.command("list")
|
|
93
|
+
def tasks_list(
|
|
94
|
+
status: Annotated[
|
|
95
|
+
str | None,
|
|
96
|
+
typer.Option(
|
|
97
|
+
"--status",
|
|
98
|
+
"-s",
|
|
99
|
+
help="Filter by status (backlog, unstarted, started, completed, canceled)",
|
|
100
|
+
),
|
|
101
|
+
] = None,
|
|
102
|
+
project_id: Annotated[
|
|
103
|
+
str | None,
|
|
104
|
+
typer.Option("--project-id", "-p", help="Filter by project ID (UUID)"),
|
|
105
|
+
] = None,
|
|
106
|
+
limit: Annotated[
|
|
107
|
+
int,
|
|
108
|
+
typer.Option("--limit", "-l", help="Maximum number of tasks to return"),
|
|
109
|
+
] = 20,
|
|
110
|
+
compact: Annotated[
|
|
111
|
+
bool,
|
|
112
|
+
typer.Option("--compact", "-c", help="Show truncated IDs (8 chars) instead of full UUIDs"),
|
|
113
|
+
] = False,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""List tasks with optional filters."""
|
|
116
|
+
from steerdev_agent.api.tasks import TasksClient, display_task_list
|
|
117
|
+
|
|
118
|
+
with TasksClient() as client:
|
|
119
|
+
if not client.check_api_key():
|
|
120
|
+
raise typer.Exit(1)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
tasks = client.list_tasks(status=status, project_id=project_id, limit=limit)
|
|
124
|
+
display_task_list(tasks, full_ids=not compact)
|
|
125
|
+
|
|
126
|
+
except httpx.TimeoutException:
|
|
127
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
128
|
+
raise typer.Exit(1) from None
|
|
129
|
+
except httpx.HTTPError as e:
|
|
130
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
131
|
+
raise typer.Exit(1) from None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@tasks_app.command("get")
|
|
135
|
+
def tasks_get(
|
|
136
|
+
task_id: Annotated[
|
|
137
|
+
str,
|
|
138
|
+
typer.Argument(help="Task ID (UUID) to fetch"),
|
|
139
|
+
],
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Get details of a specific task by ID."""
|
|
142
|
+
from steerdev_agent.api.tasks import TasksClient, display_task
|
|
143
|
+
|
|
144
|
+
with TasksClient() as client:
|
|
145
|
+
if not client.check_api_key():
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
task = client.get_task(task_id)
|
|
150
|
+
if task:
|
|
151
|
+
display_task(task, title="Task Details")
|
|
152
|
+
else:
|
|
153
|
+
console.print(f"[red]Task not found: {task_id}[/red]")
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
except httpx.TimeoutException:
|
|
157
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
158
|
+
raise typer.Exit(1) from None
|
|
159
|
+
except httpx.HTTPError as e:
|
|
160
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
161
|
+
raise typer.Exit(1) from None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@tasks_app.command("update")
|
|
165
|
+
def tasks_update(
|
|
166
|
+
task_id: Annotated[
|
|
167
|
+
str,
|
|
168
|
+
typer.Argument(help="Task ID (UUID) to update"),
|
|
169
|
+
],
|
|
170
|
+
status: Annotated[
|
|
171
|
+
str | None,
|
|
172
|
+
typer.Option(
|
|
173
|
+
"--status",
|
|
174
|
+
"-s",
|
|
175
|
+
help="New status (backlog, unstarted, started, completed, canceled)",
|
|
176
|
+
),
|
|
177
|
+
] = None,
|
|
178
|
+
title: Annotated[
|
|
179
|
+
str | None,
|
|
180
|
+
typer.Option("--title", "-t", help="Update task title"),
|
|
181
|
+
] = None,
|
|
182
|
+
prompt: Annotated[
|
|
183
|
+
str | None,
|
|
184
|
+
typer.Option("--prompt", help="Update task prompt"),
|
|
185
|
+
] = None,
|
|
186
|
+
priority: Annotated[
|
|
187
|
+
int | None,
|
|
188
|
+
typer.Option("--priority", "-P", help="Update task priority (0-3)"),
|
|
189
|
+
] = None,
|
|
190
|
+
result_summary: Annotated[
|
|
191
|
+
str | None,
|
|
192
|
+
typer.Option("--result", "-r", help="Result summary (for completed tasks)"),
|
|
193
|
+
] = None,
|
|
194
|
+
error_message: Annotated[
|
|
195
|
+
str | None,
|
|
196
|
+
typer.Option("--error", "-e", help="Error message (for failed tasks)"),
|
|
197
|
+
] = None,
|
|
198
|
+
comment: Annotated[
|
|
199
|
+
str | None,
|
|
200
|
+
typer.Option("--comment", "-c", help="Comment text (supports markdown)"),
|
|
201
|
+
] = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Update a task's status, title, or other fields."""
|
|
204
|
+
from steerdev_agent.api.tasks import (
|
|
205
|
+
VALID_STATUSES,
|
|
206
|
+
TasksClient,
|
|
207
|
+
display_update_success,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Validate status if provided
|
|
211
|
+
if status and status not in VALID_STATUSES:
|
|
212
|
+
console.print(
|
|
213
|
+
f"[red]Error: Invalid status '{status}'. Must be one of: {', '.join(VALID_STATUSES)}[/red]"
|
|
214
|
+
)
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
|
|
217
|
+
# Validate priority if provided
|
|
218
|
+
if priority is not None and (priority < 0 or priority > 3):
|
|
219
|
+
console.print("[red]Error: Priority must be between 0 and 3[/red]")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
# Check that at least one update is specified
|
|
223
|
+
if not any(
|
|
224
|
+
[status, title, prompt, priority is not None, result_summary, error_message, comment]
|
|
225
|
+
):
|
|
226
|
+
console.print(
|
|
227
|
+
"[yellow]No updates specified. Use --status, --title, --prompt, --priority, "
|
|
228
|
+
"--result, --error, or --comment[/yellow]"
|
|
229
|
+
)
|
|
230
|
+
raise typer.Exit(1)
|
|
231
|
+
|
|
232
|
+
with TasksClient() as client:
|
|
233
|
+
if not client.check_api_key():
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
success = client.update_task(
|
|
238
|
+
task_id=task_id,
|
|
239
|
+
status=status,
|
|
240
|
+
title=title,
|
|
241
|
+
prompt=prompt,
|
|
242
|
+
priority=priority,
|
|
243
|
+
result_summary=result_summary,
|
|
244
|
+
error_message=error_message,
|
|
245
|
+
comment=comment,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if success:
|
|
249
|
+
display_update_success(
|
|
250
|
+
task_id=task_id,
|
|
251
|
+
status=status,
|
|
252
|
+
title=title,
|
|
253
|
+
prompt=prompt,
|
|
254
|
+
priority=priority,
|
|
255
|
+
result_summary=result_summary,
|
|
256
|
+
error_message=error_message,
|
|
257
|
+
comment=comment,
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
except httpx.TimeoutException:
|
|
263
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
264
|
+
raise typer.Exit(1) from None
|
|
265
|
+
except httpx.HTTPError as e:
|
|
266
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
267
|
+
raise typer.Exit(1) from None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@tasks_app.command("create")
|
|
271
|
+
def tasks_create(
|
|
272
|
+
title: Annotated[
|
|
273
|
+
str,
|
|
274
|
+
typer.Option("--title", "-t", help="Task title (required)"),
|
|
275
|
+
],
|
|
276
|
+
prompt: Annotated[
|
|
277
|
+
str,
|
|
278
|
+
typer.Option("--prompt", help="Task prompt/description (required)"),
|
|
279
|
+
],
|
|
280
|
+
project_id: Annotated[
|
|
281
|
+
str | None,
|
|
282
|
+
typer.Option("--project-id", "-p", help="Project ID (UUID)"),
|
|
283
|
+
] = None,
|
|
284
|
+
priority: Annotated[
|
|
285
|
+
int,
|
|
286
|
+
typer.Option("--priority", help="Task priority (0=low, 1=medium, 2=high, 3=urgent)"),
|
|
287
|
+
] = 1,
|
|
288
|
+
working_directory: Annotated[
|
|
289
|
+
str | None,
|
|
290
|
+
typer.Option("--workdir", "-w", help="Working directory for the task"),
|
|
291
|
+
] = None,
|
|
292
|
+
spec_id: Annotated[
|
|
293
|
+
str | None,
|
|
294
|
+
typer.Option("--spec-id", "-s", help="Specification ID to link this task to"),
|
|
295
|
+
] = None,
|
|
296
|
+
cycle_id: Annotated[
|
|
297
|
+
str | None,
|
|
298
|
+
typer.Option("--cycle-id", "-c", help="Cycle ID to link this task to"),
|
|
299
|
+
] = None,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Create a new task."""
|
|
302
|
+
from steerdev_agent.api.tasks import TasksClient, display_task
|
|
303
|
+
|
|
304
|
+
# Validate priority
|
|
305
|
+
if priority < 0 or priority > 3:
|
|
306
|
+
console.print("[red]Error: Priority must be between 0 and 3[/red]")
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
|
|
309
|
+
with TasksClient() as client:
|
|
310
|
+
if not client.check_api_key():
|
|
311
|
+
raise typer.Exit(1)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
task = client.create_task(
|
|
315
|
+
title=title,
|
|
316
|
+
prompt=prompt,
|
|
317
|
+
project_id=project_id,
|
|
318
|
+
priority=priority,
|
|
319
|
+
working_directory=working_directory,
|
|
320
|
+
spec_id=spec_id,
|
|
321
|
+
cycle_id=cycle_id,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if task:
|
|
325
|
+
display_task(task, title="Task Created")
|
|
326
|
+
else:
|
|
327
|
+
raise typer.Exit(1)
|
|
328
|
+
|
|
329
|
+
except httpx.TimeoutException:
|
|
330
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
331
|
+
raise typer.Exit(1) from None
|
|
332
|
+
except httpx.HTTPError as e:
|
|
333
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
334
|
+
raise typer.Exit(1) from None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ============================================================================
|
|
338
|
+
# Hooks Command Group
|
|
339
|
+
# ============================================================================
|
|
340
|
+
hooks_app = typer.Typer(
|
|
341
|
+
name="hooks",
|
|
342
|
+
help="Claude Code lifecycle hooks for activity reporting",
|
|
343
|
+
no_args_is_help=True,
|
|
344
|
+
)
|
|
345
|
+
app.add_typer(hooks_app)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@hooks_app.command("session-start")
|
|
349
|
+
def hooks_session_start() -> None:
|
|
350
|
+
"""Hook called when Claude Code session starts.
|
|
351
|
+
|
|
352
|
+
Reads JSON from stdin with session_id, transcript_path, cwd, etc.
|
|
353
|
+
Reports session_start event to SteerDev API.
|
|
354
|
+
"""
|
|
355
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
356
|
+
|
|
357
|
+
client = HooksClient()
|
|
358
|
+
client.session_start()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@hooks_app.command("session-end")
|
|
362
|
+
def hooks_session_end() -> None:
|
|
363
|
+
"""Hook called when Claude Code session ends.
|
|
364
|
+
|
|
365
|
+
Reads JSON from stdin with session_id, reason, etc.
|
|
366
|
+
Reports session_end event to SteerDev API.
|
|
367
|
+
"""
|
|
368
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
369
|
+
|
|
370
|
+
client = HooksClient()
|
|
371
|
+
client.session_end()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@hooks_app.command("agent-stop")
|
|
375
|
+
def hooks_agent_stop() -> None:
|
|
376
|
+
"""Hook called when main Claude Code agent stops.
|
|
377
|
+
|
|
378
|
+
Reads JSON from stdin with session_id, stop_hook_active, etc.
|
|
379
|
+
Reports agent_stopped event to SteerDev API.
|
|
380
|
+
"""
|
|
381
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
382
|
+
|
|
383
|
+
client = HooksClient()
|
|
384
|
+
client.agent_stop()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@hooks_app.command("subagent-stop")
|
|
388
|
+
def hooks_subagent_stop() -> None:
|
|
389
|
+
"""Hook called when a Claude Code subagent stops.
|
|
390
|
+
|
|
391
|
+
Reads JSON from stdin with session_id, stop_hook_active, etc.
|
|
392
|
+
Reports subagent_stopped event to SteerDev API.
|
|
393
|
+
"""
|
|
394
|
+
from steerdev_agent.api.hooks import HooksClient
|
|
395
|
+
|
|
396
|
+
client = HooksClient()
|
|
397
|
+
client.subagent_stop()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ============================================================================
|
|
401
|
+
# Sessions Command Group
|
|
402
|
+
# ============================================================================
|
|
403
|
+
sessions_app = typer.Typer(
|
|
404
|
+
name="sessions",
|
|
405
|
+
help="Session management commands for tracking agent execution",
|
|
406
|
+
no_args_is_help=True,
|
|
407
|
+
)
|
|
408
|
+
app.add_typer(sessions_app)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@sessions_app.command("list")
|
|
412
|
+
def sessions_list(
|
|
413
|
+
project_id: Annotated[
|
|
414
|
+
str | None,
|
|
415
|
+
typer.Option("--project-id", "-p", help="Filter by project ID (UUID)"),
|
|
416
|
+
] = None,
|
|
417
|
+
status: Annotated[
|
|
418
|
+
str | None,
|
|
419
|
+
typer.Option(
|
|
420
|
+
"--status",
|
|
421
|
+
"-s",
|
|
422
|
+
help="Filter by status (pending, running, completed, failed, cancelled)",
|
|
423
|
+
),
|
|
424
|
+
] = None,
|
|
425
|
+
limit: Annotated[
|
|
426
|
+
int,
|
|
427
|
+
typer.Option("--limit", "-l", help="Maximum number of sessions to return"),
|
|
428
|
+
] = 20,
|
|
429
|
+
compact: Annotated[
|
|
430
|
+
bool,
|
|
431
|
+
typer.Option("--compact", "-c", help="Show truncated IDs instead of full UUIDs"),
|
|
432
|
+
] = False,
|
|
433
|
+
) -> None:
|
|
434
|
+
"""List sessions with optional filters."""
|
|
435
|
+
from steerdev_agent.api.sessions import SessionsClient, display_session_list
|
|
436
|
+
|
|
437
|
+
async def _list_sessions() -> None:
|
|
438
|
+
async with SessionsClient() as client:
|
|
439
|
+
result = await client.list_sessions(
|
|
440
|
+
project_id=project_id,
|
|
441
|
+
status=status,
|
|
442
|
+
limit=limit,
|
|
443
|
+
)
|
|
444
|
+
if result:
|
|
445
|
+
display_session_list(result.sessions, full_ids=not compact)
|
|
446
|
+
else:
|
|
447
|
+
console.print("[yellow]Failed to fetch sessions[/yellow]")
|
|
448
|
+
raise typer.Exit(1)
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
asyncio.run(_list_sessions())
|
|
452
|
+
except httpx.TimeoutException:
|
|
453
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
454
|
+
raise typer.Exit(1) from None
|
|
455
|
+
except httpx.HTTPError as e:
|
|
456
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
457
|
+
raise typer.Exit(1) from None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@sessions_app.command("get")
|
|
461
|
+
def sessions_get(
|
|
462
|
+
session_id: Annotated[
|
|
463
|
+
str,
|
|
464
|
+
typer.Argument(help="Session ID (UUID) to fetch"),
|
|
465
|
+
],
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Get details of a specific session by ID."""
|
|
468
|
+
from steerdev_agent.api.sessions import SessionsClient, display_session
|
|
469
|
+
|
|
470
|
+
async def _get_session() -> None:
|
|
471
|
+
async with SessionsClient() as client:
|
|
472
|
+
session = await client.get_session(session_id)
|
|
473
|
+
if session:
|
|
474
|
+
display_session(session, title="Session Details")
|
|
475
|
+
else:
|
|
476
|
+
console.print(f"[red]Session not found: {session_id}[/red]")
|
|
477
|
+
raise typer.Exit(1)
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
asyncio.run(_get_session())
|
|
481
|
+
except httpx.TimeoutException:
|
|
482
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
483
|
+
raise typer.Exit(1) from None
|
|
484
|
+
except httpx.HTTPError as e:
|
|
485
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
486
|
+
raise typer.Exit(1) from None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ============================================================================
|
|
490
|
+
# Runs Command Group (kept for backwards compatibility)
|
|
491
|
+
# ============================================================================
|
|
492
|
+
runs_app = typer.Typer(
|
|
493
|
+
name="runs",
|
|
494
|
+
help="Run management commands for tracking agent execution sessions",
|
|
495
|
+
no_args_is_help=True,
|
|
496
|
+
)
|
|
497
|
+
app.add_typer(runs_app)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@runs_app.command("list")
|
|
501
|
+
def runs_list(
|
|
502
|
+
status: Annotated[
|
|
503
|
+
str | None,
|
|
504
|
+
typer.Option(
|
|
505
|
+
"--status",
|
|
506
|
+
"-s",
|
|
507
|
+
help="Filter by status (pending, running, completed, failed, cancelled)",
|
|
508
|
+
),
|
|
509
|
+
] = None,
|
|
510
|
+
limit: Annotated[
|
|
511
|
+
int,
|
|
512
|
+
typer.Option("--limit", "-l", help="Maximum number of runs to return"),
|
|
513
|
+
] = 20,
|
|
514
|
+
compact: Annotated[
|
|
515
|
+
bool,
|
|
516
|
+
typer.Option("--compact", "-c", help="Show truncated IDs instead of full UUIDs"),
|
|
517
|
+
] = False,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""List runs with optional filters."""
|
|
520
|
+
from steerdev_agent.api.runs import RunsClient, display_run_list
|
|
521
|
+
|
|
522
|
+
async def _list_runs() -> None:
|
|
523
|
+
async with RunsClient() as client:
|
|
524
|
+
result = await client.list_runs(status=status, limit=limit)
|
|
525
|
+
if result:
|
|
526
|
+
display_run_list(result.runs, full_ids=not compact)
|
|
527
|
+
else:
|
|
528
|
+
console.print("[yellow]Failed to fetch runs[/yellow]")
|
|
529
|
+
raise typer.Exit(1)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
asyncio.run(_list_runs())
|
|
533
|
+
except httpx.TimeoutException:
|
|
534
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
535
|
+
raise typer.Exit(1) from None
|
|
536
|
+
except httpx.HTTPError as e:
|
|
537
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
538
|
+
raise typer.Exit(1) from None
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@runs_app.command("get")
|
|
542
|
+
def runs_get(
|
|
543
|
+
run_id: Annotated[
|
|
544
|
+
str,
|
|
545
|
+
typer.Argument(help="Run ID (UUID) to fetch"),
|
|
546
|
+
],
|
|
547
|
+
) -> None:
|
|
548
|
+
"""Get details of a specific run by ID."""
|
|
549
|
+
from steerdev_agent.api.runs import RunsClient, display_run
|
|
550
|
+
|
|
551
|
+
async def _get_run() -> None:
|
|
552
|
+
async with RunsClient() as client:
|
|
553
|
+
run = await client.get_run(run_id)
|
|
554
|
+
if run:
|
|
555
|
+
display_run(run, title="Run Details")
|
|
556
|
+
else:
|
|
557
|
+
console.print(f"[red]Run not found: {run_id}[/red]")
|
|
558
|
+
raise typer.Exit(1)
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
asyncio.run(_get_run())
|
|
562
|
+
except httpx.TimeoutException:
|
|
563
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
564
|
+
raise typer.Exit(1) from None
|
|
565
|
+
except httpx.HTTPError as e:
|
|
566
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
567
|
+
raise typer.Exit(1) from None
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@runs_app.command("active")
|
|
571
|
+
def runs_active(
|
|
572
|
+
compact: Annotated[
|
|
573
|
+
bool,
|
|
574
|
+
typer.Option("--compact", "-c", help="Show truncated IDs instead of full UUIDs"),
|
|
575
|
+
] = False,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""List currently active runs (pending or running)."""
|
|
578
|
+
from steerdev_agent.api.runs import RunsClient, display_run_list
|
|
579
|
+
|
|
580
|
+
async def _get_active_runs() -> None:
|
|
581
|
+
async with RunsClient() as client:
|
|
582
|
+
result = await client.get_active_runs()
|
|
583
|
+
if result:
|
|
584
|
+
if result.runs:
|
|
585
|
+
display_run_list(result.runs, full_ids=not compact)
|
|
586
|
+
console.print(f"\n[dim]Active runs: {result.count}[/dim]")
|
|
587
|
+
else:
|
|
588
|
+
console.print("[yellow]No active runs[/yellow]")
|
|
589
|
+
else:
|
|
590
|
+
console.print("[yellow]Failed to fetch active runs[/yellow]")
|
|
591
|
+
raise typer.Exit(1)
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
asyncio.run(_get_active_runs())
|
|
595
|
+
except httpx.TimeoutException:
|
|
596
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
597
|
+
raise typer.Exit(1) from None
|
|
598
|
+
except httpx.HTTPError as e:
|
|
599
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
600
|
+
raise typer.Exit(1) from None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ============================================================================
|
|
604
|
+
# Specs Command Group
|
|
605
|
+
# ============================================================================
|
|
606
|
+
specs_app = typer.Typer(
|
|
607
|
+
name="specs",
|
|
608
|
+
help="Specification document management commands for steerdev.com",
|
|
609
|
+
no_args_is_help=True,
|
|
610
|
+
)
|
|
611
|
+
app.add_typer(specs_app)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@specs_app.command("list")
|
|
615
|
+
def specs_list(
|
|
616
|
+
project_id: Annotated[
|
|
617
|
+
str | None,
|
|
618
|
+
typer.Option("--project-id", "-p", help="Filter by project ID (UUID)"),
|
|
619
|
+
] = None,
|
|
620
|
+
status: Annotated[
|
|
621
|
+
str | None,
|
|
622
|
+
typer.Option(
|
|
623
|
+
"--status",
|
|
624
|
+
"-s",
|
|
625
|
+
help="Filter by status (draft, analyzing, clarifying, planning, ready, completed)",
|
|
626
|
+
),
|
|
627
|
+
] = None,
|
|
628
|
+
limit: Annotated[
|
|
629
|
+
int,
|
|
630
|
+
typer.Option("--limit", "-l", help="Maximum number of specs to return"),
|
|
631
|
+
] = 20,
|
|
632
|
+
compact: Annotated[
|
|
633
|
+
bool,
|
|
634
|
+
typer.Option("--compact", "-c", help="Show truncated IDs instead of full UUIDs"),
|
|
635
|
+
] = False,
|
|
636
|
+
) -> None:
|
|
637
|
+
"""List specification documents with optional filters."""
|
|
638
|
+
from steerdev_agent.api.specs import SpecsClient, display_spec_list
|
|
639
|
+
|
|
640
|
+
with SpecsClient() as client:
|
|
641
|
+
if not client.check_api_key():
|
|
642
|
+
raise typer.Exit(1)
|
|
643
|
+
|
|
644
|
+
try:
|
|
645
|
+
specs = client.list_specs(project_id=project_id, status=status, limit=limit)
|
|
646
|
+
display_spec_list(specs, full_ids=not compact)
|
|
647
|
+
|
|
648
|
+
except httpx.TimeoutException:
|
|
649
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
650
|
+
raise typer.Exit(1) from None
|
|
651
|
+
except httpx.HTTPError as e:
|
|
652
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
653
|
+
raise typer.Exit(1) from None
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@specs_app.command("get")
|
|
657
|
+
def specs_get(
|
|
658
|
+
spec_id: Annotated[
|
|
659
|
+
str,
|
|
660
|
+
typer.Argument(help="Spec ID (UUID) to fetch"),
|
|
661
|
+
],
|
|
662
|
+
) -> None:
|
|
663
|
+
"""Get details of a specific specification document by ID."""
|
|
664
|
+
from steerdev_agent.api.specs import SpecsClient, display_spec
|
|
665
|
+
|
|
666
|
+
with SpecsClient() as client:
|
|
667
|
+
if not client.check_api_key():
|
|
668
|
+
raise typer.Exit(1)
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
spec = client.get_spec(spec_id)
|
|
672
|
+
if spec:
|
|
673
|
+
display_spec(spec, title="Spec Details")
|
|
674
|
+
else:
|
|
675
|
+
console.print(f"[red]Spec not found: {spec_id}[/red]")
|
|
676
|
+
raise typer.Exit(1)
|
|
677
|
+
|
|
678
|
+
except httpx.TimeoutException:
|
|
679
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
680
|
+
raise typer.Exit(1) from None
|
|
681
|
+
except httpx.HTTPError as e:
|
|
682
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
683
|
+
raise typer.Exit(1) from None
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@specs_app.command("update")
|
|
687
|
+
def specs_update(
|
|
688
|
+
spec_id: Annotated[
|
|
689
|
+
str,
|
|
690
|
+
typer.Argument(help="Spec ID (UUID) to update"),
|
|
691
|
+
],
|
|
692
|
+
content: Annotated[
|
|
693
|
+
str | None,
|
|
694
|
+
typer.Option("--content", help="New content (markdown)"),
|
|
695
|
+
] = None,
|
|
696
|
+
status: Annotated[
|
|
697
|
+
str | None,
|
|
698
|
+
typer.Option(
|
|
699
|
+
"--status",
|
|
700
|
+
"-s",
|
|
701
|
+
help="New status (draft, analyzing, clarifying, planning, ready, completed)",
|
|
702
|
+
),
|
|
703
|
+
] = None,
|
|
704
|
+
title: Annotated[
|
|
705
|
+
str | None,
|
|
706
|
+
typer.Option("--title", "-t", help="New title"),
|
|
707
|
+
] = None,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Update a specification document's content, status, or title."""
|
|
710
|
+
from steerdev_agent.api.specs import (
|
|
711
|
+
VALID_SPEC_STATUSES,
|
|
712
|
+
SpecsClient,
|
|
713
|
+
display_spec_update_success,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Validate status if provided
|
|
717
|
+
if status and status not in VALID_SPEC_STATUSES:
|
|
718
|
+
console.print(
|
|
719
|
+
f"[red]Error: Invalid status '{status}'. "
|
|
720
|
+
f"Must be one of: {', '.join(VALID_SPEC_STATUSES)}[/red]"
|
|
721
|
+
)
|
|
722
|
+
raise typer.Exit(1)
|
|
723
|
+
|
|
724
|
+
# Check that at least one update is specified
|
|
725
|
+
if not any([content, status, title]):
|
|
726
|
+
console.print("[yellow]No updates specified. Use --content, --status, or --title[/yellow]")
|
|
727
|
+
raise typer.Exit(1)
|
|
728
|
+
|
|
729
|
+
with SpecsClient() as client:
|
|
730
|
+
if not client.check_api_key():
|
|
731
|
+
raise typer.Exit(1)
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
success = client.update_spec(
|
|
735
|
+
spec_id=spec_id,
|
|
736
|
+
content=content,
|
|
737
|
+
status=status,
|
|
738
|
+
title=title,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
if success:
|
|
742
|
+
display_spec_update_success(spec_id, content, status, title)
|
|
743
|
+
else:
|
|
744
|
+
raise typer.Exit(1)
|
|
745
|
+
|
|
746
|
+
except httpx.TimeoutException:
|
|
747
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
748
|
+
raise typer.Exit(1) from None
|
|
749
|
+
except httpx.HTTPError as e:
|
|
750
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
751
|
+
raise typer.Exit(1) from None
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@specs_app.command("create")
|
|
755
|
+
def specs_create(
|
|
756
|
+
title: Annotated[
|
|
757
|
+
str,
|
|
758
|
+
typer.Option("--title", "-t", help="Spec title (required)"),
|
|
759
|
+
],
|
|
760
|
+
content: Annotated[
|
|
761
|
+
str,
|
|
762
|
+
typer.Option("--content", help="Spec content in markdown (required)"),
|
|
763
|
+
],
|
|
764
|
+
project_id: Annotated[
|
|
765
|
+
str | None,
|
|
766
|
+
typer.Option("--project-id", "-p", help="Project ID (UUID)"),
|
|
767
|
+
] = None,
|
|
768
|
+
source: Annotated[
|
|
769
|
+
str,
|
|
770
|
+
typer.Option("--source", "-s", help="Source of the spec"),
|
|
771
|
+
] = "agent",
|
|
772
|
+
) -> None:
|
|
773
|
+
"""Create a new specification document."""
|
|
774
|
+
from steerdev_agent.api.specs import SpecsClient, display_spec
|
|
775
|
+
|
|
776
|
+
with SpecsClient() as client:
|
|
777
|
+
if not client.check_api_key():
|
|
778
|
+
raise typer.Exit(1)
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
spec = client.create_spec(
|
|
782
|
+
title=title,
|
|
783
|
+
content=content,
|
|
784
|
+
project_id=project_id,
|
|
785
|
+
source=source,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
if spec:
|
|
789
|
+
display_spec(spec, title="Spec Created")
|
|
790
|
+
else:
|
|
791
|
+
raise typer.Exit(1)
|
|
792
|
+
|
|
793
|
+
except httpx.TimeoutException:
|
|
794
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
795
|
+
raise typer.Exit(1) from None
|
|
796
|
+
except httpx.HTTPError as e:
|
|
797
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
798
|
+
raise typer.Exit(1) from None
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
# ============================================================================
|
|
802
|
+
# Context Command Group
|
|
803
|
+
# ============================================================================
|
|
804
|
+
context_app = typer.Typer(
|
|
805
|
+
name="context",
|
|
806
|
+
help="Project context commands for steerdev.com",
|
|
807
|
+
no_args_is_help=True,
|
|
808
|
+
)
|
|
809
|
+
app.add_typer(context_app)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
@context_app.command("get")
|
|
813
|
+
def context_get(
|
|
814
|
+
project_id: Annotated[
|
|
815
|
+
str | None,
|
|
816
|
+
typer.Option("--project-id", "-p", help="Project ID (UUID)"),
|
|
817
|
+
] = None,
|
|
818
|
+
format: Annotated[
|
|
819
|
+
str,
|
|
820
|
+
typer.Option(
|
|
821
|
+
"--format",
|
|
822
|
+
"-f",
|
|
823
|
+
help="Output format: json (structured) or markdown (agent-ready)",
|
|
824
|
+
),
|
|
825
|
+
] = "json",
|
|
826
|
+
) -> None:
|
|
827
|
+
"""Get project context (tech stack, patterns, structure).
|
|
828
|
+
|
|
829
|
+
Returns codebase context including:
|
|
830
|
+
- Tech stack (framework, database, auth, styling)
|
|
831
|
+
- Code patterns and conventions
|
|
832
|
+
- Directory structure
|
|
833
|
+
- AI-generated summary
|
|
834
|
+
|
|
835
|
+
Use --format markdown to get agent-ready context.
|
|
836
|
+
"""
|
|
837
|
+
from steerdev_agent.api.context import ContextClient, display_context
|
|
838
|
+
|
|
839
|
+
with ContextClient() as client:
|
|
840
|
+
if not client.check_api_key():
|
|
841
|
+
raise typer.Exit(1)
|
|
842
|
+
|
|
843
|
+
try:
|
|
844
|
+
context = client.get_context(project_id=project_id, format=format)
|
|
845
|
+
if context:
|
|
846
|
+
if format == "markdown":
|
|
847
|
+
# Print raw markdown for agent consumption
|
|
848
|
+
console.print(context)
|
|
849
|
+
else:
|
|
850
|
+
display_context(context)
|
|
851
|
+
else:
|
|
852
|
+
raise typer.Exit(1)
|
|
853
|
+
|
|
854
|
+
except httpx.TimeoutException:
|
|
855
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
856
|
+
raise typer.Exit(1) from None
|
|
857
|
+
except httpx.HTTPError as e:
|
|
858
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
859
|
+
raise typer.Exit(1) from None
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
@context_app.command("refresh")
|
|
863
|
+
def context_refresh(
|
|
864
|
+
project_id: Annotated[
|
|
865
|
+
str | None,
|
|
866
|
+
typer.Option("--project-id", "-p", help="Project ID (UUID)"),
|
|
867
|
+
] = None,
|
|
868
|
+
force: Annotated[
|
|
869
|
+
bool,
|
|
870
|
+
typer.Option("--force", "-F", help="Force re-analysis even if context is up to date"),
|
|
871
|
+
] = False,
|
|
872
|
+
) -> None:
|
|
873
|
+
"""Force refresh of cached project context.
|
|
874
|
+
|
|
875
|
+
Triggers a re-analysis of the project's GitHub repositories
|
|
876
|
+
to update the cached codebase context.
|
|
877
|
+
|
|
878
|
+
This analyzes:
|
|
879
|
+
- package.json/pyproject.toml for dependencies
|
|
880
|
+
- Directory structure for patterns
|
|
881
|
+
- Config files for tech stack detection
|
|
882
|
+
"""
|
|
883
|
+
from steerdev_agent.api.context import (
|
|
884
|
+
ContextClient,
|
|
885
|
+
display_context_refresh_success,
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
with ContextClient() as client:
|
|
889
|
+
if not client.check_api_key():
|
|
890
|
+
raise typer.Exit(1)
|
|
891
|
+
|
|
892
|
+
try:
|
|
893
|
+
result = client.refresh_context(project_id=project_id, force=force)
|
|
894
|
+
if result:
|
|
895
|
+
# Show analysis results
|
|
896
|
+
analyzed = result.get("analyzed", [])
|
|
897
|
+
skipped = result.get("skipped", [])
|
|
898
|
+
|
|
899
|
+
if analyzed:
|
|
900
|
+
console.print("[green]Analyzed:[/green]")
|
|
901
|
+
for repo in analyzed:
|
|
902
|
+
status = repo.get("status", "unknown")
|
|
903
|
+
name = repo.get("full_name", repo.get("repo_name", "unknown"))
|
|
904
|
+
if status == "completed":
|
|
905
|
+
console.print(f" [green]✓[/green] {name}")
|
|
906
|
+
else:
|
|
907
|
+
error = repo.get("error", "")
|
|
908
|
+
console.print(f" [red]✗[/red] {name}: {error}")
|
|
909
|
+
|
|
910
|
+
if skipped:
|
|
911
|
+
console.print("[yellow]Skipped (up to date):[/yellow]")
|
|
912
|
+
for repo in skipped:
|
|
913
|
+
name = repo.get("full_name", repo.get("repo_name", "unknown"))
|
|
914
|
+
console.print(f" [dim]○[/dim] {name}")
|
|
915
|
+
|
|
916
|
+
pid = result.get("project_id") or project_id or "unknown"
|
|
917
|
+
display_context_refresh_success(pid)
|
|
918
|
+
else:
|
|
919
|
+
raise typer.Exit(1)
|
|
920
|
+
|
|
921
|
+
except httpx.TimeoutException:
|
|
922
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
923
|
+
raise typer.Exit(1) from None
|
|
924
|
+
except httpx.HTTPError as e:
|
|
925
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
926
|
+
raise typer.Exit(1) from None
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
# ============================================================================
|
|
930
|
+
# Activity Command Group
|
|
931
|
+
# ============================================================================
|
|
932
|
+
activity_app = typer.Typer(
|
|
933
|
+
name="activity",
|
|
934
|
+
help="Activity reporting commands for self-reporting progress and querying history",
|
|
935
|
+
no_args_is_help=True,
|
|
936
|
+
)
|
|
937
|
+
app.add_typer(activity_app)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
@activity_app.command("report")
|
|
941
|
+
def activity_report(
|
|
942
|
+
event_type: Annotated[
|
|
943
|
+
str,
|
|
944
|
+
typer.Option(
|
|
945
|
+
"--type",
|
|
946
|
+
"-t",
|
|
947
|
+
help="Event type (progress, blocker, question, milestone, error, warning, info)",
|
|
948
|
+
),
|
|
949
|
+
],
|
|
950
|
+
message: Annotated[
|
|
951
|
+
str,
|
|
952
|
+
typer.Option("--message", "-m", help="Human-readable message describing the event"),
|
|
953
|
+
],
|
|
954
|
+
metadata: Annotated[
|
|
955
|
+
str | None,
|
|
956
|
+
typer.Option(
|
|
957
|
+
"--metadata",
|
|
958
|
+
help='Additional metadata as JSON string (e.g., \'{"key": "value"}\')',
|
|
959
|
+
),
|
|
960
|
+
] = None,
|
|
961
|
+
run_id: Annotated[
|
|
962
|
+
str | None,
|
|
963
|
+
typer.Option("--run-id", "-r", help="Run ID to associate with"),
|
|
964
|
+
] = None,
|
|
965
|
+
session_name: Annotated[
|
|
966
|
+
str | None,
|
|
967
|
+
typer.Option("--session", "-s", help="Session name"),
|
|
968
|
+
] = None,
|
|
969
|
+
) -> None:
|
|
970
|
+
"""Self-report progress, blockers, or other activity events.
|
|
971
|
+
|
|
972
|
+
Use this to report your current status to the platform:
|
|
973
|
+
- progress: Report progress on a task
|
|
974
|
+
- blocker: Report something blocking progress
|
|
975
|
+
- question: Report a question or need for clarification
|
|
976
|
+
- milestone: Report reaching a milestone
|
|
977
|
+
- error: Report an error encountered
|
|
978
|
+
- warning: Report a warning or heads-up
|
|
979
|
+
- info: Report general information
|
|
980
|
+
"""
|
|
981
|
+
import json as json_module
|
|
982
|
+
|
|
983
|
+
from steerdev_agent.api.activity import (
|
|
984
|
+
VALID_EVENT_TYPES,
|
|
985
|
+
ActivityClient,
|
|
986
|
+
display_report_success,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
# Validate event type
|
|
990
|
+
if event_type not in VALID_EVENT_TYPES:
|
|
991
|
+
console.print(
|
|
992
|
+
f"[red]Error: Invalid event type '{event_type}'. "
|
|
993
|
+
f"Must be one of: {', '.join(VALID_EVENT_TYPES)}[/red]"
|
|
994
|
+
)
|
|
995
|
+
raise typer.Exit(1)
|
|
996
|
+
|
|
997
|
+
# Parse metadata if provided
|
|
998
|
+
metadata_dict = None
|
|
999
|
+
if metadata:
|
|
1000
|
+
try:
|
|
1001
|
+
metadata_dict = json_module.loads(metadata)
|
|
1002
|
+
except json_module.JSONDecodeError as e:
|
|
1003
|
+
console.print(f"[red]Error: Invalid JSON in metadata: {e}[/red]")
|
|
1004
|
+
raise typer.Exit(1) from None
|
|
1005
|
+
|
|
1006
|
+
with ActivityClient() as client:
|
|
1007
|
+
if not client.check_api_key():
|
|
1008
|
+
raise typer.Exit(1)
|
|
1009
|
+
|
|
1010
|
+
try:
|
|
1011
|
+
success = client.report(
|
|
1012
|
+
event_type=event_type,
|
|
1013
|
+
message=message,
|
|
1014
|
+
metadata=metadata_dict,
|
|
1015
|
+
run_id=run_id,
|
|
1016
|
+
session_name=session_name,
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
if success:
|
|
1020
|
+
display_report_success(event_type, message)
|
|
1021
|
+
else:
|
|
1022
|
+
raise typer.Exit(1)
|
|
1023
|
+
|
|
1024
|
+
except httpx.TimeoutException:
|
|
1025
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
1026
|
+
raise typer.Exit(1) from None
|
|
1027
|
+
except httpx.HTTPError as e:
|
|
1028
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
1029
|
+
raise typer.Exit(1) from None
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
@activity_app.command("query")
|
|
1033
|
+
def activity_query(
|
|
1034
|
+
run_id: Annotated[
|
|
1035
|
+
str | None,
|
|
1036
|
+
typer.Option("--run-id", "-r", help="Filter by run ID"),
|
|
1037
|
+
] = None,
|
|
1038
|
+
event_type: Annotated[
|
|
1039
|
+
str | None,
|
|
1040
|
+
typer.Option("--type", "-t", help="Filter by event type"),
|
|
1041
|
+
] = None,
|
|
1042
|
+
limit: Annotated[
|
|
1043
|
+
int,
|
|
1044
|
+
typer.Option("--limit", "-l", help="Maximum number of events to return"),
|
|
1045
|
+
] = 20,
|
|
1046
|
+
) -> None:
|
|
1047
|
+
"""Query activity history with optional filters.
|
|
1048
|
+
|
|
1049
|
+
Returns recent activity events, optionally filtered by run ID or event type.
|
|
1050
|
+
"""
|
|
1051
|
+
from steerdev_agent.api.activity import ActivityClient, display_activity_list
|
|
1052
|
+
|
|
1053
|
+
with ActivityClient() as client:
|
|
1054
|
+
if not client.check_api_key():
|
|
1055
|
+
raise typer.Exit(1)
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
events = client.query(
|
|
1059
|
+
run_id=run_id,
|
|
1060
|
+
event_type=event_type,
|
|
1061
|
+
limit=limit,
|
|
1062
|
+
)
|
|
1063
|
+
display_activity_list(events)
|
|
1064
|
+
|
|
1065
|
+
except httpx.TimeoutException:
|
|
1066
|
+
console.print("[red]Error: Request timed out[/red]")
|
|
1067
|
+
raise typer.Exit(1) from None
|
|
1068
|
+
except httpx.HTTPError as e:
|
|
1069
|
+
console.print(f"[red]HTTP Error: {e}[/red]")
|
|
1070
|
+
raise typer.Exit(1) from None
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
# ============================================================================
|
|
1074
|
+
# Git Workflow Command Group
|
|
1075
|
+
# ============================================================================
|
|
1076
|
+
git_app = typer.Typer(
|
|
1077
|
+
name="git",
|
|
1078
|
+
help="Git workflow commands with steerdev conventions",
|
|
1079
|
+
no_args_is_help=True,
|
|
1080
|
+
)
|
|
1081
|
+
app.add_typer(git_app)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
@git_app.command("branch")
|
|
1085
|
+
def git_branch(
|
|
1086
|
+
task_id: Annotated[
|
|
1087
|
+
str | None,
|
|
1088
|
+
typer.Argument(help="Task ID to create branch for (uses first 8 chars)"),
|
|
1089
|
+
] = None,
|
|
1090
|
+
name: Annotated[
|
|
1091
|
+
str | None,
|
|
1092
|
+
typer.Option("--name", "-n", help="Custom branch name suffix"),
|
|
1093
|
+
] = None,
|
|
1094
|
+
) -> None:
|
|
1095
|
+
"""Create a task branch with steerdev naming convention.
|
|
1096
|
+
|
|
1097
|
+
Creates a branch named `task/<task-id-short>` or `task/<task-id-short>-<name>`.
|
|
1098
|
+
Uses the first 8 characters of the task ID for the branch name.
|
|
1099
|
+
|
|
1100
|
+
Example:
|
|
1101
|
+
steerdev git branch abc12345-...
|
|
1102
|
+
# Creates: task/abc12345
|
|
1103
|
+
|
|
1104
|
+
steerdev git branch abc12345-... --name auth-flow
|
|
1105
|
+
# Creates: task/abc12345-auth-flow
|
|
1106
|
+
"""
|
|
1107
|
+
import subprocess
|
|
1108
|
+
|
|
1109
|
+
if not task_id:
|
|
1110
|
+
console.print("[red]Error: Task ID is required[/red]")
|
|
1111
|
+
raise typer.Exit(1)
|
|
1112
|
+
|
|
1113
|
+
# Use first 8 chars of task ID
|
|
1114
|
+
short_id = task_id[:8]
|
|
1115
|
+
branch_name = f"task/{short_id}"
|
|
1116
|
+
if name:
|
|
1117
|
+
branch_name = f"task/{short_id}-{name}"
|
|
1118
|
+
|
|
1119
|
+
try:
|
|
1120
|
+
# Check if branch already exists
|
|
1121
|
+
result = subprocess.run(
|
|
1122
|
+
["git", "rev-parse", "--verify", branch_name],
|
|
1123
|
+
capture_output=True,
|
|
1124
|
+
text=True,
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
if result.returncode == 0:
|
|
1128
|
+
console.print(f"[yellow]Branch '{branch_name}' already exists[/yellow]")
|
|
1129
|
+
# Switch to the branch
|
|
1130
|
+
subprocess.run(["git", "checkout", branch_name], check=True)
|
|
1131
|
+
console.print(f"[green]Switched to branch '{branch_name}'[/green]")
|
|
1132
|
+
else:
|
|
1133
|
+
# Create and switch to new branch
|
|
1134
|
+
subprocess.run(["git", "checkout", "-b", branch_name], check=True)
|
|
1135
|
+
console.print(
|
|
1136
|
+
Panel(
|
|
1137
|
+
f"[bold green]Branch created[/bold green]\n\n"
|
|
1138
|
+
f"Branch: {branch_name}\n"
|
|
1139
|
+
f"Task ID: {task_id}",
|
|
1140
|
+
title="Success",
|
|
1141
|
+
border_style="green",
|
|
1142
|
+
)
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
except subprocess.CalledProcessError as e:
|
|
1146
|
+
console.print(f"[red]Git error: {e}[/red]")
|
|
1147
|
+
raise typer.Exit(1) from None
|
|
1148
|
+
except FileNotFoundError:
|
|
1149
|
+
console.print("[red]Error: git not found. Make sure git is installed.[/red]")
|
|
1150
|
+
raise typer.Exit(1) from None
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
@git_app.command("pr")
|
|
1154
|
+
def git_pr(
|
|
1155
|
+
title: Annotated[
|
|
1156
|
+
str,
|
|
1157
|
+
typer.Option("--title", "-t", help="PR title"),
|
|
1158
|
+
],
|
|
1159
|
+
body: Annotated[
|
|
1160
|
+
str | None,
|
|
1161
|
+
typer.Option("--body", "-b", help="PR body/description"),
|
|
1162
|
+
] = None,
|
|
1163
|
+
task_id: Annotated[
|
|
1164
|
+
str | None,
|
|
1165
|
+
typer.Option("--task-id", help="Task ID to reference in PR"),
|
|
1166
|
+
] = None,
|
|
1167
|
+
draft: Annotated[
|
|
1168
|
+
bool,
|
|
1169
|
+
typer.Option("--draft", "-d", help="Create as draft PR"),
|
|
1170
|
+
] = False,
|
|
1171
|
+
) -> None:
|
|
1172
|
+
"""Create a pull request with steerdev conventions.
|
|
1173
|
+
|
|
1174
|
+
Uses GitHub CLI (gh) to create a PR. Automatically includes
|
|
1175
|
+
task reference if task_id is provided.
|
|
1176
|
+
|
|
1177
|
+
Example:
|
|
1178
|
+
steerdev git pr --title "Add auth" --body "Implements JWT auth"
|
|
1179
|
+
steerdev git pr --title "Add auth" --task-id abc123...
|
|
1180
|
+
"""
|
|
1181
|
+
import subprocess
|
|
1182
|
+
|
|
1183
|
+
# Build PR body
|
|
1184
|
+
pr_body = body or ""
|
|
1185
|
+
if task_id:
|
|
1186
|
+
task_ref = f"\n\n## Task Reference\nTask ID: {task_id}"
|
|
1187
|
+
pr_body += task_ref
|
|
1188
|
+
|
|
1189
|
+
# Add steerdev signature
|
|
1190
|
+
pr_body += "\n\n---\nCreated with [steerdev](https://steerdev.com)"
|
|
1191
|
+
|
|
1192
|
+
try:
|
|
1193
|
+
# Build the gh pr create command
|
|
1194
|
+
cmd = ["gh", "pr", "create", "--title", title, "--body", pr_body]
|
|
1195
|
+
if draft:
|
|
1196
|
+
cmd.append("--draft")
|
|
1197
|
+
|
|
1198
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1199
|
+
|
|
1200
|
+
if result.returncode != 0:
|
|
1201
|
+
console.print(f"[red]Error creating PR: {result.stderr}[/red]")
|
|
1202
|
+
raise typer.Exit(1)
|
|
1203
|
+
|
|
1204
|
+
# Extract PR URL from output
|
|
1205
|
+
pr_url = result.stdout.strip()
|
|
1206
|
+
console.print(
|
|
1207
|
+
Panel(
|
|
1208
|
+
f"[bold green]Pull request created[/bold green]\n\nTitle: {title}\nURL: {pr_url}",
|
|
1209
|
+
title="Success",
|
|
1210
|
+
border_style="green",
|
|
1211
|
+
)
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
except FileNotFoundError:
|
|
1215
|
+
console.print("[red]Error: gh not found. Install GitHub CLI: https://cli.github.com[/red]")
|
|
1216
|
+
raise typer.Exit(1) from None
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
@git_app.command("status")
|
|
1220
|
+
def git_status() -> None:
|
|
1221
|
+
"""Show current branch and task context.
|
|
1222
|
+
|
|
1223
|
+
Displays the current git branch and extracts task ID if the branch
|
|
1224
|
+
follows the task/<id> naming convention.
|
|
1225
|
+
"""
|
|
1226
|
+
import subprocess
|
|
1227
|
+
|
|
1228
|
+
try:
|
|
1229
|
+
# Get current branch
|
|
1230
|
+
result = subprocess.run(
|
|
1231
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
1232
|
+
capture_output=True,
|
|
1233
|
+
text=True,
|
|
1234
|
+
check=True,
|
|
1235
|
+
)
|
|
1236
|
+
current_branch = result.stdout.strip()
|
|
1237
|
+
|
|
1238
|
+
# Get status summary
|
|
1239
|
+
status_result = subprocess.run(
|
|
1240
|
+
["git", "status", "--short"],
|
|
1241
|
+
capture_output=True,
|
|
1242
|
+
text=True,
|
|
1243
|
+
)
|
|
1244
|
+
status_lines = (
|
|
1245
|
+
status_result.stdout.strip().split("\n") if status_result.stdout.strip() else []
|
|
1246
|
+
)
|
|
1247
|
+
changes_count = len(status_lines)
|
|
1248
|
+
|
|
1249
|
+
# Extract task ID if branch follows convention
|
|
1250
|
+
task_id = None
|
|
1251
|
+
if current_branch.startswith("task/"):
|
|
1252
|
+
# Extract the task ID part (everything after "task/")
|
|
1253
|
+
task_part = current_branch[5:] # Remove "task/" prefix
|
|
1254
|
+
# Task ID is the first part before any hyphen (if name suffix was used)
|
|
1255
|
+
task_id = task_part.split("-")[0] if "-" in task_part else task_part
|
|
1256
|
+
|
|
1257
|
+
# Build info display
|
|
1258
|
+
info_lines = [
|
|
1259
|
+
f"[bold cyan]Branch:[/bold cyan] {current_branch}",
|
|
1260
|
+
]
|
|
1261
|
+
|
|
1262
|
+
if task_id:
|
|
1263
|
+
info_lines.append(f"[bold cyan]Task ID:[/bold cyan] {task_id}")
|
|
1264
|
+
|
|
1265
|
+
if changes_count > 0:
|
|
1266
|
+
info_lines.append(
|
|
1267
|
+
f"[bold cyan]Uncommitted changes:[/bold cyan] {changes_count} file(s)"
|
|
1268
|
+
)
|
|
1269
|
+
else:
|
|
1270
|
+
info_lines.append("[bold cyan]Uncommitted changes:[/bold cyan] None")
|
|
1271
|
+
|
|
1272
|
+
# Check if branch is pushed
|
|
1273
|
+
tracking_result = subprocess.run(
|
|
1274
|
+
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
|
1275
|
+
capture_output=True,
|
|
1276
|
+
text=True,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
if tracking_result.returncode == 0:
|
|
1280
|
+
upstream = tracking_result.stdout.strip()
|
|
1281
|
+
info_lines.append(f"[bold cyan]Tracking:[/bold cyan] {upstream}")
|
|
1282
|
+
|
|
1283
|
+
# Check ahead/behind
|
|
1284
|
+
ahead_behind = subprocess.run(
|
|
1285
|
+
["git", "rev-list", "--left-right", "--count", f"{upstream}...HEAD"],
|
|
1286
|
+
capture_output=True,
|
|
1287
|
+
text=True,
|
|
1288
|
+
)
|
|
1289
|
+
if ahead_behind.returncode == 0:
|
|
1290
|
+
parts = ahead_behind.stdout.strip().split()
|
|
1291
|
+
if len(parts) == 2:
|
|
1292
|
+
behind, ahead = parts
|
|
1293
|
+
if int(ahead) > 0 or int(behind) > 0:
|
|
1294
|
+
info_lines.append(
|
|
1295
|
+
f"[bold cyan]Status:[/bold cyan] {ahead} ahead, {behind} behind"
|
|
1296
|
+
)
|
|
1297
|
+
else:
|
|
1298
|
+
info_lines.append("[bold cyan]Tracking:[/bold cyan] Not pushed to remote")
|
|
1299
|
+
|
|
1300
|
+
console.print(
|
|
1301
|
+
Panel(
|
|
1302
|
+
"\n".join(info_lines),
|
|
1303
|
+
title="Git Status",
|
|
1304
|
+
border_style="blue",
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
except subprocess.CalledProcessError as e:
|
|
1309
|
+
console.print(f"[red]Git error: {e}[/red]")
|
|
1310
|
+
raise typer.Exit(1) from None
|
|
1311
|
+
except FileNotFoundError:
|
|
1312
|
+
console.print("[red]Error: git not found. Make sure git is installed.[/red]")
|
|
1313
|
+
raise typer.Exit(1) from None
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def version_callback(value: bool) -> None:
|
|
1317
|
+
"""Print version and exit."""
|
|
1318
|
+
if value:
|
|
1319
|
+
console.print(f"steerdev version {get_version()}")
|
|
1320
|
+
raise typer.Exit()
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
@app.callback()
|
|
1324
|
+
def main(
|
|
1325
|
+
ctx: typer.Context,
|
|
1326
|
+
version: Annotated[
|
|
1327
|
+
bool,
|
|
1328
|
+
typer.Option(
|
|
1329
|
+
"--version",
|
|
1330
|
+
"-v",
|
|
1331
|
+
help="Show version and exit",
|
|
1332
|
+
callback=version_callback,
|
|
1333
|
+
is_eager=True,
|
|
1334
|
+
),
|
|
1335
|
+
] = False,
|
|
1336
|
+
config_file: Annotated[
|
|
1337
|
+
Path | None,
|
|
1338
|
+
typer.Option(
|
|
1339
|
+
"--config",
|
|
1340
|
+
"-C",
|
|
1341
|
+
help="Path to config file (default: steerdev.yaml)",
|
|
1342
|
+
),
|
|
1343
|
+
] = None,
|
|
1344
|
+
) -> None:
|
|
1345
|
+
"""SteerDev Agent - orchestrates CLI coding agents with activity reporting."""
|
|
1346
|
+
# Load config file
|
|
1347
|
+
config_path = config_file or Path.cwd() / "steerdev.yaml"
|
|
1348
|
+
if config_path.exists():
|
|
1349
|
+
try:
|
|
1350
|
+
ctx.obj = SteerDevConfig.from_yaml(config_path)
|
|
1351
|
+
except Exception as e:
|
|
1352
|
+
console.print(f"[yellow]Warning: Failed to load config {config_path}: {e}[/yellow]")
|
|
1353
|
+
ctx.obj = SteerDevConfig()
|
|
1354
|
+
else:
|
|
1355
|
+
ctx.obj = SteerDevConfig()
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@app.command()
|
|
1359
|
+
def run(
|
|
1360
|
+
ctx: typer.Context,
|
|
1361
|
+
project_id: Annotated[
|
|
1362
|
+
str | None,
|
|
1363
|
+
typer.Option(
|
|
1364
|
+
"--project-id",
|
|
1365
|
+
"-p",
|
|
1366
|
+
help="SteerDev project ID",
|
|
1367
|
+
envvar="STEERDEV_PROJECT_ID",
|
|
1368
|
+
),
|
|
1369
|
+
] = None,
|
|
1370
|
+
task_id: Annotated[
|
|
1371
|
+
str | None,
|
|
1372
|
+
typer.Option(
|
|
1373
|
+
"--task-id",
|
|
1374
|
+
"-t",
|
|
1375
|
+
help="Specific task ID to run (optional, fetches next task if not provided)",
|
|
1376
|
+
),
|
|
1377
|
+
] = None,
|
|
1378
|
+
working_dir: Annotated[
|
|
1379
|
+
Path | None,
|
|
1380
|
+
typer.Option(
|
|
1381
|
+
"--workdir",
|
|
1382
|
+
"-w",
|
|
1383
|
+
help="Working directory for the agent (defaults to current directory)",
|
|
1384
|
+
exists=True,
|
|
1385
|
+
file_okay=False,
|
|
1386
|
+
dir_okay=True,
|
|
1387
|
+
),
|
|
1388
|
+
] = None,
|
|
1389
|
+
agent_name: Annotated[
|
|
1390
|
+
str | None,
|
|
1391
|
+
typer.Option(
|
|
1392
|
+
"--agent-name",
|
|
1393
|
+
"-n",
|
|
1394
|
+
help="Agent name for session tracking (reads from STEERDEV_AGENT_NAME env var)",
|
|
1395
|
+
envvar="STEERDEV_AGENT_NAME",
|
|
1396
|
+
),
|
|
1397
|
+
] = None,
|
|
1398
|
+
model: Annotated[
|
|
1399
|
+
str | None,
|
|
1400
|
+
typer.Option(
|
|
1401
|
+
"--model",
|
|
1402
|
+
"-m",
|
|
1403
|
+
help="Model to use (e.g., claude-sonnet-4-20250514)",
|
|
1404
|
+
),
|
|
1405
|
+
] = None,
|
|
1406
|
+
max_turns: Annotated[
|
|
1407
|
+
int | None,
|
|
1408
|
+
typer.Option(
|
|
1409
|
+
"--max-turns",
|
|
1410
|
+
help="Maximum number of agent turns per task",
|
|
1411
|
+
),
|
|
1412
|
+
] = None,
|
|
1413
|
+
max_tasks: Annotated[
|
|
1414
|
+
int,
|
|
1415
|
+
typer.Option(
|
|
1416
|
+
"--max-tasks",
|
|
1417
|
+
help="Maximum number of tasks to process (default: 1, use 0 for unlimited)",
|
|
1418
|
+
),
|
|
1419
|
+
] = 1,
|
|
1420
|
+
timeout: Annotated[
|
|
1421
|
+
int | None,
|
|
1422
|
+
typer.Option(
|
|
1423
|
+
"--timeout",
|
|
1424
|
+
help="Timeout in seconds (default: from config or 3600)",
|
|
1425
|
+
),
|
|
1426
|
+
] = None,
|
|
1427
|
+
api_key: Annotated[
|
|
1428
|
+
str | None,
|
|
1429
|
+
typer.Option(
|
|
1430
|
+
"--key",
|
|
1431
|
+
"-k",
|
|
1432
|
+
help="API key for steerdev.com (overrides STEERDEV_API_KEY env var)",
|
|
1433
|
+
envvar="STEERDEV_API_KEY",
|
|
1434
|
+
),
|
|
1435
|
+
] = None,
|
|
1436
|
+
worktrees: Annotated[
|
|
1437
|
+
bool | None,
|
|
1438
|
+
typer.Option(
|
|
1439
|
+
"--worktrees/--no-worktrees",
|
|
1440
|
+
"-W",
|
|
1441
|
+
help="Enable git worktree isolation (default: from config or disabled)",
|
|
1442
|
+
),
|
|
1443
|
+
] = None,
|
|
1444
|
+
dry_run: Annotated[
|
|
1445
|
+
bool,
|
|
1446
|
+
typer.Option(
|
|
1447
|
+
"--dry",
|
|
1448
|
+
help="Print the command that would be run without executing it",
|
|
1449
|
+
),
|
|
1450
|
+
] = False,
|
|
1451
|
+
workflow_id: Annotated[
|
|
1452
|
+
str | None,
|
|
1453
|
+
typer.Option(
|
|
1454
|
+
"--workflow-id",
|
|
1455
|
+
help="Workflow ID for multi-phase execution (overrides config)",
|
|
1456
|
+
),
|
|
1457
|
+
] = None,
|
|
1458
|
+
) -> None:
|
|
1459
|
+
"""Run the agent for a project!
|
|
1460
|
+
|
|
1461
|
+
Fetches the next available task (or a specific task if --task-id is provided)
|
|
1462
|
+
and executes it using Claude Code.
|
|
1463
|
+
|
|
1464
|
+
Configuration values are loaded from steerdev.yaml (if present) and can
|
|
1465
|
+
be overridden by CLI options. Priority: CLI > env var > config file > default.
|
|
1466
|
+
|
|
1467
|
+
Example:
|
|
1468
|
+
steerdev run --project-id abc123
|
|
1469
|
+
steerdev run --project-id abc123 --task-id def456
|
|
1470
|
+
steerdev run --project-id abc123 --worktrees
|
|
1471
|
+
steerdev run --project-id abc123 --workflow-id wf-abc123
|
|
1472
|
+
steerdev run --project-id abc123 --agent-name my-dev-agent
|
|
1473
|
+
steerdev run --config custom.yaml --project-id abc123
|
|
1474
|
+
"""
|
|
1475
|
+
from steerdev_agent.runner import run_agent
|
|
1476
|
+
|
|
1477
|
+
# Get config from context (loaded in app callback)
|
|
1478
|
+
config: SteerDevConfig = ctx.obj or SteerDevConfig()
|
|
1479
|
+
|
|
1480
|
+
# Resolve project_id: CLI > env (handled by typer) > config env var
|
|
1481
|
+
resolved_project_id = project_id
|
|
1482
|
+
if not resolved_project_id:
|
|
1483
|
+
# Try config's specified env var
|
|
1484
|
+
resolved_project_id = os.environ.get(config.api.project_id_env)
|
|
1485
|
+
if not resolved_project_id:
|
|
1486
|
+
console.print("[red]Error: --project-id required (or set via env/config)[/red]")
|
|
1487
|
+
raise typer.Exit(1)
|
|
1488
|
+
|
|
1489
|
+
# Resolve other options: CLI > config > hardcoded default
|
|
1490
|
+
resolved_model = model if model is not None else config.agent.model
|
|
1491
|
+
resolved_max_turns = max_turns if max_turns is not None else config.agent.max_turns
|
|
1492
|
+
resolved_timeout = timeout if timeout is not None else config.agent.timeout_seconds
|
|
1493
|
+
resolved_workflow_id = workflow_id if workflow_id is not None else config.agent.workflow_id
|
|
1494
|
+
resolved_worktrees = worktrees if worktrees is not None else config.worktrees.enabled
|
|
1495
|
+
|
|
1496
|
+
# API key: CLI > env (via envvar) > config env var
|
|
1497
|
+
resolved_api_key = api_key
|
|
1498
|
+
if not resolved_api_key:
|
|
1499
|
+
resolved_api_key = os.environ.get(config.api.api_key_env)
|
|
1500
|
+
|
|
1501
|
+
worktree_status = "enabled" if resolved_worktrees else "disabled"
|
|
1502
|
+
dry_run_status = "enabled" if dry_run else "disabled"
|
|
1503
|
+
max_tasks_display = "unlimited" if max_tasks == 0 else str(max_tasks)
|
|
1504
|
+
workflow_status = resolved_workflow_id or "single-phase"
|
|
1505
|
+
agent_name_display = agent_name or "(auto from env)"
|
|
1506
|
+
|
|
1507
|
+
console.print(
|
|
1508
|
+
Panel(
|
|
1509
|
+
f"[bold blue]SteerDev Agent[/bold blue]\n"
|
|
1510
|
+
f"Project ID: {resolved_project_id}\n"
|
|
1511
|
+
f"Agent Name: {agent_name_display}\n"
|
|
1512
|
+
f"Task ID: {task_id or 'auto (next available)'}\n"
|
|
1513
|
+
f"Working Directory: {working_dir or 'current'}\n"
|
|
1514
|
+
f"Model: {resolved_model or 'default'}\n"
|
|
1515
|
+
f"Max Tasks: {max_tasks_display}\n"
|
|
1516
|
+
f"Timeout: {resolved_timeout}s\n"
|
|
1517
|
+
f"Workflow: {workflow_status}\n"
|
|
1518
|
+
f"Worktrees: {worktree_status}\n"
|
|
1519
|
+
f"Dry Run: {dry_run_status}",
|
|
1520
|
+
title="Starting",
|
|
1521
|
+
)
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
# Check if shared settings are stale
|
|
1525
|
+
_check_sync_staleness(working_dir or Path.cwd())
|
|
1526
|
+
|
|
1527
|
+
try:
|
|
1528
|
+
result = asyncio.run(
|
|
1529
|
+
run_agent(
|
|
1530
|
+
project_id=resolved_project_id,
|
|
1531
|
+
task_id=task_id,
|
|
1532
|
+
working_directory=str(working_dir) if working_dir else None,
|
|
1533
|
+
api_key=resolved_api_key,
|
|
1534
|
+
agent_name=agent_name,
|
|
1535
|
+
model=resolved_model,
|
|
1536
|
+
max_turns=resolved_max_turns,
|
|
1537
|
+
max_tasks=max_tasks,
|
|
1538
|
+
timeout_seconds=resolved_timeout,
|
|
1539
|
+
enable_worktrees=resolved_worktrees,
|
|
1540
|
+
workflow_id=resolved_workflow_id,
|
|
1541
|
+
dry_run=dry_run,
|
|
1542
|
+
)
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
console.print(
|
|
1546
|
+
Panel(
|
|
1547
|
+
f"[bold green]Run completed[/bold green]\n"
|
|
1548
|
+
f"Run ID: {result['run_id']}\n"
|
|
1549
|
+
f"Duration: {result.get('duration_seconds', 0):.1f}s\n"
|
|
1550
|
+
f"Tasks Executed: {result.get('tasks_executed', 0)}\n"
|
|
1551
|
+
f"Succeeded: {result.get('tasks_succeeded', 0)}\n"
|
|
1552
|
+
f"Failed: {result.get('tasks_failed', 0)}\n"
|
|
1553
|
+
f"Events Sent: {result.get('events_sent', 0)}",
|
|
1554
|
+
title="Complete",
|
|
1555
|
+
)
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
except KeyboardInterrupt:
|
|
1559
|
+
console.print("\n[yellow]Interrupted by user[/yellow]")
|
|
1560
|
+
raise typer.Exit(130) from None
|
|
1561
|
+
except Exception as e:
|
|
1562
|
+
console.print(f"\n[red]Error: {e}[/red]")
|
|
1563
|
+
raise typer.Exit(1) from e
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
@app.command()
|
|
1567
|
+
def daemon(
|
|
1568
|
+
ctx: typer.Context,
|
|
1569
|
+
project_id: Annotated[
|
|
1570
|
+
str | None,
|
|
1571
|
+
typer.Option(
|
|
1572
|
+
"--project-id",
|
|
1573
|
+
"-p",
|
|
1574
|
+
help="SteerDev project ID",
|
|
1575
|
+
envvar="STEERDEV_PROJECT_ID",
|
|
1576
|
+
),
|
|
1577
|
+
] = None,
|
|
1578
|
+
agent_name: Annotated[
|
|
1579
|
+
str | None,
|
|
1580
|
+
typer.Option(
|
|
1581
|
+
"--agent-name",
|
|
1582
|
+
"-n",
|
|
1583
|
+
help="Agent name (required, reads from STEERDEV_AGENT_NAME env var)",
|
|
1584
|
+
envvar="STEERDEV_AGENT_NAME",
|
|
1585
|
+
),
|
|
1586
|
+
] = None,
|
|
1587
|
+
working_dir: Annotated[
|
|
1588
|
+
Path | None,
|
|
1589
|
+
typer.Option(
|
|
1590
|
+
"--workdir",
|
|
1591
|
+
"-w",
|
|
1592
|
+
help="Working directory for the agent (defaults to current directory)",
|
|
1593
|
+
exists=True,
|
|
1594
|
+
file_okay=False,
|
|
1595
|
+
dir_okay=True,
|
|
1596
|
+
),
|
|
1597
|
+
] = None,
|
|
1598
|
+
model: Annotated[
|
|
1599
|
+
str | None,
|
|
1600
|
+
typer.Option(
|
|
1601
|
+
"--model",
|
|
1602
|
+
"-m",
|
|
1603
|
+
help="Model to use (e.g., claude-sonnet-4-20250514)",
|
|
1604
|
+
),
|
|
1605
|
+
] = None,
|
|
1606
|
+
max_turns: Annotated[
|
|
1607
|
+
int | None,
|
|
1608
|
+
typer.Option(
|
|
1609
|
+
"--max-turns",
|
|
1610
|
+
help="Maximum number of agent turns per command/task",
|
|
1611
|
+
),
|
|
1612
|
+
] = None,
|
|
1613
|
+
poll_interval: Annotated[
|
|
1614
|
+
float | None,
|
|
1615
|
+
typer.Option(
|
|
1616
|
+
"--poll-interval",
|
|
1617
|
+
help="Seconds between command queue polls (default: from config or 5.0)",
|
|
1618
|
+
),
|
|
1619
|
+
] = None,
|
|
1620
|
+
auto_fetch_tasks: Annotated[
|
|
1621
|
+
bool | None,
|
|
1622
|
+
typer.Option(
|
|
1623
|
+
"--auto-fetch-tasks/--no-auto-fetch-tasks",
|
|
1624
|
+
help="Fall back to task queue when command queue is empty",
|
|
1625
|
+
),
|
|
1626
|
+
] = None,
|
|
1627
|
+
api_key: Annotated[
|
|
1628
|
+
str | None,
|
|
1629
|
+
typer.Option(
|
|
1630
|
+
"--key",
|
|
1631
|
+
"-k",
|
|
1632
|
+
help="API key for steerdev.com (overrides STEERDEV_API_KEY env var)",
|
|
1633
|
+
envvar="STEERDEV_API_KEY",
|
|
1634
|
+
),
|
|
1635
|
+
] = None,
|
|
1636
|
+
) -> None:
|
|
1637
|
+
"""Run the agent in persistent daemon mode.
|
|
1638
|
+
|
|
1639
|
+
The daemon runs indefinitely, polling for commands from the dashboard/API.
|
|
1640
|
+
When the command queue is empty and --auto-fetch-tasks is enabled (default),
|
|
1641
|
+
it falls back to fetching the next task from the task queue.
|
|
1642
|
+
|
|
1643
|
+
Each command/task execution creates a session with full event streaming.
|
|
1644
|
+
Send a 'shutdown' control command or press Ctrl+C to stop.
|
|
1645
|
+
|
|
1646
|
+
Example:
|
|
1647
|
+
steerdev daemon --project-id abc123 --agent-name my-agent
|
|
1648
|
+
steerdev daemon --project-id abc123 --agent-name my-agent --no-auto-fetch-tasks
|
|
1649
|
+
steerdev daemon --project-id abc123 --agent-name my-agent --poll-interval 10
|
|
1650
|
+
"""
|
|
1651
|
+
from steerdev_agent.daemon import run_daemon
|
|
1652
|
+
|
|
1653
|
+
# Get config from context (loaded in app callback)
|
|
1654
|
+
config: SteerDevConfig = ctx.obj or SteerDevConfig()
|
|
1655
|
+
|
|
1656
|
+
# Resolve project_id: CLI > env (handled by typer) > config env var
|
|
1657
|
+
resolved_project_id = project_id
|
|
1658
|
+
if not resolved_project_id:
|
|
1659
|
+
resolved_project_id = os.environ.get(config.api.project_id_env)
|
|
1660
|
+
if not resolved_project_id:
|
|
1661
|
+
console.print("[red]Error: --project-id required (or set via env/config)[/red]")
|
|
1662
|
+
raise typer.Exit(1)
|
|
1663
|
+
|
|
1664
|
+
# Agent name is required for daemon mode
|
|
1665
|
+
if not agent_name:
|
|
1666
|
+
console.print(
|
|
1667
|
+
"[red]Error: --agent-name required (or set STEERDEV_AGENT_NAME env var)[/red]"
|
|
1668
|
+
)
|
|
1669
|
+
raise typer.Exit(1)
|
|
1670
|
+
|
|
1671
|
+
# Resolve options: CLI > config > default
|
|
1672
|
+
resolved_model = model if model is not None else config.agent.model
|
|
1673
|
+
resolved_max_turns = max_turns if max_turns is not None else config.agent.max_turns
|
|
1674
|
+
|
|
1675
|
+
# API key: CLI > env (via envvar) > config env var
|
|
1676
|
+
resolved_api_key = api_key
|
|
1677
|
+
if not resolved_api_key:
|
|
1678
|
+
resolved_api_key = os.environ.get(config.api.api_key_env)
|
|
1679
|
+
|
|
1680
|
+
# Build daemon config with CLI overrides
|
|
1681
|
+
daemon_config = config.daemon.model_copy()
|
|
1682
|
+
if poll_interval is not None:
|
|
1683
|
+
daemon_config.poll_interval_seconds = poll_interval
|
|
1684
|
+
if auto_fetch_tasks is not None:
|
|
1685
|
+
daemon_config.auto_fetch_tasks = auto_fetch_tasks
|
|
1686
|
+
|
|
1687
|
+
try:
|
|
1688
|
+
asyncio.run(
|
|
1689
|
+
run_daemon(
|
|
1690
|
+
project_id=resolved_project_id,
|
|
1691
|
+
agent_name=agent_name,
|
|
1692
|
+
working_directory=str(working_dir) if working_dir else None,
|
|
1693
|
+
api_key=resolved_api_key,
|
|
1694
|
+
model=resolved_model,
|
|
1695
|
+
max_turns=resolved_max_turns,
|
|
1696
|
+
daemon_config=daemon_config,
|
|
1697
|
+
executor_config=config.executor,
|
|
1698
|
+
)
|
|
1699
|
+
)
|
|
1700
|
+
except KeyboardInterrupt:
|
|
1701
|
+
console.print("\n[yellow]Daemon stopped[/yellow]")
|
|
1702
|
+
raise typer.Exit(0) from None
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
console.print(f"\n[red]Daemon error: {e}[/red]")
|
|
1705
|
+
raise typer.Exit(1) from e
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
@app.command()
|
|
1709
|
+
def resume(
|
|
1710
|
+
ctx: typer.Context,
|
|
1711
|
+
session_id: Annotated[
|
|
1712
|
+
str,
|
|
1713
|
+
typer.Option(
|
|
1714
|
+
"--session-id",
|
|
1715
|
+
"-s",
|
|
1716
|
+
help="Session ID to resume (required)",
|
|
1717
|
+
),
|
|
1718
|
+
],
|
|
1719
|
+
message: Annotated[
|
|
1720
|
+
str,
|
|
1721
|
+
typer.Option(
|
|
1722
|
+
"--message",
|
|
1723
|
+
"-m",
|
|
1724
|
+
help="Message to continue the conversation (required)",
|
|
1725
|
+
),
|
|
1726
|
+
],
|
|
1727
|
+
api_key: Annotated[
|
|
1728
|
+
str | None,
|
|
1729
|
+
typer.Option(
|
|
1730
|
+
"--key",
|
|
1731
|
+
"-k",
|
|
1732
|
+
help="API key for steerdev.com",
|
|
1733
|
+
envvar="STEERDEV_API_KEY",
|
|
1734
|
+
),
|
|
1735
|
+
] = None,
|
|
1736
|
+
dry_run: Annotated[
|
|
1737
|
+
bool,
|
|
1738
|
+
typer.Option(
|
|
1739
|
+
"--dry",
|
|
1740
|
+
help="Print the command that would be run without executing it",
|
|
1741
|
+
),
|
|
1742
|
+
] = False,
|
|
1743
|
+
) -> None:
|
|
1744
|
+
"""Resume an existing session with a new message.
|
|
1745
|
+
|
|
1746
|
+
Example:
|
|
1747
|
+
steerdev resume --session-id abc123 --message "Continue working..."
|
|
1748
|
+
"""
|
|
1749
|
+
from steerdev_agent.runner import resume_session
|
|
1750
|
+
|
|
1751
|
+
# Get config from context (loaded in app callback)
|
|
1752
|
+
config: SteerDevConfig = ctx.obj or SteerDevConfig()
|
|
1753
|
+
|
|
1754
|
+
# Resolve API key: CLI > env (via envvar) > config env var
|
|
1755
|
+
resolved_api_key = api_key
|
|
1756
|
+
if not resolved_api_key:
|
|
1757
|
+
resolved_api_key = os.environ.get(config.api.api_key_env)
|
|
1758
|
+
|
|
1759
|
+
dry_run_status = "enabled" if dry_run else "disabled"
|
|
1760
|
+
|
|
1761
|
+
console.print(
|
|
1762
|
+
Panel(
|
|
1763
|
+
f"[bold blue]Resuming Session[/bold blue]\n"
|
|
1764
|
+
f"Session ID: {session_id}\n"
|
|
1765
|
+
f"Message: {message[:100]}{'...' if len(message) > 100 else ''}\n"
|
|
1766
|
+
f"Dry Run: {dry_run_status}",
|
|
1767
|
+
title="Resume",
|
|
1768
|
+
)
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
try:
|
|
1772
|
+
result = asyncio.run(
|
|
1773
|
+
resume_session(
|
|
1774
|
+
session_id=session_id,
|
|
1775
|
+
message=message,
|
|
1776
|
+
api_key=resolved_api_key,
|
|
1777
|
+
dry_run=dry_run,
|
|
1778
|
+
)
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
if result.get("success"):
|
|
1782
|
+
console.print(
|
|
1783
|
+
Panel(
|
|
1784
|
+
f"[bold green]Session resumed successfully[/bold green]\n"
|
|
1785
|
+
f"Events Sent: {result.get('events_sent', 0)}",
|
|
1786
|
+
title="Complete",
|
|
1787
|
+
)
|
|
1788
|
+
)
|
|
1789
|
+
else:
|
|
1790
|
+
console.print(f"[red]Resume failed: {result.get('error', 'Unknown')}[/red]")
|
|
1791
|
+
raise typer.Exit(1)
|
|
1792
|
+
|
|
1793
|
+
except KeyboardInterrupt:
|
|
1794
|
+
console.print("\n[yellow]Interrupted by user[/yellow]")
|
|
1795
|
+
raise typer.Exit(130) from None
|
|
1796
|
+
except Exception as e:
|
|
1797
|
+
console.print(f"\n[red]Error: {e}[/red]")
|
|
1798
|
+
raise typer.Exit(1) from e
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def _prompt_install_target() -> str:
|
|
1802
|
+
"""Prompt the user to choose where to install Claude configs."""
|
|
1803
|
+
choices = {
|
|
1804
|
+
"1": ("project", ".claude/ in the project directory (project-specific)"),
|
|
1805
|
+
"2": ("user", "~/.claude/ in your home directory (shared across projects)"),
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
console.print("\n[bold]Where should steerdev agent configs be installed?[/bold]\n")
|
|
1809
|
+
for key, (_, description) in choices.items():
|
|
1810
|
+
console.print(f" [cyan]{key}[/cyan]) {description}")
|
|
1811
|
+
console.print()
|
|
1812
|
+
|
|
1813
|
+
while True:
|
|
1814
|
+
choice = typer.prompt("Select install target", default="1")
|
|
1815
|
+
if choice in choices:
|
|
1816
|
+
selected = choices[choice][0]
|
|
1817
|
+
console.print(f"\n[dim]Selected: {selected}[/dim]\n")
|
|
1818
|
+
return selected
|
|
1819
|
+
console.print(f"[red]Invalid choice '{choice}'. Enter 1 or 2.[/red]")
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
@app.command()
|
|
1823
|
+
def setup(
|
|
1824
|
+
project_dir: Annotated[
|
|
1825
|
+
Path | None,
|
|
1826
|
+
typer.Option(
|
|
1827
|
+
"--dir",
|
|
1828
|
+
"-d",
|
|
1829
|
+
help="Target project directory (defaults to current directory)",
|
|
1830
|
+
exists=True,
|
|
1831
|
+
file_okay=False,
|
|
1832
|
+
dir_okay=True,
|
|
1833
|
+
),
|
|
1834
|
+
] = None,
|
|
1835
|
+
project_id: Annotated[
|
|
1836
|
+
str | None,
|
|
1837
|
+
typer.Option(
|
|
1838
|
+
"--project-id",
|
|
1839
|
+
"-p",
|
|
1840
|
+
help="SteerDev project ID to configure",
|
|
1841
|
+
),
|
|
1842
|
+
] = None,
|
|
1843
|
+
api_key: Annotated[
|
|
1844
|
+
str | None,
|
|
1845
|
+
typer.Option(
|
|
1846
|
+
"--api-key",
|
|
1847
|
+
"-k",
|
|
1848
|
+
help="SteerDev API key to configure",
|
|
1849
|
+
),
|
|
1850
|
+
] = None,
|
|
1851
|
+
agent_name: Annotated[
|
|
1852
|
+
str | None,
|
|
1853
|
+
typer.Option(
|
|
1854
|
+
"--agent-name",
|
|
1855
|
+
"-n",
|
|
1856
|
+
help="Agent name for session tracking (generates a random name if not provided)",
|
|
1857
|
+
),
|
|
1858
|
+
] = None,
|
|
1859
|
+
install_target: Annotated[
|
|
1860
|
+
str | None,
|
|
1861
|
+
typer.Option(
|
|
1862
|
+
"--install-target",
|
|
1863
|
+
"-i",
|
|
1864
|
+
help="Where to install configs: 'project' (.claude/) or 'user' (~/.claude/)",
|
|
1865
|
+
),
|
|
1866
|
+
] = None,
|
|
1867
|
+
force: Annotated[
|
|
1868
|
+
bool,
|
|
1869
|
+
typer.Option(
|
|
1870
|
+
"--force",
|
|
1871
|
+
"-f",
|
|
1872
|
+
help="Overwrite existing files",
|
|
1873
|
+
),
|
|
1874
|
+
] = False,
|
|
1875
|
+
skip_claude_md: Annotated[
|
|
1876
|
+
bool,
|
|
1877
|
+
typer.Option(
|
|
1878
|
+
"--skip-claude-md",
|
|
1879
|
+
help="Skip updating CLAUDE.md",
|
|
1880
|
+
),
|
|
1881
|
+
] = False,
|
|
1882
|
+
) -> None:
|
|
1883
|
+
"""Set up Claude Code integration for steerdev.com task management.
|
|
1884
|
+
|
|
1885
|
+
This command configures a project with:
|
|
1886
|
+
- .claude/settings.json with permissions for CLI commands and hooks
|
|
1887
|
+
- .claude/skills/task-management/ skill for autonomous task management
|
|
1888
|
+
- .env file with steerdev configuration variables (including agent name)
|
|
1889
|
+
- CLAUDE.md section with task management instructions
|
|
1890
|
+
|
|
1891
|
+
Use --install-target to choose where configs are installed:
|
|
1892
|
+
- project: .claude/ in the project directory (default for team/project-specific configs)
|
|
1893
|
+
- user: ~/.claude/ in your home directory (shared across all projects)
|
|
1894
|
+
"""
|
|
1895
|
+
from steerdev_agent.setup import ClaudeSetup
|
|
1896
|
+
|
|
1897
|
+
target_dir = project_dir or Path.cwd()
|
|
1898
|
+
|
|
1899
|
+
# Prompt for install target if not provided
|
|
1900
|
+
if install_target is None:
|
|
1901
|
+
install_target = _prompt_install_target()
|
|
1902
|
+
|
|
1903
|
+
# Validate install target
|
|
1904
|
+
if install_target not in ("project", "user"):
|
|
1905
|
+
console.print(
|
|
1906
|
+
f"[red]Error: Invalid install target '{install_target}'. "
|
|
1907
|
+
f"Must be 'project' or 'user'.[/red]"
|
|
1908
|
+
)
|
|
1909
|
+
raise typer.Exit(1)
|
|
1910
|
+
|
|
1911
|
+
setup_instance = ClaudeSetup(target_dir, install_target=install_target)
|
|
1912
|
+
|
|
1913
|
+
claude_dir_display = "~/.claude/" if install_target == "user" else f"{target_dir}/.claude/"
|
|
1914
|
+
|
|
1915
|
+
console.print(
|
|
1916
|
+
Panel(
|
|
1917
|
+
f"[bold blue]Setting up Claude Code integration[/bold blue]\n"
|
|
1918
|
+
f"Target directory: {target_dir}\n"
|
|
1919
|
+
f"Install target: {install_target} ({claude_dir_display})",
|
|
1920
|
+
title="SteerDev Setup",
|
|
1921
|
+
)
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1924
|
+
def _display_path(p: Path) -> str:
|
|
1925
|
+
"""Display a path relative to project dir, or using ~ for home."""
|
|
1926
|
+
try:
|
|
1927
|
+
return str(p.relative_to(target_dir))
|
|
1928
|
+
except ValueError:
|
|
1929
|
+
# Path is outside project dir (e.g., ~/.claude/)
|
|
1930
|
+
home = Path.home()
|
|
1931
|
+
try:
|
|
1932
|
+
return "~/" + str(p.relative_to(home))
|
|
1933
|
+
except ValueError:
|
|
1934
|
+
return str(p)
|
|
1935
|
+
|
|
1936
|
+
try:
|
|
1937
|
+
# Setup settings (with hooks config)
|
|
1938
|
+
settings_path = setup_instance.setup_settings(force=force, include_hooks=True)
|
|
1939
|
+
console.print(f"[green]✓[/green] Settings: {_display_path(settings_path)}")
|
|
1940
|
+
|
|
1941
|
+
# Setup skills
|
|
1942
|
+
skills_path = setup_instance.setup_skills(force=force)
|
|
1943
|
+
console.print(f"[green]✓[/green] Skills: {_display_path(skills_path)}")
|
|
1944
|
+
|
|
1945
|
+
# Setup .env file (generates agent name if not provided)
|
|
1946
|
+
env_path, env_updated = setup_instance.setup_env(
|
|
1947
|
+
project_id=project_id,
|
|
1948
|
+
api_key=api_key,
|
|
1949
|
+
agent_name=agent_name,
|
|
1950
|
+
)
|
|
1951
|
+
if env_updated:
|
|
1952
|
+
console.print(f"[green]✓[/green] .env: {_display_path(env_path)}")
|
|
1953
|
+
else:
|
|
1954
|
+
console.print(f"[dim]○[/dim] .env already configured: {_display_path(env_path)}")
|
|
1955
|
+
|
|
1956
|
+
# Setup CLAUDE.md
|
|
1957
|
+
if not skip_claude_md:
|
|
1958
|
+
updated = setup_instance.update_claude_md(force=force)
|
|
1959
|
+
if updated:
|
|
1960
|
+
console.print("[green]✓[/green] CLAUDE.md updated with task management section")
|
|
1961
|
+
else:
|
|
1962
|
+
console.print("[dim]○[/dim] CLAUDE.md already has task management section")
|
|
1963
|
+
else:
|
|
1964
|
+
console.print("[dim]○[/dim] Skipped CLAUDE.md update")
|
|
1965
|
+
|
|
1966
|
+
# Add steerdev.yaml config
|
|
1967
|
+
config_path, config_created = setup_instance.setup_steerdev_config(force=force)
|
|
1968
|
+
if config_created:
|
|
1969
|
+
console.print(f"[green]✓[/green] Config: {_display_path(config_path)}")
|
|
1970
|
+
else:
|
|
1971
|
+
console.print(f"[dim]○[/dim] Config already exists: {_display_path(config_path)}")
|
|
1972
|
+
|
|
1973
|
+
# Sync shared agent settings if credentials are available
|
|
1974
|
+
if project_id and api_key:
|
|
1975
|
+
try:
|
|
1976
|
+
console.print("\n[blue]Syncing shared agent settings...[/blue]")
|
|
1977
|
+
platform_config = asyncio.run(_sync_configs(project_id))
|
|
1978
|
+
if platform_config is None:
|
|
1979
|
+
raise RuntimeError("Failed to fetch platform configs")
|
|
1980
|
+
counts = setup_instance.apply_platform_configs(platform_config)
|
|
1981
|
+
_save_synced_at(target_dir, platform_config.synced_at)
|
|
1982
|
+
total = sum(counts.values())
|
|
1983
|
+
if total > 0:
|
|
1984
|
+
console.print(f"[green]✓[/green] Synced {total} shared setting(s)")
|
|
1985
|
+
else:
|
|
1986
|
+
console.print("[dim]○[/dim] No shared settings configured")
|
|
1987
|
+
except Exception as e:
|
|
1988
|
+
console.print(f"[yellow]⚠[/yellow] Could not sync settings: {e}")
|
|
1989
|
+
|
|
1990
|
+
# Show different messages based on whether credentials were provided
|
|
1991
|
+
if project_id and api_key:
|
|
1992
|
+
next_steps = (
|
|
1993
|
+
"[bold green]Setup complete![/bold green]\n\n"
|
|
1994
|
+
"Your project is configured and ready to use.\n\n"
|
|
1995
|
+
"Run the agent:\n"
|
|
1996
|
+
" steerdev run\n\n"
|
|
1997
|
+
"Commands available:\n"
|
|
1998
|
+
" steerdev run --help\n"
|
|
1999
|
+
" steerdev resume --help\n"
|
|
2000
|
+
" steerdev tasks --help\n"
|
|
2001
|
+
" steerdev sessions --help"
|
|
2002
|
+
)
|
|
2003
|
+
else:
|
|
2004
|
+
next_steps = (
|
|
2005
|
+
"[bold green]Setup complete![/bold green]\n\n"
|
|
2006
|
+
"Next steps:\n"
|
|
2007
|
+
"1. Edit .env and set your STEERDEV_API_KEY and STEERDEV_PROJECT_ID\n"
|
|
2008
|
+
"2. Run: steerdev run\n\n"
|
|
2009
|
+
"Commands available:\n"
|
|
2010
|
+
" steerdev run --help\n"
|
|
2011
|
+
" steerdev resume --help\n"
|
|
2012
|
+
" steerdev tasks --help\n"
|
|
2013
|
+
" steerdev sessions --help"
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
console.print(
|
|
2017
|
+
Panel(
|
|
2018
|
+
next_steps,
|
|
2019
|
+
title="Done",
|
|
2020
|
+
border_style="green",
|
|
2021
|
+
)
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
except Exception as e:
|
|
2025
|
+
console.print(f"[red]Error during setup: {e}[/red]")
|
|
2026
|
+
raise typer.Exit(1) from e
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
@app.command()
|
|
2030
|
+
def sync(
|
|
2031
|
+
project_dir: Annotated[
|
|
2032
|
+
Path | None,
|
|
2033
|
+
typer.Option(
|
|
2034
|
+
"--dir",
|
|
2035
|
+
"-d",
|
|
2036
|
+
help="Target project directory (defaults to current directory)",
|
|
2037
|
+
exists=True,
|
|
2038
|
+
file_okay=False,
|
|
2039
|
+
dir_okay=True,
|
|
2040
|
+
),
|
|
2041
|
+
] = None,
|
|
2042
|
+
project_id: Annotated[
|
|
2043
|
+
str | None,
|
|
2044
|
+
typer.Option(
|
|
2045
|
+
"--project-id",
|
|
2046
|
+
"-p",
|
|
2047
|
+
help="SteerDev project ID",
|
|
2048
|
+
envvar="STEERDEV_PROJECT_ID",
|
|
2049
|
+
),
|
|
2050
|
+
] = None,
|
|
2051
|
+
install_target: Annotated[
|
|
2052
|
+
str,
|
|
2053
|
+
typer.Option(
|
|
2054
|
+
"--install-target",
|
|
2055
|
+
"-i",
|
|
2056
|
+
help="Where configs are installed: 'project' (.claude/) or 'user' (~/.claude/)",
|
|
2057
|
+
),
|
|
2058
|
+
] = "project",
|
|
2059
|
+
) -> None:
|
|
2060
|
+
"""Sync shared agent settings from steerdev.com.
|
|
2061
|
+
|
|
2062
|
+
Pulls the latest shared configurations (system prompts, skills, MCP servers,
|
|
2063
|
+
env variables) from the platform and applies them to the local project.
|
|
2064
|
+
|
|
2065
|
+
This updates:
|
|
2066
|
+
- CLAUDE.md with shared system prompts
|
|
2067
|
+
- .claude/skills/ with synced skills
|
|
2068
|
+
- .claude/settings.json with MCP server configurations
|
|
2069
|
+
- .env with shared environment variables
|
|
2070
|
+
"""
|
|
2071
|
+
import yaml
|
|
2072
|
+
|
|
2073
|
+
target_dir = project_dir or Path.cwd()
|
|
2074
|
+
|
|
2075
|
+
# Resolve project_id from env/config if not provided
|
|
2076
|
+
resolved_project_id = project_id
|
|
2077
|
+
if not resolved_project_id:
|
|
2078
|
+
resolved_project_id = os.environ.get("STEERDEV_PROJECT_ID")
|
|
2079
|
+
if not resolved_project_id:
|
|
2080
|
+
# Try from steerdev.yaml
|
|
2081
|
+
config_path = target_dir / "steerdev.yaml"
|
|
2082
|
+
if config_path.exists():
|
|
2083
|
+
config_data = yaml.safe_load(config_path.read_text())
|
|
2084
|
+
env_name = config_data.get("api", {}).get("project_id_env", "STEERDEV_PROJECT_ID")
|
|
2085
|
+
resolved_project_id = os.environ.get(env_name)
|
|
2086
|
+
|
|
2087
|
+
if not resolved_project_id:
|
|
2088
|
+
console.print("[red]Error: --project-id required (or set STEERDEV_PROJECT_ID)[/red]")
|
|
2089
|
+
raise typer.Exit(1)
|
|
2090
|
+
|
|
2091
|
+
console.print(
|
|
2092
|
+
Panel(
|
|
2093
|
+
f"[bold blue]Syncing shared agent settings[/bold blue]\n"
|
|
2094
|
+
f"Project: {resolved_project_id}\n"
|
|
2095
|
+
f"Directory: {target_dir}",
|
|
2096
|
+
title="SteerDev Sync",
|
|
2097
|
+
)
|
|
2098
|
+
)
|
|
2099
|
+
|
|
2100
|
+
try:
|
|
2101
|
+
from steerdev_agent.setup import ClaudeSetup
|
|
2102
|
+
|
|
2103
|
+
# Fetch configs from API
|
|
2104
|
+
platform_config = asyncio.run(_sync_configs(resolved_project_id))
|
|
2105
|
+
if platform_config is None:
|
|
2106
|
+
console.print("[yellow]⚠[/yellow] No configs returned from API")
|
|
2107
|
+
raise typer.Exit(1)
|
|
2108
|
+
|
|
2109
|
+
# Apply to local project
|
|
2110
|
+
setup_instance = ClaudeSetup(target_dir, install_target=install_target)
|
|
2111
|
+
counts = setup_instance.apply_platform_configs(platform_config)
|
|
2112
|
+
|
|
2113
|
+
# Save synced_at timestamp to steerdev.yaml
|
|
2114
|
+
_save_synced_at(target_dir, platform_config.synced_at)
|
|
2115
|
+
|
|
2116
|
+
# Display results
|
|
2117
|
+
total = sum(counts.values())
|
|
2118
|
+
if total > 0:
|
|
2119
|
+
details = []
|
|
2120
|
+
if counts["system_prompts"]:
|
|
2121
|
+
details.append(f"System prompts: {counts['system_prompts']}")
|
|
2122
|
+
if counts["skills"]:
|
|
2123
|
+
details.append(f"Skills: {counts['skills']}")
|
|
2124
|
+
if counts["mcps"]:
|
|
2125
|
+
details.append(f"MCP servers: {counts['mcps']}")
|
|
2126
|
+
if counts["env_vars"]:
|
|
2127
|
+
details.append(f"Env variables: {counts['env_vars']}")
|
|
2128
|
+
console.print(
|
|
2129
|
+
Panel(
|
|
2130
|
+
"[bold green]Sync complete[/bold green]\n" + "\n".join(details),
|
|
2131
|
+
title="Done",
|
|
2132
|
+
border_style="green",
|
|
2133
|
+
)
|
|
2134
|
+
)
|
|
2135
|
+
else:
|
|
2136
|
+
console.print("[yellow]No shared settings configured for this project.[/yellow]")
|
|
2137
|
+
|
|
2138
|
+
except Exception as e:
|
|
2139
|
+
console.print(f"[red]Error during sync: {e}[/red]")
|
|
2140
|
+
raise typer.Exit(1) from e
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
async def _sync_configs(project_id: str):
|
|
2144
|
+
"""Fetch platform configs via async API client."""
|
|
2145
|
+
from steerdev_agent.api.configs import ConfigsClient
|
|
2146
|
+
|
|
2147
|
+
async with ConfigsClient() as client:
|
|
2148
|
+
return await client.sync_configs(project_id)
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
def _save_synced_at(target_dir: Path, synced_at: str) -> None:
|
|
2152
|
+
"""Save the synced_at timestamp to steerdev.yaml."""
|
|
2153
|
+
import yaml
|
|
2154
|
+
|
|
2155
|
+
config_path = target_dir / "steerdev.yaml"
|
|
2156
|
+
config_data = yaml.safe_load(config_path.read_text()) or {} if config_path.exists() else {}
|
|
2157
|
+
config_data["synced_at"] = synced_at
|
|
2158
|
+
config_path.write_text(yaml.dump(config_data, default_flow_style=False))
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
def _check_sync_staleness(target_dir: Path) -> None:
|
|
2162
|
+
"""Check if synced configs are stale and print a warning."""
|
|
2163
|
+
from datetime import UTC, datetime, timedelta
|
|
2164
|
+
|
|
2165
|
+
import yaml
|
|
2166
|
+
|
|
2167
|
+
config_path = target_dir / "steerdev.yaml"
|
|
2168
|
+
if not config_path.exists():
|
|
2169
|
+
return
|
|
2170
|
+
|
|
2171
|
+
try:
|
|
2172
|
+
config_data = yaml.safe_load(config_path.read_text()) or {}
|
|
2173
|
+
synced_at_str = config_data.get("synced_at")
|
|
2174
|
+
if not synced_at_str:
|
|
2175
|
+
return
|
|
2176
|
+
|
|
2177
|
+
synced_at = datetime.fromisoformat(synced_at_str.replace("Z", "+00:00"))
|
|
2178
|
+
now = datetime.now(UTC)
|
|
2179
|
+
if now - synced_at > timedelta(hours=1):
|
|
2180
|
+
age_hours = (now - synced_at).total_seconds() / 3600
|
|
2181
|
+
console.print(
|
|
2182
|
+
f"[yellow]Warning: Shared agent settings may be out of date "
|
|
2183
|
+
f"(last synced {age_hours:.0f}h ago). "
|
|
2184
|
+
f"Run `steerdev sync` to update.[/yellow]"
|
|
2185
|
+
)
|
|
2186
|
+
except Exception:
|
|
2187
|
+
pass # Don't block execution on staleness check errors
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
@app.command("test")
|
|
2191
|
+
def integration_test(
|
|
2192
|
+
project_id: Annotated[
|
|
2193
|
+
str,
|
|
2194
|
+
typer.Option(
|
|
2195
|
+
"--project-id",
|
|
2196
|
+
"-p",
|
|
2197
|
+
help="SteerDev project ID to add tasks to",
|
|
2198
|
+
),
|
|
2199
|
+
],
|
|
2200
|
+
project_dir: Annotated[
|
|
2201
|
+
Path,
|
|
2202
|
+
typer.Option(
|
|
2203
|
+
"--dir",
|
|
2204
|
+
"-d",
|
|
2205
|
+
help="Directory to create the test project in",
|
|
2206
|
+
),
|
|
2207
|
+
],
|
|
2208
|
+
tasks_file: Annotated[
|
|
2209
|
+
Path | None,
|
|
2210
|
+
typer.Option(
|
|
2211
|
+
"--tasks-file",
|
|
2212
|
+
"-t",
|
|
2213
|
+
help="JSON file with custom tasks (uses default todo-api tasks if not provided)",
|
|
2214
|
+
),
|
|
2215
|
+
] = None,
|
|
2216
|
+
skip_tasks: Annotated[
|
|
2217
|
+
bool,
|
|
2218
|
+
typer.Option(
|
|
2219
|
+
"--skip-tasks",
|
|
2220
|
+
help="Skip adding tasks to the project",
|
|
2221
|
+
),
|
|
2222
|
+
] = False,
|
|
2223
|
+
auto_start: Annotated[
|
|
2224
|
+
bool,
|
|
2225
|
+
typer.Option(
|
|
2226
|
+
"--auto-start",
|
|
2227
|
+
"-y",
|
|
2228
|
+
help="Start the agent without confirmation",
|
|
2229
|
+
),
|
|
2230
|
+
] = False,
|
|
2231
|
+
) -> None:
|
|
2232
|
+
"""Run an integration test with a fresh todo-list API project.
|
|
2233
|
+
|
|
2234
|
+
Creates a new Python project using `uv init` and adds tasks for building
|
|
2235
|
+
a todo-list API with FastAPI and SQLite. Useful for testing the agent
|
|
2236
|
+
end-to-end in a controlled environment.
|
|
2237
|
+
|
|
2238
|
+
Example:
|
|
2239
|
+
steerdev integration-test -p YOUR_PROJECT_UUID
|
|
2240
|
+
steerdev integration-test -p YOUR_PROJECT_UUID -y # auto-start
|
|
2241
|
+
"""
|
|
2242
|
+
from steerdev_agent.integration import run_integration_test
|
|
2243
|
+
|
|
2244
|
+
run_integration_test(
|
|
2245
|
+
project_id=project_id,
|
|
2246
|
+
project_dir=project_dir,
|
|
2247
|
+
tasks_file=tasks_file,
|
|
2248
|
+
skip_tasks=skip_tasks,
|
|
2249
|
+
auto_start=auto_start,
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
if __name__ == "__main__":
|
|
2254
|
+
app()
|