deepagent-code 0.1.4__tar.gz → 0.1.6__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.
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/PKG-INFO +45 -13
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/README.md +44 -12
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code/cli.py +132 -76
- deepagent_code-0.1.6/deepagent_code/config.py +113 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/PKG-INFO +45 -13
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/SOURCES.txt +2 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/pyproject.toml +1 -1
- deepagent_code-0.1.6/tests/test_config.py +113 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/LICENSE +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code/__init__.py +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code/utils.py +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/dependency_links.txt +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/entry_points.txt +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/requires.txt +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/deepagent_code.egg-info/top_level.txt +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/setup.cfg +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/tests/test_cli.py +0 -0
- {deepagent_code-0.1.4 → deepagent_code-0.1.6}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagent-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
|
|
5
5
|
Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -57,7 +57,7 @@ deepagent-code
|
|
|
57
57
|
|
|
58
58
|
Or specify your own agent:
|
|
59
59
|
```bash
|
|
60
|
-
deepagent-code path/to/your_agent.py:graph
|
|
60
|
+
deepagent-code -a path/to/your_agent.py:graph
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
This launches an interactive conversation loop with your agent.
|
|
@@ -68,14 +68,17 @@ This launches an interactive conversation loop with your agent.
|
|
|
68
68
|
# Use the default agent
|
|
69
69
|
deepagent-code
|
|
70
70
|
|
|
71
|
+
# Send a message directly
|
|
72
|
+
deepagent-code "Hello, agent!"
|
|
73
|
+
|
|
71
74
|
# Specify a custom agent file
|
|
72
|
-
deepagent-code my_agent.py:graph
|
|
75
|
+
deepagent-code -a my_agent.py:graph
|
|
73
76
|
|
|
74
77
|
# Use a module path
|
|
75
|
-
deepagent-code mypackage.agents:chatbot
|
|
78
|
+
deepagent-code -a mypackage.agents:chatbot
|
|
76
79
|
|
|
77
|
-
#
|
|
78
|
-
deepagent-code -
|
|
80
|
+
# Read message from a file
|
|
81
|
+
deepagent-code -f ./prompt.md
|
|
79
82
|
|
|
80
83
|
# Non-interactive mode (auto-approve tool calls)
|
|
81
84
|
deepagent-code --no-interactive
|
|
@@ -101,24 +104,53 @@ deepagent-code
|
|
|
101
104
|
# Working directory
|
|
102
105
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
103
106
|
|
|
104
|
-
#
|
|
105
|
-
export
|
|
107
|
+
# Stream mode (updates or values)
|
|
108
|
+
export DEEPAGENT_STREAM_MODE="updates"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration Files
|
|
112
|
+
|
|
113
|
+
`deepagent-code` reads TOML config from two locations and merges them
|
|
114
|
+
(project overrides global):
|
|
115
|
+
|
|
116
|
+
- **Global**: `~/.deepagents/config.toml` (shared with the upstream
|
|
117
|
+
`deepagents` CLI)
|
|
118
|
+
- **Project**: `deepagents.toml` in the current directory or any ancestor
|
|
119
|
+
|
|
120
|
+
Precedence: CLI args > env vars > project TOML > global TOML > defaults.
|
|
121
|
+
|
|
122
|
+
Example `deepagents.toml`:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[agent]
|
|
126
|
+
spec = "my_agent.py:graph"
|
|
127
|
+
workspace_root = "."
|
|
128
|
+
|
|
129
|
+
[ui]
|
|
130
|
+
verbose = true
|
|
131
|
+
async_mode = false
|
|
132
|
+
stream_mode = "updates"
|
|
133
|
+
|
|
134
|
+
[configurable]
|
|
135
|
+
# seeds LangGraph RunnableConfig.configurable
|
|
136
|
+
thread_id = "my-thread"
|
|
106
137
|
```
|
|
107
138
|
|
|
108
139
|
## CLI Options
|
|
109
140
|
|
|
110
141
|
```
|
|
111
|
-
Usage: deepagent-code [OPTIONS] [
|
|
142
|
+
Usage: deepagent-code [OPTIONS] [MESSAGE]
|
|
112
143
|
|
|
113
144
|
Arguments:
|
|
114
|
-
|
|
145
|
+
MESSAGE Optional input to send to the agent immediately
|
|
115
146
|
|
|
116
147
|
Options:
|
|
148
|
+
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
117
149
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
118
|
-
-
|
|
119
|
-
-c, --config TEXT Config JSON or file path
|
|
150
|
+
-f, --file PATH Read message from a file (any extension)
|
|
120
151
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
121
152
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
153
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
122
154
|
-v, --verbose Verbose output
|
|
123
155
|
```
|
|
124
156
|
|
|
@@ -140,7 +172,7 @@ agent = create_deep_agent(
|
|
|
140
172
|
|
|
141
173
|
Then run it:
|
|
142
174
|
```bash
|
|
143
|
-
deepagent-code my_agent.py:agent
|
|
175
|
+
deepagent-code -a my_agent.py:agent
|
|
144
176
|
```
|
|
145
177
|
|
|
146
178
|
## Programmatic Use
|
|
@@ -25,7 +25,7 @@ deepagent-code
|
|
|
25
25
|
|
|
26
26
|
Or specify your own agent:
|
|
27
27
|
```bash
|
|
28
|
-
deepagent-code path/to/your_agent.py:graph
|
|
28
|
+
deepagent-code -a path/to/your_agent.py:graph
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
This launches an interactive conversation loop with your agent.
|
|
@@ -36,14 +36,17 @@ This launches an interactive conversation loop with your agent.
|
|
|
36
36
|
# Use the default agent
|
|
37
37
|
deepagent-code
|
|
38
38
|
|
|
39
|
+
# Send a message directly
|
|
40
|
+
deepagent-code "Hello, agent!"
|
|
41
|
+
|
|
39
42
|
# Specify a custom agent file
|
|
40
|
-
deepagent-code my_agent.py:graph
|
|
43
|
+
deepagent-code -a my_agent.py:graph
|
|
41
44
|
|
|
42
45
|
# Use a module path
|
|
43
|
-
deepagent-code mypackage.agents:chatbot
|
|
46
|
+
deepagent-code -a mypackage.agents:chatbot
|
|
44
47
|
|
|
45
|
-
#
|
|
46
|
-
deepagent-code -
|
|
48
|
+
# Read message from a file
|
|
49
|
+
deepagent-code -f ./prompt.md
|
|
47
50
|
|
|
48
51
|
# Non-interactive mode (auto-approve tool calls)
|
|
49
52
|
deepagent-code --no-interactive
|
|
@@ -69,24 +72,53 @@ deepagent-code
|
|
|
69
72
|
# Working directory
|
|
70
73
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
71
74
|
|
|
72
|
-
#
|
|
73
|
-
export
|
|
75
|
+
# Stream mode (updates or values)
|
|
76
|
+
export DEEPAGENT_STREAM_MODE="updates"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration Files
|
|
80
|
+
|
|
81
|
+
`deepagent-code` reads TOML config from two locations and merges them
|
|
82
|
+
(project overrides global):
|
|
83
|
+
|
|
84
|
+
- **Global**: `~/.deepagents/config.toml` (shared with the upstream
|
|
85
|
+
`deepagents` CLI)
|
|
86
|
+
- **Project**: `deepagents.toml` in the current directory or any ancestor
|
|
87
|
+
|
|
88
|
+
Precedence: CLI args > env vars > project TOML > global TOML > defaults.
|
|
89
|
+
|
|
90
|
+
Example `deepagents.toml`:
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[agent]
|
|
94
|
+
spec = "my_agent.py:graph"
|
|
95
|
+
workspace_root = "."
|
|
96
|
+
|
|
97
|
+
[ui]
|
|
98
|
+
verbose = true
|
|
99
|
+
async_mode = false
|
|
100
|
+
stream_mode = "updates"
|
|
101
|
+
|
|
102
|
+
[configurable]
|
|
103
|
+
# seeds LangGraph RunnableConfig.configurable
|
|
104
|
+
thread_id = "my-thread"
|
|
74
105
|
```
|
|
75
106
|
|
|
76
107
|
## CLI Options
|
|
77
108
|
|
|
78
109
|
```
|
|
79
|
-
Usage: deepagent-code [OPTIONS] [
|
|
110
|
+
Usage: deepagent-code [OPTIONS] [MESSAGE]
|
|
80
111
|
|
|
81
112
|
Arguments:
|
|
82
|
-
|
|
113
|
+
MESSAGE Optional input to send to the agent immediately
|
|
83
114
|
|
|
84
115
|
Options:
|
|
116
|
+
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
85
117
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
86
|
-
-
|
|
87
|
-
-c, --config TEXT Config JSON or file path
|
|
118
|
+
-f, --file PATH Read message from a file (any extension)
|
|
88
119
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
89
120
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
121
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
90
122
|
-v, --verbose Verbose output
|
|
91
123
|
```
|
|
92
124
|
|
|
@@ -108,7 +140,7 @@ agent = create_deep_agent(
|
|
|
108
140
|
|
|
109
141
|
Then run it:
|
|
110
142
|
```bash
|
|
111
|
-
deepagent-code my_agent.py:agent
|
|
143
|
+
deepagent-code -a my_agent.py:agent
|
|
112
144
|
```
|
|
113
145
|
|
|
114
146
|
## Programmatic Use
|
|
@@ -37,6 +37,7 @@ from deepagent_code.utils import (
|
|
|
37
37
|
stream_graph_updates,
|
|
38
38
|
astream_graph_updates,
|
|
39
39
|
)
|
|
40
|
+
from deepagent_code import config as config_module
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
# ANSI color codes (matching nanocode style)
|
|
@@ -54,7 +55,7 @@ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
# Version info
|
|
57
|
-
__version__ = "0.1.
|
|
58
|
+
__version__ = "0.1.6"
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
# Slash command registry
|
|
@@ -1000,40 +1001,50 @@ def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
|
1000
1001
|
config = context.get("config", {})
|
|
1001
1002
|
|
|
1002
1003
|
if not args:
|
|
1003
|
-
# Show current config
|
|
1004
1004
|
print(f"\n{BOLD}{BRIGHT_CYAN}Configuration{RESET}")
|
|
1005
1005
|
print(f"{DIM}{'─' * 30}{RESET}")
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1006
|
+
|
|
1007
|
+
sources = config.get("_toml_sources", [])
|
|
1008
|
+
if sources:
|
|
1009
|
+
print(f" {DIM}TOML sources:{RESET}")
|
|
1010
|
+
for src in sources:
|
|
1011
|
+
print(f" {DIM}- {src}{RESET}")
|
|
1012
|
+
else:
|
|
1013
|
+
print(f" {DIM}TOML sources:{RESET} {DIM}(none — using defaults){RESET}")
|
|
1014
|
+
|
|
1015
|
+
print(f" {DIM}Resolved settings:{RESET}")
|
|
1016
|
+
print(f" {CYAN}verbose:{RESET} {context.get('verbose', False)}")
|
|
1017
|
+
print(f" {CYAN}async_mode:{RESET} {context.get('use_async', False)}")
|
|
1018
|
+
print(f" {CYAN}stream_mode:{RESET} {context.get('stream_mode', 'updates')}")
|
|
1019
|
+
|
|
1020
|
+
configurable = config.get("configurable", {})
|
|
1021
|
+
if configurable:
|
|
1022
|
+
print(f" {DIM}LangGraph configurable:{RESET}")
|
|
1023
|
+
for k, v in configurable.items():
|
|
1024
|
+
v_str = str(v)
|
|
1025
|
+
if len(v_str) > 50:
|
|
1026
|
+
v_str = v_str[:50] + "..."
|
|
1027
|
+
print(f" {CYAN}{k}:{RESET} {v_str}")
|
|
1017
1028
|
print()
|
|
1018
1029
|
else:
|
|
1019
1030
|
parts = args.split(maxsplit=1)
|
|
1020
1031
|
if len(parts) == 1:
|
|
1021
|
-
# Show specific config key
|
|
1022
1032
|
key = parts[0]
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1033
|
+
configurable = config.get("configurable", {})
|
|
1034
|
+
if key in configurable:
|
|
1035
|
+
print(f"\n{CYAN}{key}:{RESET} {configurable[key]}\n")
|
|
1036
|
+
elif key in ("verbose", "async_mode", "stream_mode"):
|
|
1037
|
+
ctx_key = "use_async" if key == "async_mode" else key
|
|
1038
|
+
print(f"\n{CYAN}{key}:{RESET} {context.get(ctx_key)}\n")
|
|
1027
1039
|
else:
|
|
1028
1040
|
print(f"{YELLOW}Unknown config key: {key}{RESET}")
|
|
1029
1041
|
else:
|
|
1030
|
-
# Set config value
|
|
1031
1042
|
key, value = parts
|
|
1032
1043
|
if key == "verbose":
|
|
1033
1044
|
context["verbose"] = value.lower() in ("true", "1", "on", "yes")
|
|
1034
1045
|
print(f"{GREEN}✓ Set verbose = {context['verbose']}{RESET}")
|
|
1035
1046
|
else:
|
|
1036
|
-
print(f"{YELLOW}Cannot modify {key} at runtime{RESET}")
|
|
1047
|
+
print(f"{YELLOW}Cannot modify {key} at runtime (edit deepagents.toml){RESET}")
|
|
1037
1048
|
return None
|
|
1038
1049
|
|
|
1039
1050
|
|
|
@@ -1202,10 +1213,13 @@ def run_conversation_loop(
|
|
|
1202
1213
|
verbose: bool = False,
|
|
1203
1214
|
stream_mode: str = "updates",
|
|
1204
1215
|
initial_message: Optional[str] = None,
|
|
1216
|
+
single_shot: bool = False,
|
|
1205
1217
|
):
|
|
1206
1218
|
"""
|
|
1207
1219
|
Run a continuous conversation loop with the LangGraph agent.
|
|
1208
1220
|
Styled after Claude Code / nanocode.
|
|
1221
|
+
|
|
1222
|
+
If single_shot is True and initial_message is provided, exit after processing.
|
|
1209
1223
|
"""
|
|
1210
1224
|
# Set up tab completion for slash commands
|
|
1211
1225
|
setup_readline_completion()
|
|
@@ -1242,6 +1256,10 @@ def run_conversation_loop(
|
|
|
1242
1256
|
print_timing(duration, verbose)
|
|
1243
1257
|
print()
|
|
1244
1258
|
|
|
1259
|
+
# Exit after single-shot execution
|
|
1260
|
+
if single_shot:
|
|
1261
|
+
return
|
|
1262
|
+
|
|
1245
1263
|
# Main conversation loop
|
|
1246
1264
|
while True:
|
|
1247
1265
|
try:
|
|
@@ -1322,21 +1340,24 @@ def run_conversation_loop(
|
|
|
1322
1340
|
|
|
1323
1341
|
|
|
1324
1342
|
@click.command()
|
|
1325
|
-
@click.argument("
|
|
1343
|
+
@click.argument("message", required=False)
|
|
1344
|
+
@click.option(
|
|
1345
|
+
"--agent",
|
|
1346
|
+
"-a",
|
|
1347
|
+
"agent_spec",
|
|
1348
|
+
help="Agent spec: path/to/file.py, path/to/file.py:graph, or module.path:graph",
|
|
1349
|
+
)
|
|
1326
1350
|
@click.option(
|
|
1327
1351
|
"--graph-name",
|
|
1328
1352
|
"-g",
|
|
1329
1353
|
help="Name of the graph variable (default: 'graph', overridden if spec includes :name)",
|
|
1330
1354
|
)
|
|
1331
1355
|
@click.option(
|
|
1332
|
-
"--
|
|
1333
|
-
"-
|
|
1334
|
-
|
|
1335
|
-
)
|
|
1336
|
-
|
|
1337
|
-
"--config",
|
|
1338
|
-
"-c",
|
|
1339
|
-
help="Configuration JSON string or path to JSON file",
|
|
1356
|
+
"--file",
|
|
1357
|
+
"-f",
|
|
1358
|
+
"prompt_file",
|
|
1359
|
+
type=click.Path(exists=True),
|
|
1360
|
+
help="Read input message from a file (any extension)",
|
|
1340
1361
|
)
|
|
1341
1362
|
@click.option(
|
|
1342
1363
|
"--interactive/--no-interactive",
|
|
@@ -1346,7 +1367,7 @@ def run_conversation_loop(
|
|
|
1346
1367
|
@click.option(
|
|
1347
1368
|
"--async-mode/--sync-mode",
|
|
1348
1369
|
"use_async",
|
|
1349
|
-
default=
|
|
1370
|
+
default=None,
|
|
1350
1371
|
help="Use async streaming (default: sync)",
|
|
1351
1372
|
)
|
|
1352
1373
|
@click.option(
|
|
@@ -1357,22 +1378,25 @@ def run_conversation_loop(
|
|
|
1357
1378
|
"--verbose",
|
|
1358
1379
|
"-v",
|
|
1359
1380
|
is_flag=True,
|
|
1381
|
+
default=None,
|
|
1360
1382
|
help="Show verbose output including node names",
|
|
1361
1383
|
)
|
|
1362
1384
|
def main(
|
|
1385
|
+
message: Optional[str],
|
|
1363
1386
|
agent_spec: Optional[str],
|
|
1364
1387
|
graph_name: Optional[str],
|
|
1365
|
-
|
|
1366
|
-
config: Optional[str],
|
|
1388
|
+
prompt_file: Optional[str],
|
|
1367
1389
|
interactive: bool,
|
|
1368
|
-
use_async: bool,
|
|
1390
|
+
use_async: Optional[bool],
|
|
1369
1391
|
stream_mode: Optional[str],
|
|
1370
|
-
verbose: bool,
|
|
1392
|
+
verbose: Optional[bool],
|
|
1371
1393
|
):
|
|
1372
1394
|
"""
|
|
1373
1395
|
Run a LangGraph agent from the command line.
|
|
1374
1396
|
|
|
1375
|
-
|
|
1397
|
+
MESSAGE is an optional input to send to the agent immediately.
|
|
1398
|
+
|
|
1399
|
+
Agent spec (-a/--agent) can be:
|
|
1376
1400
|
\b
|
|
1377
1401
|
- path/to/file.py (uses default graph name 'graph')
|
|
1378
1402
|
- path/to/file.py:agent (specifies graph variable name)
|
|
@@ -1384,28 +1408,74 @@ def main(
|
|
|
1384
1408
|
\b
|
|
1385
1409
|
- DEEPAGENT_SPEC: Agent location (same formats as above)
|
|
1386
1410
|
- DEEPAGENT_WORKSPACE_ROOT: Working directory for the agent
|
|
1387
|
-
- DEEPAGENT_CONFIG: Configuration JSON string or path to JSON file
|
|
1388
1411
|
- DEEPAGENT_STREAM_MODE: Stream mode for LangGraph (updates or values)
|
|
1389
1412
|
|
|
1390
|
-
|
|
1413
|
+
Reads ~/.deepagents/config.toml (global) and deepagents.toml (project,
|
|
1414
|
+
walks up from cwd). Precedence: CLI args > env vars > project TOML >
|
|
1415
|
+
global TOML > built-in defaults.
|
|
1391
1416
|
|
|
1392
1417
|
\b
|
|
1393
1418
|
Examples:
|
|
1394
|
-
deepagent-code
|
|
1395
|
-
deepagent-code my_agent.py
|
|
1396
|
-
deepagent-code
|
|
1397
|
-
deepagent-code -
|
|
1419
|
+
deepagent-code "Hello, agent!"
|
|
1420
|
+
deepagent-code -a my_agent.py "What can you do?"
|
|
1421
|
+
deepagent-code -a my_agent.py:graph
|
|
1422
|
+
deepagent-code -f ./prompt.md
|
|
1398
1423
|
"""
|
|
1399
1424
|
try:
|
|
1400
|
-
#
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1425
|
+
# Handle -f/--file option: read message from file
|
|
1426
|
+
if prompt_file and message:
|
|
1427
|
+
print(f"{RED}⏺ Error: Cannot use both MESSAGE argument and -f/--file option{RESET}")
|
|
1428
|
+
sys.exit(1)
|
|
1429
|
+
|
|
1430
|
+
if prompt_file:
|
|
1431
|
+
try:
|
|
1432
|
+
with open(prompt_file, 'r', encoding='utf-8') as f:
|
|
1433
|
+
message = f.read().strip()
|
|
1434
|
+
if not message:
|
|
1435
|
+
print(f"{RED}⏺ Error: File '{prompt_file}' is empty{RESET}")
|
|
1436
|
+
sys.exit(1)
|
|
1437
|
+
except Exception as e:
|
|
1438
|
+
print(f"{RED}⏺ Error reading file '{prompt_file}': {e}{RESET}")
|
|
1439
|
+
sys.exit(1)
|
|
1405
1440
|
|
|
1406
|
-
#
|
|
1407
|
-
|
|
1408
|
-
|
|
1441
|
+
# Load TOML configuration (global + project, merged)
|
|
1442
|
+
try:
|
|
1443
|
+
toml_config, toml_sources = config_module.load_config()
|
|
1444
|
+
except config_module.ConfigError as e:
|
|
1445
|
+
print(f"{RED}⏺ {e}{RESET}")
|
|
1446
|
+
sys.exit(1)
|
|
1447
|
+
|
|
1448
|
+
# Resolve settings with precedence: CLI > env > TOML > default
|
|
1449
|
+
final_spec = config_module.resolve(
|
|
1450
|
+
toml_config, "agent.spec",
|
|
1451
|
+
cli_value=agent_spec,
|
|
1452
|
+
env_var="DEEPAGENT_SPEC",
|
|
1453
|
+
) or os.getenv("DEEPAGENT_AGENT_SPEC") # legacy env var
|
|
1454
|
+
final_graph_name_default = config_module.resolve(
|
|
1455
|
+
toml_config, "agent.graph_name",
|
|
1456
|
+
cli_value=graph_name,
|
|
1457
|
+
default="graph",
|
|
1458
|
+
)
|
|
1459
|
+
workspace_root = config_module.resolve(
|
|
1460
|
+
toml_config, "agent.workspace_root",
|
|
1461
|
+
env_var="DEEPAGENT_WORKSPACE_ROOT",
|
|
1462
|
+
)
|
|
1463
|
+
final_stream_mode = config_module.resolve(
|
|
1464
|
+
toml_config, "ui.stream_mode",
|
|
1465
|
+
cli_value=stream_mode,
|
|
1466
|
+
env_var="DEEPAGENT_STREAM_MODE",
|
|
1467
|
+
default="updates",
|
|
1468
|
+
)
|
|
1469
|
+
use_async = config_module.resolve(
|
|
1470
|
+
toml_config, "ui.async_mode",
|
|
1471
|
+
cli_value=use_async,
|
|
1472
|
+
default=False,
|
|
1473
|
+
)
|
|
1474
|
+
verbose = config_module.resolve(
|
|
1475
|
+
toml_config, "ui.verbose",
|
|
1476
|
+
cli_value=verbose,
|
|
1477
|
+
default=False,
|
|
1478
|
+
)
|
|
1409
1479
|
|
|
1410
1480
|
# If no spec provided, try the default agent
|
|
1411
1481
|
if not final_spec:
|
|
@@ -1421,50 +1491,35 @@ def main(
|
|
|
1421
1491
|
sys.exit(1)
|
|
1422
1492
|
|
|
1423
1493
|
# Change to workspace root if specified
|
|
1424
|
-
if
|
|
1425
|
-
workspace_path = Path(
|
|
1494
|
+
if workspace_root:
|
|
1495
|
+
workspace_path = Path(workspace_root).expanduser().resolve()
|
|
1426
1496
|
if workspace_path.exists():
|
|
1427
1497
|
os.chdir(workspace_path)
|
|
1428
1498
|
|
|
1429
1499
|
# Load the graph with a spinner
|
|
1430
1500
|
spinner = Spinner("Loading agent")
|
|
1431
1501
|
spinner.start()
|
|
1432
|
-
graph, final_graph_name = load_graph(final_spec,
|
|
1502
|
+
graph, final_graph_name = load_graph(final_spec, final_graph_name_default)
|
|
1433
1503
|
spinner.stop()
|
|
1434
1504
|
print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
|
|
1435
1505
|
|
|
1436
|
-
#
|
|
1437
|
-
config_dict =
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
config_path = Path(config_source)
|
|
1442
|
-
if config_path.exists():
|
|
1443
|
-
with open(config_path) as f:
|
|
1444
|
-
config_dict = json.load(f)
|
|
1445
|
-
else:
|
|
1446
|
-
try:
|
|
1447
|
-
config_dict = json.loads(config_source)
|
|
1448
|
-
except json.JSONDecodeError as e:
|
|
1449
|
-
print(f"{RED}⏺ Invalid config JSON: {e}{RESET}")
|
|
1450
|
-
sys.exit(1)
|
|
1451
|
-
|
|
1452
|
-
# Get stream mode
|
|
1453
|
-
final_stream_mode = stream_mode or env_stream_mode
|
|
1454
|
-
|
|
1455
|
-
# Ensure config has a thread_id for checkpointer support
|
|
1456
|
-
if config_dict is None:
|
|
1457
|
-
config_dict = {}
|
|
1458
|
-
if "configurable" not in config_dict:
|
|
1459
|
-
config_dict["configurable"] = {}
|
|
1506
|
+
# Seed LangGraph RunnableConfig from TOML [configurable] table if present
|
|
1507
|
+
config_dict: Dict[str, Any] = {"configurable": {}}
|
|
1508
|
+
toml_configurable = config_module.get(toml_config, "configurable")
|
|
1509
|
+
if isinstance(toml_configurable, dict):
|
|
1510
|
+
config_dict["configurable"].update(toml_configurable)
|
|
1460
1511
|
if "thread_id" not in config_dict["configurable"]:
|
|
1461
1512
|
config_dict["configurable"]["thread_id"] = str(uuid.uuid4())
|
|
1462
1513
|
|
|
1514
|
+
# Expose TOML sources to slash commands via the config dict
|
|
1515
|
+
config_dict["_toml_sources"] = [str(p) for p in toml_sources]
|
|
1516
|
+
|
|
1463
1517
|
# Extract agent name and description from graph object
|
|
1464
1518
|
agent_name = get_agent_name(graph)
|
|
1465
1519
|
agent_description = get_agent_description(graph)
|
|
1466
1520
|
|
|
1467
1521
|
# Run the conversation loop
|
|
1522
|
+
# Single-shot mode: exit after processing if message was provided via CLI
|
|
1468
1523
|
run_conversation_loop(
|
|
1469
1524
|
graph=graph,
|
|
1470
1525
|
config=config_dict,
|
|
@@ -1475,6 +1530,7 @@ def main(
|
|
|
1475
1530
|
verbose=verbose,
|
|
1476
1531
|
stream_mode=final_stream_mode,
|
|
1477
1532
|
initial_message=message,
|
|
1533
|
+
single_shot=bool(message),
|
|
1478
1534
|
)
|
|
1479
1535
|
|
|
1480
1536
|
except FileNotFoundError as e:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""TOML configuration loader for deepagent-code.
|
|
2
|
+
|
|
3
|
+
Reads two files and merges them (project wins on conflict):
|
|
4
|
+
- ~/.deepagents/config.toml — shared with the upstream deepagents CLI
|
|
5
|
+
- deepagents.toml — nearest ancestor of cwd, per-project overrides
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
GLOBAL_CONFIG_PATH = Path.home() / ".deepagents" / "config.toml"
|
|
16
|
+
PROJECT_CONFIG_NAME = "deepagents.toml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigError(Exception):
|
|
20
|
+
"""Raised when a config file exists but cannot be parsed."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def global_config_path() -> Path:
|
|
24
|
+
override = os.getenv("DEEPAGENTS_CONFIG_HOME")
|
|
25
|
+
if override:
|
|
26
|
+
return Path(override).expanduser() / "config.toml"
|
|
27
|
+
return GLOBAL_CONFIG_PATH
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def find_project_config(start: Optional[Path] = None) -> Optional[Path]:
|
|
31
|
+
"""Walk up from `start` (or cwd) looking for deepagents.toml."""
|
|
32
|
+
here = (start or Path.cwd()).resolve()
|
|
33
|
+
for directory in (here, *here.parents):
|
|
34
|
+
candidate = directory / PROJECT_CONFIG_NAME
|
|
35
|
+
if candidate.is_file():
|
|
36
|
+
return candidate
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _read_toml(path: Path) -> Dict[str, Any]:
|
|
41
|
+
try:
|
|
42
|
+
with path.open("rb") as f:
|
|
43
|
+
return tomllib.load(f)
|
|
44
|
+
except tomllib.TOMLDecodeError as e:
|
|
45
|
+
raise ConfigError(f"Invalid TOML in {path}: {e}") from e
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _deep_merge(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
|
|
49
|
+
"""Recursively merge overlay into base. Overlay wins on leaf conflicts."""
|
|
50
|
+
result = dict(base)
|
|
51
|
+
for key, value in overlay.items():
|
|
52
|
+
if (
|
|
53
|
+
key in result
|
|
54
|
+
and isinstance(result[key], dict)
|
|
55
|
+
and isinstance(value, dict)
|
|
56
|
+
):
|
|
57
|
+
result[key] = _deep_merge(result[key], value)
|
|
58
|
+
else:
|
|
59
|
+
result[key] = value
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_config(start: Optional[Path] = None) -> Tuple[Dict[str, Any], List[Path]]:
|
|
64
|
+
"""Load global + project TOML, merged. Returns (config, sources_used)."""
|
|
65
|
+
sources: List[Path] = []
|
|
66
|
+
merged: Dict[str, Any] = {}
|
|
67
|
+
|
|
68
|
+
gpath = global_config_path()
|
|
69
|
+
if gpath.exists():
|
|
70
|
+
merged = _deep_merge(merged, _read_toml(gpath))
|
|
71
|
+
sources.append(gpath)
|
|
72
|
+
|
|
73
|
+
ppath = find_project_config(start)
|
|
74
|
+
if ppath is not None:
|
|
75
|
+
merged = _deep_merge(merged, _read_toml(ppath))
|
|
76
|
+
sources.append(ppath)
|
|
77
|
+
|
|
78
|
+
return merged, sources
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get(config: Dict[str, Any], dotted_key: str, default: Any = None) -> Any:
|
|
82
|
+
"""Fetch a nested value via dotted path, e.g. 'ui.verbose'."""
|
|
83
|
+
node: Any = config
|
|
84
|
+
for part in dotted_key.split("."):
|
|
85
|
+
if not isinstance(node, dict) or part not in node:
|
|
86
|
+
return default
|
|
87
|
+
node = node[part]
|
|
88
|
+
return node
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def resolve(
|
|
92
|
+
config: Dict[str, Any],
|
|
93
|
+
dotted_key: str,
|
|
94
|
+
cli_value: Any = None,
|
|
95
|
+
env_var: Optional[str] = None,
|
|
96
|
+
default: Any = None,
|
|
97
|
+
cast: Optional[type] = None,
|
|
98
|
+
) -> Any:
|
|
99
|
+
"""Resolve a value with precedence: CLI > env > TOML > default."""
|
|
100
|
+
if cli_value is not None:
|
|
101
|
+
return cli_value
|
|
102
|
+
if env_var:
|
|
103
|
+
env_value = os.getenv(env_var)
|
|
104
|
+
if env_value is not None:
|
|
105
|
+
if cast is bool:
|
|
106
|
+
return env_value.lower() in ("1", "true", "yes", "on")
|
|
107
|
+
if cast is not None:
|
|
108
|
+
return cast(env_value)
|
|
109
|
+
return env_value
|
|
110
|
+
toml_value = get(config, dotted_key)
|
|
111
|
+
if toml_value is not None:
|
|
112
|
+
return toml_value
|
|
113
|
+
return default
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagent-code
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A Claude Code-style CLI for running LangGraph agents from the terminal
|
|
5
5
|
Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -57,7 +57,7 @@ deepagent-code
|
|
|
57
57
|
|
|
58
58
|
Or specify your own agent:
|
|
59
59
|
```bash
|
|
60
|
-
deepagent-code path/to/your_agent.py:graph
|
|
60
|
+
deepagent-code -a path/to/your_agent.py:graph
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
This launches an interactive conversation loop with your agent.
|
|
@@ -68,14 +68,17 @@ This launches an interactive conversation loop with your agent.
|
|
|
68
68
|
# Use the default agent
|
|
69
69
|
deepagent-code
|
|
70
70
|
|
|
71
|
+
# Send a message directly
|
|
72
|
+
deepagent-code "Hello, agent!"
|
|
73
|
+
|
|
71
74
|
# Specify a custom agent file
|
|
72
|
-
deepagent-code my_agent.py:graph
|
|
75
|
+
deepagent-code -a my_agent.py:graph
|
|
73
76
|
|
|
74
77
|
# Use a module path
|
|
75
|
-
deepagent-code mypackage.agents:chatbot
|
|
78
|
+
deepagent-code -a mypackage.agents:chatbot
|
|
76
79
|
|
|
77
|
-
#
|
|
78
|
-
deepagent-code -
|
|
80
|
+
# Read message from a file
|
|
81
|
+
deepagent-code -f ./prompt.md
|
|
79
82
|
|
|
80
83
|
# Non-interactive mode (auto-approve tool calls)
|
|
81
84
|
deepagent-code --no-interactive
|
|
@@ -101,24 +104,53 @@ deepagent-code
|
|
|
101
104
|
# Working directory
|
|
102
105
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
103
106
|
|
|
104
|
-
#
|
|
105
|
-
export
|
|
107
|
+
# Stream mode (updates or values)
|
|
108
|
+
export DEEPAGENT_STREAM_MODE="updates"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration Files
|
|
112
|
+
|
|
113
|
+
`deepagent-code` reads TOML config from two locations and merges them
|
|
114
|
+
(project overrides global):
|
|
115
|
+
|
|
116
|
+
- **Global**: `~/.deepagents/config.toml` (shared with the upstream
|
|
117
|
+
`deepagents` CLI)
|
|
118
|
+
- **Project**: `deepagents.toml` in the current directory or any ancestor
|
|
119
|
+
|
|
120
|
+
Precedence: CLI args > env vars > project TOML > global TOML > defaults.
|
|
121
|
+
|
|
122
|
+
Example `deepagents.toml`:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[agent]
|
|
126
|
+
spec = "my_agent.py:graph"
|
|
127
|
+
workspace_root = "."
|
|
128
|
+
|
|
129
|
+
[ui]
|
|
130
|
+
verbose = true
|
|
131
|
+
async_mode = false
|
|
132
|
+
stream_mode = "updates"
|
|
133
|
+
|
|
134
|
+
[configurable]
|
|
135
|
+
# seeds LangGraph RunnableConfig.configurable
|
|
136
|
+
thread_id = "my-thread"
|
|
106
137
|
```
|
|
107
138
|
|
|
108
139
|
## CLI Options
|
|
109
140
|
|
|
110
141
|
```
|
|
111
|
-
Usage: deepagent-code [OPTIONS] [
|
|
142
|
+
Usage: deepagent-code [OPTIONS] [MESSAGE]
|
|
112
143
|
|
|
113
144
|
Arguments:
|
|
114
|
-
|
|
145
|
+
MESSAGE Optional input to send to the agent immediately
|
|
115
146
|
|
|
116
147
|
Options:
|
|
148
|
+
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
117
149
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
118
|
-
-
|
|
119
|
-
-c, --config TEXT Config JSON or file path
|
|
150
|
+
-f, --file PATH Read message from a file (any extension)
|
|
120
151
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
121
152
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
153
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
122
154
|
-v, --verbose Verbose output
|
|
123
155
|
```
|
|
124
156
|
|
|
@@ -140,7 +172,7 @@ agent = create_deep_agent(
|
|
|
140
172
|
|
|
141
173
|
Then run it:
|
|
142
174
|
```bash
|
|
143
|
-
deepagent-code my_agent.py:agent
|
|
175
|
+
deepagent-code -a my_agent.py:agent
|
|
144
176
|
```
|
|
145
177
|
|
|
146
178
|
## Programmatic Use
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
deepagent_code/__init__.py
|
|
5
5
|
deepagent_code/cli.py
|
|
6
|
+
deepagent_code/config.py
|
|
6
7
|
deepagent_code/utils.py
|
|
7
8
|
deepagent_code.egg-info/PKG-INFO
|
|
8
9
|
deepagent_code.egg-info/SOURCES.txt
|
|
@@ -11,4 +12,5 @@ deepagent_code.egg-info/entry_points.txt
|
|
|
11
12
|
deepagent_code.egg-info/requires.txt
|
|
12
13
|
deepagent_code.egg-info/top_level.txt
|
|
13
14
|
tests/test_cli.py
|
|
15
|
+
tests/test_config.py
|
|
14
16
|
tests/test_utils.py
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Tests for deepagent_code.config."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from deepagent_code import config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _write(path: Path, content: str) -> None:
|
|
12
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
path.write_text(content)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_returns_empty_when_no_files(tmp_path, monkeypatch):
|
|
17
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
|
|
18
|
+
monkeypatch.chdir(tmp_path)
|
|
19
|
+
cfg, sources = config.load_config()
|
|
20
|
+
assert cfg == {}
|
|
21
|
+
assert sources == []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_loads_global_only(tmp_path, monkeypatch):
|
|
25
|
+
home = tmp_path / "home"
|
|
26
|
+
_write(home / "config.toml", '[ui]\nverbose = true\n')
|
|
27
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
|
|
28
|
+
monkeypatch.chdir(tmp_path)
|
|
29
|
+
cfg, sources = config.load_config()
|
|
30
|
+
assert cfg == {"ui": {"verbose": True}}
|
|
31
|
+
assert sources == [home / "config.toml"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_loads_project_only(tmp_path, monkeypatch):
|
|
35
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
|
|
36
|
+
project = tmp_path / "proj"
|
|
37
|
+
_write(project / "deepagents.toml", '[agent]\nspec = "foo.py:agent"\n')
|
|
38
|
+
monkeypatch.chdir(project)
|
|
39
|
+
cfg, sources = config.load_config()
|
|
40
|
+
assert cfg == {"agent": {"spec": "foo.py:agent"}}
|
|
41
|
+
assert sources == [project / "deepagents.toml"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_project_overrides_global(tmp_path, monkeypatch):
|
|
45
|
+
home = tmp_path / "home"
|
|
46
|
+
_write(home / "config.toml", '[ui]\nverbose = false\nstream_mode = "values"\n')
|
|
47
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
|
|
48
|
+
|
|
49
|
+
project = tmp_path / "proj"
|
|
50
|
+
_write(project / "deepagents.toml", '[ui]\nverbose = true\n')
|
|
51
|
+
monkeypatch.chdir(project)
|
|
52
|
+
|
|
53
|
+
cfg, sources = config.load_config()
|
|
54
|
+
# project wins on overlapping keys, global keys still present
|
|
55
|
+
assert cfg == {"ui": {"verbose": True, "stream_mode": "values"}}
|
|
56
|
+
assert sources == [home / "config.toml", project / "deepagents.toml"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_walks_up_for_project_config(tmp_path, monkeypatch):
|
|
60
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(tmp_path / "empty"))
|
|
61
|
+
project = tmp_path / "proj"
|
|
62
|
+
nested = project / "a" / "b" / "c"
|
|
63
|
+
nested.mkdir(parents=True)
|
|
64
|
+
_write(project / "deepagents.toml", '[ui]\nverbose = true\n')
|
|
65
|
+
monkeypatch.chdir(nested)
|
|
66
|
+
|
|
67
|
+
cfg, sources = config.load_config()
|
|
68
|
+
assert cfg == {"ui": {"verbose": True}}
|
|
69
|
+
assert sources == [project / "deepagents.toml"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_invalid_toml_raises(tmp_path, monkeypatch):
|
|
73
|
+
home = tmp_path / "home"
|
|
74
|
+
_write(home / "config.toml", "this is = not = valid toml\n")
|
|
75
|
+
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
|
|
76
|
+
monkeypatch.chdir(tmp_path)
|
|
77
|
+
with pytest.raises(config.ConfigError):
|
|
78
|
+
config.load_config()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_get_dotted_path():
|
|
82
|
+
data = {"ui": {"verbose": True}, "agent": {"spec": "x"}}
|
|
83
|
+
assert config.get(data, "ui.verbose") is True
|
|
84
|
+
assert config.get(data, "agent.spec") == "x"
|
|
85
|
+
assert config.get(data, "missing") is None
|
|
86
|
+
assert config.get(data, "ui.missing", default="d") == "d"
|
|
87
|
+
assert config.get(data, "ui.verbose.nested", default="d") == "d"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_resolve_precedence(monkeypatch):
|
|
91
|
+
cfg = {"ui": {"verbose": True}}
|
|
92
|
+
monkeypatch.delenv("UI_VERBOSE", raising=False)
|
|
93
|
+
|
|
94
|
+
# default only
|
|
95
|
+
assert config.resolve({}, "ui.verbose", default=False) is False
|
|
96
|
+
# toml beats default
|
|
97
|
+
assert config.resolve(cfg, "ui.verbose", default=False) is True
|
|
98
|
+
# env beats toml
|
|
99
|
+
monkeypatch.setenv("UI_VERBOSE", "false")
|
|
100
|
+
assert config.resolve(cfg, "ui.verbose", env_var="UI_VERBOSE", cast=bool) is False
|
|
101
|
+
# cli beats env
|
|
102
|
+
assert config.resolve(
|
|
103
|
+
cfg, "ui.verbose", cli_value=True, env_var="UI_VERBOSE", cast=bool
|
|
104
|
+
) is True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_resolve_bool_cast(monkeypatch):
|
|
108
|
+
for truthy in ("1", "true", "yes", "on", "TRUE"):
|
|
109
|
+
monkeypatch.setenv("X", truthy)
|
|
110
|
+
assert config.resolve({}, "missing", env_var="X", cast=bool) is True
|
|
111
|
+
for falsy in ("0", "false", "no", "off", ""):
|
|
112
|
+
monkeypatch.setenv("X", falsy)
|
|
113
|
+
assert config.resolve({}, "missing", env_var="X", cast=bool) is False
|
|
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
|