devin-cli 1.0.0__tar.gz → 1.1.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.1.1/CHANGELOG.md +35 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/PKG-INFO +11 -2
- {devin_cli-1.0.0 → devin_cli-1.1.1}/README.md +10 -1
- devin_cli-1.1.1/generate_facades.py +21 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/pyproject.toml +1 -1
- devin_cli-1.1.1/src/devin_cli/api/attachments.py +13 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/src/devin_cli/api/client.py +69 -27
- devin_cli-1.1.1/src/devin_cli/api/consumption.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/knowledge.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/members.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/organizations.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/playbooks.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/repositories.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/schedules.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/secrets.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/sessions.py +13 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/__init__.py +0 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/attachments.py +16 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/knowledge.py +49 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/playbooks.py +27 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/secrets.py +7 -0
- devin_cli-1.1.1/src/devin_cli/api/v1/sessions.py +65 -0
- devin_cli-1.1.1/src/devin_cli/api/v3/__init__.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/playbooks.py +4 -8
- {devin_cli-1.0.0 → devin_cli-1.1.1}/src/devin_cli/cli.py +130 -20
- devin_cli-1.1.1/src/devin_cli/config.py +153 -0
- devin_cli-1.1.1/test_duplicate.py +19 -0
- devin_cli-1.1.1/tests/conftest.py +8 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_playbooks.py +1 -1
- devin_cli-1.0.0/CHANGELOG.md +0 -12
- devin_cli-1.0.0/src/devin_cli/config.py +0 -86
- {devin_cli-1.0.0 → devin_cli-1.1.1}/.github/workflows/pypi-publish.yml +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/.gitignore +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/LICENSE +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/assets/logo.png +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/setup.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/src/devin_cli/__init__.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/src/devin_cli/api/__init__.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/attachments.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/consumption.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/knowledge.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/members.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/organizations.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/repositories.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/schedules.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/secrets.py +0 -0
- {devin_cli-1.0.0/src/devin_cli/api → devin_cli-1.1.1/src/devin_cli/api/v3}/sessions.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/__init__.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_attachments.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_knowledge.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_repositories.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_schedules.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_secrets.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_api/test_sessions.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_cli.py +0 -0
- {devin_cli-1.0.0 → devin_cli-1.1.1}/tests/test_config.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.1.1] - 2026-03-11
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed v1 API HTTP redirects stripping authorization headers on org-scoped endpoints (403 errors).
|
|
11
|
+
- Fixed `devin configure` crash due to a missing `Prompt` module import.
|
|
12
|
+
- Fixed v1 `devin sessions messages` command by creating a polyfill data extractor for legacy payloads.
|
|
13
|
+
- Fixed v1 `devin sessions insights` to gracefully exit instead of throwing a generic missing endpoint error.
|
|
14
|
+
- Fixed v1 API payload formats by universally switching from URL-encoded `data=` to strictly typed `json=` bodies.
|
|
15
|
+
- Fixed message display loop to properly fallback between v3 `content` and v1 `message` string definitions.
|
|
16
|
+
|
|
17
|
+
## [1.1.0] - 2026-03-11
|
|
18
|
+
### Added
|
|
19
|
+
- **Dual-Token Profiles**: Support for securely managing Personal (`apk_user_`) and Service Account (`cog_`) tokens via the `--profile` global flag.
|
|
20
|
+
- **Legacy API Proxy**: Native v1 API backward compatibility routing, allowing older legacy scripts to execute gracefully over the new v3 architecture.
|
|
21
|
+
- **Session Deduplication (Anti-Spam)**: Built-in SHA-256 caching of previous prompts to intercept and prevent accidental duplicate session creation loops.
|
|
22
|
+
- **Advanced Mode Auth**: Secure web browser intersection for `advanced_mode_url` requirements when initiating CLI sessions.
|
|
23
|
+
- **Playbook Safeguards**: Implemented strict 500KB payload limit warnings with manual overrides for Playbook creation and updates to prevent 413 HTTP crashes.
|
|
24
|
+
- **Dynamic Help Menus**: Typer help commands successfully dynamically reflect `(V1 Legacy Mode)` tags based on active profile switching.
|
|
25
|
+
- **Global `--version`**: New binary flag reporting accurate package metadata matching PyPI.
|
|
26
|
+
|
|
27
|
+
## [0.1.0] - 2024-05-23
|
|
28
|
+
### Added
|
|
29
|
+
- Initial release
|
|
30
|
+
- Session management (create, list, get, terminate)
|
|
31
|
+
- Message sending and history viewing
|
|
32
|
+
- Watch command for live status polling
|
|
33
|
+
- File attachments
|
|
34
|
+
- Knowledge, Playbook, and Secret management commands
|
|
35
|
+
- Shell completion support
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devin-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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
|
|
@@ -46,7 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
46
46
|
<a href="https://github.com/revanthpobala/devin-cli/actions/workflows/pypi-publish.yml"><img src="https://github.com/revanthpobala/devin-cli/actions/workflows/pypi-publish.yml/badge.svg" alt="Build Status"></a>
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
|
-
> **The first unofficial CLI for the world's first AI Software Engineer. Now
|
|
49
|
+
> **The first unofficial CLI for the world's first AI Software Engineer. Now upgraded to v1.1.0 with Multi-Profile support and legacy v1 backward-compatibility.**
|
|
50
50
|
|
|
51
51
|
Devin CLI is designed for high-velocity engineering teams. It strips away the friction of the web UI, allowing you to orchestrate autonomous agents, manage complex contexts, and automate multi-step development workflows through a robust, terminal-first interface. Built for performance, SEO, and developer productivity.
|
|
52
52
|
|
|
@@ -84,6 +84,15 @@ devin configure
|
|
|
84
84
|
devin sessions create -t "Identify and fix the race condition in our Redis cache layer"
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
## 🌟 What's New in v1.1.0
|
|
88
|
+
|
|
89
|
+
- **Dual-Token Profiles:** Seamlessly switch between personal (`apk_user_`) and Service Account (`cog_`) tokens on the fly using `--profile`.
|
|
90
|
+
- `devin configure --profile service`
|
|
91
|
+
- `devin --profile service sessions list`
|
|
92
|
+
- **Legacy v1 Backward Compatibility:** Need to run an older integration that depends on the original Devin API shapes? The CLI now acts as a dynamic proxy. Simply configure a profile to use `api_version: v1`, and the CLI will route authentic payloads directly to the deprecated v1 endpoints without breaking your v3 integrations.
|
|
93
|
+
- **Session Deduplication (Anti-Spam):** Avoid burning ACUs on accidental retries. The CLI now actively caches a SHA-256 hash of your last 50 prompts per profile, immediately halting and alerting you if you attempt to launch a duplicate session.
|
|
94
|
+
- **Advanced Mode Auth:** Automatically traps `advanced_mode_url` requirements and securely prompts you to finish the connection handshake directly in your web browser.
|
|
95
|
+
|
|
87
96
|
---
|
|
88
97
|
|
|
89
98
|
## 🛠 Command Cheat Sheet (v3 Architecture)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="https://github.com/revanthpobala/devin-cli/actions/workflows/pypi-publish.yml"><img src="https://github.com/revanthpobala/devin-cli/actions/workflows/pypi-publish.yml/badge.svg" alt="Build Status"></a>
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
-
> **The first unofficial CLI for the world's first AI Software Engineer. Now
|
|
13
|
+
> **The first unofficial CLI for the world's first AI Software Engineer. Now upgraded to v1.1.0 with Multi-Profile support and legacy v1 backward-compatibility.**
|
|
14
14
|
|
|
15
15
|
Devin CLI is designed for high-velocity engineering teams. It strips away the friction of the web UI, allowing you to orchestrate autonomous agents, manage complex contexts, and automate multi-step development workflows through a robust, terminal-first interface. Built for performance, SEO, and developer productivity.
|
|
16
16
|
|
|
@@ -48,6 +48,15 @@ devin configure
|
|
|
48
48
|
devin sessions create -t "Identify and fix the race condition in our Redis cache layer"
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## 🌟 What's New in v1.1.0
|
|
52
|
+
|
|
53
|
+
- **Dual-Token Profiles:** Seamlessly switch between personal (`apk_user_`) and Service Account (`cog_`) tokens on the fly using `--profile`.
|
|
54
|
+
- `devin configure --profile service`
|
|
55
|
+
- `devin --profile service sessions list`
|
|
56
|
+
- **Legacy v1 Backward Compatibility:** Need to run an older integration that depends on the original Devin API shapes? The CLI now acts as a dynamic proxy. Simply configure a profile to use `api_version: v1`, and the CLI will route authentic payloads directly to the deprecated v1 endpoints without breaking your v3 integrations.
|
|
57
|
+
- **Session Deduplication (Anti-Spam):** Avoid burning ACUs on accidental retries. The CLI now actively caches a SHA-256 hash of your last 50 prompts per profile, immediately halting and alerting you if you attempt to launch a duplicate session.
|
|
58
|
+
- **Advanced Mode Auth:** Automatically traps `advanced_mode_url` requirements and securely prompts you to finish the connection handshake directly in your web browser.
|
|
59
|
+
|
|
51
60
|
---
|
|
52
61
|
|
|
53
62
|
## 🛠 Command Cheat Sheet (v3 Architecture)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
modules = ["attachments", "consumption", "knowledge", "members", "organizations", "playbooks", "repositories", "schedules", "secrets", "sessions"]
|
|
4
|
+
|
|
5
|
+
for mod in modules:
|
|
6
|
+
content = f"""from devin_cli.config import config
|
|
7
|
+
import importlib
|
|
8
|
+
|
|
9
|
+
def _get_impl():
|
|
10
|
+
if config.api_version == "v1":
|
|
11
|
+
try:
|
|
12
|
+
return importlib.import_module(f"devin_cli.api.v1.{mod}")
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise NotImplementedError(f"'{mod}' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
15
|
+
return importlib.import_module(f"devin_cli.api.v3.{mod}")
|
|
16
|
+
|
|
17
|
+
def __getattr__(name):
|
|
18
|
+
return getattr(_get_impl(), name)
|
|
19
|
+
"""
|
|
20
|
+
with open(f"src/devin_cli/api/{mod}.py", "w") as f:
|
|
21
|
+
f.write(content)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.attachments")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'attachments' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.attachments")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -11,7 +11,7 @@ class APIError(Exception):
|
|
|
11
11
|
super().__init__(message)
|
|
12
12
|
self.status_code = status_code
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class BaseClient:
|
|
15
15
|
def __init__(self):
|
|
16
16
|
pass
|
|
17
17
|
|
|
@@ -43,19 +43,33 @@ class APIClient:
|
|
|
43
43
|
response.raise_for_status()
|
|
44
44
|
except httpx.HTTPStatusError as e:
|
|
45
45
|
status = e.response.status_code
|
|
46
|
+
path = str(e.request.url.path)
|
|
47
|
+
|
|
48
|
+
error_detail = ""
|
|
49
|
+
try:
|
|
50
|
+
error_body = e.response.json()
|
|
51
|
+
error_detail = error_body.get("detail") or error_body.get("message") or error_body.get("error") or str(error_body)
|
|
52
|
+
except Exception:
|
|
53
|
+
error_detail = e.response.text.strip()
|
|
54
|
+
|
|
55
|
+
msg_suffix = f"\n Path: {path}"
|
|
56
|
+
if error_detail:
|
|
57
|
+
msg_suffix += f"\n Detail: {error_detail}"
|
|
58
|
+
|
|
46
59
|
if status == 401:
|
|
47
|
-
raise APIError("Invalid or expired API token (401).
|
|
60
|
+
raise APIError(f"Invalid or expired API token (401).{msg_suffix}", status)
|
|
48
61
|
elif status == 403:
|
|
49
|
-
raise APIError("Insufficient permissions (403).", status)
|
|
62
|
+
raise APIError(f"Insufficient permissions (403).{msg_suffix}", status)
|
|
50
63
|
elif status == 404:
|
|
51
|
-
raise APIError("Resource not found (404).", status)
|
|
64
|
+
raise APIError(f"Resource not found (404).{msg_suffix}", status)
|
|
65
|
+
elif status == 422:
|
|
66
|
+
raise APIError(f"Validation Error (422).{msg_suffix}", status)
|
|
52
67
|
elif status == 429:
|
|
53
|
-
raise APIError("Rate limit exceeded (429). Please try again later.", status)
|
|
68
|
+
raise APIError(f"Rate limit exceeded (429). Please try again later.", status)
|
|
54
69
|
elif status >= 500:
|
|
55
|
-
raise APIError(f"Server error ({status}).", status)
|
|
70
|
+
raise APIError(f"Server error ({status}).{msg_suffix}", status)
|
|
56
71
|
else:
|
|
57
|
-
raise APIError(f"HTTP Error {status}
|
|
58
|
-
|
|
72
|
+
raise APIError(f"HTTP Error {status}.{msg_suffix}", status)
|
|
59
73
|
if response.status_code == 204:
|
|
60
74
|
return None
|
|
61
75
|
|
|
@@ -72,25 +86,12 @@ class APIClient:
|
|
|
72
86
|
|
|
73
87
|
return response.text
|
|
74
88
|
|
|
89
|
+
def _format_url(self, endpoint: str) -> str:
|
|
90
|
+
raise NotImplementedError()
|
|
91
|
+
|
|
75
92
|
def request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
76
93
|
self._ensure_token()
|
|
77
|
-
|
|
78
|
-
endpoint = endpoint.lstrip("/")
|
|
79
|
-
|
|
80
|
-
# Determine the base URL and whether to inject organization
|
|
81
|
-
if endpoint.startswith("v3beta1/"):
|
|
82
|
-
base = config.base_url.replace("/v3", "").replace("/v2", "").replace("/v1", "").rstrip("/")
|
|
83
|
-
url = f"{base}/{endpoint}"
|
|
84
|
-
# Inject organization for v3beta1 if not present
|
|
85
|
-
if config.org_id and "/organizations/" not in url:
|
|
86
|
-
url = url.replace("v3beta1/", f"v3beta1/organizations/{config.org_id}/")
|
|
87
|
-
elif endpoint.startswith("enterprise/"):
|
|
88
|
-
url = f"{self.BASE_URL}/{endpoint}"
|
|
89
|
-
else:
|
|
90
|
-
# Standard path, inject organization if configured
|
|
91
|
-
if config.org_id and not endpoint.startswith("organizations/"):
|
|
92
|
-
endpoint = f"organizations/{config.org_id}/{endpoint}"
|
|
93
|
-
url = f"{self.BASE_URL}/{endpoint}"
|
|
94
|
+
url = self._format_url(endpoint)
|
|
94
95
|
|
|
95
96
|
# Merge headers if needed, but usually self.headers is enough
|
|
96
97
|
headers = self.headers.copy()
|
|
@@ -102,7 +103,7 @@ class APIClient:
|
|
|
102
103
|
headers.pop("Content-Type", None)
|
|
103
104
|
|
|
104
105
|
try:
|
|
105
|
-
with httpx.Client(follow_redirects=
|
|
106
|
+
with httpx.Client(follow_redirects=False, verify=False, timeout=httpx.Timeout(120.0)) as client:
|
|
106
107
|
response = client.request(method, url, headers=headers, **kwargs)
|
|
107
108
|
return self._handle_response(response)
|
|
108
109
|
except httpx.RequestError as e:
|
|
@@ -120,4 +121,45 @@ class APIClient:
|
|
|
120
121
|
def delete(self, endpoint: str, **kwargs) -> Any:
|
|
121
122
|
return self.request("DELETE", endpoint, **kwargs)
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
class V3Client(BaseClient):
|
|
125
|
+
def _format_url(self, endpoint: str) -> str:
|
|
126
|
+
endpoint = endpoint.lstrip("/")
|
|
127
|
+
if endpoint.startswith("v3beta1/"):
|
|
128
|
+
base = config.base_url.replace("/v3", "").replace("/v2", "").replace("/v1", "").rstrip("/")
|
|
129
|
+
url = f"{base}/{endpoint}"
|
|
130
|
+
if config.org_id and "/organizations/" not in url:
|
|
131
|
+
url = url.replace("v3beta1/", f"v3beta1/organizations/{config.org_id}/")
|
|
132
|
+
return url
|
|
133
|
+
elif endpoint.startswith("enterprise/"):
|
|
134
|
+
return f"{self.BASE_URL}/{endpoint}"
|
|
135
|
+
else:
|
|
136
|
+
if config.org_id and not endpoint.startswith("organizations/"):
|
|
137
|
+
endpoint = f"organizations/{config.org_id}/{endpoint}"
|
|
138
|
+
return f"{self.BASE_URL}/{endpoint}"
|
|
139
|
+
|
|
140
|
+
class V1Client(BaseClient):
|
|
141
|
+
def _format_url(self, endpoint: str) -> str:
|
|
142
|
+
return f"{self.BASE_URL}/{endpoint.lstrip('/')}"
|
|
143
|
+
|
|
144
|
+
class ClientProxy:
|
|
145
|
+
def __init__(self):
|
|
146
|
+
self._v1 = V1Client()
|
|
147
|
+
self._v3 = V3Client()
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def _client(self) -> BaseClient:
|
|
151
|
+
return self._v1 if config.api_version == "v1" else self._v3
|
|
152
|
+
|
|
153
|
+
def get(self, endpoint: str, **kwargs) -> Any:
|
|
154
|
+
return self._client.get(endpoint, **kwargs)
|
|
155
|
+
|
|
156
|
+
def post(self, endpoint: str, **kwargs) -> Any:
|
|
157
|
+
return self._client.post(endpoint, **kwargs)
|
|
158
|
+
|
|
159
|
+
def put(self, endpoint: str, **kwargs) -> Any:
|
|
160
|
+
return self._client.put(endpoint, **kwargs)
|
|
161
|
+
|
|
162
|
+
def delete(self, endpoint: str, **kwargs) -> Any:
|
|
163
|
+
return self._client.delete(endpoint, **kwargs)
|
|
164
|
+
|
|
165
|
+
client = ClientProxy()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.consumption")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'consumption' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.consumption")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.knowledge")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'knowledge' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.knowledge")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.members")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'members' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.members")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.organizations")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'organizations' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.organizations")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.playbooks")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'playbooks' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.playbooks")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.repositories")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'repositories' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.repositories")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.schedules")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'schedules' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.schedules")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.secrets")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'secrets' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.secrets")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from devin_cli.config import config
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
def _get_impl():
|
|
5
|
+
if config.api_version == "v1":
|
|
6
|
+
try:
|
|
7
|
+
return importlib.import_module(f"devin_cli.api.v1.sessions")
|
|
8
|
+
except ImportError:
|
|
9
|
+
raise NotImplementedError(f"'sessions' feature is only available in Devin API v3, or is not implemented for v1. Run 'devin configure' to switch your API version to v3.")
|
|
10
|
+
return importlib.import_module(f"devin_cli.api.v3.sessions")
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
return getattr(_get_impl(), name)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from devin_cli.api.client import client
|
|
4
|
+
|
|
5
|
+
def upload_file(file_path: str, **kwargs):
|
|
6
|
+
path = Path(file_path)
|
|
7
|
+
mime_type, _ = mimetypes.guess_type(path)
|
|
8
|
+
mime_type = mime_type or "application/octet-stream"
|
|
9
|
+
|
|
10
|
+
with open(path, "rb") as f:
|
|
11
|
+
# V1 used multipart/form-data with 'file' argument
|
|
12
|
+
return client.post("attachments", files={"file": (path.name, f, mime_type)})
|
|
13
|
+
|
|
14
|
+
def download_attachment(attachment_id: str, file_name: str, **kwargs):
|
|
15
|
+
# Depending on exactly what v1 required, it might just be the direct path or stream
|
|
16
|
+
return client.get(f"attachments/{attachment_id}")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from devin_cli.api.client import client
|
|
3
|
+
|
|
4
|
+
def list_knowledge():
|
|
5
|
+
return client.get("knowledge")
|
|
6
|
+
|
|
7
|
+
def create_knowledge(
|
|
8
|
+
name: str,
|
|
9
|
+
body: str,
|
|
10
|
+
trigger_description: str,
|
|
11
|
+
parent_folder_id: str = None,
|
|
12
|
+
pinned_repo: str = None,
|
|
13
|
+
title: str = None, # v3 fallback
|
|
14
|
+
**kwargs
|
|
15
|
+
):
|
|
16
|
+
data = {
|
|
17
|
+
"name": name or title,
|
|
18
|
+
"body": body,
|
|
19
|
+
"trigger_description": trigger_description or ""
|
|
20
|
+
}
|
|
21
|
+
if macro:
|
|
22
|
+
data["macro"] = macro
|
|
23
|
+
if parent_folder_id:
|
|
24
|
+
data["parent_folder_id"] = parent_folder_id
|
|
25
|
+
if pinned_repo:
|
|
26
|
+
data["pinned_repo"] = pinned_repo
|
|
27
|
+
|
|
28
|
+
return client.post("knowledge", json=data)
|
|
29
|
+
|
|
30
|
+
def update_knowledge(
|
|
31
|
+
knowledge_id: str,
|
|
32
|
+
name: str = None,
|
|
33
|
+
body: str = None,
|
|
34
|
+
trigger_description: str = None,
|
|
35
|
+
title: str = None, # v3 fallback
|
|
36
|
+
**kwargs
|
|
37
|
+
):
|
|
38
|
+
data = {}
|
|
39
|
+
if name or title:
|
|
40
|
+
data["name"] = name or title
|
|
41
|
+
if body:
|
|
42
|
+
data["body"] = body
|
|
43
|
+
if trigger_description:
|
|
44
|
+
data["trigger_description"] = trigger_description
|
|
45
|
+
|
|
46
|
+
return client.put(f"knowledge/{knowledge_id}", json=data)
|
|
47
|
+
|
|
48
|
+
def delete_knowledge(knowledge_id: str):
|
|
49
|
+
return client.delete(f"knowledge/{knowledge_id}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from devin_cli.api.client import client
|
|
3
|
+
|
|
4
|
+
def list_playbooks():
|
|
5
|
+
return client.get("playbooks")
|
|
6
|
+
|
|
7
|
+
def create_playbook(title: str, body: str, macro: str = None, **kwargs):
|
|
8
|
+
data = {"title": title, "body": body}
|
|
9
|
+
if macro:
|
|
10
|
+
data["macro"] = macro
|
|
11
|
+
return client.post("playbooks", json=data)
|
|
12
|
+
|
|
13
|
+
def get_playbook(playbook_id: str):
|
|
14
|
+
return client.get(f"playbooks/{playbook_id}")
|
|
15
|
+
|
|
16
|
+
def update_playbook(playbook_id: str, title: str = None, body: str = None, macro: str = None, **kwargs):
|
|
17
|
+
data = {}
|
|
18
|
+
if title:
|
|
19
|
+
data["title"] = title
|
|
20
|
+
if body:
|
|
21
|
+
data["body"] = body
|
|
22
|
+
if macro:
|
|
23
|
+
data["macro"] = macro
|
|
24
|
+
return client.put(f"playbooks/{playbook_id}", json=data)
|
|
25
|
+
|
|
26
|
+
def delete_playbook(playbook_id: str):
|
|
27
|
+
return client.delete(f"playbooks/{playbook_id}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from devin_cli.api.client import client
|
|
3
|
+
|
|
4
|
+
def list_sessions(limit: int = 100, offset: int = 0, tags: List[str] = None):
|
|
5
|
+
params = {"limit": limit, "offset": offset}
|
|
6
|
+
if tags:
|
|
7
|
+
params["tags"] = tags
|
|
8
|
+
return client.get("sessions", params=params)
|
|
9
|
+
|
|
10
|
+
def create_session(
|
|
11
|
+
prompt: str,
|
|
12
|
+
idempotent: bool = False,
|
|
13
|
+
snapshot_id: str = None,
|
|
14
|
+
playbook_id: str = None,
|
|
15
|
+
unlisted: bool = False,
|
|
16
|
+
tags: List[str] = None,
|
|
17
|
+
session_secrets: List[dict] = None,
|
|
18
|
+
title: Optional[str] = None,
|
|
19
|
+
knowledge_ids: Optional[List[str]] = None,
|
|
20
|
+
secret_ids: Optional[List[str]] = None,
|
|
21
|
+
max_acu_limit: Optional[int] = None,
|
|
22
|
+
**kwargs
|
|
23
|
+
):
|
|
24
|
+
data = {
|
|
25
|
+
"prompt": prompt,
|
|
26
|
+
"idempotent": idempotent,
|
|
27
|
+
"unlisted": unlisted
|
|
28
|
+
}
|
|
29
|
+
if snapshot_id:
|
|
30
|
+
data["snapshot_id"] = snapshot_id
|
|
31
|
+
if playbook_id:
|
|
32
|
+
data["playbook_id"] = playbook_id
|
|
33
|
+
if tags:
|
|
34
|
+
data["tags"] = tags
|
|
35
|
+
if session_secrets:
|
|
36
|
+
data["session_secrets"] = session_secrets
|
|
37
|
+
if title:
|
|
38
|
+
data["title"] = title
|
|
39
|
+
if knowledge_ids:
|
|
40
|
+
data["knowledge_ids"] = knowledge_ids
|
|
41
|
+
if secret_ids:
|
|
42
|
+
data["secret_ids"] = secret_ids
|
|
43
|
+
if max_acu_limit:
|
|
44
|
+
data["max_acu_limit"] = max_acu_limit
|
|
45
|
+
|
|
46
|
+
return client.post("sessions", json=data)
|
|
47
|
+
|
|
48
|
+
def get_session(session_id: str):
|
|
49
|
+
return client.get(f"sessions/{session_id}")
|
|
50
|
+
|
|
51
|
+
def get_session_messages(session_id: str):
|
|
52
|
+
resp = get_session(session_id)
|
|
53
|
+
return {"messages": resp.get("messages", [])}
|
|
54
|
+
|
|
55
|
+
def get_session_insights(session_id: str):
|
|
56
|
+
return {"error": "Insights are not available in the v1 API. Please upgrade to v3."}
|
|
57
|
+
|
|
58
|
+
def send_message(session_id: str, message: str):
|
|
59
|
+
return client.post(f"sessions/{session_id}/message", json={"message": message})
|
|
60
|
+
|
|
61
|
+
def update_session_tags(session_id: str, tags: List[str]):
|
|
62
|
+
return client.put(f"sessions/{session_id}/tags", json={"tags": tags})
|
|
63
|
+
|
|
64
|
+
def terminate_session(session_id: str):
|
|
65
|
+
return client.delete(f"sessions/{session_id}")
|
|
File without changes
|
|
@@ -13,10 +13,8 @@ def create_playbook(title: str, body: str, macro: Optional[str] = None):
|
|
|
13
13
|
def get_playbook(playbook_id: str):
|
|
14
14
|
return client.get(f"playbooks/{playbook_id}")
|
|
15
15
|
|
|
16
|
-
def update_playbook(playbook_id: str, title:
|
|
17
|
-
data = {}
|
|
18
|
-
if title:
|
|
19
|
-
data["title"] = title
|
|
16
|
+
def update_playbook(playbook_id: str, title: str, body: Optional[str] = None, macro: Optional[str] = None):
|
|
17
|
+
data = {"title": title}
|
|
20
18
|
if body:
|
|
21
19
|
data["body"] = body
|
|
22
20
|
if macro:
|
|
@@ -39,10 +37,8 @@ def create_enterprise_playbook(title: str, body: str, macro: Optional[str] = Non
|
|
|
39
37
|
def get_enterprise_playbook(playbook_id: str):
|
|
40
38
|
return client.get(f"enterprise/playbooks/{playbook_id}")
|
|
41
39
|
|
|
42
|
-
def update_enterprise_playbook(playbook_id: str, title:
|
|
43
|
-
data = {}
|
|
44
|
-
if title:
|
|
45
|
-
data["title"] = title
|
|
40
|
+
def update_enterprise_playbook(playbook_id: str, title: str, body: Optional[str] = None, macro: Optional[str] = None):
|
|
41
|
+
data = {"title": title}
|
|
46
42
|
if body:
|
|
47
43
|
data["body"] = body
|
|
48
44
|
if macro:
|
|
@@ -16,23 +16,45 @@ from devin_cli.api.client import client, APIError
|
|
|
16
16
|
import webbrowser
|
|
17
17
|
import functools
|
|
18
18
|
import sys
|
|
19
|
+
import hashlib
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
from rich.prompt import Prompt
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = importlib.metadata.version("devin-cli")
|
|
25
|
+
except importlib.metadata.PackageNotFoundError:
|
|
26
|
+
__version__ = "unknown"
|
|
27
|
+
|
|
28
|
+
# Early profile detection for accurate --help menus
|
|
29
|
+
if "--profile" in sys.argv:
|
|
30
|
+
idx = sys.argv.index("--profile")
|
|
31
|
+
if idx + 1 < len(sys.argv):
|
|
32
|
+
config.active_profile = sys.argv[idx + 1]
|
|
33
|
+
elif "-p" in sys.argv:
|
|
34
|
+
idx = sys.argv.index("-p")
|
|
35
|
+
if idx + 1 < len(sys.argv):
|
|
36
|
+
config.active_profile = sys.argv[idx + 1]
|
|
37
|
+
|
|
38
|
+
IS_V1 = config.api_version == "v1"
|
|
39
|
+
v1_tag = " [bold yellow](Legacy v1 API)[/bold yellow]" if IS_V1 else ""
|
|
40
|
+
v3_only = " [bold red](v3 Only)[/bold red]" if IS_V1 else ""
|
|
19
41
|
|
|
20
42
|
app = typer.Typer(
|
|
21
|
-
help="Unofficial CLI for Devin AI v3",
|
|
43
|
+
help=f"Unofficial CLI for Devin AI{' (V1 Legacy Mode)' if IS_V1 else ' v3'}",
|
|
22
44
|
no_args_is_help=True,
|
|
23
45
|
rich_markup_mode="rich"
|
|
24
46
|
)
|
|
25
47
|
console = Console()
|
|
26
48
|
|
|
27
49
|
# --- Sub-Apps for Organization API ---
|
|
28
|
-
session_app = typer.Typer(help="Manage Devin sessions", no_args_is_help=True)
|
|
29
|
-
knowledge_app = typer.Typer(help="Manage knowledge notes", no_args_is_help=True)
|
|
30
|
-
playbook_app = typer.Typer(help="Manage team playbooks", no_args_is_help=True)
|
|
31
|
-
secret_app = typer.Typer(help="Manage organization secrets", no_args_is_help=True)
|
|
32
|
-
schedule_app = typer.Typer(help="Manage session schedules", no_args_is_help=True)
|
|
33
|
-
attachment_app = typer.Typer(help="Manage session attachments", no_args_is_help=True)
|
|
34
|
-
repo_app = typer.Typer(help="Manage organization repositories", no_args_is_help=True)
|
|
35
|
-
enterprise_app = typer.Typer(help="Enterprise-scoped operations", no_args_is_help=True)
|
|
50
|
+
session_app = typer.Typer(help=f"Manage Devin sessions{v1_tag}", no_args_is_help=True)
|
|
51
|
+
knowledge_app = typer.Typer(help=f"Manage knowledge notes{v1_tag}", no_args_is_help=True)
|
|
52
|
+
playbook_app = typer.Typer(help=f"Manage team playbooks{v1_tag}", no_args_is_help=True)
|
|
53
|
+
secret_app = typer.Typer(help=f"Manage organization secrets{v1_tag}", no_args_is_help=True)
|
|
54
|
+
schedule_app = typer.Typer(help=f"Manage session schedules{v3_only}", no_args_is_help=True)
|
|
55
|
+
attachment_app = typer.Typer(help=f"Manage session attachments{v1_tag}", no_args_is_help=True)
|
|
56
|
+
repo_app = typer.Typer(help=f"Manage organization repositories{v3_only}", no_args_is_help=True)
|
|
57
|
+
enterprise_app = typer.Typer(help=f"Enterprise-scoped operations{v3_only}", no_args_is_help=True)
|
|
36
58
|
|
|
37
59
|
app.add_typer(session_app, name="sessions", help="Manage Devin sessions")
|
|
38
60
|
app.add_typer(session_app, name="session", hidden=True)
|
|
@@ -53,11 +75,20 @@ ASCII_LOGO = r"""
|
|
|
53
75
|
[/bold cyan]
|
|
54
76
|
"""
|
|
55
77
|
|
|
56
|
-
@app.callback()
|
|
57
|
-
def main(
|
|
78
|
+
@app.callback(invoke_without_command=True)
|
|
79
|
+
def main(
|
|
80
|
+
ctx: typer.Context,
|
|
81
|
+
profile: str = typer.Option("default", "--profile", "-p", help="Configuration profile to use (e.g., service, personal)"),
|
|
82
|
+
version: Optional[bool] = typer.Option(None, "--version", "-v", help="Show the application's version and exit."),
|
|
83
|
+
):
|
|
58
84
|
"""
|
|
59
85
|
Unofficial CLI for Devin AI v3.
|
|
60
86
|
"""
|
|
87
|
+
if version:
|
|
88
|
+
console.print(f"devin CLI version: {__version__}")
|
|
89
|
+
raise typer.Exit()
|
|
90
|
+
|
|
91
|
+
config.active_profile = profile
|
|
61
92
|
if ctx.invoked_subcommand is None:
|
|
62
93
|
console.print(ASCII_LOGO)
|
|
63
94
|
|
|
@@ -89,16 +120,28 @@ def configure(
|
|
|
89
120
|
token: str = typer.Option(..., prompt="Devin API Token (starts with apk_ or cog_)", help="Your Devin API Token"),
|
|
90
121
|
base_url: str = typer.Option("https://api.devin.ai/v3", prompt="Devin API Base URL", help="Devin API Base URL"),
|
|
91
122
|
org_id: Optional[str] = typer.Option(None, "--org", prompt="Organization ID (optional)", help="Default Organization ID"),
|
|
123
|
+
profile: Optional[str] = typer.Option(None, "--profile", help="Profile to configure (overrides global --profile)"),
|
|
92
124
|
):
|
|
93
125
|
"""
|
|
94
126
|
Configure the CLI with your Devin API token and organization.
|
|
95
127
|
"""
|
|
128
|
+
if profile:
|
|
129
|
+
config.active_profile = profile
|
|
130
|
+
|
|
96
131
|
# Validation based on v3 requirements
|
|
97
132
|
if not (token.startswith("apk_") or token.startswith("cog_")):
|
|
98
133
|
console.print("[bold yellow]Warning:[/bold yellow] Token format might be outdated. v3 tokens usually start with 'apk_' or 'cog_'.")
|
|
99
134
|
|
|
135
|
+
api_version_prompt = Prompt.ask(f"API Version for profile '{config.active_profile}' (v3/v1)", default=config.api_version or "v3")
|
|
136
|
+
|
|
137
|
+
if "api.devin.ai" in base_url and "v3" in base_url and api_version_prompt == "v1":
|
|
138
|
+
base_url = base_url.replace("/v3", "/v1")
|
|
139
|
+
elif "api.devin.ai" in base_url and "v1" in base_url and api_version_prompt == "v3":
|
|
140
|
+
base_url = base_url.replace("/v1", "/v3")
|
|
141
|
+
|
|
100
142
|
config.api_token = token
|
|
101
143
|
config.base_url = base_url
|
|
144
|
+
config.api_version = api_version_prompt
|
|
102
145
|
if org_id:
|
|
103
146
|
config.org_id = org_id
|
|
104
147
|
console.print(f"[green]Configuration saved to {config.config_file}[/green]")
|
|
@@ -112,6 +155,8 @@ def create_session_cmd(
|
|
|
112
155
|
title: Optional[str] = typer.Option(None, "--title", "-t", help="Custom session title"),
|
|
113
156
|
org: Optional[str] = typer.Option(None, "--org", help="Override organization ID"),
|
|
114
157
|
max_acu: Optional[int] = typer.Option(None, "--max-acu", help="Maximum ACU limit"),
|
|
158
|
+
advanced_mode: Optional[str] = typer.Option(None, "--advanced-mode", help="Enable advanced mode (e.g. CLI, browser) for the session"),
|
|
159
|
+
force: bool = typer.Option(False, "--force", help="Force creation even if duplicate prompt is detected"),
|
|
115
160
|
):
|
|
116
161
|
"""Create a new Devin session."""
|
|
117
162
|
if org: config.temporary_org_id = org
|
|
@@ -122,13 +167,44 @@ def create_session_cmd(
|
|
|
122
167
|
else:
|
|
123
168
|
console.print("[bold red]Error:[/bold red] Must provide prompt or --file")
|
|
124
169
|
raise typer.Exit(1)
|
|
170
|
+
|
|
171
|
+
prompt_hash = hashlib.sha256(prompt_text.encode('utf-8')).hexdigest()
|
|
172
|
+
existing_sid = config.get_session_by_prompt_hash(prompt_hash)
|
|
173
|
+
|
|
174
|
+
if existing_sid and not force:
|
|
175
|
+
console.print(f"[bold yellow]Duplicate Detected:[/bold yellow] You recently created a session with this exact prompt.")
|
|
176
|
+
console.print(f"Existing Session ID: [bold cyan]{existing_sid}[/bold cyan]")
|
|
177
|
+
import typer
|
|
178
|
+
if not typer.confirm("Are you sure you want to create a duplicate session?"):
|
|
179
|
+
console.print("Session creation cancelled. Use the existing session ID above to resume.")
|
|
180
|
+
raise typer.Exit()
|
|
125
181
|
|
|
126
182
|
with console.status("[bold green]Creating session...[/bold green]"):
|
|
127
|
-
resp = sessions.create_session(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
resp = sessions.create_session(
|
|
184
|
+
prompt=prompt_text,
|
|
185
|
+
title=title,
|
|
186
|
+
max_acu_limit=max_acu,
|
|
187
|
+
advanced_mode=advanced_mode
|
|
188
|
+
)
|
|
189
|
+
sid = resp.get("session_id")
|
|
190
|
+
|
|
191
|
+
if "advanced_mode_url" in resp:
|
|
192
|
+
adv_url = resp["advanced_mode_url"]
|
|
193
|
+
console.print(f"[bold yellow]Advanced Mode Authorization Required![/bold yellow]")
|
|
194
|
+
console.print(f"Please complete verify the advanced mode setup in your browser:")
|
|
195
|
+
console.print(f"[bold cyan]{adv_url}[/bold cyan]")
|
|
196
|
+
import typer
|
|
197
|
+
if typer.confirm("Open browser now?"):
|
|
198
|
+
import webbrowser
|
|
199
|
+
webbrowser.open(adv_url)
|
|
200
|
+
|
|
201
|
+
if sid:
|
|
202
|
+
config.current_session_id = sid
|
|
203
|
+
config.save_prompt_hash(prompt_hash, sid)
|
|
204
|
+
console.print(f"[green]Session created:[/green] {sid}")
|
|
205
|
+
console.print(f"[bold cyan]URL:[/bold cyan] {resp.get('url')}")
|
|
206
|
+
else:
|
|
207
|
+
console.print("[yellow]Session created, but no ID returned immediately (awaiting advanced mode setup).[/yellow]")
|
|
132
208
|
|
|
133
209
|
@session_app.command("list")
|
|
134
210
|
@handle_api_error
|
|
@@ -141,6 +217,10 @@ def list_sessions_cmd(
|
|
|
141
217
|
if org: config.temporary_org_id = org
|
|
142
218
|
resp = sessions.list_sessions(limit=limit)
|
|
143
219
|
sess_list = resp.get("sessions", [])
|
|
220
|
+
|
|
221
|
+
if not sess_list and config.api_token and config.api_token.startswith("cog_"):
|
|
222
|
+
console.print("[yellow]Warning: Service tokens (cog_) may only have visibility into sessions they explicitly created, not all organization sessions.[/yellow]")
|
|
223
|
+
|
|
144
224
|
if json_output:
|
|
145
225
|
console.print(json.dumps(sess_list, indent=2))
|
|
146
226
|
else:
|
|
@@ -179,7 +259,7 @@ def session_insights_cmd(
|
|
|
179
259
|
@session_app.command("cost")
|
|
180
260
|
@handle_api_error
|
|
181
261
|
def session_cost_cmd(
|
|
182
|
-
session_id: Optional[str] = typer.
|
|
262
|
+
session_id: Optional[str] = typer.Argument(None, help="Specific session ID to check cost for"),
|
|
183
263
|
org: Optional[str] = typer.Option(None, "--org"),
|
|
184
264
|
):
|
|
185
265
|
"""View ACU consumption."""
|
|
@@ -216,7 +296,7 @@ def list_messages_cmd(
|
|
|
216
296
|
msgs = resp.get("messages", [])
|
|
217
297
|
for m in msgs:
|
|
218
298
|
role = m.get("role", "unknown")
|
|
219
|
-
content = m.get("content", "")
|
|
299
|
+
content = m.get("message", "") or m.get("content", "")
|
|
220
300
|
console.print(f"[bold cyan]{role}:[/bold cyan] {content}")
|
|
221
301
|
console.print("---")
|
|
222
302
|
|
|
@@ -279,13 +359,36 @@ def list_playbooks_cmd(org: Optional[str] = typer.Option(None, "--org")):
|
|
|
279
359
|
"""List team playbooks."""
|
|
280
360
|
if org: config.temporary_org_id = org
|
|
281
361
|
resp = playbooks.list_playbooks()
|
|
362
|
+
items = resp.get("items", []) if isinstance(resp, dict) else resp
|
|
282
363
|
table = Table(title="Playbooks")
|
|
283
364
|
table.add_column("ID", style="cyan")
|
|
284
365
|
table.add_column("Title")
|
|
285
|
-
for item in
|
|
286
|
-
|
|
366
|
+
for item in items:
|
|
367
|
+
if isinstance(item, dict):
|
|
368
|
+
table.add_row(item.get("playbook_id", ""), item.get("title", ""))
|
|
287
369
|
console.print(table)
|
|
288
370
|
|
|
371
|
+
@playbook_app.command("update")
|
|
372
|
+
@handle_api_error
|
|
373
|
+
def update_playbook_cmd(
|
|
374
|
+
playbook_id: str,
|
|
375
|
+
title: str,
|
|
376
|
+
body: Optional[str] = typer.Option(None, "--body"),
|
|
377
|
+
macro: Optional[str] = typer.Option(None, "--macro"),
|
|
378
|
+
org: Optional[str] = typer.Option(None, "--org"),
|
|
379
|
+
):
|
|
380
|
+
"""Update a team playbook."""
|
|
381
|
+
body_bytes = len(body.encode('utf-8')) if body else 0
|
|
382
|
+
if body_bytes > 500 * 1024:
|
|
383
|
+
console.print(f"[bold yellow]Warning:[/bold yellow] Playbook body is {body_bytes / 1024:.1f}KB. Devin API may reject payloads > 500KB.")
|
|
384
|
+
import typer
|
|
385
|
+
if not typer.confirm("Attempt to send anyway?"):
|
|
386
|
+
raise typer.Exit()
|
|
387
|
+
|
|
388
|
+
if org: config.temporary_org_id = org
|
|
389
|
+
resp = playbooks.update_playbook(playbook_id, title=title, body=body, macro=macro)
|
|
390
|
+
console.print(f"[green]Playbook updated:[/green] {playbook_id}")
|
|
391
|
+
|
|
289
392
|
@playbook_app.command("create")
|
|
290
393
|
@handle_api_error
|
|
291
394
|
def create_playbook_cmd(
|
|
@@ -295,6 +398,13 @@ def create_playbook_cmd(
|
|
|
295
398
|
org: Optional[str] = typer.Option(None, "--org"),
|
|
296
399
|
):
|
|
297
400
|
"""Create a new team playbook."""
|
|
401
|
+
body_bytes = len(body.encode('utf-8')) if body else 0
|
|
402
|
+
if body_bytes > 500 * 1024:
|
|
403
|
+
console.print(f"[bold yellow]Warning:[/bold yellow] Playbook body is {body_bytes / 1024:.1f}KB. Devin API may reject payloads > 500KB.")
|
|
404
|
+
import typer
|
|
405
|
+
if not typer.confirm("Attempt to send anyway?"):
|
|
406
|
+
raise typer.Exit()
|
|
407
|
+
|
|
298
408
|
if org: config.temporary_org_id = org
|
|
299
409
|
resp = playbooks.create_playbook(title, body, macro)
|
|
300
410
|
console.print(f"[green]Playbook created:[/green] {resp.get('playbook_id')}")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".config" / "devin"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
|
+
|
|
9
|
+
class Config:
|
|
10
|
+
def __init__(self, config_dir: Path = None):
|
|
11
|
+
self._config_dir = config_dir or CONFIG_DIR
|
|
12
|
+
self._config_file = self._config_dir / "config.json"
|
|
13
|
+
self._ensure_config_exists()
|
|
14
|
+
self._load()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def config_file(self) -> Path:
|
|
18
|
+
return self._config_file
|
|
19
|
+
|
|
20
|
+
def _ensure_config_exists(self):
|
|
21
|
+
if not self._config_dir.exists():
|
|
22
|
+
self._config_dir.mkdir(parents=True, mode=0o700)
|
|
23
|
+
if not self._config_file.exists():
|
|
24
|
+
with open(self._config_file, "w") as f:
|
|
25
|
+
json.dump({}, f)
|
|
26
|
+
self._config_file.chmod(0o600)
|
|
27
|
+
|
|
28
|
+
def _load(self):
|
|
29
|
+
try:
|
|
30
|
+
with open(self._config_file, "r") as f:
|
|
31
|
+
self._data = json.load(f)
|
|
32
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
33
|
+
self._data = {}
|
|
34
|
+
|
|
35
|
+
if "profiles" not in self._data:
|
|
36
|
+
# Migrate legacy flat config to default profile
|
|
37
|
+
legacy_keys = ["api_token", "base_url", "org_id", "api_version", "current_session_id"]
|
|
38
|
+
legacy_data = {}
|
|
39
|
+
for key in legacy_keys:
|
|
40
|
+
if key in self._data:
|
|
41
|
+
legacy_data[key] = self._data.pop(key)
|
|
42
|
+
self._data["profiles"] = {
|
|
43
|
+
"default": legacy_data
|
|
44
|
+
}
|
|
45
|
+
if "active_profile" not in self._data:
|
|
46
|
+
self._data["active_profile"] = "default"
|
|
47
|
+
self._save()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save(self):
|
|
51
|
+
with open(self._config_file, "w") as f:
|
|
52
|
+
json.dump(self._data, f, indent=2)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def active_profile(self) -> str:
|
|
56
|
+
return getattr(self, "_runtime_profile", None) or self._data.get("active_profile", "default")
|
|
57
|
+
|
|
58
|
+
@active_profile.setter
|
|
59
|
+
def active_profile(self, value: str):
|
|
60
|
+
self._runtime_profile = value
|
|
61
|
+
if "profiles" not in self._data:
|
|
62
|
+
self._data["profiles"] = {}
|
|
63
|
+
if value not in self._data["profiles"]:
|
|
64
|
+
self._data["profiles"][value] = {}
|
|
65
|
+
|
|
66
|
+
def _get_profile_data(self) -> dict:
|
|
67
|
+
return self._data.get("profiles", {}).get(self.active_profile, {})
|
|
68
|
+
|
|
69
|
+
def _set_profile_data(self, key: str, value: str):
|
|
70
|
+
profile_name = self.active_profile
|
|
71
|
+
if "profiles" not in self._data:
|
|
72
|
+
self._data["profiles"] = {}
|
|
73
|
+
if profile_name not in self._data["profiles"]:
|
|
74
|
+
self._data["profiles"][profile_name] = {}
|
|
75
|
+
self._data["profiles"][profile_name][key] = value
|
|
76
|
+
self._save()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def api_token(self) -> Optional[str]:
|
|
80
|
+
# Env var takes precedence
|
|
81
|
+
return os.environ.get("DEVIN_API_TOKEN") or self._get_profile_data().get("api_token")
|
|
82
|
+
|
|
83
|
+
@api_token.setter
|
|
84
|
+
def api_token(self, value: str):
|
|
85
|
+
self._set_profile_data("api_token", value)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def org_id(self) -> Optional[str]:
|
|
89
|
+
# Temporary override takes highest precedence (CLI flag)
|
|
90
|
+
# then env var, then config file
|
|
91
|
+
return getattr(self, "_temporary_org_id", None) or os.environ.get("DEVIN_ORG_ID") or self._get_profile_data().get("org_id")
|
|
92
|
+
|
|
93
|
+
@org_id.setter
|
|
94
|
+
def org_id(self, value: str):
|
|
95
|
+
self._set_profile_data("org_id", value)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def temporary_org_id(self) -> Optional[str]:
|
|
99
|
+
return getattr(self, "_temporary_org_id", None)
|
|
100
|
+
|
|
101
|
+
@temporary_org_id.setter
|
|
102
|
+
def temporary_org_id(self, value: Optional[str]):
|
|
103
|
+
self._temporary_org_id = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def base_url(self) -> str:
|
|
107
|
+
return os.environ.get("DEVIN_BASE_URL") or self._get_profile_data().get("base_url", "https://api.devin.ai/v3")
|
|
108
|
+
|
|
109
|
+
@base_url.setter
|
|
110
|
+
def base_url(self, value: str):
|
|
111
|
+
self._set_profile_data("base_url", value)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def api_version(self) -> str:
|
|
115
|
+
return os.environ.get("DEVIN_API_VERSION") or self._get_profile_data().get("api_version", "v3")
|
|
116
|
+
|
|
117
|
+
@api_version.setter
|
|
118
|
+
def api_version(self, value: str):
|
|
119
|
+
self._set_profile_data("api_version", value)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def current_session_id(self) -> Optional[str]:
|
|
123
|
+
return self._get_profile_data().get("current_session_id")
|
|
124
|
+
|
|
125
|
+
@current_session_id.setter
|
|
126
|
+
def current_session_id(self, value: str):
|
|
127
|
+
self._set_profile_data("current_session_id", value)
|
|
128
|
+
|
|
129
|
+
def get_session_by_prompt_hash(self, prompt_hash: str) -> Optional[str]:
|
|
130
|
+
return self._get_profile_data().get("prompt_hashes", {}).get(prompt_hash)
|
|
131
|
+
|
|
132
|
+
def save_prompt_hash(self, prompt_hash: str, session_id: str):
|
|
133
|
+
profile_name = self.active_profile
|
|
134
|
+
if "profiles" not in self._data:
|
|
135
|
+
self._data["profiles"] = {}
|
|
136
|
+
if profile_name not in self._data["profiles"]:
|
|
137
|
+
self._data["profiles"][profile_name] = {}
|
|
138
|
+
|
|
139
|
+
profile_data = self._data["profiles"][profile_name]
|
|
140
|
+
if "prompt_hashes" not in profile_data:
|
|
141
|
+
profile_data["prompt_hashes"] = {}
|
|
142
|
+
|
|
143
|
+
profile_data["prompt_hashes"][prompt_hash] = session_id
|
|
144
|
+
|
|
145
|
+
# Keep only the last 50 hashes to prevent config bloat
|
|
146
|
+
if len(profile_data["prompt_hashes"]) > 50:
|
|
147
|
+
keys_to_remove = list(profile_data["prompt_hashes"].keys())[:-50]
|
|
148
|
+
for k in keys_to_remove:
|
|
149
|
+
del profile_data["prompt_hashes"][k]
|
|
150
|
+
|
|
151
|
+
self._save()
|
|
152
|
+
|
|
153
|
+
config = Config()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import httpx
|
|
3
|
+
from devin_cli.api import sessions
|
|
4
|
+
from devin_cli.config import config
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
8
|
+
logging.getLogger("httpx").setLevel(logging.DEBUG)
|
|
9
|
+
|
|
10
|
+
print("Active Token:", config.api_token[:10] if config.api_token else "None")
|
|
11
|
+
print("Base URL:", config.base_url)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
print("Sending request...")
|
|
15
|
+
resp = sessions.create_session(prompt="Testing duplicate bug. Ignore.", title="Duplicate Test")
|
|
16
|
+
print("Response:")
|
|
17
|
+
print(json.dumps(resp, indent=2))
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print("Error:", e)
|
|
@@ -42,7 +42,7 @@ def test_update_playbook():
|
|
|
42
42
|
route = respx.put("https://api.devin.ai/v3/organizations/test_org/playbooks/pb_123").mock(
|
|
43
43
|
return_value=Response(200)
|
|
44
44
|
)
|
|
45
|
-
playbooks.update_playbook("pb_123", body="new body")
|
|
45
|
+
playbooks.update_playbook("pb_123", title="new title", body="new body")
|
|
46
46
|
assert route.called
|
|
47
47
|
|
|
48
48
|
@respx.mock
|
devin_cli-1.0.0/CHANGELOG.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.1.0] - 2024-05-23
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
- Initial release
|
|
7
|
-
- Session management (create, list, get, terminate)
|
|
8
|
-
- Message sending and history viewing
|
|
9
|
-
- Watch command for live status polling
|
|
10
|
-
- File attachments
|
|
11
|
-
- Knowledge, Playbook, and Secret management commands
|
|
12
|
-
- Shell completion support
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
CONFIG_DIR = Path.home() / ".config" / "devin"
|
|
7
|
-
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
|
-
|
|
9
|
-
class Config:
|
|
10
|
-
def __init__(self, config_dir: Path = None):
|
|
11
|
-
self._config_dir = config_dir or CONFIG_DIR
|
|
12
|
-
self._config_file = self._config_dir / "config.json"
|
|
13
|
-
self._ensure_config_exists()
|
|
14
|
-
self._load()
|
|
15
|
-
|
|
16
|
-
@property
|
|
17
|
-
def config_file(self) -> Path:
|
|
18
|
-
return self._config_file
|
|
19
|
-
|
|
20
|
-
def _ensure_config_exists(self):
|
|
21
|
-
if not self._config_dir.exists():
|
|
22
|
-
self._config_dir.mkdir(parents=True, mode=0o700)
|
|
23
|
-
if not self._config_file.exists():
|
|
24
|
-
with open(self._config_file, "w") as f:
|
|
25
|
-
json.dump({}, f)
|
|
26
|
-
self._config_file.chmod(0o600)
|
|
27
|
-
|
|
28
|
-
def _load(self):
|
|
29
|
-
try:
|
|
30
|
-
with open(self._config_file, "r") as f:
|
|
31
|
-
self._data = json.load(f)
|
|
32
|
-
except (json.JSONDecodeError, FileNotFoundError):
|
|
33
|
-
self._data = {}
|
|
34
|
-
|
|
35
|
-
def _save(self):
|
|
36
|
-
with open(self._config_file, "w") as f:
|
|
37
|
-
json.dump(self._data, f, indent=2)
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def api_token(self) -> Optional[str]:
|
|
41
|
-
# Env var takes precedence
|
|
42
|
-
return os.environ.get("DEVIN_API_TOKEN") or self._data.get("api_token")
|
|
43
|
-
|
|
44
|
-
@api_token.setter
|
|
45
|
-
def api_token(self, value: str):
|
|
46
|
-
self._data["api_token"] = value
|
|
47
|
-
self._save()
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def org_id(self) -> Optional[str]:
|
|
51
|
-
# Temporary override takes highest precedence (CLI flag)
|
|
52
|
-
# then env var, then config file
|
|
53
|
-
return getattr(self, "_temporary_org_id", None) or os.environ.get("DEVIN_ORG_ID") or self._data.get("org_id")
|
|
54
|
-
|
|
55
|
-
@org_id.setter
|
|
56
|
-
def org_id(self, value: str):
|
|
57
|
-
self._data["org_id"] = value
|
|
58
|
-
self._save()
|
|
59
|
-
|
|
60
|
-
@property
|
|
61
|
-
def temporary_org_id(self) -> Optional[str]:
|
|
62
|
-
return getattr(self, "_temporary_org_id", None)
|
|
63
|
-
|
|
64
|
-
@temporary_org_id.setter
|
|
65
|
-
def temporary_org_id(self, value: Optional[str]):
|
|
66
|
-
self._temporary_org_id = value
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def base_url(self) -> str:
|
|
70
|
-
return os.environ.get("DEVIN_BASE_URL") or self._data.get("base_url", "https://api.devin.ai/v3")
|
|
71
|
-
|
|
72
|
-
@base_url.setter
|
|
73
|
-
def base_url(self, value: str):
|
|
74
|
-
self._data["base_url"] = value
|
|
75
|
-
self._save()
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def current_session_id(self) -> Optional[str]:
|
|
79
|
-
return self._data.get("current_session_id")
|
|
80
|
-
|
|
81
|
-
@current_session_id.setter
|
|
82
|
-
def current_session_id(self, value: str):
|
|
83
|
-
self._data["current_session_id"] = value
|
|
84
|
-
self._save()
|
|
85
|
-
|
|
86
|
-
config = Config()
|
|
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
|