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.
Files changed (53) hide show
  1. {devin_cli-1.1.7 → devin_cli-1.1.8}/CHANGELOG.md +8 -0
  2. {devin_cli-1.1.7 → devin_cli-1.1.8}/PKG-INFO +1 -1
  3. {devin_cli-1.1.7 → devin_cli-1.1.8}/pyproject.toml +1 -1
  4. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/cli.py +379 -0
  5. {devin_cli-1.1.7 → devin_cli-1.1.8}/.github/workflows/pypi-publish.yml +0 -0
  6. {devin_cli-1.1.7 → devin_cli-1.1.8}/.gitignore +0 -0
  7. {devin_cli-1.1.7 → devin_cli-1.1.8}/LICENSE +0 -0
  8. {devin_cli-1.1.7 → devin_cli-1.1.8}/README.md +0 -0
  9. {devin_cli-1.1.7 → devin_cli-1.1.8}/assets/logo.png +0 -0
  10. {devin_cli-1.1.7 → devin_cli-1.1.8}/generate_facades.py +0 -0
  11. {devin_cli-1.1.7 → devin_cli-1.1.8}/setup.py +0 -0
  12. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/__init__.py +0 -0
  13. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/__init__.py +0 -0
  14. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/attachments.py +0 -0
  15. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/client.py +0 -0
  16. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/consumption.py +0 -0
  17. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/knowledge.py +0 -0
  18. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/members.py +0 -0
  19. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/organizations.py +0 -0
  20. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/playbooks.py +0 -0
  21. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/repositories.py +0 -0
  22. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/schedules.py +0 -0
  23. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/secrets.py +0 -0
  24. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/sessions.py +0 -0
  25. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/__init__.py +0 -0
  26. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/attachments.py +0 -0
  27. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/knowledge.py +0 -0
  28. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/playbooks.py +0 -0
  29. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/secrets.py +0 -0
  30. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v1/sessions.py +0 -0
  31. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/__init__.py +0 -0
  32. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/attachments.py +0 -0
  33. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/consumption.py +0 -0
  34. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/knowledge.py +0 -0
  35. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/members.py +0 -0
  36. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/organizations.py +0 -0
  37. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/playbooks.py +0 -0
  38. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/repositories.py +0 -0
  39. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/schedules.py +0 -0
  40. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/secrets.py +0 -0
  41. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/api/v3/sessions.py +0 -0
  42. {devin_cli-1.1.7 → devin_cli-1.1.8}/src/devin_cli/config.py +0 -0
  43. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/__init__.py +0 -0
  44. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/conftest.py +0 -0
  45. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_attachments.py +0 -0
  46. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_knowledge.py +0 -0
  47. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_playbooks.py +0 -0
  48. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_repositories.py +0 -0
  49. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_schedules.py +0 -0
  50. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_secrets.py +0 -0
  51. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_api/test_sessions.py +0 -0
  52. {devin_cli-1.1.7 → devin_cli-1.1.8}/tests/test_cli.py +0 -0
  53. {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.7
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devin-cli"
3
- version = "1.1.7"
3
+ version = "1.1.8"
4
4
  description = "Unofficial CLI for Devin AI - The first AI Software Engineer"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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