claude-sdk-tutor 0.1.5__tar.gz → 0.1.7__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.
- claude_sdk_tutor-0.1.7/CLAUDE.md +50 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/PKG-INFO +59 -1
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/README.md +58 -0
- claude_sdk_tutor-0.1.7/app.py +379 -0
- claude_sdk_tutor-0.1.7/phosphor.tcss +122 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/pyproject.toml +1 -1
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/src/claude/claude_agent.py +8 -2
- claude_sdk_tutor-0.1.7/src/claude/mcp_commands.py +256 -0
- claude_sdk_tutor-0.1.7/src/claude/mcp_config.py +140 -0
- claude_sdk_tutor-0.1.7/src/claude/widgets.py +87 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/uv.lock +1 -1
- claude_sdk_tutor-0.1.5/CLAUDE.md +0 -22
- claude_sdk_tutor-0.1.5/app.py +0 -171
- claude_sdk_tutor-0.1.5/src/claude/widgets.py +0 -27
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/.gitignore +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/.python-version +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/LICENSE +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/Makefile +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/__init__.py +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/src/__init__.py +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/src/claude/__init__.py +0 -0
- {claude_sdk_tutor-0.1.5 → claude_sdk_tutor-0.1.7}/src/claude/history.py +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Claude SDK Tutor
|
|
2
|
+
|
|
3
|
+
A terminal-based chat interface for learning programming with Claude. Built with Textual and the Claude Agent SDK, it provides an interactive TUI where Claude acts as a programming tutor rather than simply writing code for you.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Tutor Mode**: Claude guides learning through explanations and hints instead of providing complete solutions
|
|
8
|
+
- **Web Search**: Optional web lookup capability (toggle with `/togglewebsearch`)
|
|
9
|
+
- **MCP Servers**: Connect external tools via Model Context Protocol (stdio, SSE, HTTP transports)
|
|
10
|
+
- **Command History**: Navigate previous commands with up/down arrows (persisted across sessions)
|
|
11
|
+
- **Query Interruption**: Cancel running queries with Escape or Ctrl+C
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run python app.py
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Slash Commands
|
|
26
|
+
|
|
27
|
+
- `/help` - Show available commands
|
|
28
|
+
- `/clear` - Clear conversation and reconnect
|
|
29
|
+
- `/tutor` - Toggle tutor mode on/off
|
|
30
|
+
- `/togglewebsearch` - Toggle web search capability
|
|
31
|
+
- `/mcp` - Manage MCP servers (use `/mcp help` for subcommands)
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
app.py # Main Textual app with UI and command handling
|
|
37
|
+
src/claude/
|
|
38
|
+
claude_agent.py # Claude SDK client creation and streaming
|
|
39
|
+
widgets.py # Custom HistoryInput widget with keybindings
|
|
40
|
+
history.py # Persistent command history
|
|
41
|
+
mcp_config.py # MCP server configuration storage
|
|
42
|
+
mcp_commands.py # /mcp command parsing and handlers
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Tech Stack
|
|
46
|
+
|
|
47
|
+
- Python 3.13+
|
|
48
|
+
- Textual (TUI framework)
|
|
49
|
+
- Claude Agent SDK
|
|
50
|
+
- uv (package manager)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-sdk-tutor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -15,6 +15,15 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
|
|
16
16
|
A terminal-based programming tutor powered by Claude. Built with Textual and the Claude Agent SDK.
|
|
17
17
|
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Tutor Mode** - Claude guides your learning instead of writing code for you
|
|
21
|
+
- **Web Search** - Optional online lookup capability
|
|
22
|
+
- **MCP Servers** - Extend Claude with external tools via Model Context Protocol
|
|
23
|
+
- **Command History** - Navigate previous commands with up/down arrows (persisted across sessions)
|
|
24
|
+
- **Query Interruption** - Cancel long-running queries with Escape or Ctrl+C
|
|
25
|
+
- **File Access** - Claude can read files in your codebase to provide contextual help
|
|
26
|
+
|
|
18
27
|
## Overview
|
|
19
28
|
|
|
20
29
|
Claude Tutor is a TUI (Terminal User Interface) application designed to help you learn programming concepts. Unlike a typical coding assistant, Claude Tutor focuses on teaching rather than writing code for you. It will:
|
|
@@ -58,6 +67,16 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
58
67
|
- **Grey** - Tool usage (when Claude reads files in your codebase)
|
|
59
68
|
- **Green** - Slash command feedback
|
|
60
69
|
|
|
70
|
+
### Keyboard Shortcuts
|
|
71
|
+
|
|
72
|
+
| Key | Action |
|
|
73
|
+
|-----|--------|
|
|
74
|
+
| `Enter` | Send message |
|
|
75
|
+
| `Up` / `Down` | Navigate command history |
|
|
76
|
+
| `Escape` or `Ctrl+C` | Cancel running query |
|
|
77
|
+
|
|
78
|
+
Command history is automatically saved between sessions.
|
|
79
|
+
|
|
61
80
|
## Slash Commands
|
|
62
81
|
|
|
63
82
|
| Command | Description |
|
|
@@ -66,6 +85,45 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
66
85
|
| `/clear` | Clears the conversation history and starts fresh. Your settings are preserved. |
|
|
67
86
|
| `/tutor` | Toggles tutor mode on/off. When on (default), Claude acts as a teacher. When off, Claude responds normally without the tutoring constraints. |
|
|
68
87
|
| `/togglewebsearch` | Toggles web search on/off. When on, Claude can use WebSearch and WebFetch tools to look up information online. Disabled by default. |
|
|
88
|
+
| `/mcp` | Manage MCP servers. Use `/mcp help` for detailed subcommands. |
|
|
89
|
+
|
|
90
|
+
## MCP Server Support
|
|
91
|
+
|
|
92
|
+
Claude Tutor supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, allowing you to extend Claude's capabilities with external tools.
|
|
93
|
+
|
|
94
|
+
### MCP Commands
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---------|-------------|
|
|
98
|
+
| `/mcp list` | List all configured servers with connection status |
|
|
99
|
+
| `/mcp test [name]` | Test MCP server connections |
|
|
100
|
+
| `/mcp add` | Add a new server (interactive wizard) |
|
|
101
|
+
| `/mcp add <name> <type> <cmd\|url> [args]` | Add server directly |
|
|
102
|
+
| `/mcp remove <name>` | Remove a server |
|
|
103
|
+
| `/mcp enable <name>` | Enable a disabled server |
|
|
104
|
+
| `/mcp disable <name>` | Disable a server without removing it |
|
|
105
|
+
| `/mcp status [name]` | Show server configuration details |
|
|
106
|
+
|
|
107
|
+
### Server Types
|
|
108
|
+
|
|
109
|
+
- **stdio** - Local process that communicates via stdin/stdout (e.g., `npx` packages)
|
|
110
|
+
- **sse** - Server-Sent Events endpoint
|
|
111
|
+
- **http** - HTTP endpoint
|
|
112
|
+
|
|
113
|
+
### Example: Adding an MCP Server
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
/mcp add filesystem stdio npx -y @anthropic/mcp-filesystem
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or use the interactive wizard:
|
|
120
|
+
```
|
|
121
|
+
/mcp add
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
After adding or modifying servers, use `/clear` to reconnect with the updated configuration.
|
|
125
|
+
|
|
126
|
+
MCP server configurations are persisted to `~/.local/share/claude-sdk-tutor/mcp_servers.json`.
|
|
69
127
|
|
|
70
128
|
## Tech Stack
|
|
71
129
|
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A terminal-based programming tutor powered by Claude. Built with Textual and the Claude Agent SDK.
|
|
4
4
|
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Tutor Mode** - Claude guides your learning instead of writing code for you
|
|
8
|
+
- **Web Search** - Optional online lookup capability
|
|
9
|
+
- **MCP Servers** - Extend Claude with external tools via Model Context Protocol
|
|
10
|
+
- **Command History** - Navigate previous commands with up/down arrows (persisted across sessions)
|
|
11
|
+
- **Query Interruption** - Cancel long-running queries with Escape or Ctrl+C
|
|
12
|
+
- **File Access** - Claude can read files in your codebase to provide contextual help
|
|
13
|
+
|
|
5
14
|
## Overview
|
|
6
15
|
|
|
7
16
|
Claude Tutor is a TUI (Terminal User Interface) application designed to help you learn programming concepts. Unlike a typical coding assistant, Claude Tutor focuses on teaching rather than writing code for you. It will:
|
|
@@ -45,6 +54,16 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
45
54
|
- **Grey** - Tool usage (when Claude reads files in your codebase)
|
|
46
55
|
- **Green** - Slash command feedback
|
|
47
56
|
|
|
57
|
+
### Keyboard Shortcuts
|
|
58
|
+
|
|
59
|
+
| Key | Action |
|
|
60
|
+
|-----|--------|
|
|
61
|
+
| `Enter` | Send message |
|
|
62
|
+
| `Up` / `Down` | Navigate command history |
|
|
63
|
+
| `Escape` or `Ctrl+C` | Cancel running query |
|
|
64
|
+
|
|
65
|
+
Command history is automatically saved between sessions.
|
|
66
|
+
|
|
48
67
|
## Slash Commands
|
|
49
68
|
|
|
50
69
|
| Command | Description |
|
|
@@ -53,6 +72,45 @@ Type your programming questions in the input field and press Enter to send. Clau
|
|
|
53
72
|
| `/clear` | Clears the conversation history and starts fresh. Your settings are preserved. |
|
|
54
73
|
| `/tutor` | Toggles tutor mode on/off. When on (default), Claude acts as a teacher. When off, Claude responds normally without the tutoring constraints. |
|
|
55
74
|
| `/togglewebsearch` | Toggles web search on/off. When on, Claude can use WebSearch and WebFetch tools to look up information online. Disabled by default. |
|
|
75
|
+
| `/mcp` | Manage MCP servers. Use `/mcp help` for detailed subcommands. |
|
|
76
|
+
|
|
77
|
+
## MCP Server Support
|
|
78
|
+
|
|
79
|
+
Claude Tutor supports [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers, allowing you to extend Claude's capabilities with external tools.
|
|
80
|
+
|
|
81
|
+
### MCP Commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---------|-------------|
|
|
85
|
+
| `/mcp list` | List all configured servers with connection status |
|
|
86
|
+
| `/mcp test [name]` | Test MCP server connections |
|
|
87
|
+
| `/mcp add` | Add a new server (interactive wizard) |
|
|
88
|
+
| `/mcp add <name> <type> <cmd\|url> [args]` | Add server directly |
|
|
89
|
+
| `/mcp remove <name>` | Remove a server |
|
|
90
|
+
| `/mcp enable <name>` | Enable a disabled server |
|
|
91
|
+
| `/mcp disable <name>` | Disable a server without removing it |
|
|
92
|
+
| `/mcp status [name]` | Show server configuration details |
|
|
93
|
+
|
|
94
|
+
### Server Types
|
|
95
|
+
|
|
96
|
+
- **stdio** - Local process that communicates via stdin/stdout (e.g., `npx` packages)
|
|
97
|
+
- **sse** - Server-Sent Events endpoint
|
|
98
|
+
- **http** - HTTP endpoint
|
|
99
|
+
|
|
100
|
+
### Example: Adding an MCP Server
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
/mcp add filesystem stdio npx -y @anthropic/mcp-filesystem
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or use the interactive wizard:
|
|
107
|
+
```
|
|
108
|
+
/mcp add
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
After adding or modifying servers, use `/clear` to reconnect with the updated configuration.
|
|
112
|
+
|
|
113
|
+
MCP server configurations are persisted to `~/.local/share/claude-sdk-tutor/mcp_servers.json`.
|
|
56
114
|
|
|
57
115
|
## Tech Stack
|
|
58
116
|
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from claude_agent_sdk import (
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
ClaudeAgentOptions,
|
|
6
|
+
ResultMessage,
|
|
7
|
+
SystemMessage,
|
|
8
|
+
query,
|
|
9
|
+
)
|
|
10
|
+
from rich.markdown import Markdown as RichMarkdown
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from textual.app import App, ComposeResult
|
|
13
|
+
from textual.containers import Vertical
|
|
14
|
+
from rich.box import ROUNDED
|
|
15
|
+
from textual.widgets import Static, Footer, Input, RichLog
|
|
16
|
+
|
|
17
|
+
from claude.claude_agent import (
|
|
18
|
+
connect_client,
|
|
19
|
+
create_claude_client,
|
|
20
|
+
stream_helpful_claude,
|
|
21
|
+
)
|
|
22
|
+
from claude.history import CommandHistory
|
|
23
|
+
from claude.mcp_commands import McpAsyncCommand, McpCommandHandler
|
|
24
|
+
from claude.mcp_config import McpConfigManager
|
|
25
|
+
from claude.widgets import ASCIISpinner, HistoryInput, StatusBar
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Message border colors (vivid)
|
|
29
|
+
USER_COLOR = "#00aaff" # Vivid cyan-blue
|
|
30
|
+
CLAUDE_COLOR = "#ff3333" # Vivid red
|
|
31
|
+
TOOL_COLOR = "#cccccc" # Bright grey
|
|
32
|
+
SYSTEM_COLOR = "#33ff66" # Vivid green
|
|
33
|
+
|
|
34
|
+
HEADER_TEXT = "Claude SDK Tutor"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MyApp(App):
|
|
38
|
+
CSS_PATH = "phosphor.tcss"
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.tutor_mode = True
|
|
43
|
+
self.web_search_enabled = False
|
|
44
|
+
self.mcp_config = McpConfigManager()
|
|
45
|
+
self.mcp_handler = McpCommandHandler(self.mcp_config)
|
|
46
|
+
self.mcp_add_state: dict | None = None # For interactive /mcp add wizard
|
|
47
|
+
self.client = self._create_client()
|
|
48
|
+
self.history = CommandHistory()
|
|
49
|
+
self._query_running: bool = False # Track if a query is active
|
|
50
|
+
|
|
51
|
+
def _create_client(self):
|
|
52
|
+
"""Create a new Claude client with current settings."""
|
|
53
|
+
return create_claude_client(
|
|
54
|
+
tutor_mode=self.tutor_mode,
|
|
55
|
+
web_search=self.web_search_enabled,
|
|
56
|
+
mcp_servers=self.mcp_config.get_enabled_servers_for_sdk(),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def compose(self) -> ComposeResult:
|
|
60
|
+
with Vertical(id="main"):
|
|
61
|
+
yield Static(HEADER_TEXT, id="header")
|
|
62
|
+
yield StatusBar(id="status-bar")
|
|
63
|
+
yield RichLog(markup=True, highlight=True)
|
|
64
|
+
yield ASCIISpinner(id="spinner")
|
|
65
|
+
yield HistoryInput(
|
|
66
|
+
history=self.history,
|
|
67
|
+
placeholder="Type a message or /help for commands...",
|
|
68
|
+
)
|
|
69
|
+
yield Footer()
|
|
70
|
+
|
|
71
|
+
async def on_mount(self) -> None:
|
|
72
|
+
self.query_one("#spinner", ASCIISpinner).display = False
|
|
73
|
+
self._update_status_bar()
|
|
74
|
+
await connect_client(self.client)
|
|
75
|
+
|
|
76
|
+
def _update_status_bar(self) -> None:
|
|
77
|
+
"""Update the status bar with current mode states."""
|
|
78
|
+
status_bar = self.query_one("#status-bar", StatusBar)
|
|
79
|
+
status_bar.tutor_on = self.tutor_mode
|
|
80
|
+
status_bar.web_on = self.web_search_enabled
|
|
81
|
+
status_bar.mcp_count = len(self.mcp_config.get_enabled_servers_for_sdk())
|
|
82
|
+
|
|
83
|
+
def write_user_message(self, message: str) -> None:
|
|
84
|
+
log = self.query_one(RichLog)
|
|
85
|
+
log.write(Panel(
|
|
86
|
+
RichMarkdown(message),
|
|
87
|
+
title="You",
|
|
88
|
+
border_style=USER_COLOR,
|
|
89
|
+
box=ROUNDED,
|
|
90
|
+
padding=(1, 2),
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
def write_system_message(self, message: str) -> None:
|
|
94
|
+
log = self.query_one(RichLog)
|
|
95
|
+
log.write(Panel(
|
|
96
|
+
RichMarkdown(message),
|
|
97
|
+
title="Claude",
|
|
98
|
+
border_style=CLAUDE_COLOR,
|
|
99
|
+
box=ROUNDED,
|
|
100
|
+
padding=(1, 2),
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
def write_tool_message(self, name: str, input: dict) -> None:
|
|
104
|
+
log = self.query_one(RichLog)
|
|
105
|
+
input_str = json.dumps(input, indent=2)
|
|
106
|
+
content = f"**{name}**\n```json\n{input_str}\n```"
|
|
107
|
+
log.write(Panel(
|
|
108
|
+
RichMarkdown(content),
|
|
109
|
+
title="Tool",
|
|
110
|
+
border_style=TOOL_COLOR,
|
|
111
|
+
box=ROUNDED,
|
|
112
|
+
padding=(1, 2),
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
def write_slash_message(self, message: str) -> None:
|
|
116
|
+
log = self.query_one(RichLog)
|
|
117
|
+
log.write(Panel(
|
|
118
|
+
RichMarkdown(message),
|
|
119
|
+
title="System",
|
|
120
|
+
border_style=SYSTEM_COLOR,
|
|
121
|
+
box=ROUNDED,
|
|
122
|
+
padding=(1, 2),
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
126
|
+
command = event.value.strip()
|
|
127
|
+
self.query_one(HistoryInput).value = ""
|
|
128
|
+
if command:
|
|
129
|
+
self.history.add(command)
|
|
130
|
+
|
|
131
|
+
# Handle interactive MCP add wizard
|
|
132
|
+
if self.mcp_add_state is not None:
|
|
133
|
+
if command.lower() == "/cancel":
|
|
134
|
+
self.mcp_add_state = None
|
|
135
|
+
self.write_slash_message("Cancelled MCP server setup.")
|
|
136
|
+
return
|
|
137
|
+
self._handle_mcp_add_step(command)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
if command == "/clear":
|
|
141
|
+
self.run_worker(self.clear_conversation())
|
|
142
|
+
return
|
|
143
|
+
if command == "/tutor":
|
|
144
|
+
self.run_worker(self.toggle_tutor_mode())
|
|
145
|
+
return
|
|
146
|
+
if command == "/togglewebsearch":
|
|
147
|
+
self.run_worker(self.toggle_web_search())
|
|
148
|
+
return
|
|
149
|
+
if command == "/help":
|
|
150
|
+
self.show_help()
|
|
151
|
+
return
|
|
152
|
+
if command.lower().startswith("/mcp"):
|
|
153
|
+
self._handle_mcp_command(command)
|
|
154
|
+
return
|
|
155
|
+
self.write_user_message(event.value)
|
|
156
|
+
self.query_one("#spinner", ASCIISpinner).start("Processing query...")
|
|
157
|
+
self._query_running = True
|
|
158
|
+
self.run_worker(self.get_response(event.value))
|
|
159
|
+
|
|
160
|
+
async def clear_conversation(self) -> None:
|
|
161
|
+
self.query_one(RichLog).clear()
|
|
162
|
+
self.client = self._create_client()
|
|
163
|
+
await connect_client(self.client)
|
|
164
|
+
self._update_status_bar()
|
|
165
|
+
self.write_slash_message("Context cleared")
|
|
166
|
+
|
|
167
|
+
async def toggle_tutor_mode(self) -> None:
|
|
168
|
+
self.tutor_mode = not self.tutor_mode
|
|
169
|
+
self.query_one(RichLog).clear()
|
|
170
|
+
self.client = self._create_client()
|
|
171
|
+
await connect_client(self.client)
|
|
172
|
+
self._update_status_bar()
|
|
173
|
+
status = "enabled" if self.tutor_mode else "disabled"
|
|
174
|
+
self.write_slash_message(f"Tutor mode {status}")
|
|
175
|
+
|
|
176
|
+
async def toggle_web_search(self) -> None:
|
|
177
|
+
self.web_search_enabled = not self.web_search_enabled
|
|
178
|
+
self.query_one(RichLog).clear()
|
|
179
|
+
self.client = self._create_client()
|
|
180
|
+
await connect_client(self.client)
|
|
181
|
+
self._update_status_bar()
|
|
182
|
+
status = "enabled" if self.web_search_enabled else "disabled"
|
|
183
|
+
self.write_slash_message(f"Web search {status}")
|
|
184
|
+
|
|
185
|
+
def show_help(self) -> None:
|
|
186
|
+
help_text = """**Available Commands**
|
|
187
|
+
|
|
188
|
+
- `/help` - Show this help message
|
|
189
|
+
- `/clear` - Clear conversation history and start fresh
|
|
190
|
+
- `/tutor` - Toggle tutor mode on/off (guides learning vs gives code)
|
|
191
|
+
- `/togglewebsearch` - Toggle web search on/off (allows online lookups)
|
|
192
|
+
- `/mcp` - Manage MCP servers (use `/mcp help` for details)"""
|
|
193
|
+
self.write_slash_message(help_text)
|
|
194
|
+
|
|
195
|
+
def _handle_mcp_command(self, command: str) -> None:
|
|
196
|
+
"""Handle /mcp commands."""
|
|
197
|
+
result = self.mcp_handler.handle_command(command)
|
|
198
|
+
if result is None:
|
|
199
|
+
# Start interactive add wizard
|
|
200
|
+
self.mcp_add_state = {"step": 0, "name": "", "type": "", "data": {}}
|
|
201
|
+
self.write_slash_message(
|
|
202
|
+
"**Add MCP Server**\n\nEnter server name (or `/cancel` to abort):"
|
|
203
|
+
)
|
|
204
|
+
elif isinstance(result, McpAsyncCommand):
|
|
205
|
+
# Async command needs connection testing
|
|
206
|
+
self.query_one("#spinner", ASCIISpinner).start("Testing MCP connections...")
|
|
207
|
+
self.run_worker(self._test_mcp_connections(result))
|
|
208
|
+
else:
|
|
209
|
+
self.write_slash_message(result)
|
|
210
|
+
|
|
211
|
+
async def _test_mcp_connections(self, cmd: McpAsyncCommand) -> None:
|
|
212
|
+
"""Test MCP server connections and display results."""
|
|
213
|
+
try:
|
|
214
|
+
mcp_servers = self.mcp_config.get_enabled_servers_for_sdk()
|
|
215
|
+
if not mcp_servers:
|
|
216
|
+
if cmd.command == "test":
|
|
217
|
+
self.write_slash_message(
|
|
218
|
+
"**MCP Test**\n\nNo enabled servers to test."
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
self.write_slash_message(
|
|
222
|
+
self.mcp_handler.handle_list(cmd.args, connection_status=None)
|
|
223
|
+
)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Build allowed tools for the test
|
|
227
|
+
allowed_tools = [f"mcp__{name}__*" for name in mcp_servers]
|
|
228
|
+
|
|
229
|
+
options = ClaudeAgentOptions(
|
|
230
|
+
mcp_servers=mcp_servers,
|
|
231
|
+
allowed_tools=allowed_tools,
|
|
232
|
+
max_turns=1,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
connection_status: dict[str, str] = {}
|
|
236
|
+
|
|
237
|
+
# Run a minimal query just to get the init message with MCP status
|
|
238
|
+
async for message in query(prompt="test", options=options):
|
|
239
|
+
if isinstance(message, SystemMessage) and message.subtype == "init":
|
|
240
|
+
mcp_info = message.data.get("mcp_servers", [])
|
|
241
|
+
for server in mcp_info:
|
|
242
|
+
name = server.get("name", "unknown")
|
|
243
|
+
status = server.get("status", "unknown")
|
|
244
|
+
connection_status[name] = status
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
# Display results based on command
|
|
248
|
+
if cmd.command == "test":
|
|
249
|
+
self.write_slash_message(
|
|
250
|
+
self.mcp_handler.handle_test(cmd.args, connection_status)
|
|
251
|
+
)
|
|
252
|
+
else: # list
|
|
253
|
+
self.write_slash_message(
|
|
254
|
+
self.mcp_handler.handle_list(cmd.args, connection_status)
|
|
255
|
+
)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self.write_slash_message(f"**Error** testing MCP connections: {e}")
|
|
258
|
+
finally:
|
|
259
|
+
self.query_one("#spinner", ASCIISpinner).stop()
|
|
260
|
+
|
|
261
|
+
def _handle_mcp_add_step(self, user_input: str) -> None:
|
|
262
|
+
"""Handle a step in the interactive MCP add wizard."""
|
|
263
|
+
state = self.mcp_add_state
|
|
264
|
+
if state is None:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
step = state["step"]
|
|
268
|
+
|
|
269
|
+
if step == 0:
|
|
270
|
+
# Got server name
|
|
271
|
+
name = user_input.strip()
|
|
272
|
+
if not name:
|
|
273
|
+
self.write_slash_message("**Error**: Name cannot be empty. Try again:")
|
|
274
|
+
return
|
|
275
|
+
if self.mcp_config.get_server(name):
|
|
276
|
+
self.write_slash_message(
|
|
277
|
+
f"**Error**: Server `{name}` already exists. Enter a different name:"
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
state["name"] = name
|
|
281
|
+
state["step"] = 1
|
|
282
|
+
self.write_slash_message(
|
|
283
|
+
"Select server type:\n- `stdio` - Local process\n- `sse` - Server-Sent Events\n- `http` - HTTP endpoint"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
elif step == 1:
|
|
287
|
+
# Got server type
|
|
288
|
+
server_type = user_input.strip().lower()
|
|
289
|
+
if server_type not in ("stdio", "sse", "http"):
|
|
290
|
+
self.write_slash_message(
|
|
291
|
+
f"**Error**: Invalid type `{server_type}`. Enter `stdio`, `sse`, or `http`:"
|
|
292
|
+
)
|
|
293
|
+
return
|
|
294
|
+
state["type"] = server_type
|
|
295
|
+
state["step"] = 2
|
|
296
|
+
if server_type == "stdio":
|
|
297
|
+
self.write_slash_message("Enter command to run (e.g., `npx`):")
|
|
298
|
+
else:
|
|
299
|
+
self.write_slash_message("Enter server URL:")
|
|
300
|
+
|
|
301
|
+
elif step == 2:
|
|
302
|
+
# Got command or URL
|
|
303
|
+
value = user_input.strip()
|
|
304
|
+
if not value:
|
|
305
|
+
self.write_slash_message("**Error**: Value cannot be empty. Try again:")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
if state["type"] == "stdio":
|
|
309
|
+
state["data"]["command"] = value
|
|
310
|
+
state["step"] = 3
|
|
311
|
+
self.write_slash_message(
|
|
312
|
+
"Enter arguments (space-separated, or leave empty):"
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
# SSE or HTTP - URL provided, we're done
|
|
316
|
+
config = {"type": state["type"], "url": value}
|
|
317
|
+
self.mcp_config.add_server(state["name"], config)
|
|
318
|
+
self.write_slash_message(
|
|
319
|
+
f"**Added** server `{state['name']}` ({state['type']})\n\n"
|
|
320
|
+
"Use `/clear` to reconnect with new MCP servers."
|
|
321
|
+
)
|
|
322
|
+
self.mcp_add_state = None
|
|
323
|
+
|
|
324
|
+
elif step == 3:
|
|
325
|
+
# Got args for stdio command
|
|
326
|
+
args = user_input.strip().split() if user_input.strip() else []
|
|
327
|
+
config = {
|
|
328
|
+
"type": "stdio",
|
|
329
|
+
"command": state["data"]["command"],
|
|
330
|
+
"args": args,
|
|
331
|
+
}
|
|
332
|
+
self.mcp_config.add_server(state["name"], config)
|
|
333
|
+
self.write_slash_message(
|
|
334
|
+
f"**Added** server `{state['name']}` (stdio)\n\n"
|
|
335
|
+
"Use `/clear` to reconnect with new MCP servers."
|
|
336
|
+
)
|
|
337
|
+
self.mcp_add_state = None
|
|
338
|
+
|
|
339
|
+
async def get_response(self, text: str) -> None:
|
|
340
|
+
try:
|
|
341
|
+
async for message in stream_helpful_claude(self.client, text):
|
|
342
|
+
if isinstance(message, AssistantMessage):
|
|
343
|
+
for block in message.content:
|
|
344
|
+
if hasattr(block, "text"):
|
|
345
|
+
self.write_system_message(block.text)
|
|
346
|
+
elif hasattr(block, "name"):
|
|
347
|
+
self.write_tool_message(
|
|
348
|
+
block.name, getattr(block, "input", {})
|
|
349
|
+
)
|
|
350
|
+
elif isinstance(message, ResultMessage):
|
|
351
|
+
pass # Might want to add logging later
|
|
352
|
+
finally:
|
|
353
|
+
self.query_one("#spinner", ASCIISpinner).stop()
|
|
354
|
+
self._query_running = False
|
|
355
|
+
|
|
356
|
+
def action_cancel_query(self) -> None:
|
|
357
|
+
"""Interrupt the current running query using SDK interrupt."""
|
|
358
|
+
if self._query_running:
|
|
359
|
+
self.run_worker(self._interrupt_query())
|
|
360
|
+
|
|
361
|
+
async def _interrupt_query(self) -> None:
|
|
362
|
+
"""Send interrupt signal to the Claude SDK client."""
|
|
363
|
+
try:
|
|
364
|
+
await self.client.interrupt()
|
|
365
|
+
self.write_slash_message("Query interrupted.")
|
|
366
|
+
except Exception:
|
|
367
|
+
pass # Ignore errors if not connected or no active query
|
|
368
|
+
finally:
|
|
369
|
+
self._query_running = False
|
|
370
|
+
self.query_one("#spinner", ASCIISpinner).stop()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def main():
|
|
374
|
+
app = MyApp()
|
|
375
|
+
app.run()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == "__main__":
|
|
379
|
+
main()
|