deepagent-code 0.2.0__tar.gz → 0.3.0__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.2.0
3
+ Version: 0.3.0
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
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: langgraph>=0.2.0
22
- Requires-Dist: langgraph-stream-parser<0.3,>=0.2
22
+ Requires-Dist: langgraph-stream-parser<0.3,>=0.2.2
23
23
  Requires-Dist: click>=8.0.0
24
24
  Requires-Dist: python-dotenv
25
25
  Requires-Dist: deepagents
@@ -27,7 +27,7 @@ Provides-Extra: dev
27
27
  Requires-Dist: pytest>=7.0.0; extra == "dev"
28
28
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
29
29
  Requires-Dist: black>=23.0.0; extra == "dev"
30
- Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Requires-Dist: ruff==0.15.15; extra == "dev"
31
31
  Requires-Dist: mypy>=1.0.0; extra == "dev"
32
32
  Dynamic: license-file
33
33
 
@@ -37,6 +37,19 @@ A Claude Code-style CLI for running LangGraph agents from the terminal.
37
37
 
38
38
  ![deepagent-code](examples/image.png)
39
39
 
40
+ ## One agent, every surface
41
+
42
+ deepagent-code is the terminal surface of the **deep-agent family**: write your agent once — any LangGraph `CompiledGraph` — and run it on every surface with the same spec string (`module:attr` or `path/to/file.py:attr`), the same `deepagents.toml` config file, and the same `DEEPAGENT_*` environment variables.
43
+
44
+ | Surface | Package | Try it |
45
+ |---|---|---|
46
+ | Web app | [cowork-dash](https://github.com/dkedar7/cowork-dash) | `cowork-dash run --agent my_agent.py:graph` |
47
+ | JupyterLab | [deepagent-lab](https://github.com/dkedar7/deepagent-lab) | `pip install deepagent-lab`, then the chat sidebar in `jupyter lab` |
48
+ | Terminal | deepagent-code | **you are here** |
49
+ | VS Code | [deepagent-vscode](https://github.com/dkedar7/deepagent-vscode) | chat participant + stdio sidecar |
50
+ | Reference agent | [deepagent-hermes](https://github.com/dkedar7/deepagent-hermes) | `DEEPAGENT_AGENT_SPEC=deepagent_hermes.agent:graph` on any surface |
51
+ | Shared core | [langgraph-stream-parser](https://github.com/dkedar7/langgraph-stream-parser) | typed events + config resolver behind every surface |
52
+
40
53
  ## Installation
41
54
 
42
55
  ```bash
@@ -50,6 +63,11 @@ pip install git+https://github.com/dkedar7/deepagent-code.git
50
63
 
51
64
  ## Quick Start
52
65
 
66
+ No agent or API key yet? See the CLI working in one command:
67
+ ```bash
68
+ deepagent-code --demo "hello"
69
+ ```
70
+
53
71
  Run with the default agent (requires `ANTHROPIC_API_KEY`):
54
72
  ```bash
55
73
  export ANTHROPIC_API_KEY="your_api_key"
@@ -86,6 +104,13 @@ deepagent-code --no-interactive
86
104
 
87
105
  # Verbose output
88
106
  deepagent-code -v
107
+
108
+ # Keyless demo agent (no API key needed)
109
+ deepagent-code --demo
110
+
111
+ # Print the resolved configuration: each value, its source, and the
112
+ # env var / deepagents.toml key that sets it
113
+ deepagent-code --show-config
89
114
  ```
90
115
 
91
116
  ## Commands
@@ -99,7 +124,8 @@ In the interactive loop:
99
124
 
100
125
  ```bash
101
126
  # Agent location (path/to/file.py:variable_name or module:variable)
102
- export DEEPAGENT_SPEC="my_agent.py:graph"
127
+ # (DEEPAGENT_SPEC is still accepted as a deprecated alias)
128
+ export DEEPAGENT_AGENT_SPEC="my_agent.py:graph"
103
129
  deepagent-code
104
130
 
105
131
  # Working directory
@@ -4,6 +4,19 @@ A Claude Code-style CLI for running LangGraph agents from the terminal.
4
4
 
5
5
  ![deepagent-code](examples/image.png)
6
6
 
7
+ ## One agent, every surface
8
+
9
+ deepagent-code is the terminal surface of the **deep-agent family**: write your agent once — any LangGraph `CompiledGraph` — and run it on every surface with the same spec string (`module:attr` or `path/to/file.py:attr`), the same `deepagents.toml` config file, and the same `DEEPAGENT_*` environment variables.
10
+
11
+ | Surface | Package | Try it |
12
+ |---|---|---|
13
+ | Web app | [cowork-dash](https://github.com/dkedar7/cowork-dash) | `cowork-dash run --agent my_agent.py:graph` |
14
+ | JupyterLab | [deepagent-lab](https://github.com/dkedar7/deepagent-lab) | `pip install deepagent-lab`, then the chat sidebar in `jupyter lab` |
15
+ | Terminal | deepagent-code | **you are here** |
16
+ | VS Code | [deepagent-vscode](https://github.com/dkedar7/deepagent-vscode) | chat participant + stdio sidecar |
17
+ | Reference agent | [deepagent-hermes](https://github.com/dkedar7/deepagent-hermes) | `DEEPAGENT_AGENT_SPEC=deepagent_hermes.agent:graph` on any surface |
18
+ | Shared core | [langgraph-stream-parser](https://github.com/dkedar7/langgraph-stream-parser) | typed events + config resolver behind every surface |
19
+
7
20
  ## Installation
8
21
 
9
22
  ```bash
@@ -17,6 +30,11 @@ pip install git+https://github.com/dkedar7/deepagent-code.git
17
30
 
18
31
  ## Quick Start
19
32
 
33
+ No agent or API key yet? See the CLI working in one command:
34
+ ```bash
35
+ deepagent-code --demo "hello"
36
+ ```
37
+
20
38
  Run with the default agent (requires `ANTHROPIC_API_KEY`):
21
39
  ```bash
22
40
  export ANTHROPIC_API_KEY="your_api_key"
@@ -53,6 +71,13 @@ deepagent-code --no-interactive
53
71
 
54
72
  # Verbose output
55
73
  deepagent-code -v
74
+
75
+ # Keyless demo agent (no API key needed)
76
+ deepagent-code --demo
77
+
78
+ # Print the resolved configuration: each value, its source, and the
79
+ # env var / deepagents.toml key that sets it
80
+ deepagent-code --show-config
56
81
  ```
57
82
 
58
83
  ## Commands
@@ -66,7 +91,8 @@ In the interactive loop:
66
91
 
67
92
  ```bash
68
93
  # Agent location (path/to/file.py:variable_name or module:variable)
69
- export DEEPAGENT_SPEC="my_agent.py:graph"
94
+ # (DEEPAGENT_SPEC is still accepted as a deprecated alias)
95
+ export DEEPAGENT_AGENT_SPEC="my_agent.py:graph"
70
96
  deepagent-code
71
97
 
72
98
  # Working directory
@@ -2,8 +2,8 @@
2
2
  CLI for running arbitrary LangGraph agents from the terminal.
3
3
  Styled after Claude Code / nanocode.
4
4
  """
5
+
5
6
  import asyncio
6
- import importlib.util
7
7
  import json
8
8
  import os
9
9
  import re
@@ -15,6 +15,16 @@ import uuid
15
15
  from pathlib import Path
16
16
  from typing import Any, Dict, List, Optional, Tuple
17
17
 
18
+ import click
19
+
20
+ from langgraph_stream_parser import (
21
+ prepare_agent_input,
22
+ stream_graph_updates,
23
+ astream_graph_updates,
24
+ load_agent_spec,
25
+ )
26
+ from deepagent_code import config as config_module
27
+
18
28
  # Platform-specific imports for keyboard input
19
29
  IS_WINDOWS = sys.platform == "win32"
20
30
  if IS_WINDOWS:
@@ -23,23 +33,14 @@ else:
23
33
  import termios
24
34
  import tty
25
35
 
26
- import click
27
-
28
36
  # Try to import readline for tab completion (not available on all platforms)
29
37
  try:
30
38
  import readline
39
+
31
40
  HAS_READLINE = True
32
41
  except ImportError:
33
42
  HAS_READLINE = False
34
43
 
35
- from langgraph_stream_parser import (
36
- prepare_agent_input,
37
- stream_graph_updates,
38
- astream_graph_updates,
39
- load_agent_spec,
40
- )
41
- from deepagent_code import config as config_module
42
-
43
44
 
44
45
  # ANSI color codes (matching nanocode style)
45
46
  RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
@@ -154,6 +155,7 @@ def register_command(
154
155
  usage: Optional[str] = None,
155
156
  ):
156
157
  """Decorator to register a slash command handler."""
158
+
157
159
  def decorator(func):
158
160
  command = SlashCommand(
159
161
  name=name,
@@ -164,6 +166,7 @@ def register_command(
164
166
  )
165
167
  command_registry.register(command)
166
168
  return func
169
+
167
170
  return decorator
168
171
 
169
172
 
@@ -183,7 +186,11 @@ class Spinner:
183
186
  frame = SPINNER_FRAMES[self.frame_idx % len(SPINNER_FRAMES)]
184
187
  elapsed = time.time() - self.start_time
185
188
  elapsed_str = f"{int(elapsed)}s"
186
- print(f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}", end="", flush=True)
189
+ print(
190
+ f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}",
191
+ end="",
192
+ flush=True,
193
+ )
187
194
  self.frame_idx += 1
188
195
  time.sleep(0.08)
189
196
 
@@ -246,13 +253,13 @@ def print_goodbye():
246
253
  def get_agent_name(graph) -> str:
247
254
  """Extract agent name from graph object, defaulting to 'AgentCode'."""
248
255
  # Try common attribute names for agent/graph name
249
- for attr in ('name', 'agent_name', '_name', '__name__'):
256
+ for attr in ("name", "agent_name", "_name", "__name__"):
250
257
  if hasattr(graph, attr):
251
258
  name = getattr(graph, attr)
252
259
  if name and isinstance(name, str):
253
260
  return name
254
261
  # Check if it's a compiled graph with a name in builder
255
- if hasattr(graph, 'builder') and hasattr(graph.builder, 'name'):
262
+ if hasattr(graph, "builder") and hasattr(graph.builder, "name"):
256
263
  name = graph.builder.name
257
264
  if name and isinstance(name, str):
258
265
  return name
@@ -262,13 +269,13 @@ def get_agent_name(graph) -> str:
262
269
  def get_agent_description(graph) -> Optional[str]:
263
270
  """Extract agent description from graph object, if available."""
264
271
  # Try common attribute names for agent description
265
- for attr in ('description', 'agent_description', '_description', '__doc__'):
272
+ for attr in ("description", "agent_description", "_description", "__doc__"):
266
273
  if hasattr(graph, attr):
267
274
  desc = getattr(graph, attr)
268
275
  if desc and isinstance(desc, str) and desc.strip():
269
276
  return desc.strip()
270
277
  # Check if it's a compiled graph with a description in builder
271
- if hasattr(graph, 'builder') and hasattr(graph.builder, 'description'):
278
+ if hasattr(graph, "builder") and hasattr(graph.builder, "description"):
272
279
  desc = graph.builder.description
273
280
  if desc and isinstance(desc, str) and desc.strip():
274
281
  return desc.strip()
@@ -283,59 +290,59 @@ def text_to_ascii_art(text: str) -> List[str]:
283
290
  """
284
291
  # Clean 3-line block font - each char is exactly 3 wide
285
292
  FONT = {
286
- 'A': ['▄▀▄', '█▀█', '▀ ▀'],
287
- 'B': ['█▀▄', '█▀▄', '▀▀▀'],
288
- 'C': ['▄▀▀', '', '▀▀▀'],
289
- 'D': ['█▀▄', '█ █', '▀▀▀'],
290
- 'E': ['█▀▀', '█▀▀', '▀▀▀'],
291
- 'F': ['█▀▀', '█▀▀', ''],
292
- 'G': ['▄▀▀', '█▀█', '▀▀▀'],
293
- 'H': ['█ █', '█▀█', '▀ ▀'],
294
- 'I': ['▀█▀', '', '▀▀▀'],
295
- 'J': ['▀▀█', '', '▀▀▀'],
296
- 'K': ['█ █', '█▀▄', '▀ ▀'],
297
- 'L': ['', '', '▀▀▀'],
298
- 'M': ['█▄█', '█ █', '▀ ▀'],
299
- 'N': ['█▀█', '█ █', '▀ ▀'],
300
- 'O': ['▄▀▄', '█ █', '▀▀▀'],
301
- 'P': ['█▀▄', '█▀▀', ''],
302
- 'Q': ['▄▀▄', '█ █', '▀▀█'],
303
- 'R': ['█▀▄', '█▀▄', '▀ ▀'],
304
- 'S': ['▄▀▀', '▀▀▄', '▀▀▀'],
305
- 'T': ['▀█▀', '', ''],
306
- 'U': ['█ █', '█ █', '▀▀▀'],
307
- 'V': ['█ █', '█ █', ''],
308
- 'W': ['█ █', '█▀█', '▀ ▀'],
309
- 'X': ['▀▄▀', '', '▀ ▀'],
310
- 'Y': ['█ █', '', ''],
311
- 'Z': ['▀▀█', '', '█▀▀'],
312
- '0': ['▄▀▄', '█ █', '▀▀▀'],
313
- '1': ['▄█ ', '', '▀▀▀'],
314
- '2': ['▀▀█', '▄▀▀', '▀▀▀'],
315
- '3': ['▀▀█', ' ▀█', '▀▀▀'],
316
- '4': ['█ █', '▀▀█', ''],
317
- '5': ['█▀▀', '▀▀▄', '▀▀▀'],
318
- '6': ['▄▀▀', '█▀█', '▀▀▀'],
319
- '7': ['▀▀█', '', ''],
320
- '8': ['▄▀▄', '█▀█', '▀▀▀'],
321
- '9': ['▄▀█', '▀▀█', '▀▀▀'],
322
- ' ': [' ', ' ', ' '],
323
- '-': [' ', '▀▀▀', ' '],
324
- '_': [' ', ' ', '▀▀▀'],
325
- '.': [' ', ' ', ''],
293
+ "A": ["▄▀▄", "█▀█", "▀ ▀"],
294
+ "B": ["█▀▄", "█▀▄", "▀▀▀"],
295
+ "C": ["▄▀▀", "", "▀▀▀"],
296
+ "D": ["█▀▄", "█ █", "▀▀▀"],
297
+ "E": ["█▀▀", "█▀▀", "▀▀▀"],
298
+ "F": ["█▀▀", "█▀▀", ""],
299
+ "G": ["▄▀▀", "█▀█", "▀▀▀"],
300
+ "H": ["█ █", "█▀█", "▀ ▀"],
301
+ "I": ["▀█▀", "", "▀▀▀"],
302
+ "J": ["▀▀█", "", "▀▀▀"],
303
+ "K": ["█ █", "█▀▄", "▀ ▀"],
304
+ "L": ["", "", "▀▀▀"],
305
+ "M": ["█▄█", "█ █", "▀ ▀"],
306
+ "N": ["█▀█", "█ █", "▀ ▀"],
307
+ "O": ["▄▀▄", "█ █", "▀▀▀"],
308
+ "P": ["█▀▄", "█▀▀", ""],
309
+ "Q": ["▄▀▄", "█ █", "▀▀█"],
310
+ "R": ["█▀▄", "█▀▄", "▀ ▀"],
311
+ "S": ["▄▀▀", "▀▀▄", "▀▀▀"],
312
+ "T": ["▀█▀", "", ""],
313
+ "U": ["█ █", "█ █", "▀▀▀"],
314
+ "V": ["█ █", "█ █", ""],
315
+ "W": ["█ █", "█▀█", "▀ ▀"],
316
+ "X": ["▀▄▀", "", "▀ ▀"],
317
+ "Y": ["█ █", "", ""],
318
+ "Z": ["▀▀█", "", "█▀▀"],
319
+ "0": ["▄▀▄", "█ █", "▀▀▀"],
320
+ "1": ["▄█ ", "", "▀▀▀"],
321
+ "2": ["▀▀█", "▄▀▀", "▀▀▀"],
322
+ "3": ["▀▀█", " ▀█", "▀▀▀"],
323
+ "4": ["█ █", "▀▀█", ""],
324
+ "5": ["█▀▀", "▀▀▄", "▀▀▀"],
325
+ "6": ["▄▀▀", "█▀█", "▀▀▀"],
326
+ "7": ["▀▀█", "", ""],
327
+ "8": ["▄▀▄", "█▀█", "▀▀▀"],
328
+ "9": ["▄▀█", "▀▀█", "▀▀▀"],
329
+ " ": [" ", " ", " "],
330
+ "-": [" ", "▀▀▀", " "],
331
+ "_": [" ", " ", "▀▀▀"],
332
+ ".": [" ", " ", ""],
326
333
  }
327
334
 
328
335
  # Default char for unknown characters
329
- DEFAULT = [' ', '', ' ']
336
+ DEFAULT = [" ", "", " "]
330
337
 
331
- lines = ['', '', '']
338
+ lines = ["", "", ""]
332
339
  for char in text.upper():
333
340
  char_art = FONT.get(char, DEFAULT)
334
341
  for i in range(3):
335
- lines[i] += char_art[i] + ' '
342
+ lines[i] += char_art[i] + " "
336
343
 
337
344
  # Remove only the final trailing space we added (not internal spaces from chars like T, P)
338
- return [line[:-1] if line.endswith(' ') else line for line in lines]
345
+ return [line[:-1] if line.endswith(" ") else line for line in lines]
339
346
 
340
347
 
341
348
  def print_header_box(agent_name: str, cwd: str, description: Optional[str] = None):
@@ -359,7 +366,7 @@ def print_header_box(agent_name: str, cwd: str, description: Optional[str] = Non
359
366
  # Build cwd line with label
360
367
  cwd_label = "cwd: "
361
368
  max_cwd_len = inner_width - len(cwd_label)
362
- cwd_display = cwd if len(cwd) <= max_cwd_len else "..." + cwd[-(max_cwd_len - 3):]
369
+ cwd_display = cwd if len(cwd) <= max_cwd_len else "..." + cwd[-(max_cwd_len - 3) :]
363
370
  cwd_with_label = f"{cwd_label}{cwd_display}"
364
371
  cwd_line = cwd_with_label.center(inner_width)
365
372
 
@@ -371,16 +378,24 @@ def print_header_box(agent_name: str, cwd: str, description: Optional[str] = Non
371
378
  # Print ASCII art lines centered
372
379
  for line in ascii_lines:
373
380
  centered_line = line.center(inner_width)
374
- print(f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{centered_line}{RESET} {BRIGHT_CYAN}{V}{RESET}")
381
+ print(
382
+ f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{centered_line}{RESET} {BRIGHT_CYAN}{V}{RESET}"
383
+ )
375
384
  else:
376
385
  # Fall back to plain text if ASCII art doesn't fit
377
386
  title_line = agent_name.center(inner_width)
378
- print(f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{title_line}{RESET} {BRIGHT_CYAN}{V}{RESET}")
387
+ print(
388
+ f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{title_line}{RESET} {BRIGHT_CYAN}{V}{RESET}"
389
+ )
379
390
 
380
391
  # Print description line if available
381
392
  if description:
382
393
  # Truncate description if too long
383
- desc_display = description if len(description) <= inner_width else description[:inner_width - 3] + "..."
394
+ desc_display = (
395
+ description
396
+ if len(description) <= inner_width
397
+ else description[: inner_width - 3] + "..."
398
+ )
384
399
  desc_line = desc_display.center(inner_width)
385
400
  print(f"{CYAN}{V}{RESET} {DIM}{ITALIC}{desc_line}{RESET} {CYAN}{V}{RESET}")
386
401
 
@@ -417,17 +432,17 @@ def parse_agent_spec(agent_spec: str) -> Tuple[str, str]:
417
432
  Raises:
418
433
  ValueError: If format is invalid
419
434
  """
420
- if ':' not in agent_spec:
435
+ if ":" not in agent_spec:
421
436
  raise ValueError(
422
437
  f"Invalid agent spec format: '{agent_spec}'. "
423
438
  f"Expected format: 'path/to/file.py:variable_name'"
424
439
  )
425
440
 
426
- parts = agent_spec.rsplit(':', 1)
441
+ parts = agent_spec.rsplit(":", 1)
427
442
  file_path = parts[0]
428
443
  variable_name = parts[1]
429
444
 
430
- if not file_path.endswith('.py'):
445
+ if not file_path.endswith(".py"):
431
446
  raise ValueError(f"Agent spec file must be a .py file: {file_path}")
432
447
 
433
448
  return file_path, variable_name
@@ -457,12 +472,12 @@ def load_graph(spec: str, default_graph_name: str = "graph"):
457
472
  """
458
473
  path_or_module = spec
459
474
  graph_name = default_graph_name
460
- if ':' in spec:
461
- head, _, tail = spec.rpartition(':')
475
+ if ":" in spec:
476
+ head, _, tail = spec.rpartition(":")
462
477
  # Only treat the trailing ':token' as a graph name if it looks like one
463
478
  # — i.e. it has no path separators. This avoids mistaking a Windows
464
479
  # drive-letter colon (e.g. 'C:\path\agent.py') for a name suffix.
465
- if tail and '/' not in tail and '\\' not in tail:
480
+ if tail and "/" not in tail and "\\" not in tail:
466
481
  path_or_module = head
467
482
  graph_name = tail or default_graph_name
468
483
 
@@ -561,8 +576,8 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
561
576
  print(f"\n{YELLOW}⚠ Action Required{RESET}")
562
577
  if action_requests:
563
578
  for i, action in enumerate(action_requests):
564
- tool = action.get('tool', 'unknown')
565
- args_preview = get_tool_arg_preview(action.get('args', {}))
579
+ tool = action.get("tool", "unknown")
580
+ args_preview = get_tool_arg_preview(action.get("args", {}))
566
581
  print(f" {DIM}{i + 1}. {tool}{RESET}")
567
582
  if args_preview:
568
583
  print(f" {DIM}└─ {args_preview}{RESET}")
@@ -580,18 +595,18 @@ def get_key() -> str:
580
595
  if IS_WINDOWS:
581
596
  # Windows implementation using msvcrt
582
597
  ch = msvcrt.getch()
583
- if ch in (b'\x00', b'\xe0'): # Special keys (arrows, function keys)
598
+ if ch in (b"\x00", b"\xe0"): # Special keys (arrows, function keys)
584
599
  ch2 = msvcrt.getch()
585
- if ch2 == b'H':
586
- return 'up'
587
- elif ch2 == b'P':
588
- return 'down'
589
- return ch2.decode('utf-8', errors='ignore')
590
- elif ch == b'\r':
591
- return 'enter'
592
- elif ch == b'\x03': # Ctrl+C
593
- return 'ctrl-c'
594
- return ch.decode('utf-8', errors='ignore')
600
+ if ch2 == b"H":
601
+ return "up"
602
+ elif ch2 == b"P":
603
+ return "down"
604
+ return ch2.decode("utf-8", errors="ignore")
605
+ elif ch == b"\r":
606
+ return "enter"
607
+ elif ch == b"\x03": # Ctrl+C
608
+ return "ctrl-c"
609
+ return ch.decode("utf-8", errors="ignore")
595
610
  else:
596
611
  # Unix implementation using termios/tty
597
612
  fd = sys.stdin.fileno()
@@ -600,18 +615,18 @@ def get_key() -> str:
600
615
  tty.setraw(fd)
601
616
  ch = sys.stdin.read(1)
602
617
  # Handle escape sequences (arrow keys)
603
- if ch == '\x1b':
618
+ if ch == "\x1b":
604
619
  ch2 = sys.stdin.read(1)
605
- if ch2 == '[':
620
+ if ch2 == "[":
606
621
  ch3 = sys.stdin.read(1)
607
- if ch3 == 'A':
608
- return 'up'
609
- elif ch3 == 'B':
610
- return 'down'
611
- elif ch == '\r' or ch == '\n':
612
- return 'enter'
613
- elif ch == '\x03': # Ctrl+C
614
- return 'ctrl-c'
622
+ if ch3 == "A":
623
+ return "up"
624
+ elif ch3 == "B":
625
+ return "down"
626
+ elif ch == "\r" or ch == "\n":
627
+ return "enter"
628
+ elif ch == "\x03": # Ctrl+C
629
+ return "ctrl-c"
615
630
  return ch
616
631
  finally:
617
632
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
@@ -647,13 +662,13 @@ def select_option(options: List[str], prompt: str = "Select an option:") -> int:
647
662
  while True:
648
663
  key = get_key()
649
664
 
650
- if key == 'up' and selected > 0:
665
+ if key == "up" and selected > 0:
651
666
  selected -= 1
652
- elif key == 'down' and selected < num_options - 1:
667
+ elif key == "down" and selected < num_options - 1:
653
668
  selected += 1
654
- elif key == 'enter':
669
+ elif key == "enter":
655
670
  break
656
- elif key == 'ctrl-c':
671
+ elif key == "ctrl-c":
657
672
  print("\033[?25h", end="") # Show cursor
658
673
  sys.exit(0)
659
674
 
@@ -732,7 +747,9 @@ async def run_single_turn_async(
732
747
  spinner = Spinner("Thinking")
733
748
  spinner.start()
734
749
 
735
- async for chunk in astream_graph_updates(graph, input_data, config=config, stream_mode=stream_mode):
750
+ async for chunk in astream_graph_updates(
751
+ graph, input_data, config=config, stream_mode=stream_mode
752
+ ):
736
753
  # Stop spinner on first chunk
737
754
  if first_chunk:
738
755
  spinner.stop()
@@ -779,7 +796,9 @@ def run_single_turn_sync(
779
796
  spinner = Spinner("Thinking")
780
797
  spinner.start()
781
798
 
782
- for chunk in stream_graph_updates(graph, input_data, config=config, stream_mode=stream_mode):
799
+ for chunk in stream_graph_updates(
800
+ graph, input_data, config=config, stream_mode=stream_mode
801
+ ):
783
802
  # Stop spinner on first chunk
784
803
  if first_chunk:
785
804
  spinner.stop()
@@ -832,6 +851,7 @@ def print_help():
832
851
 
833
852
  # --- Built-in Slash Commands ---
834
853
 
854
+
835
855
  @register_command(
836
856
  name="help",
837
857
  description="Show this help message",
@@ -943,6 +963,7 @@ def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
943
963
  # Full resolved view: each value, where it came from, and the env var
944
964
  # / TOML key that sets it.
945
965
  from deepagent_code.config import CodeConfig
966
+
946
967
  for line in CodeConfig.resolve().describe().splitlines():
947
968
  print(f" {line}")
948
969
 
@@ -1178,10 +1199,14 @@ def run_conversation_loop(
1178
1199
 
1179
1200
  if use_async:
1180
1201
  duration = asyncio.run(
1181
- run_single_turn_async(graph, initial_message, config, interactive, verbose, stream_mode)
1202
+ run_single_turn_async(
1203
+ graph, initial_message, config, interactive, verbose, stream_mode
1204
+ )
1182
1205
  )
1183
1206
  else:
1184
- duration = run_single_turn_sync(graph, initial_message, config, interactive, verbose, stream_mode)
1207
+ duration = run_single_turn_sync(
1208
+ graph, initial_message, config, interactive, verbose, stream_mode
1209
+ )
1185
1210
  print_timing(duration, verbose)
1186
1211
  print()
1187
1212
 
@@ -1252,10 +1277,14 @@ def run_conversation_loop(
1252
1277
  # Run the agent
1253
1278
  if use_async:
1254
1279
  duration = asyncio.run(
1255
- run_single_turn_async(graph, user_input, config, interactive, verbose, stream_mode)
1280
+ run_single_turn_async(
1281
+ graph, user_input, config, interactive, verbose, stream_mode
1282
+ )
1256
1283
  )
1257
1284
  else:
1258
- duration = run_single_turn_sync(graph, user_input, config, interactive, verbose, stream_mode)
1285
+ duration = run_single_turn_sync(
1286
+ graph, user_input, config, interactive, verbose, stream_mode
1287
+ )
1259
1288
  print_timing(duration, verbose)
1260
1289
  print()
1261
1290
 
@@ -1310,6 +1339,19 @@ def run_conversation_loop(
1310
1339
  default=None,
1311
1340
  help="Show verbose output including node names",
1312
1341
  )
1342
+ @click.option(
1343
+ "--demo",
1344
+ is_flag=True,
1345
+ default=False,
1346
+ help="Run with the built-in keyless demo agent — no API key needed",
1347
+ )
1348
+ @click.option(
1349
+ "--show-config",
1350
+ "show_config",
1351
+ is_flag=True,
1352
+ default=False,
1353
+ help="Print the resolved configuration (defaults < deepagents.toml < env < CLI) and exit",
1354
+ )
1313
1355
  def main(
1314
1356
  message: Optional[str],
1315
1357
  agent_spec: Optional[str],
@@ -1319,6 +1361,8 @@ def main(
1319
1361
  use_async: Optional[bool],
1320
1362
  stream_mode: Optional[str],
1321
1363
  verbose: Optional[bool],
1364
+ demo: bool,
1365
+ show_config: bool,
1322
1366
  ):
1323
1367
  """
1324
1368
  Run a LangGraph agent from the command line.
@@ -1350,7 +1394,20 @@ def main(
1350
1394
  deepagent-code -a my_agent.py "What can you do?"
1351
1395
  deepagent-code -a my_agent.py:graph
1352
1396
  deepagent-code -f ./prompt.md
1397
+ deepagent-code --demo "try it with no API key"
1398
+ deepagent-code --show-config
1353
1399
  """
1400
+ if show_config:
1401
+ print(config_module.CodeConfig.resolve(toml_start=Path.cwd()).describe())
1402
+ return
1403
+
1404
+ if demo:
1405
+ if agent_spec:
1406
+ print(f"{RED}⏺ Error: --demo and -a/--agent are mutually exclusive{RESET}")
1407
+ sys.exit(1)
1408
+ # The keyless echo agent shipped with the shared core.
1409
+ agent_spec = "langgraph_stream_parser.demo.stub:graph"
1410
+
1354
1411
  try:
1355
1412
  # Handle -f/--file option: read message from file
1356
1413
  if prompt_file and message:
@@ -1359,7 +1416,7 @@ def main(
1359
1416
 
1360
1417
  if prompt_file:
1361
1418
  try:
1362
- with open(prompt_file, 'r', encoding='utf-8') as f:
1419
+ with open(prompt_file, "r", encoding="utf-8") as f:
1363
1420
  message = f.read().strip()
1364
1421
  if not message:
1365
1422
  print(f"{RED}⏺ Error: File '{prompt_file}' is empty{RESET}")
@@ -1397,9 +1454,7 @@ def main(
1397
1454
  verbose = cfg.verbose
1398
1455
  # Only change directory when a workspace root was actually configured.
1399
1456
  workspace_root = (
1400
- cfg.workspace_root
1401
- if cfg.sources.get("workspace_root") != "default"
1402
- else None
1457
+ cfg.workspace_root if cfg.sources.get("workspace_root") != "default" else None
1403
1458
  )
1404
1459
 
1405
1460
  # If no spec provided, try the default agent
@@ -1410,8 +1465,8 @@ def main(
1410
1465
  else:
1411
1466
  print(f"{RED}⏺ Error: No agent specified.{RESET}")
1412
1467
  print(f"\n{DIM}Usage:{RESET}")
1413
- print(f" deepagent-code path/to/agent.py:graph")
1414
- print(f" deepagent-code mypackage.module:agent")
1468
+ print(" deepagent-code path/to/agent.py:graph")
1469
+ print(" deepagent-code mypackage.module:agent")
1415
1470
  print(f"\n{DIM}Or set DEEPAGENT_AGENT_SPEC environment variable{RESET}")
1416
1471
  sys.exit(1)
1417
1472
 
@@ -1472,6 +1527,7 @@ def main(
1472
1527
  print(f"{RED}⏺ Error: {e}{RESET}")
1473
1528
  if verbose:
1474
1529
  import traceback
1530
+
1475
1531
  print(traceback.format_exc())
1476
1532
  sys.exit(1)
1477
1533
 
@@ -9,6 +9,7 @@ vars, then CLI overrides.
9
9
  ``get`` / ``resolve`` remain for the ``[configurable]`` passthrough and ad-hoc
10
10
  lookups.
11
11
  """
12
+
12
13
  import os
13
14
  import tomllib
14
15
  import warnings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: langgraph>=0.2.0
22
- Requires-Dist: langgraph-stream-parser<0.3,>=0.2
22
+ Requires-Dist: langgraph-stream-parser<0.3,>=0.2.2
23
23
  Requires-Dist: click>=8.0.0
24
24
  Requires-Dist: python-dotenv
25
25
  Requires-Dist: deepagents
@@ -27,7 +27,7 @@ Provides-Extra: dev
27
27
  Requires-Dist: pytest>=7.0.0; extra == "dev"
28
28
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
29
29
  Requires-Dist: black>=23.0.0; extra == "dev"
30
- Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Requires-Dist: ruff==0.15.15; extra == "dev"
31
31
  Requires-Dist: mypy>=1.0.0; extra == "dev"
32
32
  Dynamic: license-file
33
33
 
@@ -37,6 +37,19 @@ A Claude Code-style CLI for running LangGraph agents from the terminal.
37
37
 
38
38
  ![deepagent-code](examples/image.png)
39
39
 
40
+ ## One agent, every surface
41
+
42
+ deepagent-code is the terminal surface of the **deep-agent family**: write your agent once — any LangGraph `CompiledGraph` — and run it on every surface with the same spec string (`module:attr` or `path/to/file.py:attr`), the same `deepagents.toml` config file, and the same `DEEPAGENT_*` environment variables.
43
+
44
+ | Surface | Package | Try it |
45
+ |---|---|---|
46
+ | Web app | [cowork-dash](https://github.com/dkedar7/cowork-dash) | `cowork-dash run --agent my_agent.py:graph` |
47
+ | JupyterLab | [deepagent-lab](https://github.com/dkedar7/deepagent-lab) | `pip install deepagent-lab`, then the chat sidebar in `jupyter lab` |
48
+ | Terminal | deepagent-code | **you are here** |
49
+ | VS Code | [deepagent-vscode](https://github.com/dkedar7/deepagent-vscode) | chat participant + stdio sidecar |
50
+ | Reference agent | [deepagent-hermes](https://github.com/dkedar7/deepagent-hermes) | `DEEPAGENT_AGENT_SPEC=deepagent_hermes.agent:graph` on any surface |
51
+ | Shared core | [langgraph-stream-parser](https://github.com/dkedar7/langgraph-stream-parser) | typed events + config resolver behind every surface |
52
+
40
53
  ## Installation
41
54
 
42
55
  ```bash
@@ -50,6 +63,11 @@ pip install git+https://github.com/dkedar7/deepagent-code.git
50
63
 
51
64
  ## Quick Start
52
65
 
66
+ No agent or API key yet? See the CLI working in one command:
67
+ ```bash
68
+ deepagent-code --demo "hello"
69
+ ```
70
+
53
71
  Run with the default agent (requires `ANTHROPIC_API_KEY`):
54
72
  ```bash
55
73
  export ANTHROPIC_API_KEY="your_api_key"
@@ -86,6 +104,13 @@ deepagent-code --no-interactive
86
104
 
87
105
  # Verbose output
88
106
  deepagent-code -v
107
+
108
+ # Keyless demo agent (no API key needed)
109
+ deepagent-code --demo
110
+
111
+ # Print the resolved configuration: each value, its source, and the
112
+ # env var / deepagents.toml key that sets it
113
+ deepagent-code --show-config
89
114
  ```
90
115
 
91
116
  ## Commands
@@ -99,7 +124,8 @@ In the interactive loop:
99
124
 
100
125
  ```bash
101
126
  # Agent location (path/to/file.py:variable_name or module:variable)
102
- export DEEPAGENT_SPEC="my_agent.py:graph"
127
+ # (DEEPAGENT_SPEC is still accepted as a deprecated alias)
128
+ export DEEPAGENT_AGENT_SPEC="my_agent.py:graph"
103
129
  deepagent-code
104
130
 
105
131
  # Working directory
@@ -1,5 +1,5 @@
1
1
  langgraph>=0.2.0
2
- langgraph-stream-parser<0.3,>=0.2
2
+ langgraph-stream-parser<0.3,>=0.2.2
3
3
  click>=8.0.0
4
4
  python-dotenv
5
5
  deepagents
@@ -8,5 +8,5 @@ deepagents
8
8
  pytest>=7.0.0
9
9
  pytest-asyncio>=0.21.0
10
10
  black>=23.0.0
11
- ruff>=0.1.0
11
+ ruff==0.15.15
12
12
  mypy>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepagent-code"
7
- version = "0.2.0"
7
+ version = "0.3.0"
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"
@@ -25,7 +25,7 @@ classifiers = [
25
25
 
26
26
  dependencies = [
27
27
  "langgraph>=0.2.0",
28
- "langgraph-stream-parser>=0.2,<0.3",
28
+ "langgraph-stream-parser>=0.2.2,<0.3",
29
29
  "click>=8.0.0",
30
30
  "python-dotenv",
31
31
  "deepagents",
@@ -36,7 +36,7 @@ dev = [
36
36
  "pytest>=7.0.0",
37
37
  "pytest-asyncio>=0.21.0",
38
38
  "black>=23.0.0",
39
- "ruff>=0.1.0",
39
+ "ruff==0.15.15",
40
40
  "mypy>=1.0.0",
41
41
  ]
42
42
 
@@ -60,6 +60,10 @@ target-version = ['py313']
60
60
  line-length = 100
61
61
  target-version = "py313"
62
62
 
63
+ [tool.ruff.lint.per-file-ignores]
64
+ # Example scripts call load_dotenv() before importing modules that read env.
65
+ "examples/*" = ["E402"]
66
+
63
67
  [tool.mypy]
64
68
  python_version = "3.13"
65
69
  warn_return_any = true
@@ -1,6 +1,32 @@
1
1
  """Tests for CLI functions."""
2
+
2
3
  import pytest
3
- from deepagent_code.cli import parse_agent_spec
4
+ from click.testing import CliRunner
5
+
6
+ from deepagent_code.cli import main, parse_agent_spec
7
+
8
+
9
+ class TestCliFlags:
10
+ """Tests for --show-config and --demo."""
11
+
12
+ def test_show_config_prints_resolved_config(self):
13
+ result = CliRunner().invoke(main, ["--show-config"])
14
+ assert result.exit_code == 0, result.output
15
+ assert "DEEPAGENT_AGENT_SPEC" in result.output
16
+
17
+ def test_demo_and_agent_are_mutually_exclusive(self):
18
+ result = CliRunner().invoke(main, ["--demo", "-a", "x.py:g", "hi"])
19
+ assert result.exit_code != 0
20
+ assert "mutually exclusive" in result.output
21
+
22
+ def test_demo_replies_without_any_key(self, monkeypatch):
23
+ # A full one-shot turn through the real stub agent — proves --demo
24
+ # works on a machine with no API keys configured.
25
+ monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
26
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
27
+ result = CliRunner().invoke(main, ["--demo", "--no-interactive", "hello demo"])
28
+ assert result.exit_code == 0, result.output
29
+ assert "hello demo" in result.output
4
30
 
5
31
 
6
32
  class TestParseAgentSpec:
@@ -1,4 +1,5 @@
1
1
  """Tests for CodeConfig — deepagent-code's HostConfig subclass."""
2
+
2
3
  from pathlib import Path
3
4
 
4
5
  import pytest
@@ -34,9 +35,11 @@ def test_env_stream_mode(isolated, tmp_path):
34
35
 
35
36
 
36
37
  def test_toml_keys(isolated, tmp_path):
37
- _toml(tmp_path,
38
- '[agent]\nspec = "a.py:g"\ngraph_name = "myg"\n'
39
- '[ui]\nverbose = true\nasync_mode = true\nstream_mode = "values"\n')
38
+ _toml(
39
+ tmp_path,
40
+ '[agent]\nspec = "a.py:g"\ngraph_name = "myg"\n'
41
+ '[ui]\nverbose = true\nasync_mode = true\nstream_mode = "values"\n',
42
+ )
40
43
  cfg = CodeConfig.resolve(env={}, toml_start=tmp_path)
41
44
  assert cfg.agent_spec == "a.py:g"
42
45
  assert cfg.graph_name == "myg"
@@ -1,4 +1,5 @@
1
1
  """Tests for deepagent_code.config."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from pathlib import Path
@@ -23,7 +24,7 @@ def test_returns_empty_when_no_files(tmp_path, monkeypatch):
23
24
 
24
25
  def test_loads_global_only(tmp_path, monkeypatch):
25
26
  home = tmp_path / "home"
26
- _write(home / "config.toml", '[ui]\nverbose = true\n')
27
+ _write(home / "config.toml", "[ui]\nverbose = true\n")
27
28
  monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
28
29
  monkeypatch.chdir(tmp_path)
29
30
  cfg, sources = config.load_config()
@@ -47,7 +48,7 @@ def test_project_overrides_global(tmp_path, monkeypatch):
47
48
  monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
48
49
 
49
50
  project = tmp_path / "proj"
50
- _write(project / "deepagents.toml", '[ui]\nverbose = true\n')
51
+ _write(project / "deepagents.toml", "[ui]\nverbose = true\n")
51
52
  monkeypatch.chdir(project)
52
53
 
53
54
  cfg, sources = config.load_config()
@@ -61,7 +62,7 @@ def test_walks_up_for_project_config(tmp_path, monkeypatch):
61
62
  project = tmp_path / "proj"
62
63
  nested = project / "a" / "b" / "c"
63
64
  nested.mkdir(parents=True)
64
- _write(project / "deepagents.toml", '[ui]\nverbose = true\n')
65
+ _write(project / "deepagents.toml", "[ui]\nverbose = true\n")
65
66
  monkeypatch.chdir(nested)
66
67
 
67
68
  cfg, sources = config.load_config()
@@ -99,9 +100,9 @@ def test_resolve_precedence(monkeypatch):
99
100
  monkeypatch.setenv("UI_VERBOSE", "false")
100
101
  assert config.resolve(cfg, "ui.verbose", env_var="UI_VERBOSE", cast=bool) is False
101
102
  # cli beats env
102
- assert config.resolve(
103
- cfg, "ui.verbose", cli_value=True, env_var="UI_VERBOSE", cast=bool
104
- ) is True
103
+ assert (
104
+ config.resolve(cfg, "ui.verbose", cli_value=True, env_var="UI_VERBOSE", cast=bool) is True
105
+ )
105
106
 
106
107
 
107
108
  def test_resolve_bool_cast(monkeypatch):
File without changes
File without changes