deepagent-code 0.1.5__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.5 → deepagent_code-0.1.6}/PKG-INFO +32 -4
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/README.md +31 -3
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code/cli.py +86 -66
- deepagent_code-0.1.6/deepagent_code/config.py +113 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/PKG-INFO +32 -4
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/SOURCES.txt +2 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/pyproject.toml +1 -1
- deepagent_code-0.1.6/tests/test_config.py +113 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/LICENSE +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code/__init__.py +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code/utils.py +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/dependency_links.txt +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/entry_points.txt +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/requires.txt +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/deepagent_code.egg-info/top_level.txt +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/setup.cfg +0 -0
- {deepagent_code-0.1.5 → deepagent_code-0.1.6}/tests/test_cli.py +0 -0
- {deepagent_code-0.1.5 → 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
|
|
@@ -104,8 +104,36 @@ deepagent-code
|
|
|
104
104
|
# Working directory
|
|
105
105
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
106
106
|
|
|
107
|
-
#
|
|
108
|
-
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"
|
|
109
137
|
```
|
|
110
138
|
|
|
111
139
|
## CLI Options
|
|
@@ -120,9 +148,9 @@ Options:
|
|
|
120
148
|
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
121
149
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
122
150
|
-f, --file PATH Read message from a file (any extension)
|
|
123
|
-
-c, --config TEXT Config JSON or file path
|
|
124
151
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
125
152
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
153
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
126
154
|
-v, --verbose Verbose output
|
|
127
155
|
```
|
|
128
156
|
|
|
@@ -72,8 +72,36 @@ deepagent-code
|
|
|
72
72
|
# Working directory
|
|
73
73
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
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"
|
|
77
105
|
```
|
|
78
106
|
|
|
79
107
|
## CLI Options
|
|
@@ -88,9 +116,9 @@ Options:
|
|
|
88
116
|
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
89
117
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
90
118
|
-f, --file PATH Read message from a file (any extension)
|
|
91
|
-
-c, --config TEXT Config JSON or file path
|
|
92
119
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
93
120
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
121
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
94
122
|
-v, --verbose Verbose output
|
|
95
123
|
```
|
|
96
124
|
|
|
@@ -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
|
|
|
@@ -1348,11 +1359,6 @@ def run_conversation_loop(
|
|
|
1348
1359
|
type=click.Path(exists=True),
|
|
1349
1360
|
help="Read input message from a file (any extension)",
|
|
1350
1361
|
)
|
|
1351
|
-
@click.option(
|
|
1352
|
-
"--config",
|
|
1353
|
-
"-c",
|
|
1354
|
-
help="Configuration JSON string or path to JSON file",
|
|
1355
|
-
)
|
|
1356
1362
|
@click.option(
|
|
1357
1363
|
"--interactive/--no-interactive",
|
|
1358
1364
|
default=True,
|
|
@@ -1361,7 +1367,7 @@ def run_conversation_loop(
|
|
|
1361
1367
|
@click.option(
|
|
1362
1368
|
"--async-mode/--sync-mode",
|
|
1363
1369
|
"use_async",
|
|
1364
|
-
default=
|
|
1370
|
+
default=None,
|
|
1365
1371
|
help="Use async streaming (default: sync)",
|
|
1366
1372
|
)
|
|
1367
1373
|
@click.option(
|
|
@@ -1372,6 +1378,7 @@ def run_conversation_loop(
|
|
|
1372
1378
|
"--verbose",
|
|
1373
1379
|
"-v",
|
|
1374
1380
|
is_flag=True,
|
|
1381
|
+
default=None,
|
|
1375
1382
|
help="Show verbose output including node names",
|
|
1376
1383
|
)
|
|
1377
1384
|
def main(
|
|
@@ -1379,11 +1386,10 @@ def main(
|
|
|
1379
1386
|
agent_spec: Optional[str],
|
|
1380
1387
|
graph_name: Optional[str],
|
|
1381
1388
|
prompt_file: Optional[str],
|
|
1382
|
-
config: Optional[str],
|
|
1383
1389
|
interactive: bool,
|
|
1384
|
-
use_async: bool,
|
|
1390
|
+
use_async: Optional[bool],
|
|
1385
1391
|
stream_mode: Optional[str],
|
|
1386
|
-
verbose: bool,
|
|
1392
|
+
verbose: Optional[bool],
|
|
1387
1393
|
):
|
|
1388
1394
|
"""
|
|
1389
1395
|
Run a LangGraph agent from the command line.
|
|
@@ -1402,10 +1408,11 @@ def main(
|
|
|
1402
1408
|
\b
|
|
1403
1409
|
- DEEPAGENT_SPEC: Agent location (same formats as above)
|
|
1404
1410
|
- DEEPAGENT_WORKSPACE_ROOT: Working directory for the agent
|
|
1405
|
-
- DEEPAGENT_CONFIG: Configuration JSON string or path to JSON file
|
|
1406
1411
|
- DEEPAGENT_STREAM_MODE: Stream mode for LangGraph (updates or values)
|
|
1407
1412
|
|
|
1408
|
-
|
|
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.
|
|
1409
1416
|
|
|
1410
1417
|
\b
|
|
1411
1418
|
Examples:
|
|
@@ -1431,15 +1438,44 @@ def main(
|
|
|
1431
1438
|
print(f"{RED}⏺ Error reading file '{prompt_file}': {e}{RESET}")
|
|
1432
1439
|
sys.exit(1)
|
|
1433
1440
|
|
|
1434
|
-
#
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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)
|
|
1439
1447
|
|
|
1440
|
-
#
|
|
1441
|
-
final_spec =
|
|
1442
|
-
|
|
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
|
+
)
|
|
1443
1479
|
|
|
1444
1480
|
# If no spec provided, try the default agent
|
|
1445
1481
|
if not final_spec:
|
|
@@ -1455,45 +1491,29 @@ def main(
|
|
|
1455
1491
|
sys.exit(1)
|
|
1456
1492
|
|
|
1457
1493
|
# Change to workspace root if specified
|
|
1458
|
-
if
|
|
1459
|
-
workspace_path = Path(
|
|
1494
|
+
if workspace_root:
|
|
1495
|
+
workspace_path = Path(workspace_root).expanduser().resolve()
|
|
1460
1496
|
if workspace_path.exists():
|
|
1461
1497
|
os.chdir(workspace_path)
|
|
1462
1498
|
|
|
1463
1499
|
# Load the graph with a spinner
|
|
1464
1500
|
spinner = Spinner("Loading agent")
|
|
1465
1501
|
spinner.start()
|
|
1466
|
-
graph, final_graph_name = load_graph(final_spec,
|
|
1502
|
+
graph, final_graph_name = load_graph(final_spec, final_graph_name_default)
|
|
1467
1503
|
spinner.stop()
|
|
1468
1504
|
print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
|
|
1469
1505
|
|
|
1470
|
-
#
|
|
1471
|
-
config_dict =
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
config_path = Path(config_source)
|
|
1476
|
-
if config_path.exists():
|
|
1477
|
-
with open(config_path) as f:
|
|
1478
|
-
config_dict = json.load(f)
|
|
1479
|
-
else:
|
|
1480
|
-
try:
|
|
1481
|
-
config_dict = json.loads(config_source)
|
|
1482
|
-
except json.JSONDecodeError as e:
|
|
1483
|
-
print(f"{RED}⏺ Invalid config JSON: {e}{RESET}")
|
|
1484
|
-
sys.exit(1)
|
|
1485
|
-
|
|
1486
|
-
# Get stream mode
|
|
1487
|
-
final_stream_mode = stream_mode or env_stream_mode
|
|
1488
|
-
|
|
1489
|
-
# Ensure config has a thread_id for checkpointer support
|
|
1490
|
-
if config_dict is None:
|
|
1491
|
-
config_dict = {}
|
|
1492
|
-
if "configurable" not in config_dict:
|
|
1493
|
-
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)
|
|
1494
1511
|
if "thread_id" not in config_dict["configurable"]:
|
|
1495
1512
|
config_dict["configurable"]["thread_id"] = str(uuid.uuid4())
|
|
1496
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
|
+
|
|
1497
1517
|
# Extract agent name and description from graph object
|
|
1498
1518
|
agent_name = get_agent_name(graph)
|
|
1499
1519
|
agent_description = get_agent_description(graph)
|
|
@@ -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
|
|
@@ -104,8 +104,36 @@ deepagent-code
|
|
|
104
104
|
# Working directory
|
|
105
105
|
export DEEPAGENT_WORKSPACE_ROOT="/path/to/workspace"
|
|
106
106
|
|
|
107
|
-
#
|
|
108
|
-
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"
|
|
109
137
|
```
|
|
110
138
|
|
|
111
139
|
## CLI Options
|
|
@@ -120,9 +148,9 @@ Options:
|
|
|
120
148
|
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
|
|
121
149
|
-g, --graph-name TEXT Graph variable name (default: "graph")
|
|
122
150
|
-f, --file PATH Read message from a file (any extension)
|
|
123
|
-
-c, --config TEXT Config JSON or file path
|
|
124
151
|
--interactive/--no-interactive Handle interrupts (default: interactive)
|
|
125
152
|
--async-mode/--sync-mode Async streaming (default: sync)
|
|
153
|
+
--stream-mode TEXT Stream mode (updates or values)
|
|
126
154
|
-v, --verbose Verbose output
|
|
127
155
|
```
|
|
128
156
|
|
|
@@ -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
|