deepagent-code 0.1.0__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.0 → deepagent_code-0.1.2}/LICENSE +1 -1
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/PKG-INFO +2 -3
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code/cli.py +631 -79
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/PKG-INFO +2 -3
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/pyproject.toml +2 -3
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/README.md +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code/__init__.py +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code/utils.py +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/SOURCES.txt +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/dependency_links.txt +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/entry_points.txt +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/requires.txt +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/deepagent_code.egg-info/top_level.txt +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/setup.cfg +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/tests/test_cli.py +0 -0
- {deepagent_code-0.1.0 → deepagent_code-0.1.2}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 deepagent-code contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagent-code
|
|
3
|
-
Version: 0.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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dkedar7/deepagent-code
|
|
8
8
|
Project-URL: Repository, https://github.com/dkedar7/deepagent-code
|
|
9
9
|
Project-URL: Issues, https://github.com/dkedar7/deepagent-code/issues
|
|
10
10
|
Keywords: langgraph,cli,agents,llm,ai,claude-code
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Programming Language :: Python :: 3
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -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,32 +40,134 @@ 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
|
-
"""A simple terminal spinner for showing activity."""
|
|
148
|
+
"""A simple terminal spinner for showing activity with elapsed time."""
|
|
38
149
|
|
|
39
150
|
def __init__(self, message: str = "Thinking"):
|
|
40
151
|
self.message = message
|
|
41
152
|
self.running = False
|
|
42
153
|
self.thread = None
|
|
43
154
|
self.frame_idx = 0
|
|
155
|
+
self.start_time = None
|
|
44
156
|
|
|
45
157
|
def _spin(self):
|
|
46
|
-
"""Run the spinner animation."""
|
|
158
|
+
"""Run the spinner animation with elapsed time display."""
|
|
47
159
|
while self.running:
|
|
48
160
|
frame = SPINNER_FRAMES[self.frame_idx % len(SPINNER_FRAMES)]
|
|
49
|
-
|
|
161
|
+
elapsed = time.time() - self.start_time
|
|
162
|
+
elapsed_str = f"{int(elapsed)}s"
|
|
163
|
+
print(f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}", end="", flush=True)
|
|
50
164
|
self.frame_idx += 1
|
|
51
165
|
time.sleep(0.08)
|
|
52
166
|
|
|
53
167
|
def start(self):
|
|
54
168
|
"""Start the spinner."""
|
|
55
169
|
self.running = True
|
|
170
|
+
self.start_time = time.time()
|
|
56
171
|
self.thread = threading.Thread(target=self._spin, daemon=True)
|
|
57
172
|
self.thread.start()
|
|
58
173
|
|
|
@@ -65,13 +180,44 @@ class Spinner:
|
|
|
65
180
|
print("\r\033[2K", end="", flush=True)
|
|
66
181
|
|
|
67
182
|
|
|
68
|
-
def
|
|
69
|
-
"""
|
|
183
|
+
def get_terminal_width() -> int:
|
|
184
|
+
"""Get terminal width, capped at 100 for readability."""
|
|
70
185
|
try:
|
|
71
|
-
|
|
186
|
+
return min(os.get_terminal_size().columns, 100)
|
|
72
187
|
except OSError:
|
|
73
|
-
|
|
74
|
-
|
|
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")
|
|
75
221
|
|
|
76
222
|
|
|
77
223
|
def get_agent_name(graph) -> str:
|
|
@@ -91,11 +237,8 @@ def get_agent_name(graph) -> str:
|
|
|
91
237
|
|
|
92
238
|
|
|
93
239
|
def print_header_box(agent_name: str, cwd: str):
|
|
94
|
-
"""Print
|
|
95
|
-
|
|
96
|
-
term_width = min(os.get_terminal_size().columns, 80)
|
|
97
|
-
except OSError:
|
|
98
|
-
term_width = 80
|
|
240
|
+
"""Print an elegant header with the agent name and version."""
|
|
241
|
+
term_width = get_terminal_width()
|
|
99
242
|
|
|
100
243
|
# Box drawing characters
|
|
101
244
|
TL, TR, BL, BR = "╭", "╮", "╰", "╯" # corners
|
|
@@ -109,17 +252,28 @@ def print_header_box(agent_name: str, cwd: str):
|
|
|
109
252
|
cwd_display = cwd if len(cwd) <= inner_width else "..." + cwd[-(inner_width - 3):]
|
|
110
253
|
cwd_line = cwd_display.center(inner_width)
|
|
111
254
|
|
|
112
|
-
# Print the box
|
|
113
|
-
print(
|
|
114
|
-
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}")
|
|
115
259
|
print(f"{CYAN}{V}{RESET} {DIM}{cwd_line}{RESET} {CYAN}{V}{RESET}")
|
|
116
260
|
print(f"{CYAN}{BL}{H * (term_width - 2)}{BR}{RESET}")
|
|
117
|
-
print()
|
|
118
261
|
|
|
119
262
|
|
|
120
263
|
def render_markdown(text: str) -> str:
|
|
121
|
-
"""
|
|
122
|
-
|
|
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
|
|
123
277
|
|
|
124
278
|
|
|
125
279
|
def parse_agent_spec(agent_spec: str) -> Tuple[str, str]:
|
|
@@ -286,6 +440,27 @@ def format_result_preview(result: str) -> str:
|
|
|
286
440
|
return preview
|
|
287
441
|
|
|
288
442
|
|
|
443
|
+
def format_duration(seconds: float) -> str:
|
|
444
|
+
"""Format duration in human-readable format."""
|
|
445
|
+
if seconds < 1:
|
|
446
|
+
return f"{seconds * 1000:.0f}ms"
|
|
447
|
+
elif seconds < 60:
|
|
448
|
+
return f"{seconds:.1f}s"
|
|
449
|
+
else:
|
|
450
|
+
minutes = int(seconds // 60)
|
|
451
|
+
secs = seconds % 60
|
|
452
|
+
return f"{minutes}m {secs:.1f}s"
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def print_timing(duration: float, verbose: bool = False):
|
|
456
|
+
"""Print response timing information."""
|
|
457
|
+
formatted = format_duration(duration)
|
|
458
|
+
if verbose:
|
|
459
|
+
print(f"\n{DIM}Response time: {formatted}{RESET}")
|
|
460
|
+
else:
|
|
461
|
+
print(f"\n{DIM}{formatted}{RESET}")
|
|
462
|
+
|
|
463
|
+
|
|
289
464
|
def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
|
|
290
465
|
"""
|
|
291
466
|
Pretty print a chunk from the stream using Claude Code styling.
|
|
@@ -304,66 +479,87 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
|
|
|
304
479
|
if verbose:
|
|
305
480
|
print(f"{DIM}[{node}]{RESET} {text}", end="")
|
|
306
481
|
else:
|
|
307
|
-
# Print text output with cyan bullet
|
|
482
|
+
# Print text output with cyan bullet
|
|
308
483
|
print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
|
|
309
484
|
|
|
310
|
-
# Handle tool calls - green
|
|
485
|
+
# Handle tool calls - green tool name
|
|
311
486
|
elif "tool_calls" in chunk:
|
|
312
487
|
for tool_call in chunk["tool_calls"]:
|
|
313
488
|
tool_name = tool_call["name"]
|
|
314
489
|
args = tool_call.get("args", {})
|
|
315
490
|
arg_preview = get_tool_arg_preview(args)
|
|
316
491
|
|
|
317
|
-
print(f"\n{GREEN}
|
|
492
|
+
print(f"\n{GREEN}● {tool_name}{RESET}")
|
|
493
|
+
if arg_preview:
|
|
494
|
+
print(f" {DIM}└─ {arg_preview}{RESET}")
|
|
318
495
|
|
|
319
496
|
# Handle tool results - indented with result preview
|
|
320
497
|
elif "tool_result" in chunk:
|
|
321
498
|
result = chunk.get("tool_result", "")
|
|
322
499
|
preview = format_result_preview(str(result))
|
|
323
|
-
print(f" {DIM}
|
|
500
|
+
print(f" {DIM} ↳ {preview}{RESET}")
|
|
324
501
|
|
|
325
502
|
elif status == "interrupt":
|
|
326
503
|
interrupt_data = chunk.get("interrupt", {})
|
|
327
504
|
action_requests = interrupt_data.get("action_requests", [])
|
|
328
505
|
|
|
329
|
-
print(f"\n{YELLOW}
|
|
506
|
+
print(f"\n{YELLOW}⚠ Action Required{RESET}")
|
|
330
507
|
if action_requests:
|
|
331
508
|
for i, action in enumerate(action_requests):
|
|
332
509
|
tool = action.get('tool', 'unknown')
|
|
333
510
|
args_preview = get_tool_arg_preview(action.get('args', {}))
|
|
334
|
-
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}")
|
|
335
514
|
|
|
336
515
|
elif status == "complete":
|
|
337
516
|
pass # No output on complete (nanocode style)
|
|
338
517
|
|
|
339
518
|
elif status == "error":
|
|
340
519
|
error_msg = chunk.get("error", "Unknown error")
|
|
341
|
-
print(f"\n{RED}
|
|
520
|
+
print(f"\n{RED}✗ Error: {error_msg}{RESET}")
|
|
342
521
|
|
|
343
522
|
|
|
344
523
|
def get_key() -> str:
|
|
345
|
-
"""Read a single keypress from stdin."""
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
ch2
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return 'up'
|
|
358
|
-
elif ch3 == 'B':
|
|
359
|
-
return 'down'
|
|
360
|
-
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':
|
|
361
536
|
return 'enter'
|
|
362
|
-
elif ch == '\x03': # Ctrl+C
|
|
537
|
+
elif ch == b'\x03': # Ctrl+C
|
|
363
538
|
return 'ctrl-c'
|
|
364
|
-
return ch
|
|
365
|
-
|
|
366
|
-
|
|
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)
|
|
367
563
|
|
|
368
564
|
|
|
369
565
|
def select_option(options: List[str], prompt: str = "Select an option:") -> int:
|
|
@@ -469,9 +665,10 @@ async def run_single_turn_async(
|
|
|
469
665
|
interactive: bool = True,
|
|
470
666
|
verbose: bool = False,
|
|
471
667
|
stream_mode: str = "updates",
|
|
472
|
-
):
|
|
473
|
-
"""Run a single turn of an async LangGraph graph."""
|
|
668
|
+
) -> float:
|
|
669
|
+
"""Run a single turn of an async LangGraph graph. Returns total duration in seconds."""
|
|
474
670
|
input_data = prepare_agent_input(message=message)
|
|
671
|
+
start_time = time.time()
|
|
475
672
|
|
|
476
673
|
while True:
|
|
477
674
|
has_interrupt = False
|
|
@@ -505,6 +702,8 @@ async def run_single_turn_async(
|
|
|
505
702
|
else:
|
|
506
703
|
break
|
|
507
704
|
|
|
705
|
+
return time.time() - start_time
|
|
706
|
+
|
|
508
707
|
|
|
509
708
|
def run_single_turn_sync(
|
|
510
709
|
graph,
|
|
@@ -513,9 +712,10 @@ def run_single_turn_sync(
|
|
|
513
712
|
interactive: bool = True,
|
|
514
713
|
verbose: bool = False,
|
|
515
714
|
stream_mode: str = "updates",
|
|
516
|
-
):
|
|
517
|
-
"""Run a single turn of a sync LangGraph graph."""
|
|
715
|
+
) -> float:
|
|
716
|
+
"""Run a single turn of a sync LangGraph graph. Returns total duration in seconds."""
|
|
518
717
|
input_data = prepare_agent_input(message=message)
|
|
718
|
+
start_time = time.time()
|
|
519
719
|
|
|
520
720
|
while True:
|
|
521
721
|
has_interrupt = False
|
|
@@ -549,6 +749,322 @@ def run_single_turn_sync(
|
|
|
549
749
|
else:
|
|
550
750
|
break
|
|
551
751
|
|
|
752
|
+
return time.time() - start_time
|
|
753
|
+
|
|
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
|
+
|
|
552
1068
|
|
|
553
1069
|
def run_conversation_loop(
|
|
554
1070
|
graph,
|
|
@@ -564,63 +1080,96 @@ def run_conversation_loop(
|
|
|
564
1080
|
Run a continuous conversation loop with the LangGraph agent.
|
|
565
1081
|
Styled after Claude Code / nanocode.
|
|
566
1082
|
"""
|
|
1083
|
+
# Set up tab completion for slash commands
|
|
1084
|
+
setup_readline_completion()
|
|
1085
|
+
|
|
567
1086
|
# Print box-drawn header with agent name
|
|
568
1087
|
print_header_box(agent_name, os.getcwd())
|
|
569
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
|
+
|
|
570
1103
|
# Process initial message if provided
|
|
571
1104
|
if initial_message:
|
|
572
|
-
print(
|
|
573
|
-
print(f"{
|
|
574
|
-
print(
|
|
1105
|
+
print(f"\n{BOLD}{BRIGHT_BLUE}You{RESET}")
|
|
1106
|
+
print(f"{initial_message}")
|
|
1107
|
+
print()
|
|
575
1108
|
|
|
576
1109
|
if use_async:
|
|
577
|
-
asyncio.run(
|
|
1110
|
+
duration = asyncio.run(
|
|
578
1111
|
run_single_turn_async(graph, initial_message, config, interactive, verbose, stream_mode)
|
|
579
1112
|
)
|
|
580
1113
|
else:
|
|
581
|
-
run_single_turn_sync(graph, initial_message, config, interactive, verbose, stream_mode)
|
|
1114
|
+
duration = run_single_turn_sync(graph, initial_message, config, interactive, verbose, stream_mode)
|
|
1115
|
+
print_timing(duration, verbose)
|
|
582
1116
|
print()
|
|
583
1117
|
|
|
584
1118
|
# Main conversation loop
|
|
585
1119
|
while True:
|
|
586
1120
|
try:
|
|
587
|
-
print(separator())
|
|
588
|
-
user_input = input(f"{BOLD}{
|
|
589
|
-
print(separator())
|
|
1121
|
+
print(separator("dots"))
|
|
1122
|
+
user_input = input(f"{BOLD}{BRIGHT_BLUE}❯{RESET} ").strip()
|
|
590
1123
|
|
|
591
1124
|
if not user_input:
|
|
592
1125
|
continue
|
|
593
1126
|
|
|
594
|
-
#
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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}")
|
|
602
1148
|
continue
|
|
603
1149
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
continue
|
|
1150
|
+
# Handle "exit" as a special case (without slash)
|
|
1151
|
+
if user_input.lower() == "exit":
|
|
1152
|
+
break
|
|
1153
|
+
|
|
1154
|
+
print() # Space before response
|
|
610
1155
|
|
|
611
1156
|
# Run the agent
|
|
612
1157
|
if use_async:
|
|
613
|
-
asyncio.run(
|
|
1158
|
+
duration = asyncio.run(
|
|
614
1159
|
run_single_turn_async(graph, user_input, config, interactive, verbose, stream_mode)
|
|
615
1160
|
)
|
|
616
1161
|
else:
|
|
617
|
-
run_single_turn_sync(graph, user_input, config, interactive, verbose, stream_mode)
|
|
1162
|
+
duration = run_single_turn_sync(graph, user_input, config, interactive, verbose, stream_mode)
|
|
1163
|
+
print_timing(duration, verbose)
|
|
618
1164
|
print()
|
|
619
1165
|
|
|
620
1166
|
except (EOFError, KeyboardInterrupt):
|
|
621
1167
|
break
|
|
622
1168
|
except Exception as err:
|
|
623
|
-
print(f"{RED}
|
|
1169
|
+
print(f"\n{RED}✗ Error: {err}{RESET}\n")
|
|
1170
|
+
|
|
1171
|
+
# Print goodbye message
|
|
1172
|
+
print_goodbye()
|
|
624
1173
|
|
|
625
1174
|
|
|
626
1175
|
@click.command()
|
|
@@ -728,9 +1277,12 @@ def main(
|
|
|
728
1277
|
if workspace_path.exists():
|
|
729
1278
|
os.chdir(workspace_path)
|
|
730
1279
|
|
|
731
|
-
# Load the graph
|
|
732
|
-
|
|
1280
|
+
# Load the graph with a spinner
|
|
1281
|
+
spinner = Spinner("Loading agent")
|
|
1282
|
+
spinner.start()
|
|
733
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}")
|
|
734
1286
|
|
|
735
1287
|
# Parse config
|
|
736
1288
|
config_dict = None
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagent-code
|
|
3
|
-
Version: 0.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
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dkedar7/deepagent-code
|
|
8
8
|
Project-URL: Repository, https://github.com/dkedar7/deepagent-code
|
|
9
9
|
Project-URL: Issues, https://github.com/dkedar7/deepagent-code/issues
|
|
10
10
|
Keywords: langgraph,cli,agents,llm,ai,claude-code
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
13
|
Classifier: Programming Language :: Python :: 3
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepagent-code"
|
|
7
|
-
version = "0.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"
|
|
11
|
-
license =
|
|
11
|
+
license = "MIT"
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Kedar Dabhadkar", email = "kdabhadk@gmail.com"}
|
|
14
14
|
]
|
|
@@ -16,7 +16,6 @@ keywords = ["langgraph", "cli", "agents", "llm", "ai", "claude-code"]
|
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 3 - Alpha",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
|
-
"License :: OSI Approved :: MIT License",
|
|
20
19
|
"Programming Language :: Python :: 3",
|
|
21
20
|
"Programming Language :: Python :: 3.11",
|
|
22
21
|
"Programming Language :: Python :: 3.12",
|
|
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
|