nc1709 1.15.4__py3-none-any.whl → 1.18.8__py3-none-any.whl
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.
- nc1709/__init__.py +1 -1
- nc1709/agent/core.py +172 -19
- nc1709/agent/permissions.py +2 -2
- nc1709/agent/tools/bash_tool.py +295 -8
- nc1709/cli.py +435 -19
- nc1709/cli_ui.py +137 -52
- nc1709/conversation_logger.py +416 -0
- nc1709/llm_adapter.py +62 -4
- nc1709/plugins/agents/database_agent.py +695 -0
- nc1709/plugins/agents/django_agent.py +11 -4
- nc1709/plugins/agents/docker_agent.py +11 -4
- nc1709/plugins/agents/fastapi_agent.py +11 -4
- nc1709/plugins/agents/git_agent.py +11 -4
- nc1709/plugins/agents/nextjs_agent.py +11 -4
- nc1709/plugins/agents/ollama_agent.py +574 -0
- nc1709/plugins/agents/test_agent.py +702 -0
- nc1709/prompts/unified_prompt.py +156 -14
- nc1709/requirements_tracker.py +526 -0
- nc1709/thinking_messages.py +337 -0
- nc1709/version_check.py +6 -2
- nc1709/web/server.py +63 -3
- nc1709/web/templates/index.html +819 -140
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/METADATA +10 -7
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/RECORD +28 -22
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/WHEEL +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/entry_points.txt +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/licenses/LICENSE +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/top_level.txt +0 -0
nc1709/cli_ui.py
CHANGED
|
@@ -9,6 +9,7 @@ Provides rich, real-time visual feedback for CLI operations with:
|
|
|
9
9
|
- Non-blocking output with line replacement
|
|
10
10
|
- Streaming text support
|
|
11
11
|
- Text wrapping for clean output
|
|
12
|
+
- Dynamic thinking messages for user engagement
|
|
12
13
|
"""
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
@@ -22,6 +23,16 @@ from enum import Enum
|
|
|
22
23
|
from dataclasses import dataclass, field
|
|
23
24
|
from contextlib import contextmanager
|
|
24
25
|
|
|
26
|
+
# Import dynamic thinking messages
|
|
27
|
+
try:
|
|
28
|
+
from .thinking_messages import (
|
|
29
|
+
ThinkingPhase, get_thinking_message, get_tool_message,
|
|
30
|
+
get_progress_message, start_thinking, set_phase, should_update_message
|
|
31
|
+
)
|
|
32
|
+
HAS_THINKING_MESSAGES = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_THINKING_MESSAGES = False
|
|
35
|
+
|
|
25
36
|
|
|
26
37
|
# =============================================================================
|
|
27
38
|
# ANSI Color Codes
|
|
@@ -82,7 +93,7 @@ if not sys.stdout.isatty():
|
|
|
82
93
|
# =============================================================================
|
|
83
94
|
|
|
84
95
|
class Icons:
|
|
85
|
-
"""Unicode icons for status display"""
|
|
96
|
+
"""Unicode icons for status display - Claude Code style"""
|
|
86
97
|
# Status indicators
|
|
87
98
|
THINKING = "✻"
|
|
88
99
|
SUCCESS = "✓"
|
|
@@ -90,11 +101,16 @@ class Icons:
|
|
|
90
101
|
WARNING = "⚠"
|
|
91
102
|
INFO = "ℹ"
|
|
92
103
|
|
|
104
|
+
# Claude Code style
|
|
105
|
+
BULLET = "●" # Yellow bullet for tool calls
|
|
106
|
+
CORNER = "⎿" # Corner for output indentation
|
|
107
|
+
HOLLOW = "○" # Hollow circle for pending
|
|
108
|
+
|
|
93
109
|
# Spinners
|
|
94
110
|
DOTS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
95
111
|
BRAILLE = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
|
|
96
112
|
|
|
97
|
-
# Tree
|
|
113
|
+
# Tree (kept for backward compat)
|
|
98
114
|
TREE_BRANCH = "└─"
|
|
99
115
|
TREE_VERTICAL = "│"
|
|
100
116
|
TREE_TEE = "├─"
|
|
@@ -174,7 +190,8 @@ class ActionSpinner:
|
|
|
174
190
|
self,
|
|
175
191
|
message: str = "Processing",
|
|
176
192
|
spinner_chars: List[str] = None,
|
|
177
|
-
interval: float = 0.08
|
|
193
|
+
interval: float = 0.08,
|
|
194
|
+
dynamic_messages: bool = True
|
|
178
195
|
):
|
|
179
196
|
"""Initialize the action spinner.
|
|
180
197
|
|
|
@@ -182,10 +199,13 @@ class ActionSpinner:
|
|
|
182
199
|
message: Initial status message
|
|
183
200
|
spinner_chars: Characters for spinner animation
|
|
184
201
|
interval: Animation interval in seconds
|
|
202
|
+
dynamic_messages: Whether to use dynamic thinking messages
|
|
185
203
|
"""
|
|
186
204
|
self.message = message
|
|
205
|
+
self.initial_message = message
|
|
187
206
|
self.spinner_chars = spinner_chars or Icons.DOTS
|
|
188
207
|
self.interval = interval
|
|
208
|
+
self.dynamic_messages = dynamic_messages and HAS_THINKING_MESSAGES
|
|
189
209
|
|
|
190
210
|
self.running = False
|
|
191
211
|
self.frame_index = 0
|
|
@@ -195,10 +215,16 @@ class ActionSpinner:
|
|
|
195
215
|
self._thread: Optional[threading.Thread] = None
|
|
196
216
|
self._lock = threading.Lock()
|
|
197
217
|
self._last_render_lines = 0
|
|
218
|
+
self._start_time: Optional[float] = None
|
|
219
|
+
self._last_message_update: float = 0
|
|
220
|
+
self._message_update_interval: float = 4.0 # Update message every 4 seconds
|
|
198
221
|
|
|
199
222
|
def start(self) -> "ActionSpinner":
|
|
200
223
|
"""Start the spinner animation."""
|
|
201
224
|
self.running = True
|
|
225
|
+
self._start_time = time.time()
|
|
226
|
+
if self.dynamic_messages:
|
|
227
|
+
start_thinking()
|
|
202
228
|
sys.stdout.write(Color.HIDE_CURSOR)
|
|
203
229
|
sys.stdout.flush()
|
|
204
230
|
self._thread = threading.Thread(target=self._animate, daemon=True)
|
|
@@ -216,6 +242,13 @@ class ActionSpinner:
|
|
|
216
242
|
def _animate(self) -> None:
|
|
217
243
|
"""Animation loop running in background thread."""
|
|
218
244
|
while self.running:
|
|
245
|
+
# Update message dynamically if enabled
|
|
246
|
+
if self.dynamic_messages and self._start_time:
|
|
247
|
+
now = time.time()
|
|
248
|
+
if now - self._last_message_update >= self._message_update_interval:
|
|
249
|
+
self._last_message_update = now
|
|
250
|
+
self.message = get_progress_message()
|
|
251
|
+
|
|
219
252
|
self._render()
|
|
220
253
|
self.frame_index = (self.frame_index + 1) % len(self.spinner_chars)
|
|
221
254
|
time.sleep(self.interval)
|
|
@@ -515,8 +548,17 @@ def status(message: str, state: str = "info") -> None:
|
|
|
515
548
|
print(f"{icon} {message}")
|
|
516
549
|
|
|
517
550
|
|
|
518
|
-
def thinking(message: str) -> None:
|
|
519
|
-
"""Print a thinking/processing status.
|
|
551
|
+
def thinking(message: str = None, dynamic: bool = True) -> None:
|
|
552
|
+
"""Print a thinking/processing status.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
message: Custom message (optional, uses dynamic message if None)
|
|
556
|
+
dynamic: Whether to use dynamic thinking messages
|
|
557
|
+
"""
|
|
558
|
+
if message is None and dynamic and HAS_THINKING_MESSAGES:
|
|
559
|
+
message = get_thinking_message()
|
|
560
|
+
elif message is None:
|
|
561
|
+
message = "Thinking..."
|
|
520
562
|
status(message, "thinking")
|
|
521
563
|
|
|
522
564
|
|
|
@@ -541,25 +583,67 @@ def info(message: str) -> None:
|
|
|
541
583
|
|
|
542
584
|
|
|
543
585
|
# =============================================================================
|
|
544
|
-
# Action Logging
|
|
586
|
+
# Action Logging - Claude Code Style
|
|
545
587
|
# =============================================================================
|
|
546
588
|
|
|
547
589
|
def log_action(action: str, target: str, state: str = "running") -> None:
|
|
548
|
-
"""Log a tool/action with its target.
|
|
590
|
+
"""Log a tool/action with its target in Claude Code style.
|
|
591
|
+
|
|
592
|
+
Format: ● Action(target)
|
|
549
593
|
|
|
550
594
|
Args:
|
|
551
595
|
action: Action name (Read, Write, Bash, etc.)
|
|
552
596
|
target: Action target (filename, command, etc.)
|
|
553
597
|
state: Action state (running, success, error)
|
|
554
598
|
"""
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
599
|
+
# Yellow bullet for all tool calls (Claude Code style)
|
|
600
|
+
bullet = f"{Color.YELLOW}{Icons.BULLET}{Color.RESET}"
|
|
601
|
+
print(f"{bullet} {Color.BOLD}{action}{Color.RESET}({Color.CYAN}{target}{Color.RESET})")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def log_output(output: str, is_error: bool = False, max_lines: int = 10) -> None:
|
|
605
|
+
"""Log tool output with Claude Code style corner indentation.
|
|
606
|
+
|
|
607
|
+
Format:
|
|
608
|
+
⎿ Output line 1
|
|
609
|
+
Output line 2
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
output: Output text to display
|
|
613
|
+
is_error: Whether this is error output
|
|
614
|
+
max_lines: Max lines to show before truncating (0 = no limit)
|
|
615
|
+
"""
|
|
616
|
+
if not output or not output.strip():
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
lines = output.strip().split('\n')
|
|
620
|
+
show_truncation = max_lines > 0 and len(lines) > max_lines
|
|
621
|
+
|
|
622
|
+
if show_truncation:
|
|
623
|
+
display_lines = lines[:max_lines]
|
|
624
|
+
else:
|
|
625
|
+
display_lines = lines
|
|
626
|
+
|
|
627
|
+
# Color for output
|
|
628
|
+
text_color = Color.RED if is_error else Color.DIM
|
|
629
|
+
|
|
630
|
+
for i, line in enumerate(display_lines):
|
|
631
|
+
if i == 0:
|
|
632
|
+
# First line gets the corner symbol
|
|
633
|
+
prefix = f" {Color.DIM}{Icons.CORNER}{Color.RESET} "
|
|
634
|
+
else:
|
|
635
|
+
# Subsequent lines get spacing to align
|
|
636
|
+
prefix = " "
|
|
637
|
+
|
|
638
|
+
if is_error and i == 0:
|
|
639
|
+
# Error prefix for first line
|
|
640
|
+
print(f"{prefix}{Color.RED}Error:{Color.RESET} {text_color}{line}{Color.RESET}")
|
|
641
|
+
else:
|
|
642
|
+
print(f"{prefix}{text_color}{line}{Color.RESET}")
|
|
643
|
+
|
|
644
|
+
if show_truncation:
|
|
645
|
+
remaining = len(lines) - max_lines
|
|
646
|
+
print(f" {Color.DIM}... (+{remaining} more lines){Color.RESET}")
|
|
563
647
|
|
|
564
648
|
|
|
565
649
|
# =============================================================================
|
|
@@ -1015,27 +1099,24 @@ def print_response(response: str, width_percentage: float = 0.75) -> None:
|
|
|
1015
1099
|
# =============================================================================
|
|
1016
1100
|
|
|
1017
1101
|
def demo():
|
|
1018
|
-
"""Demonstrate CLI UI components."""
|
|
1102
|
+
"""Demonstrate CLI UI components - Claude Code style."""
|
|
1019
1103
|
print("\n" + "=" * 60)
|
|
1020
|
-
print("NC1709 CLI UI Demo")
|
|
1104
|
+
print("NC1709 CLI UI Demo - Claude Code Style")
|
|
1021
1105
|
print("=" * 60 + "\n")
|
|
1022
1106
|
|
|
1023
|
-
# Demo 1:
|
|
1024
|
-
print("Demo 1:
|
|
1107
|
+
# Demo 1: Claude Code style tool calls
|
|
1108
|
+
print("Demo 1: Tool Calls (Claude Code Style)")
|
|
1025
1109
|
print("-" * 40)
|
|
1026
1110
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
spinner.complete_action(idx)
|
|
1037
|
-
time.sleep(0.2)
|
|
1038
|
-
spinner.success("Implementation complete")
|
|
1111
|
+
# Simulate tool calls with output
|
|
1112
|
+
log_action("Read", "main.py")
|
|
1113
|
+
log_output("def main():\n print('Hello World')\n\nif __name__ == '__main__':\n main()")
|
|
1114
|
+
|
|
1115
|
+
log_action("Bash", "python main.py")
|
|
1116
|
+
log_output("Hello World")
|
|
1117
|
+
|
|
1118
|
+
log_action("Bash", "python broken.py")
|
|
1119
|
+
log_output("ModuleNotFoundError: No module named 'nonexistent'", is_error=True)
|
|
1039
1120
|
|
|
1040
1121
|
print()
|
|
1041
1122
|
|
|
@@ -1049,29 +1130,33 @@ def demo():
|
|
|
1049
1130
|
error("Failed to parse config.json")
|
|
1050
1131
|
print()
|
|
1051
1132
|
|
|
1052
|
-
# Demo 3:
|
|
1053
|
-
print("Demo 3:
|
|
1133
|
+
# Demo 3: Multi-line output with truncation
|
|
1134
|
+
print("Demo 3: Multi-line Output")
|
|
1135
|
+
print("-" * 40)
|
|
1136
|
+
log_action("Grep", "TODO")
|
|
1137
|
+
long_output = "\n".join([f"src/file{i}.py:42: # TODO: implement feature {i}" for i in range(15)])
|
|
1138
|
+
log_output(long_output, max_lines=5)
|
|
1139
|
+
|
|
1140
|
+
print()
|
|
1141
|
+
|
|
1142
|
+
# Demo 4: Response formatting
|
|
1143
|
+
print("Demo 4: Agent Response")
|
|
1054
1144
|
print("-" * 40)
|
|
1145
|
+
sample_response = """I've analyzed your codebase and found the following:
|
|
1146
|
+
|
|
1147
|
+
1. The main entry point is in `main.py`
|
|
1148
|
+
2. Configuration is loaded from `config.json`
|
|
1149
|
+
3. There are 3 utility modules in the `utils/` directory
|
|
1150
|
+
|
|
1151
|
+
Here's a quick example of how to run it:
|
|
1152
|
+
|
|
1153
|
+
```python
|
|
1154
|
+
from myapp import main
|
|
1155
|
+
main.run()
|
|
1156
|
+
```
|
|
1055
1157
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
task.step("Analyzing requirements")
|
|
1060
|
-
idx = task.action("Read", "auth_spec.md")
|
|
1061
|
-
time.sleep(0.3)
|
|
1062
|
-
task.complete_action(idx)
|
|
1063
|
-
task.complete_step("Requirements analyzed")
|
|
1064
|
-
|
|
1065
|
-
task.step("Creating auth module")
|
|
1066
|
-
idx1 = task.action("Write", "auth/login.py")
|
|
1067
|
-
time.sleep(0.2)
|
|
1068
|
-
task.complete_action(idx1)
|
|
1069
|
-
idx2 = task.action("Write", "auth/logout.py")
|
|
1070
|
-
time.sleep(0.2)
|
|
1071
|
-
task.complete_action(idx2)
|
|
1072
|
-
task.complete_step("Auth module created")
|
|
1073
|
-
|
|
1074
|
-
task.finish()
|
|
1158
|
+
Let me know if you need any additional help!"""
|
|
1159
|
+
print_response(sample_response)
|
|
1075
1160
|
|
|
1076
1161
|
print("Demo complete!")
|
|
1077
1162
|
|