devin-cli 1.4.0__tar.gz → 1.5.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.
Files changed (80) hide show
  1. {devin_cli-1.4.0 → devin_cli-1.5.1}/.gitignore +1 -0
  2. {devin_cli-1.4.0 → devin_cli-1.5.1}/CHANGELOG.md +29 -0
  3. {devin_cli-1.4.0 → devin_cli-1.5.1}/PKG-INFO +52 -1
  4. {devin_cli-1.4.0 → devin_cli-1.5.1}/README.md +51 -0
  5. {devin_cli-1.4.0 → devin_cli-1.5.1}/pyproject.toml +1 -1
  6. devin_cli-1.5.1/src/devin_cli/__init__.py +1 -0
  7. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/client.py +6 -9
  8. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/cli.py +139 -35
  9. devin_cli-1.5.1/src/devin_cli/config.py +270 -0
  10. devin_cli-1.5.1/tests/conftest.py +15 -0
  11. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_cli.py +8 -0
  12. devin_cli-1.5.1/tests/test_cli_global_flags.py +203 -0
  13. devin_cli-1.5.1/tests/test_config.py +194 -0
  14. devin_cli-1.5.1/tests/test_configure.py +162 -0
  15. devin_cli-1.4.0/src/devin_cli/__init__.py +0 -1
  16. devin_cli-1.4.0/src/devin_cli/config.py +0 -153
  17. devin_cli-1.4.0/tests/conftest.py +0 -8
  18. devin_cli-1.4.0/tests/test_config.py +0 -27
  19. {devin_cli-1.4.0 → devin_cli-1.5.1}/.github/workflows/pypi-publish.yml +0 -0
  20. {devin_cli-1.4.0 → devin_cli-1.5.1}/LICENSE +0 -0
  21. {devin_cli-1.4.0 → devin_cli-1.5.1}/assets/logo.png +0 -0
  22. {devin_cli-1.4.0 → devin_cli-1.5.1}/generate_facades.py +0 -0
  23. {devin_cli-1.4.0 → devin_cli-1.5.1}/setup.py +0 -0
  24. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/__init__.py +0 -0
  25. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/attachments.py +0 -0
  26. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/audit_logs.py +0 -0
  27. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/blueprints.py +0 -0
  28. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/code_scans.py +0 -0
  29. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/consumption.py +0 -0
  30. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/guardrail_violations.py +0 -0
  31. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/ip_access_list.py +0 -0
  32. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/knowledge.py +0 -0
  33. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/members.py +0 -0
  34. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/organizations.py +0 -0
  35. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/playbooks.py +0 -0
  36. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/pr_reviews.py +0 -0
  37. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/queue.py +0 -0
  38. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/repositories.py +0 -0
  39. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/schedules.py +0 -0
  40. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/secrets.py +0 -0
  41. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/service_users.py +0 -0
  42. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/sessions.py +0 -0
  43. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/tags.py +0 -0
  44. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/__init__.py +0 -0
  45. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/attachments.py +0 -0
  46. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/knowledge.py +0 -0
  47. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/playbooks.py +0 -0
  48. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/secrets.py +0 -0
  49. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v1/sessions.py +0 -0
  50. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/__init__.py +0 -0
  51. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/attachments.py +0 -0
  52. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/audit_logs.py +0 -0
  53. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/blueprints.py +0 -0
  54. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/code_scans.py +0 -0
  55. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/consumption.py +0 -0
  56. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/guardrail_violations.py +0 -0
  57. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/ip_access_list.py +0 -0
  58. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/knowledge.py +0 -0
  59. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/members.py +0 -0
  60. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/organizations.py +0 -0
  61. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/playbooks.py +0 -0
  62. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/pr_reviews.py +0 -0
  63. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/queue.py +0 -0
  64. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/repositories.py +0 -0
  65. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/schedules.py +0 -0
  66. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/secrets.py +0 -0
  67. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/service_users.py +0 -0
  68. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/sessions.py +0 -0
  69. {devin_cli-1.4.0 → devin_cli-1.5.1}/src/devin_cli/api/v3/tags.py +0 -0
  70. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/__init__.py +0 -0
  71. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_attachments.py +0 -0
  72. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_enterprise_endpoints.py +0 -0
  73. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_knowledge.py +0 -0
  74. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_playbooks.py +0 -0
  75. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_pr_reviews.py +0 -0
  76. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_repositories.py +0 -0
  77. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_schedules.py +0 -0
  78. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_secrets.py +0 -0
  79. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_api/test_sessions.py +0 -0
  80. {devin_cli-1.4.0 → devin_cli-1.5.1}/tests/test_cli_extended.py +0 -0
@@ -23,6 +23,7 @@ wheels/
23
23
 
24
24
  # Virtual Environment
25
25
  venv/
26
+ .venv/
26
27
  ENV/
27
28
 
28
29
  # Testing
@@ -5,6 +5,35 @@ 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.5.1] - 2026-09-04
9
+ ### Fixed
10
+ - **Thread Context Propagation for Concurrent Commands (`repos status`, `sessions cost`)**:
11
+ - Resolved an issue where commands utilizing `concurrent.futures.ThreadPoolExecutor` lost runtime credentials (`--token`, `--org`) in worker threads, causing `repos status` to emit fake 404 `"Not registered in Devin (404)"` errors when relying solely on global flags without environment variables.
12
+ - Ensured `contextvars.copy_context().run` is executed per worker task and backed by instance-level fallback in `Config` to guarantee thread-safe credential access across any thread pool.
13
+ - **Root Callback Runtime Credential Re-application**:
14
+ - Re-applied parsed flags and supported equals syntax (`--token=...`, `--org=...`) and flag aliases (`-t`, `-o`, `-b`, `-c`) so commands execute with correct credentials regardless of CLI argument order.
15
+ - **Non-Interactive `configure` Wizard Bypass**:
16
+ - Completely eliminated interactive prompts in `devin configure` when `--token` is provided or in CI environments (`CI`, `GITHUB_ACTIONS`, `CONTINUOUS_INTEGRATION`, `TF_BUILD`), falling back cleanly to defaults (`base_url: https://api.devin.ai/v3`, `api_version: v3`) without requiring `--yes`.
17
+ - Added graceful handling of `EOFError` / `KeyboardInterrupt` to prevent unhandled tracebacks.
18
+
19
+ ## [1.5.0] - 2026-09-04
20
+ ### Added
21
+ - **On-the-Fly Configuration & Global CLI Flags**: Root command now supports `--token`, `--org`, `--base-url`, `--api-version`, and `--config-file` flags, allowing complete credential configuration on the fly without writing to disk or modifying saved configs.
22
+ - **Predictable Precedence Resolution**: Formalized resolution order across all authentication paths:
23
+ 1. CLI Flags (`--token`, `--org`, `--base-url`, `--api-version`)
24
+ 2. Environment Variables (`DEVIN_API_TOKEN`, `DEVIN_ORG_ID`, `DEVIN_BASE_URL`, `DEVIN_API_VERSION`)
25
+ 3. Config File Profile (`DEVIN_CONFIG_FILE` or `~/.config/devin/config.json`)
26
+ 4. Defaults (`base_url: https://api.devin.ai/v3`, `api_version: v3`)
27
+ - **Zero Disk Writes / Deferred File Creation**: Initializing `Config` or invoking commands with flags/environment variables never touches the filesystem. Config file/directory creation is deferred strictly to explicit save actions (such as `devin configure`), fully unblocking read-only and ephemeral CI runners.
28
+ - **Non-Interactive `configure` Command**: Added `--yes` / `-y` flag and non-TTY stdin autodetection to `devin configure`. Omitting options in non-interactive mode falls back to default values and credentials instead of aborting.
29
+ - **Dynamic Config File Override**: Full support for `DEVIN_CONFIG_FILE` environment variable and `--config-file` flag to direct the CLI to custom config files.
30
+ - **Thread/Context-Safe Runtime Overrides**: Runtime overrides are stored via `contextvars.ContextVar` and cleanly scoped per invocation with `config.reset_runtime()`.
31
+ - **Pre-flight Validation & Scoping Hints**: `validate_for_api()` validates credentials before network requests and outputs helpful diagnostic hints (`Org ID: ...`) when endpoints require organization scoping.
32
+
33
+ ### Fixed
34
+ - **Sanitized Token and Org Inputs**: Automatic trimming of leading/trailing whitespace and newlines from tokens and organization IDs.
35
+ - **Configure Prompt Bugs**: Fixed an issue where `devin configure` always prompted for API version even when non-interactive, and improved option handling so explicit flags bypass interactive prompts.
36
+
8
37
  ## [1.4.0] - 2026-08-01
9
38
  ### Added
10
39
  - **Model Selection & Agent Modes**: Full support for `--devin-mode` / `--mode` / `--model` (`normal`, `fast`, `lite`, `ultra`, `fusion`), `--platform`, `--resumable / --no-resumable`, and `--structured-output-required` on `devin sessions create` and `devin create-session`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devin-cli
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -75,9 +75,13 @@ pip install devin-cli
75
75
 
76
76
  ### 2. Configuration
77
77
  ```bash
78
+ # Interactive setup:
78
79
  devin configure
79
80
  # Paste your API token (apk_... or cog_...) from https://preview.devin.ai/settings
80
81
  # Select API version: v3 (default) or v1 (legacy)
82
+
83
+ # Or non-interactive / CI setup:
84
+ devin configure --token "$DEVIN_API_TOKEN" --org "$DEVIN_ORG_ID" --yes
81
85
  ```
82
86
 
83
87
  ### 3. Your First Session
@@ -108,6 +112,53 @@ devin --profile personal sessions create "Fix the failing tests"
108
112
 
109
113
  Profiles are stored in `~/.config/devin/config.json` — fully isolated including session caches and active session IDs.
110
114
 
115
+ ### Automation & CI / CD
116
+
117
+ `devin-cli` is fully optimized for headless CI/CD runners (GitHub Actions, GitLab CI, Jenkins) and containerized workflows.
118
+
119
+ #### Precedence Resolution
120
+ Credentials and configuration resolve in a strict, predictable order:
121
+ 1. **Global CLI Flags** (`--token`, `--org`, `--base-url`, `--api-version`)
122
+ 2. **Environment Variables** (`DEVIN_API_TOKEN`, `DEVIN_ORG_ID`, `DEVIN_BASE_URL`, `DEVIN_API_VERSION`)
123
+ 3. **Config File** (`DEVIN_CONFIG_FILE` or `~/.config/devin/config.json`)
124
+ 4. **Built-in Defaults** (`base_url: https://api.devin.ai/v3`, `api_version: v3`)
125
+
126
+ #### Ephemeral Runs (Zero Disk Writes)
127
+ In CI pipelines, you do not need to create or write configuration files. Pass credentials via environment variables or root CLI flags:
128
+
129
+ ```bash
130
+ # Option A: Environment variables
131
+ export DEVIN_API_TOKEN="cog_..."
132
+ export DEVIN_ORG_ID="org-..."
133
+ devin repos list --json
134
+
135
+ # Option B: Global CLI flags on-the-fly
136
+ devin --token "$DEVIN_API_TOKEN" --org "$DEVIN_ORG_ID" repos status my-org/my-repo --json
137
+ ```
138
+
139
+ #### Non-Interactive Configuration
140
+ If your pipeline writes a config file, use `--yes` (or run in non-TTY environments) to disable interactive prompts:
141
+
142
+ ```bash
143
+ devin configure \
144
+ --token "$DEVIN_API_TOKEN" \
145
+ --org "$DEVIN_ORG_ID" \
146
+ --base-url https://api.devin.ai/v3 \
147
+ --api-version v3 \
148
+ --yes
149
+ ```
150
+
151
+ #### Custom Config File Path
152
+ Override the default `~/.config/devin/config.json` location:
153
+
154
+ ```bash
155
+ # Via flag
156
+ devin --config-file /tmp/devin-ci.json repos list
157
+
158
+ # Via environment variable
159
+ export DEVIN_CONFIG_FILE=/tmp/devin-ci.json
160
+ ```
161
+
111
162
  ---
112
163
 
113
164
  ## 🤖 All Commands
@@ -39,9 +39,13 @@ pip install devin-cli
39
39
 
40
40
  ### 2. Configuration
41
41
  ```bash
42
+ # Interactive setup:
42
43
  devin configure
43
44
  # Paste your API token (apk_... or cog_...) from https://preview.devin.ai/settings
44
45
  # Select API version: v3 (default) or v1 (legacy)
46
+
47
+ # Or non-interactive / CI setup:
48
+ devin configure --token "$DEVIN_API_TOKEN" --org "$DEVIN_ORG_ID" --yes
45
49
  ```
46
50
 
47
51
  ### 3. Your First Session
@@ -72,6 +76,53 @@ devin --profile personal sessions create "Fix the failing tests"
72
76
 
73
77
  Profiles are stored in `~/.config/devin/config.json` — fully isolated including session caches and active session IDs.
74
78
 
79
+ ### Automation & CI / CD
80
+
81
+ `devin-cli` is fully optimized for headless CI/CD runners (GitHub Actions, GitLab CI, Jenkins) and containerized workflows.
82
+
83
+ #### Precedence Resolution
84
+ Credentials and configuration resolve in a strict, predictable order:
85
+ 1. **Global CLI Flags** (`--token`, `--org`, `--base-url`, `--api-version`)
86
+ 2. **Environment Variables** (`DEVIN_API_TOKEN`, `DEVIN_ORG_ID`, `DEVIN_BASE_URL`, `DEVIN_API_VERSION`)
87
+ 3. **Config File** (`DEVIN_CONFIG_FILE` or `~/.config/devin/config.json`)
88
+ 4. **Built-in Defaults** (`base_url: https://api.devin.ai/v3`, `api_version: v3`)
89
+
90
+ #### Ephemeral Runs (Zero Disk Writes)
91
+ In CI pipelines, you do not need to create or write configuration files. Pass credentials via environment variables or root CLI flags:
92
+
93
+ ```bash
94
+ # Option A: Environment variables
95
+ export DEVIN_API_TOKEN="cog_..."
96
+ export DEVIN_ORG_ID="org-..."
97
+ devin repos list --json
98
+
99
+ # Option B: Global CLI flags on-the-fly
100
+ devin --token "$DEVIN_API_TOKEN" --org "$DEVIN_ORG_ID" repos status my-org/my-repo --json
101
+ ```
102
+
103
+ #### Non-Interactive Configuration
104
+ If your pipeline writes a config file, use `--yes` (or run in non-TTY environments) to disable interactive prompts:
105
+
106
+ ```bash
107
+ devin configure \
108
+ --token "$DEVIN_API_TOKEN" \
109
+ --org "$DEVIN_ORG_ID" \
110
+ --base-url https://api.devin.ai/v3 \
111
+ --api-version v3 \
112
+ --yes
113
+ ```
114
+
115
+ #### Custom Config File Path
116
+ Override the default `~/.config/devin/config.json` location:
117
+
118
+ ```bash
119
+ # Via flag
120
+ devin --config-file /tmp/devin-ci.json repos list
121
+
122
+ # Via environment variable
123
+ export DEVIN_CONFIG_FILE=/tmp/devin-ci.json
124
+ ```
125
+
75
126
  ---
76
127
 
77
128
  ## 🤖 All Commands
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devin-cli"
3
- version = "1.4.0"
3
+ version = "1.5.1"
4
4
  description = "Unofficial CLI for Devin AI - The first AI Software Engineer"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1 @@
1
+ __version__ = "1.5.1"
@@ -1,16 +1,11 @@
1
1
  import httpx
2
- from devin_cli.config import config
2
+ from devin_cli.config import config, APIError
3
3
  from rich.console import Console
4
4
  import sys
5
5
  from typing import Optional, Any, Dict
6
6
 
7
7
  console = Console()
8
8
 
9
- class APIError(Exception):
10
- def __init__(self, message: str, status_code: Optional[int] = None):
11
- super().__init__(message)
12
- self.status_code = status_code
13
-
14
9
  class BaseClient:
15
10
  def __init__(self):
16
11
  pass
@@ -34,9 +29,7 @@ class BaseClient:
34
29
  return config.base_url.rstrip("/")
35
30
 
36
31
  def _ensure_token(self):
37
- if not self.token:
38
- # Only raise error if meaningful operation is attempted
39
- raise APIError("API token not found. Run 'devin configure' to set your API token.")
32
+ config.validate_for_api()
40
33
 
41
34
  def _handle_response(self, response: httpx.Response) -> Any:
42
35
  try:
@@ -53,6 +46,10 @@ class BaseClient:
53
46
  error_detail = e.response.text.strip()
54
47
 
55
48
  msg_suffix = f"\n Path: {path}"
49
+ if config.org_id:
50
+ msg_suffix += f"\n Org ID: {config.org_id}"
51
+ elif "/organizations/" not in path and not path.startswith("/enterprise"):
52
+ msg_suffix += f"\n Org ID: None (Hint: this endpoint may require --org or DEVIN_ORG_ID)"
56
53
  if error_detail:
57
54
  msg_suffix += f"\n Detail: {error_detail}"
58
55
 
@@ -36,19 +36,50 @@ import importlib.metadata
36
36
  from rich.prompt import Prompt
37
37
 
38
38
  try:
39
- __version__ = importlib.metadata.version("devin-cli")
40
- except importlib.metadata.PackageNotFoundError:
41
- __version__ = "unknown"
42
-
43
- # Early profile detection for accurate --help menus
44
- if "--profile" in sys.argv:
45
- idx = sys.argv.index("--profile")
46
- if idx + 1 < len(sys.argv):
47
- config.active_profile = sys.argv[idx + 1]
48
- elif "-p" in sys.argv:
49
- idx = sys.argv.index("-p")
50
- if idx + 1 < len(sys.argv):
51
- config.active_profile = sys.argv[idx + 1]
39
+ from devin_cli import __version__
40
+ except Exception:
41
+ try:
42
+ __version__ = importlib.metadata.version("devin-cli")
43
+ except importlib.metadata.PackageNotFoundError:
44
+ __version__ = "1.5.0"
45
+
46
+ # Early CLI flag detection for accurate menus, config loading, and mode setup
47
+ def _extract_cli_arg(flags: list) -> Optional[str]:
48
+ for flag in flags:
49
+ for idx, arg in enumerate(sys.argv):
50
+ if arg == flag:
51
+ if idx + 1 < len(sys.argv) and not sys.argv[idx + 1].startswith("-"):
52
+ return sys.argv[idx + 1]
53
+ elif arg.startswith(f"{flag}="):
54
+ return arg.split("=", 1)[1]
55
+ return None
56
+
57
+ early_config_file = _extract_cli_arg(["--config-file", "-c"])
58
+ if early_config_file:
59
+ config.set_config_file(early_config_file)
60
+ elif os.environ.get("DEVIN_CONFIG_FILE"):
61
+ config.set_config_file(os.environ["DEVIN_CONFIG_FILE"])
62
+
63
+ early_profile = _extract_cli_arg(["--profile", "-p"])
64
+ if early_profile:
65
+ config.active_profile = early_profile
66
+
67
+ early_token = _extract_cli_arg(["--token", "-t"])
68
+ if early_token:
69
+ config.runtime.api_token = early_token
70
+
71
+ early_org = _extract_cli_arg(["--org", "-o"])
72
+ if early_org:
73
+ config.runtime.org_id = early_org
74
+ config.temporary_org_id = early_org
75
+
76
+ early_base_url = _extract_cli_arg(["--base-url", "-b"])
77
+ if early_base_url:
78
+ config.runtime.base_url = early_base_url
79
+
80
+ early_api_version = _extract_cli_arg(["--api-version"])
81
+ if early_api_version:
82
+ config.runtime.api_version = early_api_version
52
83
 
53
84
  IS_V1 = config.api_version == "v1"
54
85
  v1_tag = " [bold yellow](Legacy v1 API)[/bold yellow]" if IS_V1 else ""
@@ -101,12 +132,25 @@ ASCII_LOGO = r"""
101
132
  def main(
102
133
  ctx: typer.Context,
103
134
  profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Configuration profile to use (e.g., service, personal)"),
135
+ token: Optional[str] = typer.Option(None, "--token", help="Devin API token (starts with apk_ or cog_). Fallback: DEVIN_API_TOKEN"),
136
+ org: Optional[str] = typer.Option(None, "--org", help="Organization ID (org-...). Fallback: DEVIN_ORG_ID"),
137
+ base_url: Optional[str] = typer.Option(None, "--base-url", help="Devin API base URL (default: https://api.devin.ai/v3). Fallback: DEVIN_BASE_URL"),
138
+ api_version: Optional[str] = typer.Option(None, "--api-version", help="Devin API version: v3 or v1 (default: v3). Fallback: DEVIN_API_VERSION"),
139
+ config_file: Optional[Path] = typer.Option(None, "--config-file", help="Custom path to config file. Fallback: DEVIN_CONFIG_FILE"),
104
140
  version: Optional[bool] = typer.Option(None, "--version", "-v", help="Show the application's version and exit."),
105
141
  json_format: bool = typer.Option(False, "--json", help="Output raw JSON (for AI agents & scripting)"),
106
142
  ):
107
143
  """
108
144
  Unofficial CLI for Devin AI v3.
109
145
  """
146
+ config.reset_runtime()
147
+
148
+ eff_token = token or _extract_cli_arg(["--token", "-t"])
149
+ eff_org = org or _extract_cli_arg(["--org", "-o"])
150
+ eff_base_url = base_url or _extract_cli_arg(["--base-url", "-b"])
151
+ eff_api_version = api_version or _extract_cli_arg(["--api-version"])
152
+ eff_config_file = config_file or _extract_cli_arg(["--config-file", "-c"])
153
+
110
154
  if json_format:
111
155
  os.environ["DEVIN_OUTPUT_FORMAT"] = "json"
112
156
 
@@ -114,8 +158,20 @@ def main(
114
158
  console.print(f"devin CLI version: {__version__}")
115
159
  raise typer.Exit()
116
160
 
161
+ if eff_config_file:
162
+ config.set_config_file(eff_config_file)
117
163
  if profile:
118
164
  config.active_profile = profile
165
+ if eff_token:
166
+ config.runtime.api_token = eff_token
167
+ if eff_org:
168
+ config.runtime.org_id = eff_org
169
+ config.temporary_org_id = eff_org
170
+ if eff_base_url:
171
+ config.runtime.base_url = eff_base_url
172
+ if eff_api_version:
173
+ config.runtime.api_version = eff_api_version
174
+
119
175
  if ctx.invoked_subcommand is None:
120
176
  console.print(ASCII_LOGO)
121
177
 
@@ -150,10 +206,10 @@ def handle_api_error(func):
150
206
  console.print(f"[bold red]Connection Error:[/bold red] Failed to connect to {config.base_url}")
151
207
  console.print(f"[dim]{str(e)}[/dim]")
152
208
  raise typer.Exit(1)
153
- elif hasattr(e, "response"): # Standard APIError handling
209
+ elif isinstance(e, APIError) or hasattr(e, "response"): # Standard APIError handling
154
210
  if is_json:
155
211
  try:
156
- err_json = e.response.json()
212
+ err_json = e.response.json() if hasattr(e, "response") else {"error": str(e)}
157
213
  except Exception:
158
214
  err_json = {"error": str(e)}
159
215
  print(json.dumps(err_json, indent=2))
@@ -178,10 +234,12 @@ def get_current_session_id():
178
234
 
179
235
  @app.command()
180
236
  def configure(
181
- token: str = typer.Option(..., prompt="Devin API Token (starts with apk_ or cog_)", help="Your Devin API Token"),
182
- base_url: str = typer.Option("https://api.devin.ai/v3", prompt="Devin API Base URL", help="Devin API Base URL"),
183
- org_id: Optional[str] = typer.Option(None, "--org", prompt="Organization ID (optional)", help="Default Organization ID"),
237
+ token: Optional[str] = typer.Option(None, "--token", "-t", help="Your Devin API Token (starts with apk_ or cog_)"),
238
+ base_url: Optional[str] = typer.Option(None, "--base-url", "-b", help="Devin API Base URL (default: https://api.devin.ai/v3)"),
239
+ org_id: Optional[str] = typer.Option(None, "--org", help="Default Organization ID"),
240
+ api_version: Optional[str] = typer.Option(None, "--api-version", help="API Version (v3 or v1)"),
184
241
  profile: Optional[str] = typer.Option(None, "--profile", help="Profile to configure (overrides global --profile)"),
242
+ yes: bool = typer.Option(False, "--yes", "-y", help="Non-interactive mode (never prompt for input)"),
185
243
  ):
186
244
  """
187
245
  Configure the CLI with your Devin API token and organization.
@@ -189,21 +247,63 @@ def configure(
189
247
  if profile:
190
248
  config.active_profile = profile
191
249
 
192
- if not (token.startswith("apk_") or token.startswith("cog_")):
250
+ is_ci = bool(
251
+ os.environ.get("CI")
252
+ or os.environ.get("GITHUB_ACTIONS")
253
+ or os.environ.get("CONTINUOUS_INTEGRATION")
254
+ or os.environ.get("TF_BUILD")
255
+ )
256
+
257
+ # Check if token was provided via argument, early CLI arg, or runtime
258
+ token_val = token or _extract_cli_arg(["--token", "-t"]) or config.runtime.api_token
259
+
260
+ # Prompt interactively ONLY when no token was provided, --yes is not passed, not in CI, and stdin is a TTY
261
+ is_interactive = not yes and not is_ci and sys.stdin.isatty() and token_val is None
262
+
263
+ if not is_interactive:
264
+ final_token = token_val or config.api_token
265
+ if not final_token:
266
+ console.print("[bold red]Error:[/bold red] API token is required. Pass --token or set DEVIN_API_TOKEN.")
267
+ raise typer.Exit(1)
268
+ final_base_url = base_url or _extract_cli_arg(["--base-url", "-b"]) or config.base_url or "https://api.devin.ai/v3"
269
+ final_api_version = (api_version or _extract_cli_arg(["--api-version"]) or config.api_version or "v3").lower()
270
+ final_org = org_id if org_id is not None else (_extract_cli_arg(["--org"]) or config.org_id)
271
+ else:
272
+ try:
273
+ final_token = Prompt.ask("Devin API Token (starts with apk_ or cog_)")
274
+ if not final_token:
275
+ console.print("[bold red]Error:[/bold red] Token cannot be empty.")
276
+ raise typer.Exit(1)
277
+
278
+ final_base_url = base_url or Prompt.ask("Devin API Base URL", default=config.base_url or "https://api.devin.ai/v3")
279
+
280
+ if org_id is not None:
281
+ final_org = org_id
282
+ else:
283
+ org_prompt = Prompt.ask("Organization ID (optional)", default=config.org_id or "")
284
+ final_org = org_prompt if org_prompt else None
285
+
286
+ if api_version is not None:
287
+ final_api_version = api_version.lower()
288
+ else:
289
+ final_api_version = Prompt.ask(f"API Version for profile '{config.active_profile}' (v3/v1)", default=config.api_version or "v3").lower()
290
+ except (EOFError, KeyboardInterrupt):
291
+ console.print("[bold red]Aborted.[/bold red]")
292
+ raise typer.Exit(1)
293
+
294
+ if not (final_token.startswith("apk_") or final_token.startswith("cog_")):
193
295
  console.print("[bold yellow]Warning:[/bold yellow] Token format might be outdated. v3 tokens usually start with 'apk_' or 'cog_'.")
194
296
 
195
- api_version_prompt = Prompt.ask(f"API Version for profile '{config.active_profile}' (v3/v1)", default=config.api_version or "v3")
196
-
197
- if "api.devin.ai" in base_url and "v3" in base_url and api_version_prompt == "v1":
198
- base_url = base_url.replace("/v3", "/v1")
199
- elif "api.devin.ai" in base_url and "v1" in base_url and api_version_prompt == "v3":
200
- base_url = base_url.replace("/v1", "/v3")
201
-
202
- config.api_token = token
203
- config.base_url = base_url
204
- config.api_version = api_version_prompt
205
- if org_id:
206
- config.org_id = org_id
297
+ if "api.devin.ai" in final_base_url and "v3" in final_base_url and final_api_version == "v1":
298
+ final_base_url = final_base_url.replace("/v3", "/v1")
299
+ elif "api.devin.ai" in final_base_url and "v1" in final_base_url and final_api_version == "v3":
300
+ final_base_url = final_base_url.replace("/v1", "/v3")
301
+
302
+ config.api_token = final_token
303
+ config.base_url = final_base_url
304
+ config.api_version = final_api_version
305
+ if final_org:
306
+ config.org_id = final_org
207
307
  console.print(f"[green]Configuration saved to {config.config_file}[/green]")
208
308
  return None
209
309
 
@@ -532,9 +632,10 @@ def session_cost_cmd(
532
632
  except Exception:
533
633
  return None
534
634
 
635
+ import contextvars
535
636
  with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
536
- f_session = executor.submit(fetch_session)
537
- f_cons = executor.submit(fetch_consumption)
637
+ f_session = executor.submit(contextvars.copy_context().run, fetch_session)
638
+ f_cons = executor.submit(contextvars.copy_context().run, fetch_consumption)
538
639
 
539
640
  resp = f_session.result()
540
641
  cons_resp = f_cons.result()
@@ -1161,12 +1262,15 @@ def status_repo_cmd(
1161
1262
  json_output: bool = typer.Option(False, "--json"),
1162
1263
  ):
1163
1264
  """Get indexing status for one or more repositories."""
1164
- if org: config.temporary_org_id = org
1265
+ if org:
1266
+ config.temporary_org_id = org
1267
+ config.runtime.org_id = org
1165
1268
 
1166
1269
  results = []
1167
1270
  exit_code = 0
1168
1271
 
1169
1272
  from concurrent.futures import ThreadPoolExecutor
1273
+ import contextvars
1170
1274
 
1171
1275
  def fetch_status(path):
1172
1276
  try:
@@ -1189,7 +1293,7 @@ def status_repo_cmd(
1189
1293
 
1190
1294
  max_workers = min(10, len(paths))
1191
1295
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
1192
- futures = {executor.submit(fetch_status, path): path for path in paths}
1296
+ futures = {executor.submit(contextvars.copy_context().run, fetch_status, path): path for path in paths}
1193
1297
  for future in futures:
1194
1298
  path = futures[future]
1195
1299
  try: