devin-cli 1.1.7__tar.gz → 1.1.9__tar.gz
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.
- {devin_cli-1.1.7 → devin_cli-1.1.9}/CHANGELOG.md +14 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/PKG-INFO +1 -1
- {devin_cli-1.1.7 → devin_cli-1.1.9}/pyproject.toml +1 -1
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/cli.py +418 -2
- {devin_cli-1.1.7 → devin_cli-1.1.9}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/.gitignore +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/LICENSE +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/README.md +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/assets/logo.png +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/generate_facades.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/setup.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/client.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/consumption.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/members.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/organizations.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v1/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/consumption.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/members.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/organizations.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/api/v3/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/src/devin_cli/config.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/conftest.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_cli.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.9}/tests/test_config.py +0 -0
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.9] - 2026-03-11
|
|
9
|
+
### Fixed
|
|
10
|
+
- `devin watch` terminal status message was printed inside a `Live` context, causing output corruption. Now printed after the `Live` block exits.
|
|
11
|
+
- `update-playbook` was sending an empty string title when `--title` was not passed, overwriting the existing playbook title. Now only sends fields that are explicitly provided.
|
|
12
|
+
- `chain` used bare `resp["session_id"]` key access which would raise `KeyError` on unexpected API responses. Now uses `.get()` with an explicit error guard.
|
|
13
|
+
|
|
14
|
+
## [1.1.8] - 2026-03-11
|
|
15
|
+
### Added
|
|
16
|
+
- Restored all 0.1.x flat commands: `watch`, `status`, `open`, `message`, `terminate`, `list-sessions`, `create-session`, `upload`, `list-knowledge`, `attach`, `update-tags`, `history`, `messages`, `get-session`, `update-knowledge`, `update-playbook`, `delete-playbook`, `list-secrets`, `delete-secret`, `chain`.
|
|
17
|
+
- `--wait` / `-w` flag on `create-session` and `sessions create` — blocks until the session reaches a terminal status.
|
|
18
|
+
- `sessions watch` now displays `structured_output` inline and uses exponential backoff (caps at 30s).
|
|
19
|
+
- `chain` command supports both inline `--playbooks` chaining and YAML workflow files.
|
|
20
|
+
- `attach` command correctly embeds the uploaded file URL in the session prompt.
|
|
21
|
+
|
|
8
22
|
## [1.1.7] - 2026-03-11
|
|
9
23
|
### Fixed
|
|
10
24
|
- `knowledge list` now correctly reads `notes` key from the v3 `knowledge/notes` endpoint, with `knowledge` as fallback. Also reads `title` in addition to `name` for display.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devin-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
4
4
|
Summary: Unofficial CLI for Devin AI - The first AI Software Engineer
|
|
5
5
|
Project-URL: Homepage, https://github.com/revanthpobala/devin-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/revanthpobala/devin-cli.git
|
|
@@ -157,6 +157,8 @@ def create_session_cmd(
|
|
|
157
157
|
max_acu: Optional[int] = typer.Option(None, "--max-acu", help="Maximum ACU limit"),
|
|
158
158
|
advanced_mode: Optional[str] = typer.Option(None, "--advanced-mode", help="Enable advanced mode (e.g. CLI, browser) for the session"),
|
|
159
159
|
force: bool = typer.Option(False, "--force", help="Force creation even if duplicate prompt is detected"),
|
|
160
|
+
wait: bool = typer.Option(False, "--wait", "-w", help="Block until the session reaches a terminal status"),
|
|
161
|
+
interval: int = typer.Option(5, "--interval", help="Polling interval in seconds when --wait is used"),
|
|
160
162
|
):
|
|
161
163
|
"""Create a new Devin session."""
|
|
162
164
|
if org: config.temporary_org_id = org
|
|
@@ -201,6 +203,19 @@ def create_session_cmd(
|
|
|
201
203
|
config.save_prompt_hash(prompt_hash, sid)
|
|
202
204
|
console.print(f"[green]Session created:[/green] {sid}")
|
|
203
205
|
console.print(f"[bold cyan]URL:[/bold cyan] {resp.get('url')}")
|
|
206
|
+
|
|
207
|
+
if wait:
|
|
208
|
+
terminal_statuses = {"stopped", "finished", "error", "cancelled", "failed"}
|
|
209
|
+
console.print(f"[dim]Waiting for session to complete (polling every {interval}s)...[/dim]")
|
|
210
|
+
with console.status("[bold green]Running...[/bold green]") as s:
|
|
211
|
+
while True:
|
|
212
|
+
time.sleep(interval)
|
|
213
|
+
poll = sessions.get_session(sid)
|
|
214
|
+
current_status = poll.get("status_enum", "").lower()
|
|
215
|
+
s.update(f"[bold green]Status: {current_status}[/bold green]")
|
|
216
|
+
if current_status in terminal_statuses:
|
|
217
|
+
console.print(f"[bold green]Session finished:[/bold green] {current_status}")
|
|
218
|
+
break
|
|
204
219
|
else:
|
|
205
220
|
console.print("[yellow]Session created, but no ID returned immediately (awaiting advanced mode setup).[/yellow]")
|
|
206
221
|
|
|
@@ -284,14 +299,25 @@ def session_cost_cmd(
|
|
|
284
299
|
@session_app.command("message")
|
|
285
300
|
@handle_api_error
|
|
286
301
|
def send_message_cmd(
|
|
287
|
-
text: str = typer.Argument(
|
|
302
|
+
text: Optional[str] = typer.Argument(None, help="Message text"),
|
|
303
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Read message from file"),
|
|
288
304
|
session_id: Optional[str] = typer.Option(None, "--id"),
|
|
289
305
|
org: Optional[str] = typer.Option(None, "--org"),
|
|
290
306
|
):
|
|
291
307
|
"""Send a message to a session."""
|
|
292
308
|
if org: config.temporary_org_id = org
|
|
293
309
|
sid = session_id or get_current_session_id()
|
|
294
|
-
|
|
310
|
+
if file:
|
|
311
|
+
if not file.exists():
|
|
312
|
+
console.print(f"[bold red]Error:[/bold red] File not found: {file}")
|
|
313
|
+
raise typer.Exit(1)
|
|
314
|
+
msg_text = file.read_text()
|
|
315
|
+
elif text:
|
|
316
|
+
msg_text = text
|
|
317
|
+
else:
|
|
318
|
+
console.print("[bold red]Error:[/bold red] Provide message text or --file")
|
|
319
|
+
raise typer.Exit(1)
|
|
320
|
+
sessions.send_message(sid, msg_text)
|
|
295
321
|
console.print(f"[green]Message sent to {sid}[/green]")
|
|
296
322
|
|
|
297
323
|
@session_app.command("messages")
|
|
@@ -612,5 +638,395 @@ def use_session_cmd(session_id: str):
|
|
|
612
638
|
config.current_session_id = session_id
|
|
613
639
|
console.print(f"[green]Switched to session {session_id}[/green]")
|
|
614
640
|
|
|
641
|
+
@app.command("status")
|
|
642
|
+
@handle_api_error
|
|
643
|
+
def status_cmd():
|
|
644
|
+
"""Show the status of the current active session."""
|
|
645
|
+
sid = get_current_session_id()
|
|
646
|
+
resp = sessions.get_session(sid)
|
|
647
|
+
status = resp.get("status_enum", "unknown")
|
|
648
|
+
title = resp.get("title") or resp.get("prompt", "")[:60]
|
|
649
|
+
url = resp.get("url", "")
|
|
650
|
+
acus = resp.get("acus_consumed") or resp.get("acu_used")
|
|
651
|
+
|
|
652
|
+
color = {"running": "green", "stopped": "red", "paused": "yellow"}.get(status, "cyan")
|
|
653
|
+
console.print(f"[bold]Session:[/bold] [cyan]{sid}[/cyan]")
|
|
654
|
+
console.print(f"[bold]Status:[/bold] [{color}]{status}[/{color}]")
|
|
655
|
+
if title:
|
|
656
|
+
console.print(f"[bold]Title:[/bold] {title}")
|
|
657
|
+
if acus is not None:
|
|
658
|
+
console.print(f"[bold]ACUs:[/bold] {acus}")
|
|
659
|
+
if url:
|
|
660
|
+
console.print(f"[bold]URL:[/bold] [underline]{url}[/underline]")
|
|
661
|
+
|
|
662
|
+
@app.command("open")
|
|
663
|
+
@handle_api_error
|
|
664
|
+
def open_cmd(
|
|
665
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to open (defaults to active session)"),
|
|
666
|
+
):
|
|
667
|
+
"""Open the active session in your browser."""
|
|
668
|
+
sid = session_id or get_current_session_id()
|
|
669
|
+
resp = sessions.get_session(sid)
|
|
670
|
+
url = resp.get("url")
|
|
671
|
+
if not url:
|
|
672
|
+
console.print("[bold red]Error:[/bold red] No URL available for this session.")
|
|
673
|
+
raise typer.Exit(1)
|
|
674
|
+
webbrowser.open(url)
|
|
675
|
+
console.print(f"[green]Opened:[/green] {url}")
|
|
676
|
+
|
|
677
|
+
@session_app.command("watch")
|
|
678
|
+
@handle_api_error
|
|
679
|
+
def watch_session_cmd(
|
|
680
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to watch (defaults to active session)"),
|
|
681
|
+
interval: int = typer.Option(3, "--interval", "-i", help="Polling interval in seconds"),
|
|
682
|
+
org: Optional[str] = typer.Option(None, "--org"),
|
|
683
|
+
):
|
|
684
|
+
"""Live-watch a session — polls status and streams new messages."""
|
|
685
|
+
if org: config.temporary_org_id = org
|
|
686
|
+
sid = session_id or get_current_session_id()
|
|
687
|
+
|
|
688
|
+
terminal_statuses = {"stopped", "finished", "error", "cancelled", "failed", "blocked"}
|
|
689
|
+
backoff = interval
|
|
690
|
+
|
|
691
|
+
def build_status_panel(resp: dict) -> Panel:
|
|
692
|
+
status_val = resp.get("status_enum", "unknown")
|
|
693
|
+
title_val = resp.get("title") or resp.get("prompt", "")[:60]
|
|
694
|
+
acus = resp.get("acus_consumed") or resp.get("acu_used", "n/a")
|
|
695
|
+
color = {"running": "green", "stopped": "red", "paused": "yellow",
|
|
696
|
+
"finished": "blue", "blocked": "red"}.get(status_val, "cyan")
|
|
697
|
+
text = Text()
|
|
698
|
+
text.append("Session: ", style="bold")
|
|
699
|
+
text.append(f"{sid}\n", style="cyan")
|
|
700
|
+
text.append("Status: ", style="bold")
|
|
701
|
+
text.append(f"{status_val}\n", style=color)
|
|
702
|
+
if title_val:
|
|
703
|
+
text.append(f"Title: {title_val}\n")
|
|
704
|
+
text.append(f"ACUs: {acus}\n")
|
|
705
|
+
so = resp.get("structured_output")
|
|
706
|
+
if so:
|
|
707
|
+
text.append("\nStructured Output:\n", style="bold")
|
|
708
|
+
text.append(json.dumps(so, indent=2))
|
|
709
|
+
text.append("\nPress Ctrl+C to stop watching.", style="dim")
|
|
710
|
+
return Panel(text, title="[bold cyan]Devin Watch[/bold cyan]", border_style="cyan")
|
|
711
|
+
|
|
712
|
+
console.print(f"[bold cyan]Watching session {sid}[/bold cyan] (Ctrl+C to stop)")
|
|
713
|
+
|
|
714
|
+
with Live(console=console, refresh_per_second=4) as live:
|
|
715
|
+
try:
|
|
716
|
+
while True:
|
|
717
|
+
resp = sessions.get_session(sid)
|
|
718
|
+
live.update(build_status_panel(resp))
|
|
719
|
+
|
|
720
|
+
if resp.get("status_enum", "").lower() in terminal_statuses:
|
|
721
|
+
live.update(build_status_panel(resp))
|
|
722
|
+
break
|
|
723
|
+
|
|
724
|
+
time.sleep(min(backoff, 30))
|
|
725
|
+
backoff = min(backoff * 1.5, 30)
|
|
726
|
+
except KeyboardInterrupt:
|
|
727
|
+
console.print("\n[dim]Watch stopped.[/dim]")
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
console.print(f"[bold green]Session {resp.get('status_enum')}![/bold green]")
|
|
731
|
+
|
|
732
|
+
# --- Flat top-level aliases (backward compat with 0.1.x command structure) ---
|
|
733
|
+
|
|
734
|
+
@app.command("watch", hidden=True)
|
|
735
|
+
@handle_api_error
|
|
736
|
+
def watch_cmd(
|
|
737
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
738
|
+
interval: int = typer.Option(3, "--interval", "-i"),
|
|
739
|
+
):
|
|
740
|
+
"""Alias for: devin sessions watch"""
|
|
741
|
+
watch_session_cmd(session_id=session_id, interval=interval, org=None)
|
|
742
|
+
|
|
743
|
+
@app.command("message")
|
|
744
|
+
@handle_api_error
|
|
745
|
+
def message_cmd(
|
|
746
|
+
text: Optional[str] = typer.Argument(None, help="Message to send"),
|
|
747
|
+
file: Optional[Path] = typer.Option(None, "--file", "-f", help="Read message from file"),
|
|
748
|
+
session_id: Optional[str] = typer.Option(None, "--id"),
|
|
749
|
+
):
|
|
750
|
+
"""Send a message to the active session."""
|
|
751
|
+
sid = session_id or get_current_session_id()
|
|
752
|
+
if file:
|
|
753
|
+
if not file.exists():
|
|
754
|
+
console.print(f"[bold red]Error:[/bold red] File not found: {file}")
|
|
755
|
+
raise typer.Exit(1)
|
|
756
|
+
msg_text = file.read_text()
|
|
757
|
+
elif text:
|
|
758
|
+
msg_text = text
|
|
759
|
+
else:
|
|
760
|
+
console.print("[bold red]Error:[/bold red] Provide message text or --file")
|
|
761
|
+
raise typer.Exit(1)
|
|
762
|
+
sessions.send_message(sid, msg_text)
|
|
763
|
+
console.print(f"[green]Message sent to {sid}[/green]")
|
|
764
|
+
|
|
765
|
+
@app.command("terminate")
|
|
766
|
+
@handle_api_error
|
|
767
|
+
def terminate_cmd(
|
|
768
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
769
|
+
):
|
|
770
|
+
"""Terminate the active session."""
|
|
771
|
+
sid = session_id or get_current_session_id()
|
|
772
|
+
if typer.confirm(f"Terminate session {sid}?"):
|
|
773
|
+
sessions.terminate_session(sid)
|
|
774
|
+
console.print(f"[green]Session {sid} terminated.[/green]")
|
|
775
|
+
|
|
776
|
+
@app.command("list-sessions")
|
|
777
|
+
@handle_api_error
|
|
778
|
+
def list_sessions_top_cmd(
|
|
779
|
+
limit: int = typer.Option(10, "--limit", "-n"),
|
|
780
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
781
|
+
):
|
|
782
|
+
"""List recent sessions (alias for: devin sessions list)."""
|
|
783
|
+
resp = sessions.list_sessions(limit=limit)
|
|
784
|
+
sess_list = resp.get("sessions", [])
|
|
785
|
+
if json_output:
|
|
786
|
+
console.print(json.dumps(sess_list, indent=2))
|
|
787
|
+
return
|
|
788
|
+
table = Table(title="Devin Sessions")
|
|
789
|
+
table.add_column("ID", style="cyan")
|
|
790
|
+
table.add_column("Status", style="magenta")
|
|
791
|
+
table.add_column("Title")
|
|
792
|
+
for s in sess_list:
|
|
793
|
+
table.add_row(s.get("session_id"), s.get("status_enum"), s.get("title") or s.get("prompt", "")[:50])
|
|
794
|
+
console.print(table)
|
|
795
|
+
|
|
796
|
+
@app.command("create-session")
|
|
797
|
+
@handle_api_error
|
|
798
|
+
def create_session_top_cmd(
|
|
799
|
+
prompt: str = typer.Argument(..., help="The prompt for the session"),
|
|
800
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
801
|
+
wait: bool = typer.Option(False, "--wait", "-w", help="Block until the session completes"),
|
|
802
|
+
interval: int = typer.Option(5, "--interval"),
|
|
803
|
+
force: bool = typer.Option(False, "--force"),
|
|
804
|
+
):
|
|
805
|
+
"""Create a new session (alias for: devin sessions create)."""
|
|
806
|
+
create_session_cmd(prompt=prompt, file=None, title=title, org=None, max_acu=None,
|
|
807
|
+
advanced_mode=None, force=force, wait=wait, interval=interval)
|
|
808
|
+
|
|
809
|
+
@app.command("upload")
|
|
810
|
+
@handle_api_error
|
|
811
|
+
def upload_cmd(path: Path = typer.Argument(..., help="File to upload")):
|
|
812
|
+
"""Upload a file to Devin."""
|
|
813
|
+
resp = attachments.upload_file(str(path))
|
|
814
|
+
console.print(f"[green]Uploaded:[/green] {resp}")
|
|
815
|
+
|
|
816
|
+
@app.command("list-knowledge")
|
|
817
|
+
@handle_api_error
|
|
818
|
+
def list_knowledge_top_cmd():
|
|
819
|
+
"""List knowledge notes (alias for: devin knowledge list)."""
|
|
820
|
+
resp = knowledge.list_knowledge()
|
|
821
|
+
items = resp.get("notes", resp.get("knowledge", []))
|
|
822
|
+
table = Table(title="Knowledge Base")
|
|
823
|
+
table.add_column("ID", style="cyan")
|
|
824
|
+
table.add_column("Title")
|
|
825
|
+
for item in items:
|
|
826
|
+
table.add_row(item.get("id"), item.get("title") or item.get("name"))
|
|
827
|
+
console.print(table)
|
|
828
|
+
|
|
829
|
+
@app.command("attach")
|
|
830
|
+
@handle_api_error
|
|
831
|
+
def attach_cmd(
|
|
832
|
+
file: Path = typer.Argument(..., help="Context file to upload and attach"),
|
|
833
|
+
prompt: str = typer.Argument(..., help="Task prompt to include with the attachment"),
|
|
834
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
835
|
+
wait: bool = typer.Option(False, "--wait", "-w"),
|
|
836
|
+
interval: int = typer.Option(5, "--interval"),
|
|
837
|
+
):
|
|
838
|
+
"""Upload a context file and start a session using it."""
|
|
839
|
+
with console.status("[bold green]Uploading file...[/bold green]"):
|
|
840
|
+
upload_resp = attachments.upload_file(str(file))
|
|
841
|
+
url = upload_resp.strip('"') if isinstance(upload_resp, str) else str(upload_resp)
|
|
842
|
+
console.print(f"[green]Uploaded:[/green] {url}")
|
|
843
|
+
full_prompt = f"{prompt}\n\nATTACHMENT: \"{url}\""
|
|
844
|
+
create_session_cmd(prompt=full_prompt, file=None, title=title, org=None, max_acu=None,
|
|
845
|
+
advanced_mode=None, force=False, wait=wait, interval=interval)
|
|
846
|
+
|
|
847
|
+
@app.command("update-tags")
|
|
848
|
+
@handle_api_error
|
|
849
|
+
def update_tags_cmd(
|
|
850
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
851
|
+
tags: List[str] = typer.Option(..., "--tag", "-t", help="Tags to set (can repeat)"),
|
|
852
|
+
):
|
|
853
|
+
"""Update tags for a session."""
|
|
854
|
+
sid = session_id or get_current_session_id()
|
|
855
|
+
sessions.update_session_tags(sid, tags)
|
|
856
|
+
console.print(f"[green]Tags updated for session {sid}.[/green]")
|
|
857
|
+
|
|
858
|
+
@app.command("history")
|
|
859
|
+
def history_cmd():
|
|
860
|
+
"""Show the locally cached current session ID."""
|
|
861
|
+
sid = config.current_session_id
|
|
862
|
+
if sid:
|
|
863
|
+
console.print(f"Current local session: [cyan]{sid}[/cyan]")
|
|
864
|
+
else:
|
|
865
|
+
console.print("No current local session.")
|
|
866
|
+
|
|
867
|
+
@app.command("messages")
|
|
868
|
+
@handle_api_error
|
|
869
|
+
def messages_cmd(
|
|
870
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
871
|
+
):
|
|
872
|
+
"""Show conversation history for a session."""
|
|
873
|
+
sid = session_id or get_current_session_id()
|
|
874
|
+
resp = sessions.get_session_messages(sid)
|
|
875
|
+
msgs = resp.get("messages", [])
|
|
876
|
+
console.print(f"[bold]Conversation for Session {sid}[/bold]")
|
|
877
|
+
console.print("─" * 40)
|
|
878
|
+
for m in msgs:
|
|
879
|
+
role = m.get("role", "unknown")
|
|
880
|
+
content = m.get("message", "") or m.get("content", "")
|
|
881
|
+
console.print(f"[bold cyan]{role}:[/bold cyan] {content}")
|
|
882
|
+
|
|
883
|
+
@app.command("get-session")
|
|
884
|
+
@handle_api_error
|
|
885
|
+
def get_session_top_cmd(
|
|
886
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
887
|
+
):
|
|
888
|
+
"""Get details for a session."""
|
|
889
|
+
sid = session_id or get_current_session_id()
|
|
890
|
+
resp = sessions.get_session(sid)
|
|
891
|
+
console.print(Panel(
|
|
892
|
+
f"[bold]Status:[/bold] {resp.get('status_enum')}\n"
|
|
893
|
+
f"[bold]URL:[/bold] {resp.get('url')}\n"
|
|
894
|
+
f"[bold]Created:[/bold] {resp.get('created_at')}",
|
|
895
|
+
title=f"Session {sid}"
|
|
896
|
+
))
|
|
897
|
+
if "structured_output" in resp:
|
|
898
|
+
console.print("[bold]Structured Output:[/bold]")
|
|
899
|
+
console.print(json.dumps(resp["structured_output"], indent=2))
|
|
900
|
+
|
|
901
|
+
@app.command("update-knowledge")
|
|
902
|
+
@handle_api_error
|
|
903
|
+
def update_knowledge_cmd(
|
|
904
|
+
knowledge_id: str = typer.Argument(...),
|
|
905
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
906
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
907
|
+
trigger: Optional[str] = typer.Option(None, "--trigger"),
|
|
908
|
+
):
|
|
909
|
+
"""Update an existing knowledge entry."""
|
|
910
|
+
knowledge.update_knowledge(knowledge_id, title=name, body=body)
|
|
911
|
+
console.print(f"[green]Knowledge {knowledge_id} updated.[/green]")
|
|
912
|
+
|
|
913
|
+
@app.command("update-playbook")
|
|
914
|
+
@handle_api_error
|
|
915
|
+
def update_playbook_top_cmd(
|
|
916
|
+
playbook_id: str = typer.Argument(...),
|
|
917
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
918
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
919
|
+
macro: Optional[str] = typer.Option(None, "--macro"),
|
|
920
|
+
):
|
|
921
|
+
"""Update an existing playbook."""
|
|
922
|
+
data: dict = {}
|
|
923
|
+
if title is not None:
|
|
924
|
+
data["title"] = title
|
|
925
|
+
if body is not None:
|
|
926
|
+
data["body"] = body
|
|
927
|
+
if macro is not None:
|
|
928
|
+
data["macro"] = macro
|
|
929
|
+
if not data:
|
|
930
|
+
console.print("[yellow]Nothing to update. Pass --title, --body, or --macro.[/yellow]")
|
|
931
|
+
return
|
|
932
|
+
playbooks.update_playbook(playbook_id, title=title or "", body=body, macro=macro)
|
|
933
|
+
console.print(f"[green]Playbook {playbook_id} updated.[/green]")
|
|
934
|
+
|
|
935
|
+
@app.command("delete-playbook")
|
|
936
|
+
@handle_api_error
|
|
937
|
+
def delete_playbook_top_cmd(playbook_id: str = typer.Argument(...)):
|
|
938
|
+
"""Delete a playbook."""
|
|
939
|
+
if typer.confirm(f"Delete playbook {playbook_id}?"):
|
|
940
|
+
playbooks.delete_playbook(playbook_id)
|
|
941
|
+
console.print(f"[green]Playbook {playbook_id} deleted.[/green]")
|
|
942
|
+
|
|
943
|
+
@app.command("list-secrets")
|
|
944
|
+
@handle_api_error
|
|
945
|
+
def list_secrets_top_cmd():
|
|
946
|
+
"""List all organization secrets."""
|
|
947
|
+
resp = secrets.list_secrets()
|
|
948
|
+
items = resp if isinstance(resp, list) else resp.get("secrets", resp.get("items", []))
|
|
949
|
+
table = Table(title="Organization Secrets")
|
|
950
|
+
table.add_column("ID", style="cyan")
|
|
951
|
+
table.add_column("Name")
|
|
952
|
+
for item in items:
|
|
953
|
+
if isinstance(item, dict):
|
|
954
|
+
table.add_row(item.get("id", ""), item.get("name", ""))
|
|
955
|
+
console.print(table)
|
|
956
|
+
|
|
957
|
+
@app.command("delete-secret")
|
|
958
|
+
@handle_api_error
|
|
959
|
+
def delete_secret_top_cmd(secret_id: str = typer.Argument(...)):
|
|
960
|
+
"""Delete an organization secret."""
|
|
961
|
+
if typer.confirm(f"Delete secret {secret_id}?"):
|
|
962
|
+
secrets.delete_secret(secret_id)
|
|
963
|
+
console.print(f"[green]Secret {secret_id} deleted.[/green]")
|
|
964
|
+
|
|
965
|
+
@app.command("chain")
|
|
966
|
+
@handle_api_error
|
|
967
|
+
def chain_cmd(
|
|
968
|
+
prompt: Optional[str] = typer.Argument(None, help="Initial prompt"),
|
|
969
|
+
playbooks_arg: Optional[str] = typer.Option(None, "--playbooks", help="Comma-separated playbook IDs"),
|
|
970
|
+
file: Optional[Path] = typer.Option(None, "--file", help="Workflow YAML file"),
|
|
971
|
+
):
|
|
972
|
+
"""(Beta) Orchestrate a sequential chain of playbooks.
|
|
973
|
+
|
|
974
|
+
Method 1 \u2014 Inline:
|
|
975
|
+
devin chain "Refactor utils.py" --playbooks "lint_check,unit_tests"
|
|
976
|
+
|
|
977
|
+
Method 2 \u2014 YAML workflow file:
|
|
978
|
+
devin chain --file workflow.yml
|
|
979
|
+
"""
|
|
980
|
+
steps = []
|
|
981
|
+
|
|
982
|
+
if file:
|
|
983
|
+
if not file.exists():
|
|
984
|
+
console.print(f"[bold red]Error:[/bold red] File not found: {file}")
|
|
985
|
+
raise typer.Exit(1)
|
|
986
|
+
try:
|
|
987
|
+
workflow = yaml.safe_load(file.read_text())
|
|
988
|
+
steps = workflow.get("steps", [])
|
|
989
|
+
except Exception as e:
|
|
990
|
+
console.print(f"[bold red]Error parsing YAML:[/bold red] {e}")
|
|
991
|
+
raise typer.Exit(1)
|
|
992
|
+
elif prompt and playbooks_arg:
|
|
993
|
+
pb_list = [p.strip() for p in playbooks_arg.split(",")]
|
|
994
|
+
for i, pb in enumerate(pb_list):
|
|
995
|
+
step_prompt = prompt if i == 0 else f"Execute playbook: {pb}"
|
|
996
|
+
steps.append({"prompt": step_prompt, "playbook": pb})
|
|
997
|
+
else:
|
|
998
|
+
console.print("[bold red]Error:[/bold red] Provide --file OR (prompt + --playbooks)")
|
|
999
|
+
raise typer.Exit(1)
|
|
1000
|
+
|
|
1001
|
+
current_sid = None
|
|
1002
|
+
for i, step in enumerate(steps):
|
|
1003
|
+
step_prompt = step.get("prompt", "")
|
|
1004
|
+
step_pb = step.get("playbook")
|
|
1005
|
+
console.print(f"[bold cyan]Step {i+1}/{len(steps)}:[/bold cyan] Playbook={step_pb}")
|
|
1006
|
+
|
|
1007
|
+
if i == 0:
|
|
1008
|
+
with console.status("Starting session..."):
|
|
1009
|
+
resp = sessions.create_session(prompt=step_prompt, playbook_id=step_pb)
|
|
1010
|
+
current_sid = resp.get("session_id")
|
|
1011
|
+
if not current_sid:
|
|
1012
|
+
console.print("[bold red]Error:[/bold red] No session ID returned from API.")
|
|
1013
|
+
raise typer.Exit(1)
|
|
1014
|
+
config.current_session_id = current_sid
|
|
1015
|
+
console.print(f"[green]Session started:[/green] {current_sid}")
|
|
1016
|
+
else:
|
|
1017
|
+
sessions.send_message(current_sid, f"{step_prompt} (Playbook: {step_pb})")
|
|
1018
|
+
|
|
1019
|
+
backoff = 2
|
|
1020
|
+
while True:
|
|
1021
|
+
resp = sessions.get_session(current_sid)
|
|
1022
|
+
status = resp.get("status_enum", "")
|
|
1023
|
+
if status in {"blocked", "finished", "stopped", "error"}:
|
|
1024
|
+
console.print(f"Step {i+1} done (status: {status}).")
|
|
1025
|
+
break
|
|
1026
|
+
time.sleep(min(backoff, 10))
|
|
1027
|
+
backoff = min(backoff * 1.5, 10)
|
|
1028
|
+
|
|
1029
|
+
console.print("[bold green]Chain completed![/bold green]")
|
|
1030
|
+
|
|
615
1031
|
if __name__ == "__main__":
|
|
616
1032
|
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|