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.
@@ -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.5
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()