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.
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/PKG-INFO +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code/cli.py +587 -68
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/PKG-INFO +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/pyproject.toml +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/LICENSE +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/README.md +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code/__init__.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code/utils.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/SOURCES.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/dependency_links.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/entry_points.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/requires.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/deepagent_code.egg-info/top_level.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/setup.cfg +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/tests/test_cli.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.2}/tests/test_utils.py +0 -0
|
@@ -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
|
|
73
|
-
"""
|
|
183
|
+
def get_terminal_width() -> int:
|
|
184
|
+
"""Get terminal width, capped at 100 for readability."""
|
|
74
185
|
try:
|
|
75
|
-
|
|
186
|
+
return min(os.get_terminal_size().columns, 100)
|
|
76
187
|
except OSError:
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
99
|
-
|
|
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(
|
|
118
|
-
print(f"{
|
|
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
|
-
"""
|
|
126
|
-
|
|
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
|
|
482
|
+
# Print text output with cyan bullet
|
|
333
483
|
print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
|
|
334
484
|
|
|
335
|
-
# Handle tool calls - green
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
ch2
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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(
|
|
604
|
-
print(f"{
|
|
605
|
-
print(
|
|
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}{
|
|
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
|
-
#
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|