From e94bb3a341b1643f9dbc2484d9d8645196e6a190 Mon Sep 17 00:00:00 2001 From: Fede Luzzi Date: Mon, 27 Jul 2026 15:47:51 -0300 Subject: [PATCH] feat(cli): add multiline input support for AI prompts, update docs and bump to v6.0.5 --- connpy/_version.py | 2 +- connpy/cli/run_handler.py | 36 +- connpy/cli/terminal_ui.py | 20 +- connpy/tests/test_cli_run_ai.py | 44 +- docs/connpy/cli/node_handler.html | 4 +- docs/connpy/cli/plugin_handler.html | 20 +- docs/connpy/cli/run_handler.html | 72 ++- docs/connpy/cli/terminal_ui.html | 36 +- docs/connpy/index.html | 571 +++++++++++++---------- docs/connpy/services/index.html | 20 +- docs/connpy/services/plugin_service.html | 20 +- 11 files changed, 544 insertions(+), 301 deletions(-) diff --git a/connpy/_version.py b/connpy/_version.py index bb78858..35504ed 100644 --- a/connpy/_version.py +++ b/connpy/_version.py @@ -1 +1 @@ -__version__ = "6.0.4" +__version__ = "6.0.5" diff --git a/connpy/cli/run_handler.py b/connpy/cli/run_handler.py index 749f63f..7a2c6e6 100644 --- a/connpy/cli/run_handler.py +++ b/connpy/cli/run_handler.py @@ -379,6 +379,35 @@ class RunHandler: from rich.rule import Rule from rich.panel import Panel from rich.syntax import Syntax + from prompt_toolkit import PromptSession + from prompt_toolkit.formatted_text import HTML + from prompt_toolkit.key_binding import KeyBindings + + # Helper to get active theme color + def get_theme_color(style_name, fallback="white"): + try: + style = printer.connpy_theme.styles.get(style_name) + if style and style.color: + if style.color.is_default: return fallback + return style.color.triplet.hex if style.color.triplet else style.color.name + except: pass + return fallback + + user_color = get_theme_color("user_prompt", "#00afd7") + + # Configure multiline key bindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter for newlines + kb = KeyBindings() + + @kb.add('enter') + def _(event): + event.current_buffer.validate_and_handle() + + @kb.add('c-j') + @kb.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + + session = PromptSession(key_bindings=kb) dest_file = args.data[0] if os.path.exists(dest_file): @@ -390,12 +419,15 @@ class RunHandler: # Consistent layout opening matching global AI (engineer style) from rich.markdown import Markdown printer.console.print(Rule(style="engineer")) - printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n")) + printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n*Press Enter to submit, or Ctrl+Enter (Alt+Enter) to add a new line.*\n")) printer.console.print(Rule(style="engineer")) while True: try: - user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]") + user_prompt = session.prompt( + HTML(f'\n'), + multiline=True + ) except (KeyboardInterrupt, EOFError): printer.console.print() printer.warning("Operation cancelled by user.") diff --git a/connpy/cli/terminal_ui.py b/connpy/cli/terminal_ui.py index 5efb670..8f7299b 100644 --- a/connpy/cli/terminal_ui.py +++ b/connpy/cli/terminal_ui.py @@ -14,6 +14,8 @@ from rich.panel import Panel from rich.markdown import Markdown from prompt_toolkit import PromptSession from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.filters import has_completions + from prompt_toolkit.formatted_text import HTML from prompt_toolkit.history import InMemoryHistory @@ -126,6 +128,16 @@ class CopilotInterface: state['cancelled'] = True event.app.exit(result='') + # Multiline keybindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter to add a newline + @bindings.add('enter', filter=~has_completions) + def _(event): + event.current_buffer.validate_and_handle() + + @bindings.add('c-j') + @bindings.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + def get_active_buffer(): if state['context_mode'] == self.mode_lines: return '\n'.join(buffer.split('\n')[-state['context_lines']:]) @@ -271,7 +283,8 @@ class CopilotInterface: question = await session.prompt_async( get_prompt_text, key_bindings=bindings, - bottom_toolbar=get_toolbar + bottom_toolbar=get_toolbar, + multiline=True ) except (KeyboardInterrupt, EOFError): state['cancelled'] = True @@ -284,13 +297,14 @@ class CopilotInterface: directive = self.ai_service.process_copilot_input(question, self.session_state) if directive["action"] == "state_update": - state['toolbar_msg'] = directive['message'] + msg = directive['message'] + state['toolbar_msg'] = msg state['msg_expiry'] = time.time() + 3 # 3 seconds timeout async def delayed_refresh(): await asyncio.sleep(3.1) # Only invalidate if the message hasn't been replaced by a newer one - if state.get('toolbar_msg') == directive['message']: + if state.get('toolbar_msg') == msg: state['toolbar_msg'] = '' # Explicitly clear try: from prompt_toolkit.application.current import get_app diff --git a/connpy/tests/test_cli_run_ai.py b/connpy/tests/test_cli_run_ai.py index 50bfbb0..13e4342 100644 --- a/connpy/tests/test_cli_run_ai.py +++ b/connpy/tests/test_cli_run_ai.py @@ -100,15 +100,16 @@ def test_ai_generate_wizard_save(app, tmp_path): }) app.services.ai.build_playbook_chat = mock_chat - # Mock rich.prompt.Prompt.ask to simulate User inputting prompt and then 'y' to save - with patch("rich.prompt.Prompt.ask", side_effect=["create a basic task", "y"]): - app.start(["run", "--generate-ai", str(dest_yaml)]) + # Mock prompt_toolkit PromptSession.prompt for user input, and Prompt.ask for save confirmation + with patch("prompt_toolkit.PromptSession.prompt", return_value="create a basic task"): + with patch("rich.prompt.Prompt.ask", return_value="y"): + app.start(["run", "--generate-ai", str(dest_yaml)]) - mock_chat.assert_called_once_with("create a basic task", chat_history=[], chunk_callback=ANY) - assert os.path.exists(dest_yaml) - with open(dest_yaml) as f: - content = f.read() - assert "tasks:" in content + mock_chat.assert_called_once_with("create a basic task", chat_history=[], chunk_callback=ANY) + assert os.path.exists(dest_yaml) + with open(dest_yaml) as f: + content = f.read() + assert "tasks:" in content def test_ai_generate_wizard_run(app, tmp_path): """Test that ai_generate wizard runs, saves the playbook and executes it when choosing 'run'.""" @@ -121,16 +122,17 @@ def test_ai_generate_wizard_run(app, tmp_path): }) app.services.ai.build_playbook_chat = mock_chat - with patch("rich.prompt.Prompt.ask", side_effect=["create task", "run"]): - with patch("connpy.cli.run_handler.RunHandler.yaml_run") as mock_yaml_run: - app.start(["run", "--generate-ai", str(dest_yaml)]) - - mock_chat.assert_called_once_with("create task", chat_history=[], chunk_callback=ANY) - assert os.path.exists(dest_yaml) - with open(dest_yaml) as f: - content = f.read() - assert "tasks:" in content - - mock_yaml_run.assert_called_once() - args = mock_yaml_run.call_args[0][0] - assert args.data == [str(dest_yaml)] + with patch("prompt_toolkit.PromptSession.prompt", return_value="create task"): + with patch("rich.prompt.Prompt.ask", return_value="run"): + with patch("connpy.cli.run_handler.RunHandler.yaml_run") as mock_yaml_run: + app.start(["run", "--generate-ai", str(dest_yaml)]) + + mock_chat.assert_called_once_with("create task", chat_history=[], chunk_callback=ANY) + assert os.path.exists(dest_yaml) + with open(dest_yaml) as f: + content = f.read() + assert "tasks:" in content + + mock_yaml_run.assert_called_once() + args = mock_yaml_run.call_args[0][0] + assert args.data == [str(dest_yaml)] diff --git a/docs/connpy/cli/node_handler.html b/docs/connpy/cli/node_handler.html index 419dab5..49688f0 100644 --- a/docs/connpy/cli/node_handler.html +++ b/docs/connpy/cli/node_handler.html @@ -122,7 +122,7 @@ el.replaceWith(d); debug=args.debug, logger=self.app._service_logger ) - except ConnpyError as e: + except (ConnpyError, ValueError) as e: printer.error(str(e)) sys.exit(1) @@ -400,7 +400,7 @@ el.replaceWith(d); debug=args.debug, logger=self.app._service_logger ) - except ConnpyError as e: + except (ConnpyError, ValueError) as e: printer.error(str(e)) sys.exit(1) diff --git a/docs/connpy/cli/plugin_handler.html b/docs/connpy/cli/plugin_handler.html index 17142e7..76ede05 100644 --- a/docs/connpy/cli/plugin_handler.html +++ b/docs/connpy/cli/plugin_handler.html @@ -167,6 +167,8 @@ el.replaceWith(d); # Populate local plugins for name, details in local_plugins.items(): + if details.get("origin") == "core": + continue state = "Disabled" if not details.get("enabled", True) else "Active" color = "red" if state == "Disabled" else "green" @@ -175,11 +177,14 @@ el.replaceWith(d); state = "Shadowed (Override by Remote)" color = "yellow" - table.add_row(name, f"[{color}]{state}[/{color}]", "Local") + origin = details.get("origin", "Local").capitalize() + table.add_row(name, f"[{color}]{state}[/{color}]", origin) # Populate remote plugins if self.app.services.mode == "remote": for name, details in remote_plugins.items(): + if details.get("origin") == "core": + continue state = "Disabled" if not details.get("enabled", True) else "Active" color = "red" if state == "Disabled" else "green" @@ -190,7 +195,8 @@ el.replaceWith(d); state = "Shadowed (Override by Local)" color = "yellow" - table.add_row(name, f"[{color}]{state}[/{color}]", "Remote") + origin = details.get("origin", "Remote").capitalize() + table.add_row(name, f"[{color}]{state}[/{color}]", origin) if not local_plugins and not remote_plugins: printer.console.print(" No plugins found.") @@ -320,6 +326,8 @@ el.replaceWith(d); # Populate local plugins for name, details in local_plugins.items(): + if details.get("origin") == "core": + continue state = "Disabled" if not details.get("enabled", True) else "Active" color = "red" if state == "Disabled" else "green" @@ -328,11 +336,14 @@ el.replaceWith(d); state = "Shadowed (Override by Remote)" color = "yellow" - table.add_row(name, f"[{color}]{state}[/{color}]", "Local") + origin = details.get("origin", "Local").capitalize() + table.add_row(name, f"[{color}]{state}[/{color}]", origin) # Populate remote plugins if self.app.services.mode == "remote": for name, details in remote_plugins.items(): + if details.get("origin") == "core": + continue state = "Disabled" if not details.get("enabled", True) else "Active" color = "red" if state == "Disabled" else "green" @@ -343,7 +354,8 @@ el.replaceWith(d); state = "Shadowed (Override by Local)" color = "yellow" - table.add_row(name, f"[{color}]{state}[/{color}]", "Remote") + origin = details.get("origin", "Remote").capitalize() + table.add_row(name, f"[{color}]{state}[/{color}]", origin) if not local_plugins and not remote_plugins: printer.console.print(" No plugins found.") diff --git a/docs/connpy/cli/run_handler.html b/docs/connpy/cli/run_handler.html index 9492172..7018ef5 100644 --- a/docs/connpy/cli/run_handler.html +++ b/docs/connpy/cli/run_handler.html @@ -427,6 +427,35 @@ el.replaceWith(d); from rich.rule import Rule from rich.panel import Panel from rich.syntax import Syntax + from prompt_toolkit import PromptSession + from prompt_toolkit.formatted_text import HTML + from prompt_toolkit.key_binding import KeyBindings + + # Helper to get active theme color + def get_theme_color(style_name, fallback="white"): + try: + style = printer.connpy_theme.styles.get(style_name) + if style and style.color: + if style.color.is_default: return fallback + return style.color.triplet.hex if style.color.triplet else style.color.name + except: pass + return fallback + + user_color = get_theme_color("user_prompt", "#00afd7") + + # Configure multiline key bindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter for newlines + kb = KeyBindings() + + @kb.add('enter') + def _(event): + event.current_buffer.validate_and_handle() + + @kb.add('c-j') + @kb.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + + session = PromptSession(key_bindings=kb) dest_file = args.data[0] if os.path.exists(dest_file): @@ -438,12 +467,15 @@ el.replaceWith(d); # Consistent layout opening matching global AI (engineer style) from rich.markdown import Markdown printer.console.print(Rule(style="engineer")) - printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n")) + printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n*Press Enter to submit, or Ctrl+Enter (Alt+Enter) to add a new line.*\n")) printer.console.print(Rule(style="engineer")) while True: try: - user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]") + user_prompt = session.prompt( + HTML(f'<style fg="{user_color}">User (Enter to submit, Ctrl+Enter for newline):</style>\n'), + multiline=True + ) except (KeyboardInterrupt, EOFError): printer.console.print() printer.warning("Operation cancelled by user.") @@ -547,6 +579,35 @@ el.replaceWith(d); from rich.rule import Rule from rich.panel import Panel from rich.syntax import Syntax + from prompt_toolkit import PromptSession + from prompt_toolkit.formatted_text import HTML + from prompt_toolkit.key_binding import KeyBindings + + # Helper to get active theme color + def get_theme_color(style_name, fallback="white"): + try: + style = printer.connpy_theme.styles.get(style_name) + if style and style.color: + if style.color.is_default: return fallback + return style.color.triplet.hex if style.color.triplet else style.color.name + except: pass + return fallback + + user_color = get_theme_color("user_prompt", "#00afd7") + + # Configure multiline key bindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter for newlines + kb = KeyBindings() + + @kb.add('enter') + def _(event): + event.current_buffer.validate_and_handle() + + @kb.add('c-j') + @kb.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + + session = PromptSession(key_bindings=kb) dest_file = args.data[0] if os.path.exists(dest_file): @@ -558,12 +619,15 @@ el.replaceWith(d); # Consistent layout opening matching global AI (engineer style) from rich.markdown import Markdown printer.console.print(Rule(style="engineer")) - printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n")) + printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n*Press Enter to submit, or Ctrl+Enter (Alt+Enter) to add a new line.*\n")) printer.console.print(Rule(style="engineer")) while True: try: - user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]") + user_prompt = session.prompt( + HTML(f'<style fg="{user_color}">User (Enter to submit, Ctrl+Enter for newline):</style>\n'), + multiline=True + ) except (KeyboardInterrupt, EOFError): printer.console.print() printer.warning("Operation cancelled by user.") diff --git a/docs/connpy/cli/terminal_ui.html b/docs/connpy/cli/terminal_ui.html index 19c2046..f53544e 100644 --- a/docs/connpy/cli/terminal_ui.html +++ b/docs/connpy/cli/terminal_ui.html @@ -160,6 +160,16 @@ el.replaceWith(d); state['cancelled'] = True event.app.exit(result='') + # Multiline keybindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter to add a newline + @bindings.add('enter', filter=~has_completions) + def _(event): + event.current_buffer.validate_and_handle() + + @bindings.add('c-j') + @bindings.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + def get_active_buffer(): if state['context_mode'] == self.mode_lines: return '\n'.join(buffer.split('\n')[-state['context_lines']:]) @@ -305,7 +315,8 @@ el.replaceWith(d); question = await session.prompt_async( get_prompt_text, key_bindings=bindings, - bottom_toolbar=get_toolbar + bottom_toolbar=get_toolbar, + multiline=True ) except (KeyboardInterrupt, EOFError): state['cancelled'] = True @@ -318,13 +329,14 @@ el.replaceWith(d); directive = self.ai_service.process_copilot_input(question, self.session_state) if directive["action"] == "state_update": - state['toolbar_msg'] = directive['message'] + msg = directive['message'] + state['toolbar_msg'] = msg state['msg_expiry'] = time.time() + 3 # 3 seconds timeout async def delayed_refresh(): await asyncio.sleep(3.1) # Only invalidate if the message hasn't been replaced by a newer one - if state.get('toolbar_msg') == directive['message']: + if state.get('toolbar_msg') == msg: state['toolbar_msg'] = '' # Explicitly clear try: from prompt_toolkit.application.current import get_app @@ -614,6 +626,16 @@ el.replaceWith(d); state['cancelled'] = True event.app.exit(result='') + # Multiline keybindings: Enter to submit, Ctrl+Enter (c-j) or Alt+Enter to add a newline + @bindings.add('enter', filter=~has_completions) + def _(event): + event.current_buffer.validate_and_handle() + + @bindings.add('c-j') + @bindings.add('escape', 'enter') + def _(event): + event.current_buffer.insert_text('\n') + def get_active_buffer(): if state['context_mode'] == self.mode_lines: return '\n'.join(buffer.split('\n')[-state['context_lines']:]) @@ -759,7 +781,8 @@ el.replaceWith(d); question = await session.prompt_async( get_prompt_text, key_bindings=bindings, - bottom_toolbar=get_toolbar + bottom_toolbar=get_toolbar, + multiline=True ) except (KeyboardInterrupt, EOFError): state['cancelled'] = True @@ -772,13 +795,14 @@ el.replaceWith(d); directive = self.ai_service.process_copilot_input(question, self.session_state) if directive["action"] == "state_update": - state['toolbar_msg'] = directive['message'] + msg = directive['message'] + state['toolbar_msg'] = msg state['msg_expiry'] = time.time() + 3 # 3 seconds timeout async def delayed_refresh(): await asyncio.sleep(3.1) # Only invalidate if the message hasn't been replaced by a newer one - if state.get('toolbar_msg') == directive['message']: + if state.get('toolbar_msg') == msg: state['toolbar_msg'] = '' # Explicitly clear try: from prompt_toolkit.application.current import get_app diff --git a/docs/connpy/index.html b/docs/connpy/index.html index 42f19c1..facfec7 100644 --- a/docs/connpy/index.html +++ b/docs/connpy/index.html @@ -41,169 +41,196 @@ el.replaceWith(d);

App Logo

-

Connpy

+

Connpy (v6.0.3)

- -

+ + + + + +

Connpy is a powerful Connection Manager and Network Automation Platform for Linux, Mac, and Docker. It provides a unified interface for SSH, SFTP, Telnet, kubectl, Docker pods, and AWS SSM.

-

The v6 release introduces the AI Copilot, an interactive terminal assistant that understands your network context and helps you manage your infrastructure more intelligently.

-

🤖 AI Copilot (New in v6)

-

The AI Copilot is deeply integrated into your terminal workflow: -- Terminal Context Awareness: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time. -- Dynamic Context Selection: Flexibly select single, range, or line-based terminal blocks to feed the Copilot, filtering out interactive scrolling garbage automatically (e.g., Cisco IOS/XR scrolling, paginators). -- Hybrid Multi-Agent System: Automatically escalates complex tasks between the Network Engineer (execution) and the Network Architect (strategy). -- MCP Integration: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol. -- Flexible Auth & Keyless AI: Support for advanced LiteLLM credentials (--engineer-auth / --architect-auth) allowing keyless local models (Ollama), cloud engines (Vertex AI), or custom endpoints. -- Enhanced Session Management: Uniquely generated sessions, robust pagination, and interactive styling translating prompt themes directly to terminal escapes. -- Semantic Prompt Integration: Emit standard OSC prompt sequences (]133;B) for real-time remote/web front-end command tracking. -- Interactive Chat: Launch with conn ai for a collaborative troubleshooting session.

-

Core Features

+

The v6 release introduces a comprehensive AI Copilot and AI Playbook Engine, transforming your terminal into an interactive network assistant that understands your device outputs, configures parameters safely, and runs simulations.

+
+

1. 🤖 AI System

+

1a. Terminal Copilot (Ctrl+Space)

+

Invoke the context-aware AI Copilot directly inside any active terminal session by pressing Ctrl + Space. +* Context Modes: Cycles through LINES (sends raw scroll buffer), SINGLE (captures exactly one command + output block), and RANGE (logical group of recent commands) using Ctrl+Up/Down. +* Slash Commands (/): Control the AI persona and safety settings: +* /architect / /engineer: Swaps the agent between high-level strategist and technical executor. +* /trust / /untrust: Configures auto-run behavior for suggested non-destructive commands. +* /os [system]: Manually overrides target OS parsing rules (e.g. /os cisco_ios). +* /prompt [regex]: Overrides command prompt detection bounds. +* /clear: Clear context history.

+

1b. AI Chat (conn ai)

+

Start a standalone persistent session with the AI Copilot. Manage sessions using --list, --resume, --session <id> (to restore a specific history), --delete <id>, or send a quick single-shot question directly from the terminal prompt:

+
conn ai "how do i check bgp summary on cisco?"
+
+

1c. MCP Integration

+

Connect to external data sources and tools dynamically via the Model Context Protocol (MCP). Use the interactive wizard or command actions to configure MCP servers:

+
conn ai --mcp
+
+
+

2. ⚙️ Automation & Playbooks

+

2a. Quick Run (conn run)

+

Run commands in parallel directly on target nodes or folder structures:

+
conn run router1 "show interface"
+
+

2b. YAML Playbook Engine

+

Execute complex structured automation playbooks defined in YAML configuration files. Supports multi-task execution, variables (using global, per-node, or regex matching definitions), timeouts, and variable parallel execution bounds.

+
# example_playbook.yaml
+- name: Verify Network Operations
+  hosts: "@office"
+  parallel: true
+  tasks:
+    - name: Get interface brief
+      run: "show ip interface brief"
+    - name: Check OSPF state
+      run: "show ip ospf neighbor"
+      test: "FULL"
+
+

Execute using the playbooks runner:

+
conn run example_playbook.yaml
+
+

2c. AI-Assisted Automation

+

Leverage AI to generate playbook templates (--generate-ai), simulate command changes before execution (--preflight-ai), or analyze consolidated execution logs post-run (--analyze). Use --test "expected text1" "expected text2" to specify assert-style output validations. +* To generate an empty template: conn run --generate

+
+

3. 📂 Inventory Management

+

3a. Nodes

+

Manage connections using standard commands: add (conn --add node1), edit (conn --mod node1), delete (conn --del node1), show configuration (conn --show node1), or connect (conn node1).

+

3b. Profiles

+

Define credentials and templates globally and reference them inside node fields using the @profile_name placeholder. Manage profiles interactively or via commands:

+
conn profile -a profile_name
+# Or equivalently:
+conn -a profile profile_name
+
+

During the interactive conn --add prompt, you can input @profile_name in the username or password fields to reference it.

+

3c. Folders, Move, Copy, List

+

Organize nodes into logical folder hierarchies (@office, @datacenter@office). Move items (conn move [src] [dst]), copy (conn copy [src] [dst]), or list items with custom filters and formatting:

+
conn list nodes --filter ".*-prod" --format "{name} ({host}) runs {protocol}"
+
+

3d. Bulk, Export, Import

+

Bulk import connections from formatted text files (conn bulk -f nodes.txt), or export/import connection folders using YAML configurations (conn export @folder > backup.yaml / conn import backup.yaml).

+

3e. Tags System

+

Customize connection settings dynamically using tags. Configure per-node settings like custom OS types (os), prompt regex rules (prompt), and page length triggers (screen_length_command).

+
# Custom tags dictionary (YANG / VSR context)
+tags: { "os": "cisco_ios", "prompt": ".*#", "screen_length_command": "terminal length 0" }
+
+
+

4. 🔌 Protocols & Connection Features

+

4a. SSH / SFTP / Telnet / kubectl / Docker / AWS SSM

+

Connect to various architectures using native protocols: +* SSH / Telnet: Standard CLI protocols. +* SFTP: Transfer files securely (conn --sftp node). +* Docker: Connect directly to local container names (host set to container name/ID). +* Kubernetes (kubectl): Connect to pods (namespace customizable via options). +* AWS SSM: Connect to EC2 instances using Instance IDs as hosts.

+

4b. Jumphosts

+

Support for single or chained intermediate gateway nodes (SSH, SSM, kubectl, or docker jumphosts) to tunnel traffic safely into target environments.

+

4c. Debug Mode, Keepalive, Logging

+

Track connection steps (conn --debug node), set idle keepalive intervals (conn config --keepalive <seconds>), or define dynamic output log files using variables like ${unique}, ${host}, ${port}, ${user}, ${protocol}, or ${date 'format'}.

+
+

5. 🖥️ Remote Capture (conn capture - Core Plugin)

+

Perform remote packet capture (tcpdump) on hosts over secure SSH reverse tunnels and stream packets live into your local Wireshark GUI:

+
conn capture router1 eth0 -w -f "port 80"
+