devin-cli 1.2.4__tar.gz → 1.3.1__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.2.4 → devin_cli-1.3.1}/CHANGELOG.md +10 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/PKG-INFO +11 -2
- {devin_cli-1.2.4 → devin_cli-1.3.1}/README.md +10 -1
- {devin_cli-1.2.4 → devin_cli-1.3.1}/pyproject.toml +1 -1
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v1/knowledge.py +3 -0
- {devin_cli-1.2.4/src/devin_cli/api/v1 → devin_cli-1.3.1/src/devin_cli/api/v3}/secrets.py +3 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/cli.py +157 -15
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_cli.py +1 -1
- {devin_cli-1.2.4 → devin_cli-1.3.1}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/.gitignore +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/LICENSE +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/assets/logo.png +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/generate_facades.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/setup.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/__init__.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/attachments.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/client.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/consumption.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/knowledge.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/members.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/organizations.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/playbooks.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/repositories.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/schedules.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/secrets.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/sessions.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v1/__init__.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v1/attachments.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v1/playbooks.py +0 -0
- {devin_cli-1.2.4/src/devin_cli/api/v3 → devin_cli-1.3.1/src/devin_cli/api/v1}/secrets.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v1/sessions.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/attachments.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/consumption.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/knowledge.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/members.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/organizations.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/playbooks.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/repositories.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/schedules.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/api/v3/sessions.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/src/devin_cli/config.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/__init__.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/conftest.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_playbooks.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.2.4 → devin_cli-1.3.1}/tests/test_config.py +0 -0
|
@@ -5,6 +5,16 @@ 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.3.1] - 2026-03-16
|
|
9
|
+
### Added
|
|
10
|
+
- **Global `--json` Output**: New orchestration flag that converts *all* CLI output and API errors into pure, parseable JSON for AI agents.
|
|
11
|
+
- **Knowledge Get Command**: Added `devin knowledge get <id>` and `devin get-knowledge <id>` to retrieve full note content.
|
|
12
|
+
- Missing v1 and v3 `get_knowledge` / `create_secret` cross-compatibility polyfills.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Audit Fixes**: Resolved 13 critical bugs found during full codebase audit (indentation errors, missing returns, duplicate imports, and terminal status handling).
|
|
16
|
+
- **Warning Suppression**: Suppressed `urllib3` SSL warnings in JSON mode to ensure clean stdout streams.
|
|
17
|
+
|
|
8
18
|
## [1.2.0] - 2026-03-11
|
|
9
19
|
### Added
|
|
10
20
|
- `sessions create` / `create-session` now exposes all 8 previously missing v3 API parameters: `--playbook-id`, `--tag`, `--repo`, `--knowledge-id`, `--secret-id`, `--session-link`, `--attachment-url`, `--create-as-user-id`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devin-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
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
|
|
@@ -87,7 +87,16 @@ devin watch
|
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
90
|
-
##
|
|
90
|
+
## Authentication & Usage
|
|
91
|
+
|
|
92
|
+
### AI Agent Integration (JSON Output)
|
|
93
|
+
For external automation and AI agent architectures relying on the CLI, `devin-cli` supports a global `--json` flag. This will suppress all visual output/terminal colors and instead uniformly return raw JSON objects for stdout and API errors, making the CLI completely deterministic to parse.
|
|
94
|
+
```bash
|
|
95
|
+
devin --json sessions create "My prompt"
|
|
96
|
+
# { "session_id": "...", "status": "running" }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Multi-Profile Support
|
|
91
100
|
|
|
92
101
|
```bash
|
|
93
102
|
devin configure --profile personal
|
|
@@ -51,7 +51,16 @@ devin watch
|
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## Authentication & Usage
|
|
55
|
+
|
|
56
|
+
### AI Agent Integration (JSON Output)
|
|
57
|
+
For external automation and AI agent architectures relying on the CLI, `devin-cli` supports a global `--json` flag. This will suppress all visual output/terminal colors and instead uniformly return raw JSON objects for stdout and API errors, making the CLI completely deterministic to parse.
|
|
58
|
+
```bash
|
|
59
|
+
devin --json sessions create "My prompt"
|
|
60
|
+
# { "session_id": "...", "status": "running" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Multi-Profile Support
|
|
55
64
|
|
|
56
65
|
```bash
|
|
57
66
|
devin configure --profile personal
|
|
@@ -45,5 +45,8 @@ def update_knowledge(
|
|
|
45
45
|
|
|
46
46
|
return client.put(f"knowledge/{knowledge_id}", json=data)
|
|
47
47
|
|
|
48
|
+
def get_knowledge(knowledge_id: str):
|
|
49
|
+
return client.get(f"knowledge/{knowledge_id}")
|
|
50
|
+
|
|
48
51
|
def delete_knowledge(knowledge_id: str):
|
|
49
52
|
return client.delete(f"knowledge/{knowledge_id}")
|
|
@@ -3,5 +3,8 @@ from devin_cli.api.client import client
|
|
|
3
3
|
def list_secrets():
|
|
4
4
|
return client.get("secrets")
|
|
5
5
|
|
|
6
|
+
def create_secret(name: str, value: str):
|
|
7
|
+
return client.post("secrets", json={"name": name, "value": value})
|
|
8
|
+
|
|
6
9
|
def delete_secret(secret_id: str):
|
|
7
10
|
return client.delete(f"secrets/{secret_id}")
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Suppress urllib3 warnings early for clean JSON output
|
|
6
|
+
if "--json" in sys.argv or os.environ.get("DEVIN_OUTPUT_FORMAT") == "json":
|
|
7
|
+
warnings.filterwarnings("ignore")
|
|
8
|
+
os.environ["DEVIN_OUTPUT_FORMAT"] = "json"
|
|
9
|
+
|
|
1
10
|
import typer
|
|
2
11
|
import time
|
|
3
12
|
import json
|
|
13
|
+
import requests
|
|
14
|
+
import functools
|
|
4
15
|
import yaml
|
|
5
16
|
import asyncio
|
|
6
17
|
from pathlib import Path
|
|
@@ -14,7 +25,6 @@ from devin_cli.config import config
|
|
|
14
25
|
from devin_cli.api import sessions, knowledge, playbooks, secrets, attachments, repositories, schedules, organizations, members, consumption
|
|
15
26
|
from devin_cli.api.client import client, APIError
|
|
16
27
|
import webbrowser
|
|
17
|
-
import functools
|
|
18
28
|
import sys
|
|
19
29
|
import hashlib
|
|
20
30
|
import importlib.metadata
|
|
@@ -80,10 +90,14 @@ def main(
|
|
|
80
90
|
ctx: typer.Context,
|
|
81
91
|
profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Configuration profile to use (e.g., service, personal)"),
|
|
82
92
|
version: Optional[bool] = typer.Option(None, "--version", "-v", help="Show the application's version and exit."),
|
|
93
|
+
json_format: bool = typer.Option(False, "--json", help="Output raw JSON (for AI agents & scripting)"),
|
|
83
94
|
):
|
|
84
95
|
"""
|
|
85
96
|
Unofficial CLI for Devin AI v3.
|
|
86
97
|
"""
|
|
98
|
+
if json_format:
|
|
99
|
+
os.environ["DEVIN_OUTPUT_FORMAT"] = "json"
|
|
100
|
+
|
|
87
101
|
if version:
|
|
88
102
|
console.print(f"devin CLI version: {__version__}")
|
|
89
103
|
raise typer.Exit()
|
|
@@ -97,16 +111,50 @@ def handle_api_error(func):
|
|
|
97
111
|
"""Decorator to handle API errors gracefully."""
|
|
98
112
|
@functools.wraps(func)
|
|
99
113
|
def wrapper(*args, **kwargs):
|
|
114
|
+
import os
|
|
115
|
+
is_json = os.environ.get("DEVIN_OUTPUT_FORMAT") == "json"
|
|
116
|
+
original_print = console.print
|
|
100
117
|
try:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
if is_json:
|
|
119
|
+
console.print = lambda *a, **k: None
|
|
120
|
+
|
|
121
|
+
result = func(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
if is_json:
|
|
124
|
+
console.print = original_print
|
|
125
|
+
if result is not None:
|
|
126
|
+
print(json.dumps(result, indent=2))
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
except BaseException as e:
|
|
130
|
+
if is_json:
|
|
131
|
+
console.print = original_print
|
|
132
|
+
if isinstance(e, typer.Exit):
|
|
133
|
+
raise e
|
|
134
|
+
elif isinstance(e, requests.exceptions.RequestException):
|
|
135
|
+
if is_json:
|
|
136
|
+
print(json.dumps({"error": "Connection error", "details": str(e)}, indent=2))
|
|
137
|
+
else:
|
|
138
|
+
console.print(f"[bold red]Connection Error:[/bold red] Failed to connect to {config.base_url}")
|
|
139
|
+
console.print(f"[dim]{str(e)}[/dim]")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
elif hasattr(e, "response"): # Standard APIError handling
|
|
142
|
+
if is_json:
|
|
143
|
+
try:
|
|
144
|
+
err_json = e.response.json()
|
|
145
|
+
except:
|
|
146
|
+
err_json = {"error": str(e)}
|
|
147
|
+
print(json.dumps(err_json, indent=2))
|
|
148
|
+
else:
|
|
149
|
+
console.print(f"[bold red]API Error:[/bold red] {e}")
|
|
150
|
+
raise typer.Exit(1)
|
|
151
|
+
else:
|
|
152
|
+
if is_json:
|
|
153
|
+
print(json.dumps({"error": str(e)}, indent=2))
|
|
154
|
+
else:
|
|
155
|
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
|
|
110
158
|
return wrapper
|
|
111
159
|
|
|
112
160
|
def get_current_session_id():
|
|
@@ -145,6 +193,7 @@ def configure(
|
|
|
145
193
|
if org_id:
|
|
146
194
|
config.org_id = org_id
|
|
147
195
|
console.print(f"[green]Configuration saved to {config.config_file}[/green]")
|
|
196
|
+
return None
|
|
148
197
|
|
|
149
198
|
# --- Sessions ---
|
|
150
199
|
@session_app.command("create")
|
|
@@ -260,6 +309,8 @@ def create_session_cmd(
|
|
|
260
309
|
break
|
|
261
310
|
else:
|
|
262
311
|
console.print("[yellow]Session created, but no ID returned immediately (awaiting advanced mode setup).[/yellow]")
|
|
312
|
+
|
|
313
|
+
return resp
|
|
263
314
|
|
|
264
315
|
|
|
265
316
|
@session_app.command("list")
|
|
@@ -287,6 +338,8 @@ def list_sessions_cmd(
|
|
|
287
338
|
for s in sess_list:
|
|
288
339
|
table.add_row(s.get("session_id"), s.get("status_enum"), s.get("title") or s.get("prompt", "")[:50])
|
|
289
340
|
console.print(table)
|
|
341
|
+
|
|
342
|
+
return resp
|
|
290
343
|
|
|
291
344
|
@session_app.command("get")
|
|
292
345
|
@handle_api_error
|
|
@@ -299,6 +352,8 @@ def get_session_cmd(
|
|
|
299
352
|
sid = session_id or get_current_session_id()
|
|
300
353
|
resp = sessions.get_session(sid)
|
|
301
354
|
console.print(Panel(json.dumps(resp, indent=2), title=f"Session {sid}"))
|
|
355
|
+
|
|
356
|
+
return resp
|
|
302
357
|
|
|
303
358
|
@session_app.command("insights")
|
|
304
359
|
@handle_api_error
|
|
@@ -315,6 +370,8 @@ def session_insights_cmd(
|
|
|
315
370
|
console.print("Tip: Switch to v3 with [bold cyan]devin configure[/bold cyan] or use [bold cyan]--profile [v3-profile][/bold cyan]")
|
|
316
371
|
else:
|
|
317
372
|
console.print(Panel(json.dumps(resp, indent=2), title=f"Insights for {sid}"))
|
|
373
|
+
|
|
374
|
+
return resp
|
|
318
375
|
|
|
319
376
|
@session_app.command("cost")
|
|
320
377
|
@handle_api_error
|
|
@@ -338,6 +395,8 @@ def session_cost_cmd(
|
|
|
338
395
|
else:
|
|
339
396
|
resp = consumption.get_daily_consumption_breakdown()
|
|
340
397
|
console.print(Panel(json.dumps(resp, indent=2), title="Daily Consumption"))
|
|
398
|
+
|
|
399
|
+
return resp
|
|
341
400
|
|
|
342
401
|
@session_app.command("message")
|
|
343
402
|
@handle_api_error
|
|
@@ -362,6 +421,7 @@ def send_message_cmd(
|
|
|
362
421
|
raise typer.Exit(1)
|
|
363
422
|
sessions.send_message(sid, msg_text)
|
|
364
423
|
console.print(f"[green]Message sent to {sid}[/green]")
|
|
424
|
+
return None
|
|
365
425
|
|
|
366
426
|
@session_app.command("messages")
|
|
367
427
|
@handle_api_error
|
|
@@ -379,6 +439,8 @@ def list_messages_cmd(
|
|
|
379
439
|
content = m.get("message", "") or m.get("content", "")
|
|
380
440
|
console.print(f"[bold cyan]{role}:[/bold cyan] {content}")
|
|
381
441
|
console.print("---")
|
|
442
|
+
|
|
443
|
+
return resp
|
|
382
444
|
|
|
383
445
|
@session_app.command("terminate")
|
|
384
446
|
@handle_api_error
|
|
@@ -392,6 +454,7 @@ def terminate_session_cmd(
|
|
|
392
454
|
if typer.confirm(f"Terminate session {sid}?"):
|
|
393
455
|
sessions.terminate_session(sid)
|
|
394
456
|
console.print(f"[green]Session {sid} terminated.[/green]")
|
|
457
|
+
return None
|
|
395
458
|
|
|
396
459
|
# --- Knowledge ---
|
|
397
460
|
@knowledge_app.command("list")
|
|
@@ -408,6 +471,7 @@ def list_knowledge_cmd(org: Optional[str] = typer.Option(None, "--org")):
|
|
|
408
471
|
table.add_row(item.get("id"), item.get("title") or item.get("name"))
|
|
409
472
|
console.print(table)
|
|
410
473
|
|
|
474
|
+
return resp
|
|
411
475
|
@knowledge_app.command("create")
|
|
412
476
|
@handle_api_error
|
|
413
477
|
def create_knowledge_cmd(
|
|
@@ -420,6 +484,21 @@ def create_knowledge_cmd(
|
|
|
420
484
|
if org: config.temporary_org_id = org
|
|
421
485
|
resp = knowledge.create_knowledge(title=title, body=body, trigger=trigger)
|
|
422
486
|
console.print(f"[green]Created note:[/green] {resp.get('id')}")
|
|
487
|
+
|
|
488
|
+
return resp
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@knowledge_app.command("get")
|
|
492
|
+
@handle_api_error
|
|
493
|
+
def get_knowledge_cmd(
|
|
494
|
+
knowledge_id: str,
|
|
495
|
+
org: Optional[str] = typer.Option(None, "--org"),
|
|
496
|
+
):
|
|
497
|
+
"""Get detailed content of a knowledge note."""
|
|
498
|
+
if org: config.temporary_org_id = org
|
|
499
|
+
resp = knowledge.get_knowledge(knowledge_id)
|
|
500
|
+
console.print(Panel(json.dumps(resp, indent=2), title=f"Knowledge: {knowledge_id}"))
|
|
501
|
+
return resp
|
|
423
502
|
|
|
424
503
|
@knowledge_app.command("delete")
|
|
425
504
|
@handle_api_error
|
|
@@ -432,6 +511,8 @@ def delete_knowledge_cmd(
|
|
|
432
511
|
if typer.confirm(f"Delete knowledge {knowledge_id}?"):
|
|
433
512
|
knowledge.delete_knowledge(knowledge_id)
|
|
434
513
|
console.print(f"[green]Knowledge {knowledge_id} deleted.[/green]")
|
|
514
|
+
|
|
515
|
+
return None
|
|
435
516
|
|
|
436
517
|
# --- Playbooks ---
|
|
437
518
|
@playbook_app.command("list")
|
|
@@ -456,6 +537,7 @@ def list_playbooks_cmd(
|
|
|
456
537
|
if isinstance(item, dict):
|
|
457
538
|
table.add_row(item.get("playbook_id", ""), item.get("title", ""))
|
|
458
539
|
console.print(table)
|
|
540
|
+
return resp
|
|
459
541
|
|
|
460
542
|
@playbook_app.command("update")
|
|
461
543
|
@handle_api_error
|
|
@@ -476,6 +558,8 @@ def update_playbook_cmd(
|
|
|
476
558
|
if org: config.temporary_org_id = org
|
|
477
559
|
resp = playbooks.update_playbook(playbook_id, title=title, body=body, macro=macro)
|
|
478
560
|
console.print(f"[green]Playbook updated:[/green] {playbook_id}")
|
|
561
|
+
|
|
562
|
+
return resp
|
|
479
563
|
|
|
480
564
|
@playbook_app.command("create")
|
|
481
565
|
@handle_api_error
|
|
@@ -495,6 +579,8 @@ def create_playbook_cmd(
|
|
|
495
579
|
if org: config.temporary_org_id = org
|
|
496
580
|
resp = playbooks.create_playbook(title, body, macro)
|
|
497
581
|
console.print(f"[green]Playbook created:[/green] {resp.get('playbook_id')}")
|
|
582
|
+
|
|
583
|
+
return resp
|
|
498
584
|
|
|
499
585
|
@playbook_app.command("delete")
|
|
500
586
|
@handle_api_error
|
|
@@ -507,6 +593,7 @@ def delete_playbook_cmd(
|
|
|
507
593
|
if typer.confirm(f"Delete playbook {playbook_id}?"):
|
|
508
594
|
playbooks.delete_playbook(playbook_id)
|
|
509
595
|
console.print(f"[green]Playbook {playbook_id} deleted.[/green]")
|
|
596
|
+
return None
|
|
510
597
|
|
|
511
598
|
# --- Secrets ---
|
|
512
599
|
@secret_app.command("list")
|
|
@@ -535,6 +622,7 @@ def list_secrets_cmd(
|
|
|
535
622
|
if isinstance(item, dict):
|
|
536
623
|
table.add_row(item.get("id"), item.get("name"))
|
|
537
624
|
console.print(table)
|
|
625
|
+
return resp
|
|
538
626
|
|
|
539
627
|
@secret_app.command("delete")
|
|
540
628
|
@handle_api_error
|
|
@@ -547,6 +635,7 @@ def delete_secret_cmd(
|
|
|
547
635
|
if typer.confirm(f"Delete secret {secret_id}?"):
|
|
548
636
|
secrets.delete_secret(secret_id)
|
|
549
637
|
console.print(f"[green]Secret {secret_id} deleted.[/green]")
|
|
638
|
+
return None
|
|
550
639
|
|
|
551
640
|
@secret_app.command("create")
|
|
552
641
|
@handle_api_error
|
|
@@ -559,6 +648,8 @@ def create_secret_cmd(
|
|
|
559
648
|
if org: config.temporary_org_id = org
|
|
560
649
|
secrets.create_secret(name, value)
|
|
561
650
|
console.print(f"[green]Secret '{name}' created.[/green]")
|
|
651
|
+
|
|
652
|
+
return None
|
|
562
653
|
|
|
563
654
|
# --- Schedules ---
|
|
564
655
|
@schedule_app.command("list")
|
|
@@ -575,7 +666,9 @@ def list_schedules_cmd(org: Optional[str] = typer.Option(None, "--org")):
|
|
|
575
666
|
for item in items:
|
|
576
667
|
table.add_row(item.get("id"), item.get("title"), item.get("cron"))
|
|
577
668
|
console.print(table)
|
|
669
|
+
|
|
578
670
|
|
|
671
|
+
return resp
|
|
579
672
|
@schedule_app.command("create")
|
|
580
673
|
@handle_api_error
|
|
581
674
|
def create_schedule_cmd(
|
|
@@ -589,6 +682,8 @@ def create_schedule_cmd(
|
|
|
589
682
|
resp = schedules.create_schedule(prompt=prompt, cron=cron, title=title)
|
|
590
683
|
console.print(f"[green]Created schedule:[/green] {resp.get('id')}")
|
|
591
684
|
|
|
685
|
+
return resp
|
|
686
|
+
|
|
592
687
|
# --- Repositories ---
|
|
593
688
|
@repo_app.command("list")
|
|
594
689
|
@handle_api_error
|
|
@@ -629,6 +724,7 @@ def list_repos_cmd(
|
|
|
629
724
|
)
|
|
630
725
|
table.add_row(path, "Yes" if indexed else "No")
|
|
631
726
|
console.print(table)
|
|
727
|
+
return resp
|
|
632
728
|
|
|
633
729
|
@repo_app.command("index")
|
|
634
730
|
@handle_api_error
|
|
@@ -641,6 +737,8 @@ def index_repo_cmd(
|
|
|
641
737
|
repositories.index_repository(path)
|
|
642
738
|
console.print(f"[green]Indexing started for {path}[/green]")
|
|
643
739
|
|
|
740
|
+
return None
|
|
741
|
+
|
|
644
742
|
# --- Attachments ---
|
|
645
743
|
@attachment_app.command("upload")
|
|
646
744
|
@handle_api_error
|
|
@@ -652,6 +750,7 @@ def upload_attachment_cmd(
|
|
|
652
750
|
if org: config.temporary_org_id = org
|
|
653
751
|
resp = attachments.upload_file(str(path))
|
|
654
752
|
console.print(f"[green]Uploaded:[/green] {resp}")
|
|
753
|
+
return resp
|
|
655
754
|
|
|
656
755
|
@attachment_app.command("download")
|
|
657
756
|
@handle_api_error
|
|
@@ -668,6 +767,8 @@ def download_attachment_cmd(
|
|
|
668
767
|
out_path.write_bytes(content)
|
|
669
768
|
console.print(f"[green]Downloaded to:[/green] {out_path}")
|
|
670
769
|
|
|
770
|
+
return None
|
|
771
|
+
|
|
671
772
|
# --- Enterprise Discovery ---
|
|
672
773
|
@enterprise_app.command("whoami")
|
|
673
774
|
@handle_api_error
|
|
@@ -676,6 +777,7 @@ def whoami_cmd():
|
|
|
676
777
|
resp = members.get_self()
|
|
677
778
|
console.print(Panel(json.dumps(resp, indent=2), title="Who Am I"))
|
|
678
779
|
|
|
780
|
+
return resp
|
|
679
781
|
@enterprise_app.command("list-orgs")
|
|
680
782
|
@handle_api_error
|
|
681
783
|
def list_orgs_cmd():
|
|
@@ -683,12 +785,14 @@ def list_orgs_cmd():
|
|
|
683
785
|
resp = organizations.list_organizations()
|
|
684
786
|
console.print(Panel(json.dumps(resp, indent=2), title="Organizations"))
|
|
685
787
|
|
|
788
|
+
return resp
|
|
686
789
|
# --- Global Commands ---
|
|
687
790
|
@app.command("use")
|
|
688
791
|
def use_session_cmd(session_id: str):
|
|
689
792
|
"""Switch the current active session."""
|
|
690
793
|
config.current_session_id = session_id
|
|
691
794
|
console.print(f"[green]Switched to session {session_id}[/green]")
|
|
795
|
+
return {"current_session_id": session_id}
|
|
692
796
|
|
|
693
797
|
@app.command("status")
|
|
694
798
|
@handle_api_error
|
|
@@ -711,6 +815,7 @@ def status_cmd():
|
|
|
711
815
|
if url:
|
|
712
816
|
console.print(f"[bold]URL:[/bold] [underline]{url}[/underline]")
|
|
713
817
|
|
|
818
|
+
return resp
|
|
714
819
|
@app.command("open")
|
|
715
820
|
@handle_api_error
|
|
716
821
|
def open_cmd(
|
|
@@ -726,6 +831,8 @@ def open_cmd(
|
|
|
726
831
|
webbrowser.open(url)
|
|
727
832
|
console.print(f"[green]Opened:[/green] {url}")
|
|
728
833
|
|
|
834
|
+
return resp
|
|
835
|
+
|
|
729
836
|
@session_app.command("watch")
|
|
730
837
|
@handle_api_error
|
|
731
838
|
def watch_session_cmd(
|
|
@@ -759,7 +866,6 @@ def watch_session_cmd(
|
|
|
759
866
|
text.append("\nStructured Output:\n", style="bold")
|
|
760
867
|
text.append(json.dumps(so, indent=2))
|
|
761
868
|
text.append("\nPress Ctrl+C to stop watching.", style="dim")
|
|
762
|
-
return Panel(text, title="[bold cyan]Devin Watch[/bold cyan]", border_style="cyan")
|
|
763
869
|
|
|
764
870
|
console.print(f"[bold cyan]Watching session {sid}[/bold cyan] (Ctrl+C to stop)")
|
|
765
871
|
|
|
@@ -780,6 +886,7 @@ def watch_session_cmd(
|
|
|
780
886
|
return
|
|
781
887
|
|
|
782
888
|
console.print(f"[bold green]Session {resp.get('status_enum')}![/bold green]")
|
|
889
|
+
return resp
|
|
783
890
|
|
|
784
891
|
# --- Flat top-level aliases (backward compat with 0.1.x command structure) ---
|
|
785
892
|
|
|
@@ -791,6 +898,7 @@ def watch_cmd(
|
|
|
791
898
|
):
|
|
792
899
|
"""Alias for: devin sessions watch"""
|
|
793
900
|
watch_session_cmd(session_id=session_id, interval=interval, org=None)
|
|
901
|
+
return None
|
|
794
902
|
|
|
795
903
|
@app.command("message")
|
|
796
904
|
@handle_api_error
|
|
@@ -813,6 +921,7 @@ def message_cmd(
|
|
|
813
921
|
raise typer.Exit(1)
|
|
814
922
|
sessions.send_message(sid, msg_text)
|
|
815
923
|
console.print(f"[green]Message sent to {sid}[/green]")
|
|
924
|
+
return None
|
|
816
925
|
|
|
817
926
|
@app.command("terminate")
|
|
818
927
|
@handle_api_error
|
|
@@ -824,6 +933,7 @@ def terminate_cmd(
|
|
|
824
933
|
if typer.confirm(f"Terminate session {sid}?"):
|
|
825
934
|
sessions.terminate_session(sid)
|
|
826
935
|
console.print(f"[green]Session {sid} terminated.[/green]")
|
|
936
|
+
return None
|
|
827
937
|
|
|
828
938
|
@app.command("list-sessions")
|
|
829
939
|
@handle_api_error
|
|
@@ -844,6 +954,7 @@ def list_sessions_top_cmd(
|
|
|
844
954
|
for s in sess_list:
|
|
845
955
|
table.add_row(s.get("session_id"), s.get("status_enum"), s.get("title") or s.get("prompt", "")[:50])
|
|
846
956
|
console.print(table)
|
|
957
|
+
return resp
|
|
847
958
|
|
|
848
959
|
@app.command("create-session")
|
|
849
960
|
@handle_api_error
|
|
@@ -856,7 +967,12 @@ def create_session_top_cmd(
|
|
|
856
967
|
):
|
|
857
968
|
"""Create a new session (alias for: devin sessions create)."""
|
|
858
969
|
create_session_cmd(prompt=prompt, file=None, title=title, org=None, max_acu=None,
|
|
859
|
-
advanced_mode=None,
|
|
970
|
+
advanced_mode=None, playbook_id=None, child_playbook_id=None,
|
|
971
|
+
tags=None, repos=None, knowledge_ids=None, secret_ids=None,
|
|
972
|
+
session_links=None, attachment_urls=None, create_as_user_id=None,
|
|
973
|
+
bypass_approval=False, structured_output_schema=None,
|
|
974
|
+
force=force, wait=wait, interval=interval)
|
|
975
|
+
return None
|
|
860
976
|
|
|
861
977
|
@app.command("upload")
|
|
862
978
|
@handle_api_error
|
|
@@ -865,6 +981,7 @@ def upload_cmd(path: Path = typer.Argument(..., help="File to upload")):
|
|
|
865
981
|
resp = attachments.upload_file(str(path))
|
|
866
982
|
console.print(f"[green]Uploaded:[/green] {resp}")
|
|
867
983
|
|
|
984
|
+
return resp
|
|
868
985
|
@app.command("list-knowledge")
|
|
869
986
|
@handle_api_error
|
|
870
987
|
def list_knowledge_top_cmd():
|
|
@@ -878,6 +995,7 @@ def list_knowledge_top_cmd():
|
|
|
878
995
|
table.add_row(item.get("id"), item.get("title") or item.get("name"))
|
|
879
996
|
console.print(table)
|
|
880
997
|
|
|
998
|
+
return resp
|
|
881
999
|
@app.command("attach")
|
|
882
1000
|
@handle_api_error
|
|
883
1001
|
def attach_cmd(
|
|
@@ -895,6 +1013,7 @@ def attach_cmd(
|
|
|
895
1013
|
full_prompt = f"{prompt}\n\nATTACHMENT: \"{url}\""
|
|
896
1014
|
create_session_cmd(prompt=full_prompt, file=None, title=title, org=None, max_acu=None,
|
|
897
1015
|
advanced_mode=None, force=False, wait=wait, interval=interval)
|
|
1016
|
+
return None
|
|
898
1017
|
|
|
899
1018
|
@app.command("update-tags")
|
|
900
1019
|
@handle_api_error
|
|
@@ -907,6 +1026,8 @@ def update_tags_cmd(
|
|
|
907
1026
|
sessions.update_session_tags(sid, tags)
|
|
908
1027
|
console.print(f"[green]Tags updated for session {sid}.[/green]")
|
|
909
1028
|
|
|
1029
|
+
return None
|
|
1030
|
+
|
|
910
1031
|
@app.command("history")
|
|
911
1032
|
def history_cmd():
|
|
912
1033
|
"""Show the locally cached current session ID."""
|
|
@@ -915,6 +1036,9 @@ def history_cmd():
|
|
|
915
1036
|
console.print(f"Current local session: [cyan]{sid}[/cyan]")
|
|
916
1037
|
else:
|
|
917
1038
|
console.print("No current local session.")
|
|
1039
|
+
return {"current_session_id": sid}
|
|
1040
|
+
|
|
1041
|
+
return {"current_session_id": sid}
|
|
918
1042
|
|
|
919
1043
|
@app.command("messages")
|
|
920
1044
|
@handle_api_error
|
|
@@ -932,6 +1056,8 @@ def messages_cmd(
|
|
|
932
1056
|
content = m.get("message", "") or m.get("content", "")
|
|
933
1057
|
console.print(f"[bold cyan]{role}:[/bold cyan] {content}")
|
|
934
1058
|
|
|
1059
|
+
return resp
|
|
1060
|
+
|
|
935
1061
|
@app.command("get-session")
|
|
936
1062
|
@handle_api_error
|
|
937
1063
|
def get_session_top_cmd(
|
|
@@ -943,12 +1069,14 @@ def get_session_top_cmd(
|
|
|
943
1069
|
console.print(Panel(
|
|
944
1070
|
f"[bold]Status:[/bold] {resp.get('status_enum')}\n"
|
|
945
1071
|
f"[bold]URL:[/bold] {resp.get('url')}\n"
|
|
946
|
-
f"
|
|
1072
|
+
f"Created: {resp.get('created_at')}",
|
|
947
1073
|
title=f"Session {sid}"
|
|
948
1074
|
))
|
|
949
1075
|
if "structured_output" in resp:
|
|
950
1076
|
console.print("[bold]Structured Output:[/bold]")
|
|
951
1077
|
console.print(json.dumps(resp["structured_output"], indent=2))
|
|
1078
|
+
|
|
1079
|
+
return resp
|
|
952
1080
|
|
|
953
1081
|
@app.command("update-knowledge")
|
|
954
1082
|
@handle_api_error
|
|
@@ -961,6 +1089,15 @@ def update_knowledge_cmd(
|
|
|
961
1089
|
"""Update an existing knowledge entry."""
|
|
962
1090
|
knowledge.update_knowledge(knowledge_id, title=name, body=body, trigger=trigger)
|
|
963
1091
|
console.print(f"[green]Knowledge {knowledge_id} updated.[/green]")
|
|
1092
|
+
return None
|
|
1093
|
+
|
|
1094
|
+
@app.command("get-knowledge")
|
|
1095
|
+
@handle_api_error
|
|
1096
|
+
def get_knowledge_top_cmd(knowledge_id: str = typer.Argument(...)):
|
|
1097
|
+
"""Get content of a knowledge note (alias for: devin knowledge get)."""
|
|
1098
|
+
resp = knowledge.get_knowledge(knowledge_id)
|
|
1099
|
+
console.print(Panel(json.dumps(resp, indent=2), title=f"Knowledge: {knowledge_id}"))
|
|
1100
|
+
return resp
|
|
964
1101
|
|
|
965
1102
|
@app.command("update-playbook")
|
|
966
1103
|
@handle_api_error
|
|
@@ -981,8 +1118,9 @@ def update_playbook_top_cmd(
|
|
|
981
1118
|
if not data:
|
|
982
1119
|
console.print("[yellow]Nothing to update. Pass --title, --body, or --macro.[/yellow]")
|
|
983
1120
|
return
|
|
984
|
-
playbooks.update_playbook(playbook_id, title=title
|
|
1121
|
+
playbooks.update_playbook(playbook_id, title=title, body=body, macro=macro)
|
|
985
1122
|
console.print(f"[green]Playbook {playbook_id} updated.[/green]")
|
|
1123
|
+
return None
|
|
986
1124
|
|
|
987
1125
|
@app.command("delete-playbook")
|
|
988
1126
|
@handle_api_error
|
|
@@ -991,6 +1129,7 @@ def delete_playbook_top_cmd(playbook_id: str = typer.Argument(...)):
|
|
|
991
1129
|
if typer.confirm(f"Delete playbook {playbook_id}?"):
|
|
992
1130
|
playbooks.delete_playbook(playbook_id)
|
|
993
1131
|
console.print(f"[green]Playbook {playbook_id} deleted.[/green]")
|
|
1132
|
+
return None
|
|
994
1133
|
|
|
995
1134
|
@app.command("list-secrets")
|
|
996
1135
|
@handle_api_error
|
|
@@ -1006,6 +1145,7 @@ def list_secrets_top_cmd():
|
|
|
1006
1145
|
table.add_row(item.get("id", ""), item.get("name", ""))
|
|
1007
1146
|
console.print(table)
|
|
1008
1147
|
|
|
1148
|
+
return resp
|
|
1009
1149
|
@app.command("delete-secret")
|
|
1010
1150
|
@handle_api_error
|
|
1011
1151
|
def delete_secret_top_cmd(secret_id: str = typer.Argument(...)):
|
|
@@ -1013,6 +1153,7 @@ def delete_secret_top_cmd(secret_id: str = typer.Argument(...)):
|
|
|
1013
1153
|
if typer.confirm(f"Delete secret {secret_id}?"):
|
|
1014
1154
|
secrets.delete_secret(secret_id)
|
|
1015
1155
|
console.print(f"[green]Secret {secret_id} deleted.[/green]")
|
|
1156
|
+
return None
|
|
1016
1157
|
|
|
1017
1158
|
@app.command("chain")
|
|
1018
1159
|
@handle_api_error
|
|
@@ -1072,13 +1213,14 @@ def chain_cmd(
|
|
|
1072
1213
|
while True:
|
|
1073
1214
|
resp = sessions.get_session(current_sid)
|
|
1074
1215
|
status = resp.get("status_enum", "")
|
|
1075
|
-
if status in {"
|
|
1216
|
+
if status in {"finished", "stopped", "error"}:
|
|
1076
1217
|
console.print(f"Step {i+1} done (status: {status}).")
|
|
1077
1218
|
break
|
|
1078
1219
|
time.sleep(min(backoff, 10))
|
|
1079
1220
|
backoff = min(backoff * 1.5, 10)
|
|
1080
1221
|
|
|
1081
1222
|
console.print("[bold green]Chain completed![/bold green]")
|
|
1223
|
+
return resp
|
|
1082
1224
|
|
|
1083
1225
|
if __name__ == "__main__":
|
|
1084
1226
|
app()
|
|
@@ -22,6 +22,6 @@ def test_cli_handles_api_error():
|
|
|
22
22
|
# command is now 'sessions create'
|
|
23
23
|
result = runner.invoke(app, ["sessions", "create", "test prompt"])
|
|
24
24
|
assert result.exit_code == 1
|
|
25
|
-
assert "
|
|
25
|
+
assert "Error" in result.stdout
|
|
26
26
|
assert "API BOOM" in result.stdout
|
|
27
27
|
|
|
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
|