deepagent-code 0.1.1__tar.gz → 0.1.2__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.1
3
+ Version: 0.1.2
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
@@ -8,16 +8,29 @@ import json
8
8
  import os
9
9
  import re
10
10
  import sys
11
- import termios
12
11
  import threading
13
12
  import time
14
- import tty
15
13
  import uuid
16
14
  from pathlib import Path
17
15
  from typing import Any, Dict, List, Optional, Tuple
18
16
 
17
+ # Platform-specific imports for keyboard input
18
+ IS_WINDOWS = sys.platform == "win32"
19
+ if IS_WINDOWS:
20
+ import msvcrt
21
+ else:
22
+ import termios
23
+ import tty
24
+
19
25
  import click
20
26
 
27
+ # Try to import readline for tab completion (not available on all platforms)
28
+ try:
29
+ import readline
30
+ HAS_READLINE = True
31
+ except ImportError:
32
+ HAS_READLINE = False
33
+
21
34
  from deepagent_code.utils import (
22
35
  prepare_agent_input,
23
36
  stream_graph_updates,
@@ -27,12 +40,110 @@ from deepagent_code.utils import (
27
40
 
28
41
  # ANSI color codes (matching nanocode style)
29
42
  RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
43
+ ITALIC, UNDERLINE = "\033[3m", "\033[4m"
30
44
  BLUE, CYAN, GREEN, YELLOW, RED = "\033[34m", "\033[36m", "\033[32m", "\033[33m", "\033[31m"
45
+ MAGENTA, WHITE, GRAY = "\033[35m", "\033[37m", "\033[90m"
46
+
47
+ # Bright variants for gradient effects
48
+ BRIGHT_CYAN, BRIGHT_BLUE = "\033[96m", "\033[94m"
49
+ BRIGHT_GREEN, BRIGHT_YELLOW = "\033[92m", "\033[93m"
31
50
 
32
51
  # Spinner frames for thinking animation
33
52
  SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
34
53
 
35
54
 
55
+ # Version info
56
+ __version__ = "0.1.2"
57
+
58
+
59
+ # Slash command registry
60
+ class SlashCommand:
61
+ """Represents a slash command with its handler and metadata."""
62
+
63
+ def __init__(
64
+ self,
65
+ name: str,
66
+ handler: callable,
67
+ description: str,
68
+ aliases: Optional[List[str]] = None,
69
+ usage: Optional[str] = None,
70
+ ):
71
+ self.name = name
72
+ self.handler = handler
73
+ self.description = description
74
+ self.aliases = aliases or []
75
+ self.usage = usage or f"/{name}"
76
+
77
+ def execute(self, args: str, context: Dict[str, Any]) -> Optional[str]:
78
+ """Execute the command with given arguments and context."""
79
+ return self.handler(args, context)
80
+
81
+
82
+ class CommandRegistry:
83
+ """Registry for slash commands."""
84
+
85
+ def __init__(self):
86
+ self._commands: Dict[str, SlashCommand] = {}
87
+ self._alias_map: Dict[str, str] = {}
88
+
89
+ def register(self, command: SlashCommand):
90
+ """Register a slash command."""
91
+ self._commands[command.name] = command
92
+ for alias in command.aliases:
93
+ self._alias_map[alias] = command.name
94
+
95
+ def get(self, name: str) -> Optional[SlashCommand]:
96
+ """Get a command by name or alias."""
97
+ # Check if it's an alias
98
+ if name in self._alias_map:
99
+ name = self._alias_map[name]
100
+ return self._commands.get(name)
101
+
102
+ def all_commands(self) -> List[SlashCommand]:
103
+ """Get all registered commands."""
104
+ return list(self._commands.values())
105
+
106
+ def parse_input(self, user_input: str) -> Tuple[Optional[str], str]:
107
+ """Parse user input to extract command name and arguments.
108
+
109
+ Returns:
110
+ Tuple of (command_name, arguments) or (None, original_input) if not a command
111
+ """
112
+ if not user_input.startswith("/"):
113
+ return None, user_input
114
+
115
+ # Split into command and args
116
+ parts = user_input[1:].split(maxsplit=1)
117
+ cmd_name = parts[0].lower() if parts else ""
118
+ args = parts[1] if len(parts) > 1 else ""
119
+
120
+ return cmd_name, args
121
+
122
+
123
+ # Global command registry
124
+ command_registry = CommandRegistry()
125
+
126
+
127
+ def register_command(
128
+ name: str,
129
+ description: str,
130
+ aliases: Optional[List[str]] = None,
131
+ usage: Optional[str] = None,
132
+ ):
133
+ """Decorator to register a slash command handler."""
134
+ def decorator(func):
135
+ command = SlashCommand(
136
+ name=name,
137
+ handler=func,
138
+ description=description,
139
+ aliases=aliases or [],
140
+ usage=usage,
141
+ )
142
+ command_registry.register(command)
143
+ return func
144
+ return decorator
145
+
146
+
36
147
  class Spinner:
37
148
  """A simple terminal spinner for showing activity with elapsed time."""
38
149
 
@@ -69,13 +180,44 @@ class Spinner:
69
180
  print("\r\033[2K", end="", flush=True)
70
181
 
71
182
 
72
- def separator() -> str:
73
- """Return a dim separator line."""
183
+ def get_terminal_width() -> int:
184
+ """Get terminal width, capped at 100 for readability."""
74
185
  try:
75
- width = min(os.get_terminal_size().columns, 80)
186
+ return min(os.get_terminal_size().columns, 100)
76
187
  except OSError:
77
- width = 80
78
- return f"{DIM}{'─' * width}{RESET}"
188
+ return 80
189
+
190
+
191
+ def separator(style: str = "light") -> str:
192
+ """Return a styled separator line.
193
+
194
+ Args:
195
+ style: 'light' for thin line, 'heavy' for thick line, 'dots' for dotted
196
+ """
197
+ width = get_terminal_width()
198
+ if style == "heavy":
199
+ return f"{DIM}{'━' * width}{RESET}"
200
+ elif style == "dots":
201
+ return f"{DIM}{'·' * width}{RESET}"
202
+ else:
203
+ return f"{DIM}{'─' * width}{RESET}"
204
+
205
+
206
+ def print_welcome():
207
+ """Print a welcome message with tips."""
208
+ tips = [
209
+ f"Type {CYAN}/help{RESET} for commands",
210
+ f"Use {CYAN}/c{RESET} to clear conversation",
211
+ f"Press {CYAN}Ctrl+C{RESET} to exit",
212
+ f"Press {CYAN}Tab{RESET} to autocomplete commands",
213
+ ]
214
+ tip = tips[int(time.time()) % len(tips)] # Rotate tips
215
+ print(f"\n{DIM}Tip: {tip}{RESET}\n")
216
+
217
+
218
+ def print_goodbye():
219
+ """Print a goodbye message."""
220
+ print(f"\n{DIM}Goodbye!{RESET}\n")
79
221
 
80
222
 
81
223
  def get_agent_name(graph) -> str:
@@ -95,11 +237,8 @@ def get_agent_name(graph) -> str:
95
237
 
96
238
 
97
239
  def print_header_box(agent_name: str, cwd: str):
98
- """Print a box-drawn header with the agent name."""
99
- try:
100
- term_width = min(os.get_terminal_size().columns, 80)
101
- except OSError:
102
- term_width = 80
240
+ """Print an elegant header with the agent name and version."""
241
+ term_width = get_terminal_width()
103
242
 
104
243
  # Box drawing characters
105
244
  TL, TR, BL, BR = "╭", "╮", "╰", "╯" # corners
@@ -113,17 +252,28 @@ def print_header_box(agent_name: str, cwd: str):
113
252
  cwd_display = cwd if len(cwd) <= inner_width else "..." + cwd[-(inner_width - 3):]
114
253
  cwd_line = cwd_display.center(inner_width)
115
254
 
116
- # Print the box
117
- print(f"{CYAN}{TL}{H * (term_width - 2)}{TR}{RESET}")
118
- print(f"{CYAN}{V}{RESET} {BOLD}{title_line}{RESET} {CYAN}{V}{RESET}")
255
+ # Print the box with gradient-style coloring
256
+ print()
257
+ print(f"{BRIGHT_CYAN}{TL}{H * (term_width - 2)}{TR}{RESET}")
258
+ print(f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{title_line}{RESET} {BRIGHT_CYAN}{V}{RESET}")
119
259
  print(f"{CYAN}{V}{RESET} {DIM}{cwd_line}{RESET} {CYAN}{V}{RESET}")
120
260
  print(f"{CYAN}{BL}{H * (term_width - 2)}{BR}{RESET}")
121
- print()
122
261
 
123
262
 
124
263
  def render_markdown(text: str) -> str:
125
- """Simple markdown rendering for **bold** text."""
126
- return re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
264
+ """Render markdown formatting for terminal display.
265
+
266
+ Supports: **bold**, *italic*, `code`, [links](url)
267
+ """
268
+ # Bold: **text**
269
+ text = re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
270
+ # Italic: *text* (but not inside **)
271
+ text = re.sub(r"(?<!\*)\*([^*]+?)\*(?!\*)", f"{ITALIC}\\1{RESET}", text)
272
+ # Inline code: `code`
273
+ text = re.sub(r"`([^`]+?)`", f"{CYAN}\\1{RESET}", text)
274
+ # Links: [text](url) - show text in underline
275
+ text = re.sub(r"\[([^\]]+?)\]\([^)]+?\)", f"{UNDERLINE}\\1{RESET}", text)
276
+ return text
127
277
 
128
278
 
129
279
  def parse_agent_spec(agent_spec: str) -> Tuple[str, str]:
@@ -329,66 +479,87 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
329
479
  if verbose:
330
480
  print(f"{DIM}[{node}]{RESET} {text}", end="")
331
481
  else:
332
- # Print text output with cyan bullet (only on first chunk or after newline)
482
+ # Print text output with cyan bullet
333
483
  print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
334
484
 
335
- # Handle tool calls - green bullet with tool name
485
+ # Handle tool calls - green tool name
336
486
  elif "tool_calls" in chunk:
337
487
  for tool_call in chunk["tool_calls"]:
338
488
  tool_name = tool_call["name"]
339
489
  args = tool_call.get("args", {})
340
490
  arg_preview = get_tool_arg_preview(args)
341
491
 
342
- print(f"\n{GREEN} {tool_name.capitalize()}{RESET}({DIM}{arg_preview}{RESET})")
492
+ print(f"\n{GREEN} {tool_name}{RESET}")
493
+ if arg_preview:
494
+ print(f" {DIM}└─ {arg_preview}{RESET}")
343
495
 
344
496
  # Handle tool results - indented with result preview
345
497
  elif "tool_result" in chunk:
346
498
  result = chunk.get("tool_result", "")
347
499
  preview = format_result_preview(str(result))
348
- print(f" {DIM}{preview}{RESET}")
500
+ print(f" {DIM}{preview}{RESET}")
349
501
 
350
502
  elif status == "interrupt":
351
503
  interrupt_data = chunk.get("interrupt", {})
352
504
  action_requests = interrupt_data.get("action_requests", [])
353
505
 
354
- print(f"\n{YELLOW} Interrupt{RESET}")
506
+ print(f"\n{YELLOW} Action Required{RESET}")
355
507
  if action_requests:
356
508
  for i, action in enumerate(action_requests):
357
509
  tool = action.get('tool', 'unknown')
358
510
  args_preview = get_tool_arg_preview(action.get('args', {}))
359
- print(f" {DIM}{i + 1}. {tool}({args_preview}){RESET}")
511
+ print(f" {DIM}{i + 1}. {tool}{RESET}")
512
+ if args_preview:
513
+ print(f" {DIM}└─ {args_preview}{RESET}")
360
514
 
361
515
  elif status == "complete":
362
516
  pass # No output on complete (nanocode style)
363
517
 
364
518
  elif status == "error":
365
519
  error_msg = chunk.get("error", "Unknown error")
366
- print(f"\n{RED} Error: {error_msg}{RESET}")
520
+ print(f"\n{RED} Error: {error_msg}{RESET}")
367
521
 
368
522
 
369
523
  def get_key() -> str:
370
- """Read a single keypress from stdin."""
371
- fd = sys.stdin.fileno()
372
- old_settings = termios.tcgetattr(fd)
373
- try:
374
- tty.setraw(fd)
375
- ch = sys.stdin.read(1)
376
- # Handle escape sequences (arrow keys)
377
- if ch == '\x1b':
378
- ch2 = sys.stdin.read(1)
379
- if ch2 == '[':
380
- ch3 = sys.stdin.read(1)
381
- if ch3 == 'A':
382
- return 'up'
383
- elif ch3 == 'B':
384
- return 'down'
385
- elif ch == '\r' or ch == '\n':
524
+ """Read a single keypress from stdin (cross-platform)."""
525
+ if IS_WINDOWS:
526
+ # Windows implementation using msvcrt
527
+ ch = msvcrt.getch()
528
+ if ch in (b'\x00', b'\xe0'): # Special keys (arrows, function keys)
529
+ ch2 = msvcrt.getch()
530
+ if ch2 == b'H':
531
+ return 'up'
532
+ elif ch2 == b'P':
533
+ return 'down'
534
+ return ch2.decode('utf-8', errors='ignore')
535
+ elif ch == b'\r':
386
536
  return 'enter'
387
- elif ch == '\x03': # Ctrl+C
537
+ elif ch == b'\x03': # Ctrl+C
388
538
  return 'ctrl-c'
389
- return ch
390
- finally:
391
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
539
+ return ch.decode('utf-8', errors='ignore')
540
+ else:
541
+ # Unix implementation using termios/tty
542
+ fd = sys.stdin.fileno()
543
+ old_settings = termios.tcgetattr(fd)
544
+ try:
545
+ tty.setraw(fd)
546
+ ch = sys.stdin.read(1)
547
+ # Handle escape sequences (arrow keys)
548
+ if ch == '\x1b':
549
+ ch2 = sys.stdin.read(1)
550
+ if ch2 == '[':
551
+ ch3 = sys.stdin.read(1)
552
+ if ch3 == 'A':
553
+ return 'up'
554
+ elif ch3 == 'B':
555
+ return 'down'
556
+ elif ch == '\r' or ch == '\n':
557
+ return 'enter'
558
+ elif ch == '\x03': # Ctrl+C
559
+ return 'ctrl-c'
560
+ return ch
561
+ finally:
562
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
392
563
 
393
564
 
394
565
  def select_option(options: List[str], prompt: str = "Select an option:") -> int:
@@ -581,6 +752,320 @@ def run_single_turn_sync(
581
752
  return time.time() - start_time
582
753
 
583
754
 
755
+ def print_help():
756
+ """Print formatted help information."""
757
+ print(f"\n{BOLD}{BRIGHT_CYAN}Commands{RESET}")
758
+ print(f"{DIM}{'─' * 40}{RESET}")
759
+
760
+ # Get all registered commands and display them
761
+ commands = command_registry.all_commands()
762
+ for cmd in sorted(commands, key=lambda c: c.name):
763
+ aliases_str = ""
764
+ if cmd.aliases:
765
+ aliases_str = f", {CYAN}/{RESET}, {CYAN}/".join([""] + cmd.aliases)[4:]
766
+ print(f" {CYAN}/{cmd.name}{RESET}{aliases_str}")
767
+ print(f" {DIM}{cmd.description}{RESET}")
768
+
769
+ print()
770
+ print(f"{BOLD}{BRIGHT_CYAN}Shortcuts{RESET}")
771
+ print(f"{DIM}{'─' * 40}{RESET}")
772
+ print(f" {CYAN}Tab{RESET} Autocomplete commands")
773
+ print(f" {CYAN}Ctrl+C{RESET} Exit at any time")
774
+ print(f" {CYAN}↑/↓{RESET} Navigate options")
775
+ print()
776
+
777
+
778
+ # --- Built-in Slash Commands ---
779
+
780
+ @register_command(
781
+ name="help",
782
+ description="Show this help message",
783
+ aliases=["h", "?"],
784
+ )
785
+ def cmd_help(args: str, context: Dict[str, Any]) -> Optional[str]:
786
+ """Display help information."""
787
+ if args:
788
+ # Show help for a specific command
789
+ cmd = command_registry.get(args)
790
+ if cmd:
791
+ print(f"\n{BOLD}{BRIGHT_CYAN}/{cmd.name}{RESET}")
792
+ print(f" {cmd.description}")
793
+ if cmd.aliases:
794
+ print(f" {DIM}Aliases: /{', /'.join(cmd.aliases)}{RESET}")
795
+ if cmd.usage:
796
+ print(f" {DIM}Usage: {cmd.usage}{RESET}")
797
+ print()
798
+ else:
799
+ print(f"{YELLOW}Unknown command: /{args}{RESET}")
800
+ else:
801
+ print_help()
802
+ return None
803
+
804
+
805
+ @register_command(
806
+ name="quit",
807
+ description="Exit the CLI",
808
+ aliases=["q", "exit"],
809
+ )
810
+ def cmd_quit(args: str, context: Dict[str, Any]) -> Optional[str]:
811
+ """Exit the CLI."""
812
+ return "exit" # Special return value to signal exit
813
+
814
+
815
+ @register_command(
816
+ name="clear",
817
+ description="Clear conversation history",
818
+ aliases=["c"],
819
+ )
820
+ def cmd_clear(args: str, context: Dict[str, Any]) -> Optional[str]:
821
+ """Clear the conversation history."""
822
+ context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
823
+ print(f"\n{GREEN}✓ Conversation cleared{RESET}\n")
824
+ return None
825
+
826
+
827
+ @register_command(
828
+ name="version",
829
+ description="Show version information",
830
+ aliases=["v"],
831
+ )
832
+ def cmd_version(args: str, context: Dict[str, Any]) -> Optional[str]:
833
+ """Display version information."""
834
+ print(f"\n{BOLD}{BRIGHT_CYAN}deepagent-code{RESET} v{__version__}")
835
+ agent_name = context.get("agent_name", "Unknown")
836
+ print(f"{DIM}Agent: {agent_name}{RESET}\n")
837
+ return None
838
+
839
+
840
+ @register_command(
841
+ name="status",
842
+ description="Show current session status",
843
+ aliases=["s"],
844
+ )
845
+ def cmd_status(args: str, context: Dict[str, Any]) -> Optional[str]:
846
+ """Display current session status."""
847
+ config = context.get("config", {})
848
+ thread_id = config.get("configurable", {}).get("thread_id", "N/A")
849
+ agent_name = context.get("agent_name", "Unknown")
850
+ verbose = context.get("verbose", False)
851
+ use_async = context.get("use_async", False)
852
+ stream_mode = context.get("stream_mode", "updates")
853
+
854
+ print(f"\n{BOLD}{BRIGHT_CYAN}Session Status{RESET}")
855
+ print(f"{DIM}{'─' * 30}{RESET}")
856
+ print(f" {DIM}Agent:{RESET} {agent_name}")
857
+ print(f" {DIM}Thread ID:{RESET} {thread_id[:8]}...")
858
+ print(f" {DIM}Mode:{RESET} {'async' if use_async else 'sync'}")
859
+ print(f" {DIM}Stream:{RESET} {stream_mode}")
860
+ print(f" {DIM}Verbose:{RESET} {'on' if verbose else 'off'}")
861
+ print(f" {DIM}CWD:{RESET} {os.getcwd()}")
862
+ print()
863
+ return None
864
+
865
+
866
+ @register_command(
867
+ name="config",
868
+ description="Show or set configuration",
869
+ aliases=["cfg"],
870
+ usage="/config [key] [value]",
871
+ )
872
+ def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
873
+ """Show or modify configuration."""
874
+ config = context.get("config", {})
875
+
876
+ if not args:
877
+ # Show current config
878
+ print(f"\n{BOLD}{BRIGHT_CYAN}Configuration{RESET}")
879
+ print(f"{DIM}{'─' * 30}{RESET}")
880
+ for key, value in config.items():
881
+ if isinstance(value, dict):
882
+ print(f" {CYAN}{key}:{RESET}")
883
+ for k, v in value.items():
884
+ # Truncate long values
885
+ v_str = str(v)
886
+ if len(v_str) > 30:
887
+ v_str = v_str[:30] + "..."
888
+ print(f" {DIM}{k}:{RESET} {v_str}")
889
+ else:
890
+ print(f" {CYAN}{key}:{RESET} {value}")
891
+ print()
892
+ else:
893
+ parts = args.split(maxsplit=1)
894
+ if len(parts) == 1:
895
+ # Show specific config key
896
+ key = parts[0]
897
+ if key in config:
898
+ print(f"\n{CYAN}{key}:{RESET} {config[key]}\n")
899
+ elif "configurable" in config and key in config["configurable"]:
900
+ print(f"\n{CYAN}{key}:{RESET} {config['configurable'][key]}\n")
901
+ else:
902
+ print(f"{YELLOW}Unknown config key: {key}{RESET}")
903
+ else:
904
+ # Set config value
905
+ key, value = parts
906
+ if key == "verbose":
907
+ context["verbose"] = value.lower() in ("true", "1", "on", "yes")
908
+ print(f"{GREEN}✓ Set verbose = {context['verbose']}{RESET}")
909
+ else:
910
+ print(f"{YELLOW}Cannot modify {key} at runtime{RESET}")
911
+ return None
912
+
913
+
914
+ @register_command(
915
+ name="history",
916
+ description="Show recent messages (if available)",
917
+ aliases=["hist"],
918
+ )
919
+ def cmd_history(args: str, context: Dict[str, Any]) -> Optional[str]:
920
+ """Display conversation history if available."""
921
+ graph = context.get("graph")
922
+ config = context.get("config", {})
923
+
924
+ if graph is None:
925
+ print(f"{YELLOW}No graph available{RESET}")
926
+ return None
927
+
928
+ try:
929
+ # Try to get state from the graph's checkpointer
930
+ if hasattr(graph, "get_state"):
931
+ state = graph.get_state(config)
932
+ if state and hasattr(state, "values"):
933
+ messages = state.values.get("messages", [])
934
+ if messages:
935
+ print(f"\n{BOLD}{BRIGHT_CYAN}Conversation History{RESET}")
936
+ print(f"{DIM}{'─' * 40}{RESET}")
937
+
938
+ # Show last N messages
939
+ limit = 10
940
+ if args:
941
+ try:
942
+ limit = int(args)
943
+ except ValueError:
944
+ pass
945
+
946
+ for msg in messages[-limit:]:
947
+ role = getattr(msg, "type", "unknown")
948
+ content = getattr(msg, "content", str(msg))
949
+
950
+ if role == "human":
951
+ print(f"\n {BRIGHT_BLUE}You:{RESET}")
952
+ elif role == "ai":
953
+ print(f"\n {BRIGHT_CYAN}Agent:{RESET}")
954
+ else:
955
+ print(f"\n {DIM}{role}:{RESET}")
956
+
957
+ # Truncate long content
958
+ if len(content) > 200:
959
+ content = content[:200] + "..."
960
+ print(f" {DIM}{content}{RESET}")
961
+ print()
962
+ else:
963
+ print(f"{DIM}No messages in history{RESET}")
964
+ else:
965
+ print(f"{DIM}No state available{RESET}")
966
+ else:
967
+ print(f"{DIM}Graph does not support state retrieval{RESET}")
968
+ except Exception as e:
969
+ print(f"{DIM}Could not retrieve history: {e}{RESET}")
970
+
971
+ return None
972
+
973
+
974
+ @register_command(
975
+ name="reset",
976
+ description="Reset the session (clear history and restart)",
977
+ aliases=["restart"],
978
+ )
979
+ def cmd_reset(args: str, context: Dict[str, Any]) -> Optional[str]:
980
+ """Reset the session."""
981
+ context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
982
+ print(f"\n{GREEN}✓ Session reset{RESET}")
983
+ print(f"{DIM}New thread ID: {context['config']['configurable']['thread_id'][:8]}...{RESET}\n")
984
+ return None
985
+
986
+
987
+ @register_command(
988
+ name="verbose",
989
+ description="Toggle verbose output mode",
990
+ usage="/verbose [on|off]",
991
+ )
992
+ def cmd_verbose(args: str, context: Dict[str, Any]) -> Optional[str]:
993
+ """Toggle or show verbose output mode."""
994
+ verbose = context.get("verbose", False)
995
+ if args:
996
+ if args.lower() in ("on", "true", "1"):
997
+ context["verbose"] = True
998
+ print(f"{GREEN}✓ Verbose mode enabled{RESET}")
999
+ elif args.lower() in ("off", "false", "0"):
1000
+ context["verbose"] = False
1001
+ print(f"{GREEN}✓ Verbose mode disabled{RESET}")
1002
+ else:
1003
+ print(f"{DIM}Verbose mode: {'on' if verbose else 'off'}{RESET}")
1004
+ print(f"{DIM}Use /verbose on or /verbose off to change{RESET}")
1005
+ return None
1006
+
1007
+
1008
+ def get_command_suggestions(partial: str) -> List[str]:
1009
+ """Get command suggestions based on partial input.
1010
+
1011
+ Args:
1012
+ partial: Partial command name (without leading /)
1013
+
1014
+ Returns:
1015
+ List of matching command names
1016
+ """
1017
+ partial_lower = partial.lower()
1018
+ suggestions = []
1019
+
1020
+ for cmd in command_registry.all_commands():
1021
+ # Check main command name
1022
+ if cmd.name.startswith(partial_lower):
1023
+ suggestions.append(cmd.name)
1024
+ # Check aliases
1025
+ for alias in cmd.aliases:
1026
+ if alias.startswith(partial_lower) and cmd.name not in suggestions:
1027
+ suggestions.append(cmd.name)
1028
+
1029
+ return sorted(suggestions)
1030
+
1031
+
1032
+ def command_completer(text: str, state: int) -> Optional[str]:
1033
+ """Readline completer for slash commands.
1034
+
1035
+ Args:
1036
+ text: Current text being completed
1037
+ state: State index for multiple completions
1038
+
1039
+ Returns:
1040
+ Next completion or None
1041
+ """
1042
+ # Only complete if starting with /
1043
+ if not text.startswith("/"):
1044
+ return None
1045
+
1046
+ partial = text[1:] # Remove leading /
1047
+ suggestions = ["/" + s for s in get_command_suggestions(partial)]
1048
+
1049
+ if state < len(suggestions):
1050
+ return suggestions[state]
1051
+ return None
1052
+
1053
+
1054
+ def setup_readline_completion():
1055
+ """Set up readline for tab completion of slash commands."""
1056
+ if not HAS_READLINE:
1057
+ return
1058
+
1059
+ readline.set_completer(command_completer)
1060
+ readline.set_completer_delims(" \t\n")
1061
+
1062
+ # Use tab for completion
1063
+ if sys.platform == "darwin":
1064
+ readline.parse_and_bind("bind ^I rl_complete")
1065
+ else:
1066
+ readline.parse_and_bind("tab: complete")
1067
+
1068
+
584
1069
  def run_conversation_loop(
585
1070
  graph,
586
1071
  config: Dict[str, Any],
@@ -595,14 +1080,31 @@ def run_conversation_loop(
595
1080
  Run a continuous conversation loop with the LangGraph agent.
596
1081
  Styled after Claude Code / nanocode.
597
1082
  """
1083
+ # Set up tab completion for slash commands
1084
+ setup_readline_completion()
1085
+
598
1086
  # Print box-drawn header with agent name
599
1087
  print_header_box(agent_name, os.getcwd())
600
1088
 
1089
+ # Print welcome message with tips
1090
+ print_welcome()
1091
+
1092
+ # Create command context (mutable dict that commands can modify)
1093
+ command_context = {
1094
+ "graph": graph,
1095
+ "config": config,
1096
+ "agent_name": agent_name,
1097
+ "use_async": use_async,
1098
+ "interactive": interactive,
1099
+ "verbose": verbose,
1100
+ "stream_mode": stream_mode,
1101
+ }
1102
+
601
1103
  # Process initial message if provided
602
1104
  if initial_message:
603
- print(separator())
604
- print(f"{BOLD}{BLUE}❯{RESET} {initial_message}")
605
- print(separator())
1105
+ print(f"\n{BOLD}{BRIGHT_BLUE}You{RESET}")
1106
+ print(f"{initial_message}")
1107
+ print()
606
1108
 
607
1109
  if use_async:
608
1110
  duration = asyncio.run(
@@ -616,29 +1118,40 @@ def run_conversation_loop(
616
1118
  # Main conversation loop
617
1119
  while True:
618
1120
  try:
619
- print(separator())
620
- user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
621
- print(separator())
1121
+ print(separator("dots"))
1122
+ user_input = input(f"{BOLD}{BRIGHT_BLUE}❯{RESET} ").strip()
622
1123
 
623
1124
  if not user_input:
624
1125
  continue
625
1126
 
626
- # Handle special commands
627
- if user_input in ("/q", "/quit", "/exit", "exit"):
628
- break
629
-
630
- if user_input == "/c":
631
- # Generate new thread_id to start fresh conversation
632
- config["configurable"]["thread_id"] = str(uuid.uuid4())
633
- print(f"{GREEN}⏺ Cleared conversation{RESET}")
1127
+ # Check if it's a slash command
1128
+ cmd_name, cmd_args = command_registry.parse_input(user_input)
1129
+
1130
+ if cmd_name is not None:
1131
+ # It's a slash command
1132
+ cmd = command_registry.get(cmd_name)
1133
+ if cmd:
1134
+ result = cmd.execute(cmd_args, command_context)
1135
+ # Update local vars from context (commands may modify these)
1136
+ verbose = command_context.get("verbose", verbose)
1137
+ if result == "exit":
1138
+ break
1139
+ else:
1140
+ # Show suggestions for unknown commands
1141
+ suggestions = get_command_suggestions(cmd_name)
1142
+ print(f"{YELLOW}Unknown command: /{cmd_name}{RESET}")
1143
+ if suggestions:
1144
+ suggestion_str = ", ".join([f"/{s}" for s in suggestions[:3]])
1145
+ print(f"{DIM}Did you mean: {suggestion_str}?{RESET}")
1146
+ else:
1147
+ print(f"{DIM}Type /help to see available commands{RESET}")
634
1148
  continue
635
1149
 
636
- if user_input in ("/h", "/help"):
637
- print(f"\n{BOLD}Commands:{RESET}")
638
- print(f" /q, /quit, exit - Exit")
639
- print(f" /c - Clear conversation")
640
- print(f" /h, /help - Show this help\n")
641
- continue
1150
+ # Handle "exit" as a special case (without slash)
1151
+ if user_input.lower() == "exit":
1152
+ break
1153
+
1154
+ print() # Space before response
642
1155
 
643
1156
  # Run the agent
644
1157
  if use_async:
@@ -653,7 +1166,10 @@ def run_conversation_loop(
653
1166
  except (EOFError, KeyboardInterrupt):
654
1167
  break
655
1168
  except Exception as err:
656
- print(f"{RED} Error: {err}{RESET}")
1169
+ print(f"\n{RED} Error: {err}{RESET}\n")
1170
+
1171
+ # Print goodbye message
1172
+ print_goodbye()
657
1173
 
658
1174
 
659
1175
  @click.command()
@@ -761,9 +1277,12 @@ def main(
761
1277
  if workspace_path.exists():
762
1278
  os.chdir(workspace_path)
763
1279
 
764
- # Load the graph
765
- print(f"{DIM}Loading {final_spec}...{RESET}")
1280
+ # Load the graph with a spinner
1281
+ spinner = Spinner("Loading agent")
1282
+ spinner.start()
766
1283
  graph, final_graph_name = load_graph(final_spec, default_graph_name)
1284
+ spinner.stop()
1285
+ print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
767
1286
 
768
1287
  # Parse config
769
1288
  config_dict = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagent-code
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepagent-code"
7
- version = "0.1.1"
7
+ version = "0.1.2"
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"
File without changes
File without changes
File without changes