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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.4
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
- # With an initial message
78
- deepagent-code -m "Hello, agent!"
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
- # Configuration
105
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
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] [AGENT_SPEC]
142
+ Usage: deepagent-code [OPTIONS] [MESSAGE]
112
143
 
113
144
  Arguments:
114
- AGENT_SPEC Agent location (path/to/file.py:graph or module:graph)
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
- -m, --message TEXT Initial message
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
- # With an initial message
46
- deepagent-code -m "Hello, agent!"
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
- # Configuration
73
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
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] [AGENT_SPEC]
110
+ Usage: deepagent-code [OPTIONS] [MESSAGE]
80
111
 
81
112
  Arguments:
82
- AGENT_SPEC Agent location (path/to/file.py:graph or module:graph)
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
- -m, --message TEXT Initial message
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.4"
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
- for key, value in config.items():
1007
- if isinstance(value, dict):
1008
- print(f" {CYAN}{key}:{RESET}")
1009
- for k, v in value.items():
1010
- # Truncate long values
1011
- v_str = str(v)
1012
- if len(v_str) > 30:
1013
- v_str = v_str[:30] + "..."
1014
- print(f" {DIM}{k}:{RESET} {v_str}")
1015
- else:
1016
- print(f" {CYAN}{key}:{RESET} {value}")
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
- if key in config:
1024
- print(f"\n{CYAN}{key}:{RESET} {config[key]}\n")
1025
- elif "configurable" in config and key in config["configurable"]:
1026
- print(f"\n{CYAN}{key}:{RESET} {config['configurable'][key]}\n")
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("agent_spec", required=False)
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
- "--message",
1333
- "-m",
1334
- help="Input message to send to the agent",
1335
- )
1336
- @click.option(
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=False,
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
- message: Optional[str],
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
- AGENT_SPEC can be:
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
- Command-line arguments override environment variables.
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 my_agent.py
1395
- deepagent-code my_agent.py:graph
1396
- deepagent-code mypackage.agents:chatbot
1397
- deepagent-code -m "Hello, agent!"
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
- # Get environment variables (DEEPAGENT_SPEC preferred, DEEPAGENT_AGENT_SPEC for backwards compat)
1401
- env_agent_spec = os.getenv('DEEPAGENT_SPEC') or os.getenv('DEEPAGENT_AGENT_SPEC')
1402
- env_workspace_root = os.getenv('DEEPAGENT_WORKSPACE_ROOT')
1403
- env_config = os.getenv('DEEPAGENT_CONFIG')
1404
- env_stream_mode = os.getenv('DEEPAGENT_STREAM_MODE', 'updates')
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
- # Determine which spec to use (CLI arg > env var > default)
1407
- final_spec = agent_spec or env_agent_spec
1408
- default_graph_name = graph_name or "graph"
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 env_workspace_root:
1425
- workspace_path = Path(env_workspace_root).resolve()
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, default_graph_name)
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
- # Parse config
1437
- config_dict = None
1438
- config_source = config or env_config
1439
-
1440
- if config_source:
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.4
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
- # With an initial message
78
- deepagent-code -m "Hello, agent!"
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
- # Configuration
105
- export DEEPAGENT_CONFIG='{"configurable": {"thread_id": "1"}}'
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] [AGENT_SPEC]
142
+ Usage: deepagent-code [OPTIONS] [MESSAGE]
112
143
 
113
144
  Arguments:
114
- AGENT_SPEC Agent location (path/to/file.py:graph or module:graph)
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
- -m, --message TEXT Initial message
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepagent-code"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "A Claude Code-style CLI for running LangGraph agents from the terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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