devin-cli 1.1.7__tar.gz → 1.1.8__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.8}/CHANGELOG.md +8 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/PKG-INFO +1 -1
- {devin_cli-1.1.7 → devin_cli-1.1.8}/pyproject.toml +1 -1
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/cli.py +379 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/.gitignore +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/LICENSE +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/README.md +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/assets/logo.png +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/generate_facades.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/setup.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/client.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/consumption.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/members.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/organizations.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/consumption.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/members.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/organizations.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/config.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/__init__.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/conftest.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_playbooks.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_cli.py +0 -0
- {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_config.py +0 -0
|
@@ -5,6 +5,14 @@ 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.8] - 2026-03-11
|
|
9
|
+
### Added
|
|
10
|
+
- 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`.
|
|
11
|
+
- `--wait` / `-w` flag on `create-session` and `sessions create` — blocks until the session reaches a terminal status.
|
|
12
|
+
- `sessions watch` now displays `structured_output` inline and uses exponential backoff (caps at 30s).
|
|
13
|
+
- `chain` command supports both inline `--playbooks` chaining and YAML workflow files.
|
|
14
|
+
- `attach` command correctly embeds the uploaded file URL in the session prompt.
|
|
15
|
+
|
|
8
16
|
## [1.1.7] - 2026-03-11
|
|
9
17
|
### Fixed
|
|
10
18
|
- `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.8
|
|
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
|
|
|
@@ -612,5 +627,369 @@ def use_session_cmd(session_id: str):
|
|
|
612
627
|
config.current_session_id = session_id
|
|
613
628
|
console.print(f"[green]Switched to session {session_id}[/green]")
|
|
614
629
|
|
|
630
|
+
@app.command("status")
|
|
631
|
+
@handle_api_error
|
|
632
|
+
def status_cmd():
|
|
633
|
+
"""Show the status of the current active session."""
|
|
634
|
+
sid = get_current_session_id()
|
|
635
|
+
resp = sessions.get_session(sid)
|
|
636
|
+
status = resp.get("status_enum", "unknown")
|
|
637
|
+
title = resp.get("title") or resp.get("prompt", "")[:60]
|
|
638
|
+
url = resp.get("url", "")
|
|
639
|
+
acus = resp.get("acus_consumed") or resp.get("acu_used")
|
|
640
|
+
|
|
641
|
+
color = {"running": "green", "stopped": "red", "paused": "yellow"}.get(status, "cyan")
|
|
642
|
+
console.print(f"[bold]Session:[/bold] [cyan]{sid}[/cyan]")
|
|
643
|
+
console.print(f"[bold]Status:[/bold] [{color}]{status}[/{color}]")
|
|
644
|
+
if title:
|
|
645
|
+
console.print(f"[bold]Title:[/bold] {title}")
|
|
646
|
+
if acus is not None:
|
|
647
|
+
console.print(f"[bold]ACUs:[/bold] {acus}")
|
|
648
|
+
if url:
|
|
649
|
+
console.print(f"[bold]URL:[/bold] [underline]{url}[/underline]")
|
|
650
|
+
|
|
651
|
+
@app.command("open")
|
|
652
|
+
@handle_api_error
|
|
653
|
+
def open_cmd(
|
|
654
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to open (defaults to active session)"),
|
|
655
|
+
):
|
|
656
|
+
"""Open the active session in your browser."""
|
|
657
|
+
sid = session_id or get_current_session_id()
|
|
658
|
+
resp = sessions.get_session(sid)
|
|
659
|
+
url = resp.get("url")
|
|
660
|
+
if not url:
|
|
661
|
+
console.print("[bold red]Error:[/bold red] No URL available for this session.")
|
|
662
|
+
raise typer.Exit(1)
|
|
663
|
+
webbrowser.open(url)
|
|
664
|
+
console.print(f"[green]Opened:[/green] {url}")
|
|
665
|
+
|
|
666
|
+
@session_app.command("watch")
|
|
667
|
+
@handle_api_error
|
|
668
|
+
def watch_session_cmd(
|
|
669
|
+
session_id: Optional[str] = typer.Argument(None, help="Session ID to watch (defaults to active session)"),
|
|
670
|
+
interval: int = typer.Option(3, "--interval", "-i", help="Polling interval in seconds"),
|
|
671
|
+
org: Optional[str] = typer.Option(None, "--org"),
|
|
672
|
+
):
|
|
673
|
+
"""Live-watch a session — polls status and streams new messages."""
|
|
674
|
+
if org: config.temporary_org_id = org
|
|
675
|
+
sid = session_id or get_current_session_id()
|
|
676
|
+
|
|
677
|
+
terminal_statuses = {"stopped", "finished", "error", "cancelled", "failed", "blocked"}
|
|
678
|
+
backoff = interval
|
|
679
|
+
|
|
680
|
+
def build_status_panel(resp: dict) -> Panel:
|
|
681
|
+
status_val = resp.get("status_enum", "unknown")
|
|
682
|
+
title_val = resp.get("title") or resp.get("prompt", "")[:60]
|
|
683
|
+
acus = resp.get("acus_consumed") or resp.get("acu_used", "n/a")
|
|
684
|
+
color = {"running": "green", "stopped": "red", "paused": "yellow",
|
|
685
|
+
"finished": "blue", "blocked": "red"}.get(status_val, "cyan")
|
|
686
|
+
text = Text()
|
|
687
|
+
text.append("Session: ", style="bold")
|
|
688
|
+
text.append(f"{sid}\n", style="cyan")
|
|
689
|
+
text.append("Status: ", style="bold")
|
|
690
|
+
text.append(f"{status_val}\n", style=color)
|
|
691
|
+
if title_val:
|
|
692
|
+
text.append(f"Title: {title_val}\n")
|
|
693
|
+
text.append(f"ACUs: {acus}\n")
|
|
694
|
+
so = resp.get("structured_output")
|
|
695
|
+
if so:
|
|
696
|
+
text.append("\nStructured Output:\n", style="bold")
|
|
697
|
+
text.append(json.dumps(so, indent=2))
|
|
698
|
+
text.append("\nPress Ctrl+C to stop watching.", style="dim")
|
|
699
|
+
return Panel(text, title="[bold cyan]Devin Watch[/bold cyan]", border_style="cyan")
|
|
700
|
+
|
|
701
|
+
console.print(f"[bold cyan]Watching session {sid}[/bold cyan] (Ctrl+C to stop)")
|
|
702
|
+
|
|
703
|
+
with Live(console=console, refresh_per_second=4) as live:
|
|
704
|
+
try:
|
|
705
|
+
while True:
|
|
706
|
+
resp = sessions.get_session(sid)
|
|
707
|
+
live.update(build_status_panel(resp))
|
|
708
|
+
|
|
709
|
+
if resp.get("status_enum", "").lower() in terminal_statuses:
|
|
710
|
+
live.update(build_status_panel(resp))
|
|
711
|
+
console.print(f"[bold green]Session {resp.get('status_enum')}![/bold green]")
|
|
712
|
+
break
|
|
713
|
+
|
|
714
|
+
time.sleep(min(backoff, 30))
|
|
715
|
+
backoff = min(backoff * 1.5, 30)
|
|
716
|
+
except KeyboardInterrupt:
|
|
717
|
+
console.print("\n[dim]Watch stopped.[/dim]")
|
|
718
|
+
|
|
719
|
+
# --- Flat top-level aliases (backward compat with 0.1.x command structure) ---
|
|
720
|
+
|
|
721
|
+
@app.command("watch", hidden=True)
|
|
722
|
+
@handle_api_error
|
|
723
|
+
def watch_cmd(
|
|
724
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
725
|
+
interval: int = typer.Option(3, "--interval", "-i"),
|
|
726
|
+
):
|
|
727
|
+
"""Alias for: devin sessions watch"""
|
|
728
|
+
watch_session_cmd(session_id=session_id, interval=interval, org=None)
|
|
729
|
+
|
|
730
|
+
@app.command("message")
|
|
731
|
+
@handle_api_error
|
|
732
|
+
def message_cmd(
|
|
733
|
+
text: str = typer.Argument(..., help="Message to send"),
|
|
734
|
+
session_id: Optional[str] = typer.Option(None, "--id"),
|
|
735
|
+
):
|
|
736
|
+
"""Send a message to the active session."""
|
|
737
|
+
sid = session_id or get_current_session_id()
|
|
738
|
+
sessions.send_message(sid, text)
|
|
739
|
+
console.print(f"[green]Message sent to {sid}[/green]")
|
|
740
|
+
|
|
741
|
+
@app.command("terminate")
|
|
742
|
+
@handle_api_error
|
|
743
|
+
def terminate_cmd(
|
|
744
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
745
|
+
):
|
|
746
|
+
"""Terminate the active session."""
|
|
747
|
+
sid = session_id or get_current_session_id()
|
|
748
|
+
if typer.confirm(f"Terminate session {sid}?"):
|
|
749
|
+
sessions.terminate_session(sid)
|
|
750
|
+
console.print(f"[green]Session {sid} terminated.[/green]")
|
|
751
|
+
|
|
752
|
+
@app.command("list-sessions")
|
|
753
|
+
@handle_api_error
|
|
754
|
+
def list_sessions_top_cmd(
|
|
755
|
+
limit: int = typer.Option(10, "--limit", "-n"),
|
|
756
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
757
|
+
):
|
|
758
|
+
"""List recent sessions (alias for: devin sessions list)."""
|
|
759
|
+
resp = sessions.list_sessions(limit=limit)
|
|
760
|
+
sess_list = resp.get("sessions", [])
|
|
761
|
+
if json_output:
|
|
762
|
+
console.print(json.dumps(sess_list, indent=2))
|
|
763
|
+
return
|
|
764
|
+
table = Table(title="Devin Sessions")
|
|
765
|
+
table.add_column("ID", style="cyan")
|
|
766
|
+
table.add_column("Status", style="magenta")
|
|
767
|
+
table.add_column("Title")
|
|
768
|
+
for s in sess_list:
|
|
769
|
+
table.add_row(s.get("session_id"), s.get("status_enum"), s.get("title") or s.get("prompt", "")[:50])
|
|
770
|
+
console.print(table)
|
|
771
|
+
|
|
772
|
+
@app.command("create-session")
|
|
773
|
+
@handle_api_error
|
|
774
|
+
def create_session_top_cmd(
|
|
775
|
+
prompt: str = typer.Argument(..., help="The prompt for the session"),
|
|
776
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
777
|
+
wait: bool = typer.Option(False, "--wait", "-w", help="Block until the session completes"),
|
|
778
|
+
interval: int = typer.Option(5, "--interval"),
|
|
779
|
+
force: bool = typer.Option(False, "--force"),
|
|
780
|
+
):
|
|
781
|
+
"""Create a new session (alias for: devin sessions create)."""
|
|
782
|
+
create_session_cmd(prompt=prompt, file=None, title=title, org=None, max_acu=None,
|
|
783
|
+
advanced_mode=None, force=force, wait=wait, interval=interval)
|
|
784
|
+
|
|
785
|
+
@app.command("upload")
|
|
786
|
+
@handle_api_error
|
|
787
|
+
def upload_cmd(path: Path = typer.Argument(..., help="File to upload")):
|
|
788
|
+
"""Upload a file to Devin."""
|
|
789
|
+
resp = attachments.upload_file(str(path))
|
|
790
|
+
console.print(f"[green]Uploaded:[/green] {resp}")
|
|
791
|
+
|
|
792
|
+
@app.command("list-knowledge")
|
|
793
|
+
@handle_api_error
|
|
794
|
+
def list_knowledge_top_cmd():
|
|
795
|
+
"""List knowledge notes (alias for: devin knowledge list)."""
|
|
796
|
+
resp = knowledge.list_knowledge()
|
|
797
|
+
items = resp.get("notes", resp.get("knowledge", []))
|
|
798
|
+
table = Table(title="Knowledge Base")
|
|
799
|
+
table.add_column("ID", style="cyan")
|
|
800
|
+
table.add_column("Title")
|
|
801
|
+
for item in items:
|
|
802
|
+
table.add_row(item.get("id"), item.get("title") or item.get("name"))
|
|
803
|
+
console.print(table)
|
|
804
|
+
|
|
805
|
+
@app.command("attach")
|
|
806
|
+
@handle_api_error
|
|
807
|
+
def attach_cmd(
|
|
808
|
+
file: Path = typer.Argument(..., help="Context file to upload and attach"),
|
|
809
|
+
prompt: str = typer.Argument(..., help="Task prompt to include with the attachment"),
|
|
810
|
+
title: Optional[str] = typer.Option(None, "--title", "-t"),
|
|
811
|
+
wait: bool = typer.Option(False, "--wait", "-w"),
|
|
812
|
+
interval: int = typer.Option(5, "--interval"),
|
|
813
|
+
):
|
|
814
|
+
"""Upload a context file and start a session using it."""
|
|
815
|
+
with console.status("[bold green]Uploading file...[/bold green]"):
|
|
816
|
+
upload_resp = attachments.upload_file(str(file))
|
|
817
|
+
url = upload_resp.strip('"') if isinstance(upload_resp, str) else str(upload_resp)
|
|
818
|
+
console.print(f"[green]Uploaded:[/green] {url}")
|
|
819
|
+
full_prompt = f"{prompt}\n\nATTACHMENT: \"{url}\""
|
|
820
|
+
create_session_cmd(prompt=full_prompt, file=None, title=title, org=None, max_acu=None,
|
|
821
|
+
advanced_mode=None, force=False, wait=wait, interval=interval)
|
|
822
|
+
|
|
823
|
+
@app.command("update-tags")
|
|
824
|
+
@handle_api_error
|
|
825
|
+
def update_tags_cmd(
|
|
826
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
827
|
+
tags: List[str] = typer.Option(..., "--tag", "-t", help="Tags to set (can repeat)"),
|
|
828
|
+
):
|
|
829
|
+
"""Update tags for a session."""
|
|
830
|
+
sid = session_id or get_current_session_id()
|
|
831
|
+
sessions.update_session_tags(sid, tags)
|
|
832
|
+
console.print(f"[green]Tags updated for session {sid}.[/green]")
|
|
833
|
+
|
|
834
|
+
@app.command("history")
|
|
835
|
+
def history_cmd():
|
|
836
|
+
"""Show the locally cached current session ID."""
|
|
837
|
+
sid = config.current_session_id
|
|
838
|
+
if sid:
|
|
839
|
+
console.print(f"Current local session: [cyan]{sid}[/cyan]")
|
|
840
|
+
else:
|
|
841
|
+
console.print("No current local session.")
|
|
842
|
+
|
|
843
|
+
@app.command("messages")
|
|
844
|
+
@handle_api_error
|
|
845
|
+
def messages_cmd(
|
|
846
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
847
|
+
):
|
|
848
|
+
"""Show conversation history for a session."""
|
|
849
|
+
sid = session_id or get_current_session_id()
|
|
850
|
+
resp = sessions.get_session_messages(sid)
|
|
851
|
+
msgs = resp.get("messages", [])
|
|
852
|
+
console.print(f"[bold]Conversation for Session {sid}[/bold]")
|
|
853
|
+
console.print("─" * 40)
|
|
854
|
+
for m in msgs:
|
|
855
|
+
role = m.get("role", "unknown")
|
|
856
|
+
content = m.get("message", "") or m.get("content", "")
|
|
857
|
+
console.print(f"[bold cyan]{role}:[/bold cyan] {content}")
|
|
858
|
+
|
|
859
|
+
@app.command("get-session")
|
|
860
|
+
@handle_api_error
|
|
861
|
+
def get_session_top_cmd(
|
|
862
|
+
session_id: Optional[str] = typer.Argument(None),
|
|
863
|
+
):
|
|
864
|
+
"""Get details for a session."""
|
|
865
|
+
sid = session_id or get_current_session_id()
|
|
866
|
+
resp = sessions.get_session(sid)
|
|
867
|
+
console.print(Panel(
|
|
868
|
+
f"[bold]Status:[/bold] {resp.get('status_enum')}\n"
|
|
869
|
+
f"[bold]URL:[/bold] {resp.get('url')}\n"
|
|
870
|
+
f"[bold]Created:[/bold] {resp.get('created_at')}",
|
|
871
|
+
title=f"Session {sid}"
|
|
872
|
+
))
|
|
873
|
+
if "structured_output" in resp:
|
|
874
|
+
console.print("[bold]Structured Output:[/bold]")
|
|
875
|
+
console.print(json.dumps(resp["structured_output"], indent=2))
|
|
876
|
+
|
|
877
|
+
@app.command("update-knowledge")
|
|
878
|
+
@handle_api_error
|
|
879
|
+
def update_knowledge_cmd(
|
|
880
|
+
knowledge_id: str = typer.Argument(...),
|
|
881
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
882
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
883
|
+
trigger: Optional[str] = typer.Option(None, "--trigger"),
|
|
884
|
+
):
|
|
885
|
+
"""Update an existing knowledge entry."""
|
|
886
|
+
knowledge.update_knowledge(knowledge_id, title=name, body=body)
|
|
887
|
+
console.print(f"[green]Knowledge {knowledge_id} updated.[/green]")
|
|
888
|
+
|
|
889
|
+
@app.command("update-playbook")
|
|
890
|
+
@handle_api_error
|
|
891
|
+
def update_playbook_top_cmd(
|
|
892
|
+
playbook_id: str = typer.Argument(...),
|
|
893
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
894
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
895
|
+
macro: Optional[str] = typer.Option(None, "--macro"),
|
|
896
|
+
):
|
|
897
|
+
"""Update an existing playbook."""
|
|
898
|
+
playbooks.update_playbook(playbook_id, title=title or "", body=body, macro=macro)
|
|
899
|
+
console.print(f"[green]Playbook {playbook_id} updated.[/green]")
|
|
900
|
+
|
|
901
|
+
@app.command("delete-playbook")
|
|
902
|
+
@handle_api_error
|
|
903
|
+
def delete_playbook_top_cmd(playbook_id: str = typer.Argument(...)):
|
|
904
|
+
"""Delete a playbook."""
|
|
905
|
+
if typer.confirm(f"Delete playbook {playbook_id}?"):
|
|
906
|
+
playbooks.delete_playbook(playbook_id)
|
|
907
|
+
console.print(f"[green]Playbook {playbook_id} deleted.[/green]")
|
|
908
|
+
|
|
909
|
+
@app.command("list-secrets")
|
|
910
|
+
@handle_api_error
|
|
911
|
+
def list_secrets_top_cmd():
|
|
912
|
+
"""List all organization secrets."""
|
|
913
|
+
resp = secrets.list_secrets()
|
|
914
|
+
items = resp if isinstance(resp, list) else resp.get("secrets", resp.get("items", []))
|
|
915
|
+
table = Table(title="Organization Secrets")
|
|
916
|
+
table.add_column("ID", style="cyan")
|
|
917
|
+
table.add_column("Name")
|
|
918
|
+
for item in items:
|
|
919
|
+
if isinstance(item, dict):
|
|
920
|
+
table.add_row(item.get("id", ""), item.get("name", ""))
|
|
921
|
+
console.print(table)
|
|
922
|
+
|
|
923
|
+
@app.command("delete-secret")
|
|
924
|
+
@handle_api_error
|
|
925
|
+
def delete_secret_top_cmd(secret_id: str = typer.Argument(...)):
|
|
926
|
+
"""Delete an organization secret."""
|
|
927
|
+
if typer.confirm(f"Delete secret {secret_id}?"):
|
|
928
|
+
secrets.delete_secret(secret_id)
|
|
929
|
+
console.print(f"[green]Secret {secret_id} deleted.[/green]")
|
|
930
|
+
|
|
931
|
+
@app.command("chain")
|
|
932
|
+
@handle_api_error
|
|
933
|
+
def chain_cmd(
|
|
934
|
+
prompt: Optional[str] = typer.Argument(None, help="Initial prompt"),
|
|
935
|
+
playbooks_arg: Optional[str] = typer.Option(None, "--playbooks", help="Comma-separated playbook IDs"),
|
|
936
|
+
file: Optional[Path] = typer.Option(None, "--file", help="Workflow YAML file"),
|
|
937
|
+
):
|
|
938
|
+
"""(Beta) Orchestrate a sequential chain of playbooks.
|
|
939
|
+
|
|
940
|
+
Method 1 \u2014 Inline:
|
|
941
|
+
devin chain "Refactor utils.py" --playbooks "lint_check,unit_tests"
|
|
942
|
+
|
|
943
|
+
Method 2 \u2014 YAML workflow file:
|
|
944
|
+
devin chain --file workflow.yml
|
|
945
|
+
"""
|
|
946
|
+
steps = []
|
|
947
|
+
|
|
948
|
+
if file:
|
|
949
|
+
if not file.exists():
|
|
950
|
+
console.print(f"[bold red]Error:[/bold red] File not found: {file}")
|
|
951
|
+
raise typer.Exit(1)
|
|
952
|
+
try:
|
|
953
|
+
workflow = yaml.safe_load(file.read_text())
|
|
954
|
+
steps = workflow.get("steps", [])
|
|
955
|
+
except Exception as e:
|
|
956
|
+
console.print(f"[bold red]Error parsing YAML:[/bold red] {e}")
|
|
957
|
+
raise typer.Exit(1)
|
|
958
|
+
elif prompt and playbooks_arg:
|
|
959
|
+
pb_list = [p.strip() for p in playbooks_arg.split(",")]
|
|
960
|
+
for i, pb in enumerate(pb_list):
|
|
961
|
+
step_prompt = prompt if i == 0 else f"Execute playbook: {pb}"
|
|
962
|
+
steps.append({"prompt": step_prompt, "playbook": pb})
|
|
963
|
+
else:
|
|
964
|
+
console.print("[bold red]Error:[/bold red] Provide --file OR (prompt + --playbooks)")
|
|
965
|
+
raise typer.Exit(1)
|
|
966
|
+
|
|
967
|
+
current_sid = None
|
|
968
|
+
for i, step in enumerate(steps):
|
|
969
|
+
step_prompt = step.get("prompt", "")
|
|
970
|
+
step_pb = step.get("playbook")
|
|
971
|
+
console.print(f"[bold cyan]Step {i+1}/{len(steps)}:[/bold cyan] Playbook={step_pb}")
|
|
972
|
+
|
|
973
|
+
if i == 0:
|
|
974
|
+
with console.status(f"Starting session..."):
|
|
975
|
+
resp = sessions.create_session(prompt=step_prompt, playbook_id=step_pb)
|
|
976
|
+
current_sid = resp["session_id"]
|
|
977
|
+
config.current_session_id = current_sid
|
|
978
|
+
console.print(f"[green]Session started:[/green] {current_sid}")
|
|
979
|
+
else:
|
|
980
|
+
sessions.send_message(current_sid, f"{step_prompt} (Playbook: {step_pb})")
|
|
981
|
+
|
|
982
|
+
backoff = 2
|
|
983
|
+
while True:
|
|
984
|
+
resp = sessions.get_session(current_sid)
|
|
985
|
+
status = resp.get("status_enum", "")
|
|
986
|
+
if status in {"blocked", "finished", "stopped", "error"}:
|
|
987
|
+
console.print(f"Step {i+1} done (status: {status}).")
|
|
988
|
+
break
|
|
989
|
+
time.sleep(min(backoff, 10))
|
|
990
|
+
backoff = min(backoff * 1.5, 10)
|
|
991
|
+
|
|
992
|
+
console.print("[bold green]Chain completed![/bold green]")
|
|
993
|
+
|
|
615
994
|
if __name__ == "__main__":
|
|
616
995
|
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
|