deepagent-code 0.1.1__tar.gz → 0.1.3__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.3}/PKG-INFO +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code/cli.py +608 -69
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/PKG-INFO +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/pyproject.toml +1 -1
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/LICENSE +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/README.md +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code/__init__.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code/utils.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/SOURCES.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/dependency_links.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/entry_points.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/requires.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/deepagent_code.egg-info/top_level.txt +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/setup.cfg +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/tests/test_cli.py +0 -0
- {deepagent_code-0.1.1 → deepagent_code-0.1.3}/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,130 @@ 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.3"
|
|
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 rl_wrap(code: str) -> str:
|
|
128
|
+
"""Wrap ANSI escape code for readline to ignore in length calculations.
|
|
129
|
+
|
|
130
|
+
On terminals, ANSI codes are invisible but counted in string length.
|
|
131
|
+
This causes issues with line wrapping when using input().
|
|
132
|
+
Wrapping with \\001 and \\002 tells readline to ignore these characters.
|
|
133
|
+
"""
|
|
134
|
+
if HAS_READLINE:
|
|
135
|
+
return f"\001{code}\002"
|
|
136
|
+
return code
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def make_prompt(text: str = "❯", color: str = BRIGHT_BLUE) -> str:
|
|
140
|
+
"""Create a prompt string with proper readline escaping for ANSI codes.
|
|
141
|
+
|
|
142
|
+
This prevents line wrapping issues on Windows and other terminals.
|
|
143
|
+
"""
|
|
144
|
+
return f"{rl_wrap(BOLD)}{rl_wrap(color)}{text}{rl_wrap(RESET)} "
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def register_command(
|
|
148
|
+
name: str,
|
|
149
|
+
description: str,
|
|
150
|
+
aliases: Optional[List[str]] = None,
|
|
151
|
+
usage: Optional[str] = None,
|
|
152
|
+
):
|
|
153
|
+
"""Decorator to register a slash command handler."""
|
|
154
|
+
def decorator(func):
|
|
155
|
+
command = SlashCommand(
|
|
156
|
+
name=name,
|
|
157
|
+
handler=func,
|
|
158
|
+
description=description,
|
|
159
|
+
aliases=aliases or [],
|
|
160
|
+
usage=usage,
|
|
161
|
+
)
|
|
162
|
+
command_registry.register(command)
|
|
163
|
+
return func
|
|
164
|
+
return decorator
|
|
165
|
+
|
|
166
|
+
|
|
36
167
|
class Spinner:
|
|
37
168
|
"""A simple terminal spinner for showing activity with elapsed time."""
|
|
38
169
|
|
|
@@ -69,13 +200,44 @@ class Spinner:
|
|
|
69
200
|
print("\r\033[2K", end="", flush=True)
|
|
70
201
|
|
|
71
202
|
|
|
72
|
-
def
|
|
73
|
-
"""
|
|
203
|
+
def get_terminal_width() -> int:
|
|
204
|
+
"""Get terminal width, capped at 100 for readability."""
|
|
74
205
|
try:
|
|
75
|
-
|
|
206
|
+
return min(os.get_terminal_size().columns, 100)
|
|
76
207
|
except OSError:
|
|
77
|
-
|
|
78
|
-
|
|
208
|
+
return 80
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def separator(style: str = "light") -> str:
|
|
212
|
+
"""Return a styled separator line.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
style: 'light' for thin line, 'heavy' for thick line, 'dots' for dotted
|
|
216
|
+
"""
|
|
217
|
+
width = get_terminal_width()
|
|
218
|
+
if style == "heavy":
|
|
219
|
+
return f"{DIM}{'━' * width}{RESET}"
|
|
220
|
+
elif style == "dots":
|
|
221
|
+
return f"{DIM}{'·' * width}{RESET}"
|
|
222
|
+
else:
|
|
223
|
+
return f"{DIM}{'─' * width}{RESET}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def print_welcome():
|
|
227
|
+
"""Print a welcome message with tips."""
|
|
228
|
+
tips = [
|
|
229
|
+
f"Type {CYAN}/help{RESET} for commands",
|
|
230
|
+
f"Use {CYAN}/c{RESET} to clear conversation",
|
|
231
|
+
f"Press {CYAN}Ctrl+C{RESET} to exit",
|
|
232
|
+
f"Press {CYAN}Tab{RESET} to autocomplete commands",
|
|
233
|
+
]
|
|
234
|
+
tip = tips[int(time.time()) % len(tips)] # Rotate tips
|
|
235
|
+
print(f"\n{DIM}Tip: {tip}{RESET}\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def print_goodbye():
|
|
239
|
+
"""Print a goodbye message."""
|
|
240
|
+
print(f"\n{DIM}Goodbye!{RESET}\n")
|
|
79
241
|
|
|
80
242
|
|
|
81
243
|
def get_agent_name(graph) -> str:
|
|
@@ -95,11 +257,8 @@ def get_agent_name(graph) -> str:
|
|
|
95
257
|
|
|
96
258
|
|
|
97
259
|
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
|
|
260
|
+
"""Print an elegant header with the agent name and version."""
|
|
261
|
+
term_width = get_terminal_width()
|
|
103
262
|
|
|
104
263
|
# Box drawing characters
|
|
105
264
|
TL, TR, BL, BR = "╭", "╮", "╰", "╯" # corners
|
|
@@ -113,17 +272,28 @@ def print_header_box(agent_name: str, cwd: str):
|
|
|
113
272
|
cwd_display = cwd if len(cwd) <= inner_width else "..." + cwd[-(inner_width - 3):]
|
|
114
273
|
cwd_line = cwd_display.center(inner_width)
|
|
115
274
|
|
|
116
|
-
# Print the box
|
|
117
|
-
print(
|
|
118
|
-
print(f"{
|
|
275
|
+
# Print the box with gradient-style coloring
|
|
276
|
+
print()
|
|
277
|
+
print(f"{BRIGHT_CYAN}{TL}{H * (term_width - 2)}{TR}{RESET}")
|
|
278
|
+
print(f"{BRIGHT_CYAN}{V}{RESET} {BOLD}{BRIGHT_CYAN}{title_line}{RESET} {BRIGHT_CYAN}{V}{RESET}")
|
|
119
279
|
print(f"{CYAN}{V}{RESET} {DIM}{cwd_line}{RESET} {CYAN}{V}{RESET}")
|
|
120
280
|
print(f"{CYAN}{BL}{H * (term_width - 2)}{BR}{RESET}")
|
|
121
|
-
print()
|
|
122
281
|
|
|
123
282
|
|
|
124
283
|
def render_markdown(text: str) -> str:
|
|
125
|
-
"""
|
|
126
|
-
|
|
284
|
+
"""Render markdown formatting for terminal display.
|
|
285
|
+
|
|
286
|
+
Supports: **bold**, *italic*, `code`, [links](url)
|
|
287
|
+
"""
|
|
288
|
+
# Bold: **text**
|
|
289
|
+
text = re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
|
|
290
|
+
# Italic: *text* (but not inside **)
|
|
291
|
+
text = re.sub(r"(?<!\*)\*([^*]+?)\*(?!\*)", f"{ITALIC}\\1{RESET}", text)
|
|
292
|
+
# Inline code: `code`
|
|
293
|
+
text = re.sub(r"`([^`]+?)`", f"{CYAN}\\1{RESET}", text)
|
|
294
|
+
# Links: [text](url) - show text in underline
|
|
295
|
+
text = re.sub(r"\[([^\]]+?)\]\([^)]+?\)", f"{UNDERLINE}\\1{RESET}", text)
|
|
296
|
+
return text
|
|
127
297
|
|
|
128
298
|
|
|
129
299
|
def parse_agent_spec(agent_spec: str) -> Tuple[str, str]:
|
|
@@ -329,66 +499,87 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
|
|
|
329
499
|
if verbose:
|
|
330
500
|
print(f"{DIM}[{node}]{RESET} {text}", end="")
|
|
331
501
|
else:
|
|
332
|
-
# Print text output with cyan bullet
|
|
502
|
+
# Print text output with cyan bullet
|
|
333
503
|
print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
|
|
334
504
|
|
|
335
|
-
# Handle tool calls - green
|
|
505
|
+
# Handle tool calls - green tool name
|
|
336
506
|
elif "tool_calls" in chunk:
|
|
337
507
|
for tool_call in chunk["tool_calls"]:
|
|
338
508
|
tool_name = tool_call["name"]
|
|
339
509
|
args = tool_call.get("args", {})
|
|
340
510
|
arg_preview = get_tool_arg_preview(args)
|
|
341
511
|
|
|
342
|
-
print(f"\n{GREEN}
|
|
512
|
+
print(f"\n{GREEN}● {tool_name}{RESET}")
|
|
513
|
+
if arg_preview:
|
|
514
|
+
print(f" {DIM}└─ {arg_preview}{RESET}")
|
|
343
515
|
|
|
344
516
|
# Handle tool results - indented with result preview
|
|
345
517
|
elif "tool_result" in chunk:
|
|
346
518
|
result = chunk.get("tool_result", "")
|
|
347
519
|
preview = format_result_preview(str(result))
|
|
348
|
-
print(f" {DIM}
|
|
520
|
+
print(f" {DIM} ↳ {preview}{RESET}")
|
|
349
521
|
|
|
350
522
|
elif status == "interrupt":
|
|
351
523
|
interrupt_data = chunk.get("interrupt", {})
|
|
352
524
|
action_requests = interrupt_data.get("action_requests", [])
|
|
353
525
|
|
|
354
|
-
print(f"\n{YELLOW}
|
|
526
|
+
print(f"\n{YELLOW}⚠ Action Required{RESET}")
|
|
355
527
|
if action_requests:
|
|
356
528
|
for i, action in enumerate(action_requests):
|
|
357
529
|
tool = action.get('tool', 'unknown')
|
|
358
530
|
args_preview = get_tool_arg_preview(action.get('args', {}))
|
|
359
|
-
print(f" {DIM}{i + 1}. {tool}
|
|
531
|
+
print(f" {DIM}{i + 1}. {tool}{RESET}")
|
|
532
|
+
if args_preview:
|
|
533
|
+
print(f" {DIM}└─ {args_preview}{RESET}")
|
|
360
534
|
|
|
361
535
|
elif status == "complete":
|
|
362
536
|
pass # No output on complete (nanocode style)
|
|
363
537
|
|
|
364
538
|
elif status == "error":
|
|
365
539
|
error_msg = chunk.get("error", "Unknown error")
|
|
366
|
-
print(f"\n{RED}
|
|
540
|
+
print(f"\n{RED}✗ Error: {error_msg}{RESET}")
|
|
367
541
|
|
|
368
542
|
|
|
369
543
|
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':
|
|
544
|
+
"""Read a single keypress from stdin (cross-platform)."""
|
|
545
|
+
if IS_WINDOWS:
|
|
546
|
+
# Windows implementation using msvcrt
|
|
547
|
+
ch = msvcrt.getch()
|
|
548
|
+
if ch in (b'\x00', b'\xe0'): # Special keys (arrows, function keys)
|
|
549
|
+
ch2 = msvcrt.getch()
|
|
550
|
+
if ch2 == b'H':
|
|
551
|
+
return 'up'
|
|
552
|
+
elif ch2 == b'P':
|
|
553
|
+
return 'down'
|
|
554
|
+
return ch2.decode('utf-8', errors='ignore')
|
|
555
|
+
elif ch == b'\r':
|
|
386
556
|
return 'enter'
|
|
387
|
-
elif ch == '\x03': # Ctrl+C
|
|
557
|
+
elif ch == b'\x03': # Ctrl+C
|
|
388
558
|
return 'ctrl-c'
|
|
389
|
-
return ch
|
|
390
|
-
|
|
391
|
-
|
|
559
|
+
return ch.decode('utf-8', errors='ignore')
|
|
560
|
+
else:
|
|
561
|
+
# Unix implementation using termios/tty
|
|
562
|
+
fd = sys.stdin.fileno()
|
|
563
|
+
old_settings = termios.tcgetattr(fd)
|
|
564
|
+
try:
|
|
565
|
+
tty.setraw(fd)
|
|
566
|
+
ch = sys.stdin.read(1)
|
|
567
|
+
# Handle escape sequences (arrow keys)
|
|
568
|
+
if ch == '\x1b':
|
|
569
|
+
ch2 = sys.stdin.read(1)
|
|
570
|
+
if ch2 == '[':
|
|
571
|
+
ch3 = sys.stdin.read(1)
|
|
572
|
+
if ch3 == 'A':
|
|
573
|
+
return 'up'
|
|
574
|
+
elif ch3 == 'B':
|
|
575
|
+
return 'down'
|
|
576
|
+
elif ch == '\r' or ch == '\n':
|
|
577
|
+
return 'enter'
|
|
578
|
+
elif ch == '\x03': # Ctrl+C
|
|
579
|
+
return 'ctrl-c'
|
|
580
|
+
return ch
|
|
581
|
+
finally:
|
|
582
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
392
583
|
|
|
393
584
|
|
|
394
585
|
def select_option(options: List[str], prompt: str = "Select an option:") -> int:
|
|
@@ -476,7 +667,7 @@ def handle_interrupt_input(num_actions: int = 1) -> List[Dict[str, Any]]:
|
|
|
476
667
|
return [{"type": "reject"} for _ in range(num_actions)]
|
|
477
668
|
elif choice == 2:
|
|
478
669
|
print("Enter your decision as JSON (will be applied to all actions):")
|
|
479
|
-
json_str = input(
|
|
670
|
+
json_str = input(make_prompt("❯", BLUE)).strip()
|
|
480
671
|
try:
|
|
481
672
|
decision = json.loads(json_str)
|
|
482
673
|
return [decision for _ in range(num_actions)]
|
|
@@ -581,6 +772,320 @@ def run_single_turn_sync(
|
|
|
581
772
|
return time.time() - start_time
|
|
582
773
|
|
|
583
774
|
|
|
775
|
+
def print_help():
|
|
776
|
+
"""Print formatted help information."""
|
|
777
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}Commands{RESET}")
|
|
778
|
+
print(f"{DIM}{'─' * 40}{RESET}")
|
|
779
|
+
|
|
780
|
+
# Get all registered commands and display them
|
|
781
|
+
commands = command_registry.all_commands()
|
|
782
|
+
for cmd in sorted(commands, key=lambda c: c.name):
|
|
783
|
+
aliases_str = ""
|
|
784
|
+
if cmd.aliases:
|
|
785
|
+
aliases_str = f", {CYAN}/{RESET}, {CYAN}/".join([""] + cmd.aliases)[4:]
|
|
786
|
+
print(f" {CYAN}/{cmd.name}{RESET}{aliases_str}")
|
|
787
|
+
print(f" {DIM}{cmd.description}{RESET}")
|
|
788
|
+
|
|
789
|
+
print()
|
|
790
|
+
print(f"{BOLD}{BRIGHT_CYAN}Shortcuts{RESET}")
|
|
791
|
+
print(f"{DIM}{'─' * 40}{RESET}")
|
|
792
|
+
print(f" {CYAN}Tab{RESET} Autocomplete commands")
|
|
793
|
+
print(f" {CYAN}Ctrl+C{RESET} Exit at any time")
|
|
794
|
+
print(f" {CYAN}↑/↓{RESET} Navigate options")
|
|
795
|
+
print()
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# --- Built-in Slash Commands ---
|
|
799
|
+
|
|
800
|
+
@register_command(
|
|
801
|
+
name="help",
|
|
802
|
+
description="Show this help message",
|
|
803
|
+
aliases=["h", "?"],
|
|
804
|
+
)
|
|
805
|
+
def cmd_help(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
806
|
+
"""Display help information."""
|
|
807
|
+
if args:
|
|
808
|
+
# Show help for a specific command
|
|
809
|
+
cmd = command_registry.get(args)
|
|
810
|
+
if cmd:
|
|
811
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}/{cmd.name}{RESET}")
|
|
812
|
+
print(f" {cmd.description}")
|
|
813
|
+
if cmd.aliases:
|
|
814
|
+
print(f" {DIM}Aliases: /{', /'.join(cmd.aliases)}{RESET}")
|
|
815
|
+
if cmd.usage:
|
|
816
|
+
print(f" {DIM}Usage: {cmd.usage}{RESET}")
|
|
817
|
+
print()
|
|
818
|
+
else:
|
|
819
|
+
print(f"{YELLOW}Unknown command: /{args}{RESET}")
|
|
820
|
+
else:
|
|
821
|
+
print_help()
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@register_command(
|
|
826
|
+
name="quit",
|
|
827
|
+
description="Exit the CLI",
|
|
828
|
+
aliases=["q", "exit"],
|
|
829
|
+
)
|
|
830
|
+
def cmd_quit(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
831
|
+
"""Exit the CLI."""
|
|
832
|
+
return "exit" # Special return value to signal exit
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
@register_command(
|
|
836
|
+
name="clear",
|
|
837
|
+
description="Clear conversation history",
|
|
838
|
+
aliases=["c"],
|
|
839
|
+
)
|
|
840
|
+
def cmd_clear(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
841
|
+
"""Clear the conversation history."""
|
|
842
|
+
context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
|
|
843
|
+
print(f"\n{GREEN}✓ Conversation cleared{RESET}\n")
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@register_command(
|
|
848
|
+
name="version",
|
|
849
|
+
description="Show version information",
|
|
850
|
+
aliases=["v"],
|
|
851
|
+
)
|
|
852
|
+
def cmd_version(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
853
|
+
"""Display version information."""
|
|
854
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}deepagent-code{RESET} v{__version__}")
|
|
855
|
+
agent_name = context.get("agent_name", "Unknown")
|
|
856
|
+
print(f"{DIM}Agent: {agent_name}{RESET}\n")
|
|
857
|
+
return None
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
@register_command(
|
|
861
|
+
name="status",
|
|
862
|
+
description="Show current session status",
|
|
863
|
+
aliases=["s"],
|
|
864
|
+
)
|
|
865
|
+
def cmd_status(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
866
|
+
"""Display current session status."""
|
|
867
|
+
config = context.get("config", {})
|
|
868
|
+
thread_id = config.get("configurable", {}).get("thread_id", "N/A")
|
|
869
|
+
agent_name = context.get("agent_name", "Unknown")
|
|
870
|
+
verbose = context.get("verbose", False)
|
|
871
|
+
use_async = context.get("use_async", False)
|
|
872
|
+
stream_mode = context.get("stream_mode", "updates")
|
|
873
|
+
|
|
874
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}Session Status{RESET}")
|
|
875
|
+
print(f"{DIM}{'─' * 30}{RESET}")
|
|
876
|
+
print(f" {DIM}Agent:{RESET} {agent_name}")
|
|
877
|
+
print(f" {DIM}Thread ID:{RESET} {thread_id[:8]}...")
|
|
878
|
+
print(f" {DIM}Mode:{RESET} {'async' if use_async else 'sync'}")
|
|
879
|
+
print(f" {DIM}Stream:{RESET} {stream_mode}")
|
|
880
|
+
print(f" {DIM}Verbose:{RESET} {'on' if verbose else 'off'}")
|
|
881
|
+
print(f" {DIM}CWD:{RESET} {os.getcwd()}")
|
|
882
|
+
print()
|
|
883
|
+
return None
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@register_command(
|
|
887
|
+
name="config",
|
|
888
|
+
description="Show or set configuration",
|
|
889
|
+
aliases=["cfg"],
|
|
890
|
+
usage="/config [key] [value]",
|
|
891
|
+
)
|
|
892
|
+
def cmd_config(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
893
|
+
"""Show or modify configuration."""
|
|
894
|
+
config = context.get("config", {})
|
|
895
|
+
|
|
896
|
+
if not args:
|
|
897
|
+
# Show current config
|
|
898
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}Configuration{RESET}")
|
|
899
|
+
print(f"{DIM}{'─' * 30}{RESET}")
|
|
900
|
+
for key, value in config.items():
|
|
901
|
+
if isinstance(value, dict):
|
|
902
|
+
print(f" {CYAN}{key}:{RESET}")
|
|
903
|
+
for k, v in value.items():
|
|
904
|
+
# Truncate long values
|
|
905
|
+
v_str = str(v)
|
|
906
|
+
if len(v_str) > 30:
|
|
907
|
+
v_str = v_str[:30] + "..."
|
|
908
|
+
print(f" {DIM}{k}:{RESET} {v_str}")
|
|
909
|
+
else:
|
|
910
|
+
print(f" {CYAN}{key}:{RESET} {value}")
|
|
911
|
+
print()
|
|
912
|
+
else:
|
|
913
|
+
parts = args.split(maxsplit=1)
|
|
914
|
+
if len(parts) == 1:
|
|
915
|
+
# Show specific config key
|
|
916
|
+
key = parts[0]
|
|
917
|
+
if key in config:
|
|
918
|
+
print(f"\n{CYAN}{key}:{RESET} {config[key]}\n")
|
|
919
|
+
elif "configurable" in config and key in config["configurable"]:
|
|
920
|
+
print(f"\n{CYAN}{key}:{RESET} {config['configurable'][key]}\n")
|
|
921
|
+
else:
|
|
922
|
+
print(f"{YELLOW}Unknown config key: {key}{RESET}")
|
|
923
|
+
else:
|
|
924
|
+
# Set config value
|
|
925
|
+
key, value = parts
|
|
926
|
+
if key == "verbose":
|
|
927
|
+
context["verbose"] = value.lower() in ("true", "1", "on", "yes")
|
|
928
|
+
print(f"{GREEN}✓ Set verbose = {context['verbose']}{RESET}")
|
|
929
|
+
else:
|
|
930
|
+
print(f"{YELLOW}Cannot modify {key} at runtime{RESET}")
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@register_command(
|
|
935
|
+
name="history",
|
|
936
|
+
description="Show recent messages (if available)",
|
|
937
|
+
aliases=["hist"],
|
|
938
|
+
)
|
|
939
|
+
def cmd_history(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
940
|
+
"""Display conversation history if available."""
|
|
941
|
+
graph = context.get("graph")
|
|
942
|
+
config = context.get("config", {})
|
|
943
|
+
|
|
944
|
+
if graph is None:
|
|
945
|
+
print(f"{YELLOW}No graph available{RESET}")
|
|
946
|
+
return None
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
# Try to get state from the graph's checkpointer
|
|
950
|
+
if hasattr(graph, "get_state"):
|
|
951
|
+
state = graph.get_state(config)
|
|
952
|
+
if state and hasattr(state, "values"):
|
|
953
|
+
messages = state.values.get("messages", [])
|
|
954
|
+
if messages:
|
|
955
|
+
print(f"\n{BOLD}{BRIGHT_CYAN}Conversation History{RESET}")
|
|
956
|
+
print(f"{DIM}{'─' * 40}{RESET}")
|
|
957
|
+
|
|
958
|
+
# Show last N messages
|
|
959
|
+
limit = 10
|
|
960
|
+
if args:
|
|
961
|
+
try:
|
|
962
|
+
limit = int(args)
|
|
963
|
+
except ValueError:
|
|
964
|
+
pass
|
|
965
|
+
|
|
966
|
+
for msg in messages[-limit:]:
|
|
967
|
+
role = getattr(msg, "type", "unknown")
|
|
968
|
+
content = getattr(msg, "content", str(msg))
|
|
969
|
+
|
|
970
|
+
if role == "human":
|
|
971
|
+
print(f"\n {BRIGHT_BLUE}You:{RESET}")
|
|
972
|
+
elif role == "ai":
|
|
973
|
+
print(f"\n {BRIGHT_CYAN}Agent:{RESET}")
|
|
974
|
+
else:
|
|
975
|
+
print(f"\n {DIM}{role}:{RESET}")
|
|
976
|
+
|
|
977
|
+
# Truncate long content
|
|
978
|
+
if len(content) > 200:
|
|
979
|
+
content = content[:200] + "..."
|
|
980
|
+
print(f" {DIM}{content}{RESET}")
|
|
981
|
+
print()
|
|
982
|
+
else:
|
|
983
|
+
print(f"{DIM}No messages in history{RESET}")
|
|
984
|
+
else:
|
|
985
|
+
print(f"{DIM}No state available{RESET}")
|
|
986
|
+
else:
|
|
987
|
+
print(f"{DIM}Graph does not support state retrieval{RESET}")
|
|
988
|
+
except Exception as e:
|
|
989
|
+
print(f"{DIM}Could not retrieve history: {e}{RESET}")
|
|
990
|
+
|
|
991
|
+
return None
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@register_command(
|
|
995
|
+
name="reset",
|
|
996
|
+
description="Reset the session (clear history and restart)",
|
|
997
|
+
aliases=["restart"],
|
|
998
|
+
)
|
|
999
|
+
def cmd_reset(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
1000
|
+
"""Reset the session."""
|
|
1001
|
+
context["config"]["configurable"]["thread_id"] = str(uuid.uuid4())
|
|
1002
|
+
print(f"\n{GREEN}✓ Session reset{RESET}")
|
|
1003
|
+
print(f"{DIM}New thread ID: {context['config']['configurable']['thread_id'][:8]}...{RESET}\n")
|
|
1004
|
+
return None
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
@register_command(
|
|
1008
|
+
name="verbose",
|
|
1009
|
+
description="Toggle verbose output mode",
|
|
1010
|
+
usage="/verbose [on|off]",
|
|
1011
|
+
)
|
|
1012
|
+
def cmd_verbose(args: str, context: Dict[str, Any]) -> Optional[str]:
|
|
1013
|
+
"""Toggle or show verbose output mode."""
|
|
1014
|
+
verbose = context.get("verbose", False)
|
|
1015
|
+
if args:
|
|
1016
|
+
if args.lower() in ("on", "true", "1"):
|
|
1017
|
+
context["verbose"] = True
|
|
1018
|
+
print(f"{GREEN}✓ Verbose mode enabled{RESET}")
|
|
1019
|
+
elif args.lower() in ("off", "false", "0"):
|
|
1020
|
+
context["verbose"] = False
|
|
1021
|
+
print(f"{GREEN}✓ Verbose mode disabled{RESET}")
|
|
1022
|
+
else:
|
|
1023
|
+
print(f"{DIM}Verbose mode: {'on' if verbose else 'off'}{RESET}")
|
|
1024
|
+
print(f"{DIM}Use /verbose on or /verbose off to change{RESET}")
|
|
1025
|
+
return None
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def get_command_suggestions(partial: str) -> List[str]:
|
|
1029
|
+
"""Get command suggestions based on partial input.
|
|
1030
|
+
|
|
1031
|
+
Args:
|
|
1032
|
+
partial: Partial command name (without leading /)
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
List of matching command names
|
|
1036
|
+
"""
|
|
1037
|
+
partial_lower = partial.lower()
|
|
1038
|
+
suggestions = []
|
|
1039
|
+
|
|
1040
|
+
for cmd in command_registry.all_commands():
|
|
1041
|
+
# Check main command name
|
|
1042
|
+
if cmd.name.startswith(partial_lower):
|
|
1043
|
+
suggestions.append(cmd.name)
|
|
1044
|
+
# Check aliases
|
|
1045
|
+
for alias in cmd.aliases:
|
|
1046
|
+
if alias.startswith(partial_lower) and cmd.name not in suggestions:
|
|
1047
|
+
suggestions.append(cmd.name)
|
|
1048
|
+
|
|
1049
|
+
return sorted(suggestions)
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def command_completer(text: str, state: int) -> Optional[str]:
|
|
1053
|
+
"""Readline completer for slash commands.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
text: Current text being completed
|
|
1057
|
+
state: State index for multiple completions
|
|
1058
|
+
|
|
1059
|
+
Returns:
|
|
1060
|
+
Next completion or None
|
|
1061
|
+
"""
|
|
1062
|
+
# Only complete if starting with /
|
|
1063
|
+
if not text.startswith("/"):
|
|
1064
|
+
return None
|
|
1065
|
+
|
|
1066
|
+
partial = text[1:] # Remove leading /
|
|
1067
|
+
suggestions = ["/" + s for s in get_command_suggestions(partial)]
|
|
1068
|
+
|
|
1069
|
+
if state < len(suggestions):
|
|
1070
|
+
return suggestions[state]
|
|
1071
|
+
return None
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def setup_readline_completion():
|
|
1075
|
+
"""Set up readline for tab completion of slash commands."""
|
|
1076
|
+
if not HAS_READLINE:
|
|
1077
|
+
return
|
|
1078
|
+
|
|
1079
|
+
readline.set_completer(command_completer)
|
|
1080
|
+
readline.set_completer_delims(" \t\n")
|
|
1081
|
+
|
|
1082
|
+
# Use tab for completion
|
|
1083
|
+
if sys.platform == "darwin":
|
|
1084
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
|
1085
|
+
else:
|
|
1086
|
+
readline.parse_and_bind("tab: complete")
|
|
1087
|
+
|
|
1088
|
+
|
|
584
1089
|
def run_conversation_loop(
|
|
585
1090
|
graph,
|
|
586
1091
|
config: Dict[str, Any],
|
|
@@ -595,14 +1100,31 @@ def run_conversation_loop(
|
|
|
595
1100
|
Run a continuous conversation loop with the LangGraph agent.
|
|
596
1101
|
Styled after Claude Code / nanocode.
|
|
597
1102
|
"""
|
|
1103
|
+
# Set up tab completion for slash commands
|
|
1104
|
+
setup_readline_completion()
|
|
1105
|
+
|
|
598
1106
|
# Print box-drawn header with agent name
|
|
599
1107
|
print_header_box(agent_name, os.getcwd())
|
|
600
1108
|
|
|
1109
|
+
# Print welcome message with tips
|
|
1110
|
+
print_welcome()
|
|
1111
|
+
|
|
1112
|
+
# Create command context (mutable dict that commands can modify)
|
|
1113
|
+
command_context = {
|
|
1114
|
+
"graph": graph,
|
|
1115
|
+
"config": config,
|
|
1116
|
+
"agent_name": agent_name,
|
|
1117
|
+
"use_async": use_async,
|
|
1118
|
+
"interactive": interactive,
|
|
1119
|
+
"verbose": verbose,
|
|
1120
|
+
"stream_mode": stream_mode,
|
|
1121
|
+
}
|
|
1122
|
+
|
|
601
1123
|
# Process initial message if provided
|
|
602
1124
|
if initial_message:
|
|
603
|
-
print(
|
|
604
|
-
print(f"{
|
|
605
|
-
print(
|
|
1125
|
+
print(f"\n{BOLD}{BRIGHT_BLUE}You{RESET}")
|
|
1126
|
+
print(f"{initial_message}")
|
|
1127
|
+
print()
|
|
606
1128
|
|
|
607
1129
|
if use_async:
|
|
608
1130
|
duration = asyncio.run(
|
|
@@ -616,29 +1138,40 @@ def run_conversation_loop(
|
|
|
616
1138
|
# Main conversation loop
|
|
617
1139
|
while True:
|
|
618
1140
|
try:
|
|
619
|
-
print(separator())
|
|
620
|
-
user_input = input(
|
|
621
|
-
print(separator())
|
|
1141
|
+
print(separator("dots"))
|
|
1142
|
+
user_input = input(make_prompt()).strip()
|
|
622
1143
|
|
|
623
1144
|
if not user_input:
|
|
624
1145
|
continue
|
|
625
1146
|
|
|
626
|
-
#
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1147
|
+
# Check if it's a slash command
|
|
1148
|
+
cmd_name, cmd_args = command_registry.parse_input(user_input)
|
|
1149
|
+
|
|
1150
|
+
if cmd_name is not None:
|
|
1151
|
+
# It's a slash command
|
|
1152
|
+
cmd = command_registry.get(cmd_name)
|
|
1153
|
+
if cmd:
|
|
1154
|
+
result = cmd.execute(cmd_args, command_context)
|
|
1155
|
+
# Update local vars from context (commands may modify these)
|
|
1156
|
+
verbose = command_context.get("verbose", verbose)
|
|
1157
|
+
if result == "exit":
|
|
1158
|
+
break
|
|
1159
|
+
else:
|
|
1160
|
+
# Show suggestions for unknown commands
|
|
1161
|
+
suggestions = get_command_suggestions(cmd_name)
|
|
1162
|
+
print(f"{YELLOW}Unknown command: /{cmd_name}{RESET}")
|
|
1163
|
+
if suggestions:
|
|
1164
|
+
suggestion_str = ", ".join([f"/{s}" for s in suggestions[:3]])
|
|
1165
|
+
print(f"{DIM}Did you mean: {suggestion_str}?{RESET}")
|
|
1166
|
+
else:
|
|
1167
|
+
print(f"{DIM}Type /help to see available commands{RESET}")
|
|
634
1168
|
continue
|
|
635
1169
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
continue
|
|
1170
|
+
# Handle "exit" as a special case (without slash)
|
|
1171
|
+
if user_input.lower() == "exit":
|
|
1172
|
+
break
|
|
1173
|
+
|
|
1174
|
+
print() # Space before response
|
|
642
1175
|
|
|
643
1176
|
# Run the agent
|
|
644
1177
|
if use_async:
|
|
@@ -653,7 +1186,10 @@ def run_conversation_loop(
|
|
|
653
1186
|
except (EOFError, KeyboardInterrupt):
|
|
654
1187
|
break
|
|
655
1188
|
except Exception as err:
|
|
656
|
-
print(f"{RED}
|
|
1189
|
+
print(f"\n{RED}✗ Error: {err}{RESET}\n")
|
|
1190
|
+
|
|
1191
|
+
# Print goodbye message
|
|
1192
|
+
print_goodbye()
|
|
657
1193
|
|
|
658
1194
|
|
|
659
1195
|
@click.command()
|
|
@@ -761,9 +1297,12 @@ def main(
|
|
|
761
1297
|
if workspace_path.exists():
|
|
762
1298
|
os.chdir(workspace_path)
|
|
763
1299
|
|
|
764
|
-
# Load the graph
|
|
765
|
-
|
|
1300
|
+
# Load the graph with a spinner
|
|
1301
|
+
spinner = Spinner("Loading agent")
|
|
1302
|
+
spinner.start()
|
|
766
1303
|
graph, final_graph_name = load_graph(final_spec, default_graph_name)
|
|
1304
|
+
spinner.stop()
|
|
1305
|
+
print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
|
|
767
1306
|
|
|
768
1307
|
# Parse config
|
|
769
1308
|
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
|