diff --git a/.gitignore b/.gitignore index 46bdc24..dc4eee6 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +scratch/ # Translations *.mo @@ -162,3 +163,4 @@ repo_consolidado_limpio.md connpy_roadmap.md MULTI_USER_PLAN.md COPILOT_PLAN.md +ARCHITECTURAL_DEBT_REFACTOR.md diff --git a/connpy/ai.py b/connpy/ai.py index 98d1cbc..09d100d 100755 --- a/connpy/ai.py +++ b/connpy/ai.py @@ -7,6 +7,7 @@ import threading import asyncio from textwrap import dedent from .core import nodes +from .mcp_client import MCPClientManager _litellm_initialized = False @@ -143,6 +144,9 @@ class ai: self.tool_status_formatters = {} # {"tool_name": formatter_callable} self.engineer_prompt_extensions = [] # Extra text for engineer prompt self.architect_prompt_extensions = [] # Extra text for architect prompt + + # MCP Manager + self.mcp_manager = MCPClientManager(self.config) # Long-term memory self.memory_path = os.path.join(self.config.defaultdir, "ai_memory.md") @@ -677,7 +681,7 @@ class ai: self.console.print("[pass]✓ Trust Mode Enabled. All future commands in this session will execute without confirmation.[/pass]") elif user_resp_lower in ['y', 'yes']: self.console.print("[pass]✓ Executing...[/pass]") - elif user_resp_lower in ['n', 'no', '']: + elif user_resp_lower in ['n', 'no', '', 'cancel']: self.console.print("[fail]✗ Execution rejected by user.[/fail]") return "Error: User rejected execution." else: @@ -773,6 +777,10 @@ class ai: cmd_str = cmds[0] if cmds else "" status.update(f"[ai_status]Engineer: [CMD] {cmd_str}") elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}") + elif fn.startswith("mcp_"): + server = fn.split("__")[0].replace("mcp_", "") + tool = fn.split("__")[1] if "__" in fn else fn + status.update(f"[ai_status]Engineer: [MCP:{server}] {tool}") elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args)) if debug: @@ -781,6 +789,8 @@ class ai: if fn == "list_nodes": obs = self.list_nodes_tool(**args) elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status) elif fn == "get_node_info": obs = self.get_node_info_tool(**args) + elif fn.startswith("mcp_"): + obs = run_ai_async(self.mcp_manager.call_tool(fn, args)).result(timeout=60) elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args) else: obs = f"Error: Unknown tool '{fn}'." @@ -801,14 +811,22 @@ class ai: except Exception as e: return f"Engineer failed: {str(e)}", usage - def _get_engineer_tools(self): + def _get_engineer_tools(self, os_filter: str = None): """Define tools available to the Engineer.""" base_tools = [ - {"type": "function", "function": {"name": "list_nodes", "description": "Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}}, - {"type": "function", "function": {"name": "run_commands", "description": "Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}}, - {"type": "function", "function": {"name": "get_node_info", "description": "Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}} + {"type": "function", "function": {"name": "list_nodes", "description": "[Universal Platform] Lists available nodes in the inventory.", "parameters": {"type": "object", "properties": {"filter_pattern": {"type": "string", "description": "Regex to filter nodes (e.g. '.*', 'border.*')."}}}}}, + {"type": "function", "function": {"name": "run_commands", "description": "[Universal Platform] Runs one or more commands on matched nodes. MANDATORY: You MUST call 'list_nodes' first to verify the target list.", "parameters": {"type": "object", "properties": {"nodes_filter": {"type": "string", "description": "Exact node name or verified filter pattern."}, "commands": {"type": "array", "items": {"type": "string"}, "description": "List of commands (e.g. ['show ip route', 'show int desc'])."}}, "required": ["nodes_filter", "commands"]}}}, + {"type": "function", "function": {"name": "get_node_info", "description": "[Universal Platform] Gets full metadata for a specific node.", "parameters": {"type": "object", "properties": {"node_name": {"type": "string"}}, "required": ["node_name"]}}} ] + # Add dynamic tools from MCP + try: + mcp_tools = run_ai_async(self.mcp_manager.get_tools_for_llm(os_filter=os_filter)).result(timeout=10) + base_tools.extend(mcp_tools) + except Exception as e: + # Silently fail for LLM tools + pass + if self.architect_key: base_tools.extend([ {"type": "function", "function": {"name": "consult_architect", "description": "Ask the Strategic Reasoning Engine for advice on complex design, architecture, or troubleshooting decisions. You remain in control and will present the response to the user. Use this for: configuration planning, design validation, complex troubleshooting.", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "Strategic question or decision needed."}, "technical_summary": {"type": "string", "description": "Technical findings and context gathered so far."}}, "required": ["question", "technical_summary"]}}}, @@ -1202,6 +1220,8 @@ class ai: elif fn == "run_commands": obs = self.run_commands_tool(**args, status=status) elif fn == "get_node_info": obs = self.get_node_info_tool(**args) elif fn == "manage_memory_tool": obs = self.manage_memory_tool(**args) + elif fn.startswith("mcp_"): + obs = run_ai_async(self.mcp_manager.call_tool(fn, args)).result(timeout=60) elif fn in self.external_tool_handlers: obs = self.external_tool_handlers[fn](self, **args) else: obs = f"Error: {fn} unknown." @@ -1268,146 +1288,6 @@ class ai: "streamed": streamed_response } - @MethodHook - def ask_copilot(self, terminal_buffer, user_question, node_info=None, chunk_callback=None): - """Single-shot copilot for augmented terminal sessions. - - Args: - terminal_buffer: Sanitized terminal screen content (últimas N líneas). - user_question: Pregunta del usuario sobre la sesión activa. - node_info: Optional dict con metadata del nodo (os, name, etc.) - chunk_callback: Optional callable for streaming the guide. - - Returns: - dict: {commands: list[str], guide: str, risk_level: str, error: str|None} - """ - import json - import re - - node_info = node_info or {} - os_info = node_info.get("os", "unknown") - node_name = node_info.get("name", "unknown") - - # Load vendor-specific command reference if available - vendor_reference = "" - if os_info and os_info != "unknown": - try: - os_filename = os_info.lower().replace(" ", "_") - ref_path = os.path.join(self.config.defaultdir, "ai_references", f"{os_filename}.md") - if os.path.exists(ref_path): - with open(ref_path, "r") as f: - vendor_reference = f.read().strip() - except Exception: - pass - - system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session. -Rules: -1. Answer the user's question directly based on the Terminal Context. -2. If the user asks you to analyze, parse, or extract data from the Terminal Context, DO IT directly in the section (you can use markdown tables or lists). Do NOT just give them a command to do it themselves. -3. If the user wants to execute an action, provide the required CLI commands inside a block, one command per line. If no commands are needed, leave it empty or omit the block. -4. ULTRA-CONCISE. Keep your guide to the point. -5. You MUST output your response in the following strict format: - -Your brief tactical guide in markdown. 3-4 sentences max. - - -command 1 -command 2 - - -low, high, or destructive - -6. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops. - -Terminal Context: -{terminal_buffer} - -Device OS: {os_info} -Node: {node_name}""" - - if vendor_reference: - system_prompt += f"\n\nVendor Command Reference:\n{vendor_reference}" - - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_question} - ] - - try: - response = completion( - model=self.engineer_model, - messages=messages, - api_key=self.engineer_key, - stream=True - ) - - full_content = "" - streamed_guide = "" - - for chunk in response: - delta = chunk.choices[0].delta - if hasattr(delta, 'content') and delta.content: - full_content += delta.content - - if chunk_callback: - start_idx = full_content.find("") - if start_idx != -1: - after_start = full_content[start_idx + 7:] - end_idx = after_start.find("") - - if end_idx != -1: - current_guide = after_start[:end_idx] - else: - current_guide = after_start - if current_guide.endswith("<"): current_guide = current_guide[:-1] - elif current_guide.endswith("(.*?)", full_content, re.DOTALL) - if guide_match: - guide = guide_match.group(1).strip() - - cmd_match = re.search(r"(.*?)", full_content, re.DOTALL) - if cmd_match: - cmds_raw = cmd_match.group(1).strip() - if cmds_raw: - commands = [c.strip() for c in cmds_raw.split('\n') if c.strip()] - - risk_match = re.search(r"(.*?)", full_content, re.DOTALL) - if risk_match: - risk_level = risk_match.group(1).strip().lower() - - if not guide and full_content and not ("" in full_content): - guide = full_content.strip() - - return { - "commands": commands, - "guide": guide, - "risk_level": risk_level, - "error": None - } - - except Exception as e: - return { - "commands": [], - "guide": "", - "risk_level": "low", - "error": str(e) - } - @MethodHook async def aask_copilot(self, terminal_buffer, user_question, node_info=None, chunk_callback=None): import json @@ -1463,49 +1343,136 @@ Node: {node_name}""" if vendor_reference: system_prompt += f"\n\nVendor Command Reference:\n{vendor_reference}" + # Fetch MCP tools for the current OS + mcp_tools = [] + try: + mcp_tools = await self.mcp_manager.get_tools_for_llm(os_filter=os_info) + except Exception: + pass + + if mcp_tools: + system_prompt += f"\n\nAvailable MCP Tools: {', '.join([t['function']['name'] for t in mcp_tools])}" + system_prompt += "\nUse these tools to validate syntax or find exact commands if needed before providing the final guide." + messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_question} ] + iteration = 0 + max_iterations = 5 # Allow up to 5 iterations for tool usage + try: - response = await acompletion( - model=self.engineer_model, - messages=messages, - api_key=self.engineer_key, - stream=True - ) - - full_content = "" - streamed_guide = "" - - async for chunk in response: - delta = chunk.choices[0].delta - if hasattr(delta, 'content') and delta.content: - full_content += delta.content + while iteration < max_iterations: + iteration += 1 + response = await acompletion( + model=self.engineer_model, + messages=messages, + tools=mcp_tools if mcp_tools else None, + api_key=self.engineer_key, + stream=True + ) + + full_content = "" + streamed_guide = "" + tool_calls = [] + + async for chunk in response: + delta = chunk.choices[0].delta - if chunk_callback: - start_idx = full_content.find("") - if start_idx != -1: - after_start = full_content[start_idx + 7:] - end_idx = after_start.find("") - - if end_idx != -1: - current_guide = after_start[:end_idx] + # Accumulate tool calls + if hasattr(delta, 'tool_calls') and delta.tool_calls: + for tc in delta.tool_calls: + idx = tc.index + if idx >= len(tool_calls): + tool_calls.append({"id": tc.id, "type": "function", "function": {"name": tc.function.name or "", "arguments": tc.function.arguments or ""}}) else: - current_guide = after_start - if current_guide.endswith("<"): current_guide = current_guide[:-1] - elif current_guide.endswith("") + if start_idx != -1: + after_start = full_content[start_idx + 7:] + end_idx = after_start.find("") + + if end_idx != -1: + current_guide = after_start[:end_idx] + else: + current_guide = after_start + if current_guide.endswith("<"): current_guide = current_guide[:-1] + elif current_guide.endswith("= max_iterations: + messages.append({"role": "user", "content": "Tool limit reached. Provide your final tactical guide now based on the findings."}) + response = await acompletion( + model=self.engineer_model, + messages=messages, + tools=None, + api_key=self.engineer_key, + stream=True + ) + + full_content = "" + streamed_guide = "" + async for chunk in response: + delta = chunk.choices[0].delta + if hasattr(delta, 'content') and delta.content: + full_content += delta.content + if chunk_callback: + start_idx = full_content.find("") + if start_idx != -1: + after_start = full_content[start_idx + 7:] + end_idx = after_start.find("") + if end_idx != -1: + current_guide = after_start[:end_idx] + else: + current_guide = after_start + if current_guide.endswith("<"): current_guide = current_guide[:-1] + elif current_guide.endswith(" [os_filter]") + return + name, url = mcp_args[1], mcp_args[2] + os_filter = mcp_args[3] if len(mcp_args) > 3 else None + try: + self.app.services.ai.configure_mcp(name, url=url, auto_load_on_os=os_filter) + printer.success(f"MCP server '{name}' added/updated.") + except Exception as e: + printer.error(str(e)) + return + + elif action == "remove": + if len(mcp_args) < 2: + printer.error("Usage: connpy ai --mcp remove ") + return + name = mcp_args[1] + try: + self.app.services.ai.configure_mcp(name, remove=True) + printer.success(f"MCP server '{name}' removed.") + except Exception as e: + printer.error(str(e)) + return + + elif action in ["enable", "disable"]: + if len(mcp_args) < 2: + printer.error(f"Usage: connpy ai --mcp {action} ") + return + name = mcp_args[1] + enabled = (action == "enable") + try: + self.app.services.ai.configure_mcp(name, enabled=enabled) + printer.success(f"MCP server '{name}' {'enabled' if enabled else 'disabled'}.") + except Exception as e: + printer.error(str(e)) + return + + else: + printer.error(f"Unknown MCP action: {action}") + printer.info("Available actions: list, add, remove, enable, disable") + return + + # 2. Interactive Wizard Mode (if no arguments provided) + # Import forms dynamically to avoid circular dependencies if any + if not hasattr(self.app, "cli_forms"): + from .forms import Forms + self.app.cli_forms = Forms(self.app) + + settings = self.app.services.config_svc.get_settings() + mcp_servers = settings.get("ai", {}).get("mcp_servers", {}) + + result = self.app.cli_forms.mcp_wizard(mcp_servers) + if not result: + return + + action = result["action"] + try: + if action == "list": + # Recursive call to the non-interactive list logic + args.mcp = ["list"] + return self.configure_mcp(args) + + elif action == "add": + self.app.services.ai.configure_mcp( + result["name"], + url=result["url"], + enabled=result["enabled"], + auto_load_on_os=result["os"] + ) + printer.success(f"MCP server '{result['name']}' saved.") + + elif action == "update": # Used for toggle + self.app.services.ai.configure_mcp( + result["name"], + enabled=result["enabled"] + ) + printer.success(f"MCP server '{result['name']}' updated.") + + elif action == "remove": + self.app.services.ai.configure_mcp(result["name"], remove=True) + printer.success(f"MCP server '{result['name']}' removed.") + + except Exception as e: + printer.error(str(e)) diff --git a/connpy/cli/forms.py b/connpy/cli/forms.py index 97542f0..2f49150 100644 --- a/connpy/cli/forms.py +++ b/connpy/cli/forms.py @@ -197,3 +197,84 @@ class Forms: answer["tags"] = ast.literal_eval(answer["tags"]) return answer + + def mcp_wizard(self, mcp_servers): + """Interactive wizard to manage MCP servers.""" + from .helpers import theme + + while True: + options = [ + ("List Configured Servers", "list"), + ("Add/Update Server", "add"), + ("Enable/Disable Server", "toggle"), + ("Remove Server", "remove"), + ("Back", "exit") + ] + + questions = [ + inquirer.List("action", message="MCP Configuration", choices=options) + ] + + answers = inquirer.prompt(questions, theme=theme) + if not answers or answers["action"] == "exit": + return None + + action = answers["action"] + + if action == "list": + if not mcp_servers: + print("\nNo MCP servers configured.\n") + else: + return {"action": "list"} + + elif action == "add": + questions = [ + inquirer.Text("name", message="Server Name (identifier)"), + inquirer.Text("url", message="SSE URL (e.g., http://localhost:8000/sse)"), + inquirer.Confirm("enabled", message="Enabled?", default=True), + inquirer.Text("auto_load_os", message="Auto-load on specific OS (blank for any)") + ] + answers = inquirer.prompt(questions, theme=theme) + if answers: + return { + "action": "add", + "name": answers["name"], + "url": answers["url"], + "enabled": answers["enabled"], + "os": answers["auto_load_os"] + } + + elif action == "toggle": + if not mcp_servers: + print("\nNo servers to toggle.\n") + continue + + choices = [] + for name, cfg in mcp_servers.items(): + status = "[Enabled]" if cfg.get("enabled", True) else "[Disabled]" + choices.append((f"{name} {status}", name)) + + questions = [ + inquirer.List("name", message="Select server to toggle", choices=choices + [("Cancel", None)]) + ] + answers = inquirer.prompt(questions, theme=theme) + if answers and answers["name"]: + current = mcp_servers[answers["name"]].get("enabled", True) + return { + "action": "update", + "name": answers["name"], + "enabled": not current + } + + elif action == "remove": + if not mcp_servers: + print("\nNo servers to remove.\n") + continue + + questions = [ + inquirer.List("name", message="Select server to remove", choices=list(mcp_servers.keys()) + ["Cancel"]) + ] + answers = inquirer.prompt(questions, theme=theme) + if answers and answers["name"] != "Cancel": + return {"action": "remove", "name": answers["name"]} + return None diff --git a/connpy/cli/terminal_ui.py b/connpy/cli/terminal_ui.py new file mode 100644 index 0000000..d02ec02 --- /dev/null +++ b/connpy/cli/terminal_ui.py @@ -0,0 +1,307 @@ +import os +import re +import sys +import asyncio +import fcntl +import termios +import tty +from typing import Any, Dict, List, Optional, Callable +from textwrap import dedent + +from rich.console import Console +from rich.panel import Panel +from rich.markdown import Markdown +from rich.live import Live +from prompt_toolkit import PromptSession +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.history import InMemoryHistory + +from ..printer import connpy_theme + +def log_cleaner(data: str) -> str: + """ + Stateless version of _logclean to remove ANSI sequences and process cursor movements. + """ + if not data: + return "" + + lines = data.split('\n') + cleaned_lines = [] + + # Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks + token_re = re.compile(r'(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)') + + for line in lines: + buffer = [] + cursor = 0 + + for token in token_re.findall(line): + if token == '\r': + cursor = 0 + elif token in ('\b', '\x7f'): + if cursor > 0: + cursor -= 1 + elif token == '\x1B[D': # Left Arrow + if cursor > 0: + cursor -= 1 + elif token == '\x1B[C': # Right Arrow + if cursor < len(buffer): + cursor += 1 + elif token == '\x1B[K': # Clear to end of line + buffer = buffer[:cursor] + elif token.startswith('\x1B'): + continue + elif len(token) == 1 and ord(token) < 32: + continue + else: + for char in token: + if cursor == len(buffer): + buffer.append(char) + else: + buffer[cursor] = char + cursor += 1 + cleaned_lines.append("".join(buffer)) + + return "\n".join(cleaned_lines).replace('\n\n', '\n').strip() + +class CopilotInterface: + def __init__(self, config, history=None): + self.config = config + self.console = Console(theme=connpy_theme) + self.history = history or InMemoryHistory() + self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2 + + def extract_blocks(self, raw_bytes: bytes, cmd_byte_positions: List[tuple], node_info: dict) -> List[tuple]: + """Identifies command blocks in the terminal history.""" + blocks = [] + if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes): + return blocks + + default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' + device_prompt = node_info.get("prompt", default_prompt) if isinstance(node_info, dict) else default_prompt + prompt_re_str = re.sub(r'(? result_dict + """ + from rich.rule import Rule + + try: + # Prepare UI state + buffer = log_cleaner(raw_bytes.decode(errors='replace')) + blocks = self.extract_blocks(raw_bytes, cmd_byte_positions, node_info) + last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)" + blocks.append((len(raw_bytes), last_line[:80])) + + state = { + 'context_cmd': 1, + 'total_cmds': len(blocks), + 'total_lines': len(buffer.split('\n')), + 'context_lines': min(50, len(buffer.split('\n'))), + 'context_mode': self.mode_range, + 'cancelled': False + } + + # 1. Visual Separation + self.console.print("") # Salto de línea real + self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan")) + self.console.print(Panel( + "[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n" + "Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]", + border_style="cyan" + )) + self.console.print("\n") # Pequeño espacio antes del prompt del copilot + + bindings = KeyBindings() + @bindings.add('c-up') + def _(event): + if state['context_mode'] == self.mode_lines: + state['context_lines'] = min(state['context_lines'] + 50, state['total_lines']) + else: + state['context_cmd'] = min(state['context_cmd'] + 1, state['total_cmds']) + event.app.invalidate() + @bindings.add('c-down') + def _(event): + if state['context_mode'] == self.mode_lines: + state['context_lines'] = max(state['context_lines'] - 50, min(50, state['total_lines'])) + else: + state['context_cmd'] = max(state['context_cmd'] - 1, 1) + event.app.invalidate() + @bindings.add('tab') + def _(event): + state['context_mode'] = (state['context_mode'] + 1) % 3 + event.app.invalidate() + @bindings.add('escape', eager=True) + @bindings.add('c-c') + def _(event): + state['cancelled'] = True + event.app.exit(result='') + + def get_active_buffer(): + if state['context_mode'] == self.mode_lines: + return '\n'.join(buffer.split('\n')[-state['context_lines']:]) + idx = max(0, state['total_cmds'] - state['context_cmd']) + start, preview = blocks[idx] + if state['context_mode'] == self.mode_single and idx + 1 < state['total_cmds']: + end = blocks[idx + 1][0] + active_raw = raw_bytes[start:end] + else: + active_raw = raw_bytes[start:] + return preview + "\n" + log_cleaner(active_raw.decode(errors='replace')) + + def get_prompt_text(): + if state['context_mode'] == self.mode_lines: + return HTML(f"Ask [Ctx: {state['context_lines']}/{state['total_lines']}L]: ") + active = get_active_buffer() + lines_count = len(active.split('\n')) + mode_str = {self.mode_range: "Range", self.mode_single: "Cmd"}[state['context_mode']] + return HTML(f"Ask [{mode_str} {state['context_cmd']} ~{lines_count}L]: ") + + def get_toolbar(): + m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']] + if state['context_mode'] == self.mode_lines: + return HTML(f"\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]") + idx = max(0, state['total_cmds'] - state['context_cmd']) + return HTML(f"\u25b6 {blocks[idx][1]} [Tab: {m_label}]") + + # 2. Ask question + session = PromptSession(history=self.history) + try: + # Usamos un try/finally interno para asegurar que si algo falla en prompt_async, + # no nos quedemos con la terminal en un estado extraño. + question = await session.prompt_async( + get_prompt_text, + key_bindings=bindings, + bottom_toolbar=get_toolbar + ) + except (KeyboardInterrupt, EOFError): + state['cancelled'] = True + question = "" + + if state['cancelled'] or not question.strip() or question.strip().lower() == 'cancel': + return "cancel", None, None + + # Enrich question + past = self.history.get_strings() + if len(past) > 1: + history_text = "\n".join(f"- {q}" for q in past[-6:-1]) + question = f"Previous questions:\n{history_text}\n\nCurrent Question:\n{question}" + + # 3. AI Execution + active_buffer = get_active_buffer() + live_text = "Thinking..." + panel = Panel(live_text, title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan") + + def on_chunk(text): + nonlocal live_text + if live_text == "Thinking...": live_text = "" + live_text += text + + with Live(panel, console=self.console, refresh_per_second=10) as live: + def update_live(t): + live.update(Panel(Markdown(t), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) + + wrapped_chunk = lambda t: (on_chunk(t), update_live(live_text)) + + # Check for interruption during AI call + ai_task = asyncio.create_task(on_ai_call(active_buffer, question, wrapped_chunk)) + + try: + while not ai_task.done(): + await asyncio.sleep(0.05) + result = await ai_task + except asyncio.CancelledError: + return "cancel", None, None + + if not result or result.get("error"): + if result and result.get("error"): self.console.print(f"[red]Error: {result['error']}[/red]") + return "cancel", None, None + + # 4. Handle result + if live_text == "Thinking..." and result.get("guide"): + self.console.print(Panel(Markdown(result["guide"]), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) + + commands = result.get("commands", []) + if not commands: + return "cancel", None, None + + risk = result.get("risk_level", "low") + style = {"low": "green", "high": "yellow", "destructive": "red"}.get(risk, "green") + cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands)) + self.console.print(Panel(cmd_text, title=f"[bold {style}]Suggested Commands [{risk.upper()}][/bold {style}]", border_style=style)) + + confirm_session = PromptSession() + c_bindings = KeyBindings() + @c_bindings.add('escape', eager=True) + @c_bindings.add('c-c') + def _(ev): ev.app.exit(result='n') + + try: + action = await confirm_session.prompt_async(HTML(f"Send? (y/n/e/number) [n]: "), key_bindings=c_bindings) + except (KeyboardInterrupt, EOFError): + action = "n" + + action_l = (action or "n").lower().strip() + if action_l in ('y', 'yes', 'all'): + return "send_all", commands, None + elif action_l.startswith('e'): + target = "\n".join(commands) + e_bindings = KeyBindings() + @e_bindings.add('c-j') + def _(ev): ev.app.exit(result=ev.app.current_buffer.text) + @e_bindings.add('escape', 'enter') + def _(ev): ev.app.exit(result=ev.app.current_buffer.text) + @e_bindings.add('escape') + def _(ev): ev.app.exit(result='') + + edited = await confirm_session.prompt_async( + HTML("Edit (Ctrl+Enter or Esc+Enter to submit):\n"), + default=target, multiline=True, key_bindings=e_bindings + ) + if edited.strip(): + # Split by lines to ensure core.py applies delay between each command + lines = [l.strip() for l in edited.split('\n') if l.strip()] + return "custom", None, lines + return "cancel", None, None + + return "cancel", None, None + + finally: + self.console.print("[dim]Returning to session...[/dim]") + diff --git a/connpy/connapp.py b/connpy/connapp.py index d41a990..25793be 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -284,6 +284,7 @@ class connapp: aiparser.add_argument("--session", nargs=1, help="Resume a specific AI session by ID") aiparser.add_argument("--resume", action="store_true", help="Resume the most recent AI session") aiparser.add_argument("--delete", "--delete-session", dest="delete_session", nargs=1, help="Delete an AI session by ID") + aiparser.add_argument("--mcp", nargs='*', metavar=('ACTION', 'NAME'), help="Manage MCP servers. Actions: list, add, remove, enable, disable. Leave empty for interactive wizard.") aiparser.set_defaults(func=self._ai.dispatch) #RUNPARSER runparser = subparsers.add_parser("run", help="Run scripts or commands on nodes", description="Run scripts or commands on nodes", formatter_class=RichHelpFormatter) diff --git a/connpy/core.py b/connpy/core.py index 68e8f21..d18e43e 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -257,61 +257,29 @@ class node: @MethodHook def _logclean(self, logfile, var = False): - # Remove special ascii characters and process terminal cursor movements to clean logs. + """Remove special ascii characters and process terminal cursor movements to clean logs.""" + from .cli.terminal_ui import log_cleaner + if var == False: - t = open(logfile, "r").read() + try: + with open(logfile, "r") as f: + t = f.read() + except: + return else: t = logfile - lines = t.split('\n') - cleaned_lines = [] - - # Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks - token_re = re.compile(r'(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)') - - for line in lines: - buffer = [] - cursor = 0 - - for token in token_re.findall(line): - if token == '\r': - cursor = 0 - elif token in ('\b', '\x7f'): - if cursor > 0: - cursor -= 1 - elif token == '\x1B[D': # Left Arrow - if cursor > 0: - cursor -= 1 - elif token == '\x1B[C': # Right Arrow - if cursor < len(buffer): - cursor += 1 - elif token == '\x1B[K': # Clear to end of line - buffer = buffer[:cursor] - elif token.startswith('\x1B'): - # Ignore other ANSI sequences (colors, etc) - continue - elif len(token) == 1 and ord(token) < 32: - # Ignore other non-printable control chars - continue - else: - # Regular printable text - for char in token: - if cursor == len(buffer): - buffer.append(char) - else: - buffer[cursor] = char - cursor += 1 - cleaned_lines.append("".join(buffer)) - - t = "\n".join(cleaned_lines).replace('\n\n', '\n').strip() + result = log_cleaner(t) if var == False: - d = open(logfile, "w") - d.write(t) - d.close() + try: + with open(logfile, "w") as f: + f.write(result) + except: + pass return else: - return t + return result @MethodHook def _savelog(self): @@ -447,20 +415,22 @@ class node: # Copilot interception if copilot_handler and b'\x00' in data: - # Extract clean buffer from session log - buffer = "" - if hasattr(self, 'mylog'): - raw = self.mylog.getvalue().decode(errors='replace') - # Move heavy log cleaning to a thread - buffer = await asyncio.to_thread(self._logclean, raw, True) - - # Build node info from available metadata - node_info = {"name": getattr(self, 'unique', 'unknown'), "host": getattr(self, 'host', 'unknown')} + # Build node info from available metadata and ensure values are strings (not bytes) + def to_str(val): + if isinstance(val, bytes): + return val.decode(errors='replace') + return str(val) if val is not None else "unknown" + + node_info = { + "name": to_str(getattr(self, 'unique', 'unknown')), + "host": to_str(getattr(self, 'host', 'unknown')) + } if isinstance(getattr(self, 'tags', None), dict): - node_info["os"] = self.tags.get("os", "unknown") + node_info["os"] = to_str(self.tags.get("os", "unknown")) + node_info["prompt"] = to_str(self.tags.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')) # Invoke copilot (async callback handles UI) - await copilot_handler(buffer, node_info, local_stream, child_fd, cmd_byte_positions) + await copilot_handler(self.mylog.getvalue(), node_info, local_stream, child_fd, cmd_byte_positions) continue # Remove any stray \x00 bytes and forward normally @@ -629,468 +599,82 @@ class node: def _build_local_copilot_handler(self): """Build copilot handler for local CLI sessions using rich for rendering.""" config = getattr(self, 'config', None) if hasattr(self, 'config') else None - if not config: - return None - - # Persistent history across copilot invocations within the same session - from prompt_toolkit.history import InMemoryHistory - copilot_history = InMemoryHistory() - - async def handler(buffer, node_info, stream, child_fd, cmd_byte_positions=None): - import termios, tty - import asyncio - import os - import sys - import fcntl - - flags = 0 - stdin_fd = sys.stdin.fileno() - - try: - # Disable LocalStream reader so it doesn't steal keystrokes from Prompt - loop = asyncio.get_running_loop() - loop.remove_reader(sys.stdin.fileno()) - - # 1. Salir de raw mode para poder usar input() y rich - stdin_fd = sys.stdin.fileno() - - # Get true original settings saved before entering raw mode - original_settings = getattr(stream, 'original_tty_settings', None) - if original_settings: - import copy - new_settings = copy.deepcopy(original_settings) - new_settings[3] = new_settings[3] & ~termios.ECHOCTL - # CRITICAL: Prevent OS from translating Ctrl+C into SIGINT - # This prevents the asyncio event loop from crashing when user hits Ctrl+C - new_settings[3] = new_settings[3] & ~termios.ISIG - termios.tcsetattr(stdin_fd, termios.TCSADRAIN, new_settings) - - # Remove O_NONBLOCK from stdin so Prompt.ask() works - import fcntl - flags = fcntl.fcntl(stdin_fd, fcntl.F_GETFL) - fcntl.fcntl(stdin_fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) - - # Force a carriage return so the UI doesn't start mid-line - sys.stdout.write('\r\n') - sys.stdout.flush() - - from rich.console import Console - from rich.panel import Panel - from rich.markdown import Markdown - from rich.prompt import Prompt - from .printer import connpy_theme + return self._copilot_handler(config) + def _copilot_handler(self, config): + """Unified copilot handler for local session.""" + from .cli.terminal_ui import CopilotInterface + from .services.ai_service import AIService + import asyncio + import os + + async def handler(buffer, node_info, stream, child_fd, cmd_byte_positions=None): + try: + interface = CopilotInterface(config, history=getattr(stream, 'copilot_history', None)) + # Save history back to stream for persistence in current session + stream.copilot_history = interface.history - console = Console(theme=connpy_theme) - console.print("\n") - console.print(Panel( - "[bold cyan]AI Terminal Copilot[/bold cyan]\n" - "[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n" - "Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]", - border_style="cyan" - )) + ai_service = AIService(config) - # 2. Capturar pregunta del usuario - cancelled = [False] + async def on_ai_call(active_buffer, question, chunk_callback): + return await ai_service.aask_copilot( + active_buffer, + question, + node_info=node_info, + chunk_callback=chunk_callback + ) + + # Get raw bytes from BytesIO + raw_bytes = self.mylog.getvalue() - from prompt_toolkit import PromptSession - from prompt_toolkit.key_binding import KeyBindings - from prompt_toolkit.formatted_text import HTML - - bindings = KeyBindings() - - # Command blocks logic - raw_bytes = self.mylog.getvalue() if hasattr(self, 'mylog') else b'' - blocks = [] - - if cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes: - import re - # Extract the prompt regex for validation - default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' - device_prompt = node_info.get("prompt", default_prompt) if isinstance(node_info, dict) else default_prompt - # Remove unescaped $ end-anchors so we can match the prompt within the line - prompt_re_str = re.sub(r'(?= total_lines: - context_lines[0] = min(50, total_lines) - else: - context_lines[0] = min(context_lines[0] + 50, total_lines) - else: - if context_cmd[0] < total_cmds: - context_cmd[0] += 1 - else: - context_cmd[0] = 1 - event.app.invalidate() - - @bindings.add('c-down') - def _(event): - if context_mode[0] == MODE_LINES: - if context_lines[0] <= min(50, total_lines): - context_lines[0] = total_lines - else: - context_lines[0] = max(context_lines[0] - 50, min(50, total_lines)) - else: - if context_cmd[0] > 1: - context_cmd[0] -= 1 - else: - context_cmd[0] = total_cmds - event.app.invalidate() - - @bindings.add('tab') - def _(event): - context_mode[0] = (context_mode[0] + 1) % 3 - event.app.invalidate() - - @bindings.add('escape', eager=True) - def _(event): - cancelled[0] = True - event.app.exit(result='') - - def get_current_block(): - idx = max(0, total_cmds - context_cmd[0]) - return idx, blocks[idx] - - def get_active_buffer(): - """Build the active buffer for the current selection mode.""" - if context_mode[0] == MODE_LINES: - buffer_lines = buffer.split('\n') - return '\n'.join(buffer_lines[-context_lines[0]:]) - - idx, (start, preview) = get_current_block() - if context_mode[0] == MODE_SINGLE and idx + 1 < total_cmds: - end = blocks[idx + 1][0] - active_raw = raw_bytes[start:end] - else: - active_raw = raw_bytes[start:] - return preview + "\n" + self._logclean(active_raw.decode(errors='replace'), var=True) - - def get_prompt_text(): - if context_mode[0] == MODE_LINES: - return HTML(f"Ask [Ctx: {context_lines[0]}/{total_lines}L]: ") - - lines_count = len(get_active_buffer().split('\n')) - if context_mode[0] == MODE_SINGLE: - return HTML(f"Ask [Cmd {context_cmd[0]} ~{lines_count}L]: ") - else: - return HTML(f"Ask [Cmd {context_cmd[0]}\u2192END ~{lines_count}L]: ") - - def get_toolbar(): - mode_labels = {MODE_RANGE: "RANGE", MODE_SINGLE: "SINGLE", MODE_LINES: "LINES"} - mode_label = mode_labels[context_mode[0]] - if context_mode[0] == MODE_LINES: - return HTML(f"\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {mode_label}]") - _, (_, preview) = get_current_block() - return HTML(f"\u25b6 {preview} [Tab: {mode_label}]") - - import threading - def preload_ai_deps(): - try: - import litellm - except Exception: - pass - threading.Thread(target=preload_ai_deps, daemon=True).start() + # Detener el lector de la terminal para que prompt_toolkit (en run_session) + # tenga control exclusivo del stdin sin interferencias de LocalStream. + if hasattr(stream, 'stop_reading'): + stream.stop_reading() + elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'): + # Fallback si no tiene el método (en LocalStream) + stream._loop.remove_reader(stream.stdin_fd) try: - session = PromptSession(history=copilot_history) - question = await session.prompt_async( - get_prompt_text, - key_bindings=bindings, - bottom_toolbar=get_toolbar - ) - except KeyboardInterrupt: - question = "" - - if cancelled[0] or not question.strip() or question.strip() == "CANCEL": - console.print("\n[dim]Copilot cancelled.[/dim]") - os.write(child_fd, b'\x15\r') - return - - active_buffer = get_active_buffer() - - # 3. Llamar al AI con spinner - from .services.ai_service import AIService - service = AIService(config) - - past_questions = copilot_history.get_strings() - if len(past_questions) > 1: - # Limit history to last 5 questions to save tokens, excluding current - recent_history = past_questions[-6:-1] - history_text = "\n".join(f"- {q}" for q in recent_history) - enriched_question = f"Previous questions in this session:\n{history_text}\n\nCurrent Question:\n{question}" - else: - enriched_question = question - - from rich.live import Live - - live_text = "Thinking..." - panel = Panel(live_text, title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan") - - def on_chunk(text): - nonlocal live_text - if live_text == "Thinking...": - live_text = "" - live_text += text - try: - # Use call_soon_threadsafe if possible, but rich Live is thread-safe enough - loop.call_soon_threadsafe( - lambda: live.update(Panel(Markdown(live_text), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) + with copilot_terminal_mode(): + action, commands, custom_cmd = await interface.run_session( + raw_bytes=raw_bytes, + cmd_byte_positions=cmd_byte_positions, + node_info=node_info, + on_ai_call=on_ai_call ) - except Exception: - live.update(Panel(Markdown(live_text), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) + finally: + # Reiniciar el lector de la terminal para volver al modo interactivo SSH/Telnet + if hasattr(stream, 'start_reading'): + stream.start_reading() + elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'): + stream._loop.add_reader(stream.stdin_fd, stream._read_ready) - with copilot_terminal_mode(), Live(panel, console=console, refresh_per_second=10) as live: - # Launch the AI call as a task - ai_task = asyncio.create_task(service.aask_copilot(active_buffer, enriched_question, node_info, chunk_callback=on_chunk)) + if action in ("send_all", "custom"): + cmds_to_send = commands if action == "send_all" else custom_cmd - # Make stdin non-blocking - import fcntl - flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK) - - cancelled = False - result = None - - try: - while not ai_task.done(): - try: - key = os.read(sys.stdin.fileno(), 1024) - if b'\x03' in key or b'\x1b' in key: - cancelled = True - ai_task.cancel() - msg = "Ctrl+C" if b'\x03' in key else "Esc" - console.print(f"\n[dim]Copilot cancelled via {msg}.[/dim]") - break - except OSError: - pass - # Yield to event loop to allow AI task to progress - await asyncio.sleep(0.05) - - if not cancelled: - result = ai_task.result() - except asyncio.CancelledError: - cancelled = True - console.print("\n[dim]Copilot cancelled.[/dim]") - except KeyboardInterrupt: - cancelled = True - ai_task.cancel() - console.print("\n[dim]Copilot cancelled via Ctrl+C.[/dim]") - finally: - # Restore stdin flags - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags) - - if cancelled or not result: - os.write(child_fd, b'\x15\r') - return - - if result.get("error"): - console.print(f"[red]Error: {result['error']}[/red]") - return - - # If nothing was streamed (fallback), or to ensure final state - if live_text == "Thinking..." and result.get("guide"): - console.print(Panel( - Markdown(result["guide"]), - title="[bold cyan]Copilot Guide[/bold cyan]", - border_style="cyan" - )) - - commands = result.get("commands", []) - risk = result.get("risk_level", "low") - risk_style = {"low": "green", "high": "yellow", "destructive": "red"}.get(risk, "green") - - if commands: - cmd_text = "\n".join(f" {i+1}. {cmd}" for i, cmd in enumerate(commands)) - console.print(Panel( - cmd_text, - title=f"[bold {risk_style}]Suggested Commands [{risk.upper()}][/bold {risk_style}]", - border_style=risk_style - )) - - # 5. Preguntar si inyectar (usando prompt_toolkit) - confirm_session = PromptSession() - confirm_bindings = KeyBindings() - - @confirm_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='n') - - pt_color = "ansi" + risk_style - try: - action = await confirm_session.prompt_async( - HTML(f"<{pt_color}>Send commands? (y/n/e/number/range) [n]: "), - key_bindings=confirm_bindings - ) - except KeyboardInterrupt: - action = "n" - - if not action.strip(): - action = "n" - - console.print("[dim]Returning to session...[/dim]\n") - - action_l = action.lower().strip() - if action_l in ('y', 'yes', 'all'): - os.write(child_fd, b'\x15') # Ctrl+U to clear line + if cmds_to_send: + os.write(child_fd, b'\x15') # Ctrl+U await asyncio.sleep(0.1) - for cmd in commands: - if cmd_byte_positions is not None and hasattr(self, 'mylog'): + + # Prepend screen length command to avoid pagination + if "screen_length_command" in self.tags: + cmds_to_send.insert(0, self.tags["screen_length_command"]) + + for cmd in cmds_to_send: + if cmd_byte_positions is not None: cmd_byte_positions.append((self.mylog.tell(), cmd)) os.write(child_fd, (cmd + "\n").encode()) - await asyncio.sleep(0.3) - elif action_l.startswith('e'): - # Edit mode - edit_session = PromptSession() - cmds_to_edit = [] - - if len(action_l) > 1 and action_l[1:].isdigit(): - idx = int(action_l[1:]) - 1 - if 0 <= idx < len(commands): - cmds_to_edit = [commands[idx]] - else: - cmds_to_edit = commands - - if cmds_to_edit: - target_cmd = "\n".join(cmds_to_edit) - edit_bindings = KeyBindings() - @edit_bindings.add('c-j') - def _(event): - event.app.exit(result=event.app.current_buffer.text) - - @edit_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='') - - try: - edited_cmd = await edit_session.prompt_async( - HTML("Edit commands (Ctrl+Enter to submit, Esc to cancel):\n"), - default=target_cmd, - multiline=True, - key_bindings=edit_bindings - ) - if edited_cmd.strip(): - os.write(child_fd, b'\x15') - await asyncio.sleep(0.1) - for cmd in edited_cmd.split('\n'): - if cmd.strip(): - if cmd_byte_positions is not None and hasattr(self, 'mylog'): - cmd_byte_positions.append((self.mylog.tell(), cmd.strip())) - os.write(child_fd, (cmd.strip() + "\n").encode()) - await asyncio.sleep(0.3) - else: - os.write(child_fd, b'\x15\r') - except KeyboardInterrupt: - os.write(child_fd, b'\x15\r') - else: - os.write(child_fd, b'\x15\r') - elif action_l not in ('n', 'no', ''): - try: - selected_indices = set() - for part in action_l.split(','): - part = part.strip() - if not part: continue - if '-' in part: - start_str, end_str = part.split('-', 1) - start = int(start_str) - 1 - end = int(end_str) - 1 - for i in range(start, end + 1): - selected_indices.add(i) - else: - selected_indices.add(int(part) - 1) - - valid_indices = sorted([i for i in selected_indices if 0 <= i < len(commands)]) - if valid_indices: - os.write(child_fd, b'\x15') # Ctrl+U to clear line - await asyncio.sleep(0.1) - if len(valid_indices) == 1: - if cmd_byte_positions is not None and hasattr(self, 'mylog'): - cmd_byte_positions.append((self.mylog.tell(), commands[valid_indices[0]])) - os.write(child_fd, (commands[valid_indices[0]] + "\n").encode()) - else: - for idx in valid_indices: - if cmd_byte_positions is not None and hasattr(self, 'mylog'): - cmd_byte_positions.append((self.mylog.tell(), commands[idx])) - os.write(child_fd, (commands[idx] + "\n").encode()) - await asyncio.sleep(0.3) - else: - os.write(child_fd, b'\x15\r') # Ctrl+U + Enter to abort line and get new prompt - except ValueError: - os.write(child_fd, b'\x15\r') - else: - os.write(child_fd, b'\x15\r') + await asyncio.sleep(0.8) else: - console.print("[dim]Returning to session...[/dim]\n") os.write(child_fd, b'\x15\r') - except KeyboardInterrupt: - if 'console' in locals(): - console.print("\n[dim]Copilot cancelled via Ctrl+C.[/dim]\n") - else: - print("\n[dim]Copilot cancelled via Ctrl+C.[/dim]\n") - os.write(child_fd, b'\x15\r') except Exception as e: import traceback print(f"\n[ERROR in Copilot Handler] {e}", flush=True) traceback.print_exc() - finally: - # 6. Restaurar raw mode, O_NONBLOCK y SIGINT - tty.setraw(stdin_fd) - fcntl.fcntl(stdin_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - - # Re-enable LocalStream reader - try: - loop = asyncio.get_running_loop() - loop.add_reader(stdin_fd, stream._read_ready) - except Exception: - pass - - return handler + os.write(child_fd, b'\x15\r') + return handler @MethodHook def run(self, commands, vars = None,*, folder = '', prompt = r'>$|#$|\$$|>.$|#.$|\$.$', stdout = False, timeout = 10, logger = None): @@ -1152,7 +736,6 @@ class node: if "prompt" in self.tags: prompt = self.tags["prompt"] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] - output = '' status = '' if not isinstance(commands, list): @@ -1263,7 +846,6 @@ class node: if "prompt" in self.tags: prompt = self.tags["prompt"] expects = [prompt, pexpect.EOF, pexpect.TIMEOUT] - output = '' if not isinstance(commands, list): commands = [commands] @@ -1329,8 +911,6 @@ class node: @MethodHook def _generate_ssh_sftp_cmd(self): cmd = self.protocol - if self.idletime > 0: - cmd += " -o ServerAliveInterval=" + str(self.idletime) if self.port: if self.protocol == "ssh": cmd += " -p " + self.port @@ -1385,6 +965,19 @@ class node: cmd += f" {self.options}" return cmd + @MethodHook + def _generate_ssm_cmd(self): + region = self.tags.get("region", "") if isinstance(self.tags, dict) else "" + profile = self.tags.get("profile", "") if isinstance(self.tags, dict) else "" + cmd = f"aws ssm start-session --target {self.host}" + if region: + cmd += f" --region {region}" + if profile: + cmd += f" --profile {profile}" + if self.options: + cmd += f" {self.options}" + return cmd + @MethodHook def _get_cmd(self): if self.protocol in ["ssh", "sftp"]: diff --git a/connpy/grpc_layer/connpy_pb2.py b/connpy/grpc_layer/connpy_pb2.py index 72bb6c0..a443c02 100644 --- a/connpy/grpc_layer/connpy_pb2.py +++ b/connpy/grpc_layer/connpy_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE -# source: connpy.proto +# source: connpy/proto/connpy.proto # Protobuf Python Version: 6.31.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor @@ -15,7 +15,7 @@ _runtime_version.ValidateProtobufRuntimeVersion( 31, 1, '', - 'connpy.proto' + 'connpy/proto/connpy.proto' ) # @@protoc_insertion_point(imports) @@ -26,89 +26,91 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onnpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xdc\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\x12\x18\n\x10\x63opilot_question\x18\x08 \x01(\t\x12\x16\n\x0e\x63opilot_action\x18\t \x01(\t\x12\x1e\n\x16\x63opilot_context_buffer\x18\n \x01(\t\"\x86\x02\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\x12\x16\n\x0e\x63opilot_prompt\x18\x04 \x01(\x08\x12\x1e\n\x16\x63opilot_buffer_preview\x18\x05 \x01(\t\x12\x1d\n\x15\x63opilot_response_json\x18\x06 \x01(\t\x12\x1e\n\x16\x63opilot_node_info_json\x18\x07 \x01(\t\x12\x1c\n\x14\x63opilot_stream_chunk\x18\x08 \x01(\t\x12 \n\x18\x63opilot_injected_command\x18\t \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\xa5\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x07 \x01(\x05\x12\x0c\n\x04name\x18\x08 \x01(\t\"\xb8\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x03(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x08 \x01(\x05\x12\x0c\n\x04name\x18\t \x01(\t\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"X\n\x0e\x43opilotRequest\x12\x17\n\x0fterminal_buffer\x18\x01 \x01(\t\x12\x15\n\ruser_question\x18\x02 \x01(\t\x12\x16\n\x0enode_info_json\x18\x03 \x01(\t\"U\n\x0f\x43opilotResponse\x12\x10\n\x08\x63ommands\x18\x01 \x03(\t\x12\r\n\x05guide\x18\x02 \x01(\t\x12\x12\n\nrisk_level\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t2\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xd0\x03\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\x0b\x61sk_copilot\x12\x16.connpy.CopilotRequest\x1a\x17.connpy.CopilotResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x63onnpy/proto/connpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xdc\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\x12\x18\n\x10\x63opilot_question\x18\x08 \x01(\t\x12\x16\n\x0e\x63opilot_action\x18\t \x01(\t\x12\x1e\n\x16\x63opilot_context_buffer\x18\n \x01(\t\"\x86\x02\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\x12\x16\n\x0e\x63opilot_prompt\x18\x04 \x01(\x08\x12\x1e\n\x16\x63opilot_buffer_preview\x18\x05 \x01(\t\x12\x1d\n\x15\x63opilot_response_json\x18\x06 \x01(\t\x12\x1e\n\x16\x63opilot_node_info_json\x18\x07 \x01(\t\x12\x1c\n\x14\x63opilot_stream_chunk\x18\x08 \x01(\t\x12 \n\x18\x63opilot_injected_command\x18\t \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\xa5\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x07 \x01(\x05\x12\x0c\n\x04name\x18\x08 \x01(\t\"\xb8\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x03(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x08 \x01(\x05\x12\x0c\n\x04name\x18\t \x01(\t\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"X\n\x0e\x43opilotRequest\x12\x17\n\x0fterminal_buffer\x18\x01 \x01(\t\x12\x15\n\ruser_question\x18\x02 \x01(\t\x12\x16\n\x0enode_info_json\x18\x03 \x01(\t\"U\n\x0f\x43opilotResponse\x12\x10\n\x08\x63ommands\x18\x01 \x03(\t\x12\r\n\x05guide\x18\x02 \x01(\t\x12\x12\n\nrisk_level\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"a\n\nMCPRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x12\x17\n\x0f\x61uto_load_on_os\x18\x04 \x01(\t\x12\x0e\n\x06remove\x18\x05 \x01(\x08\x32\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x8f\x04\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\x0b\x61sk_copilot\x12\x16.connpy.CopilotRequest\x1a\x17.connpy.CopilotResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\rconfigure_mcp\x12\x12.connpy.MCPRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connpy_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'connpy.proto.connpy_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_INTERACTREQUEST']._serialized_start=84 - _globals['_INTERACTREQUEST']._serialized_end=304 - _globals['_INTERACTRESPONSE']._serialized_start=307 - _globals['_INTERACTRESPONSE']._serialized_end=569 - _globals['_FILTERREQUEST']._serialized_start=571 - _globals['_FILTERREQUEST']._serialized_end=626 - _globals['_VALUERESPONSE']._serialized_start=628 - _globals['_VALUERESPONSE']._serialized_end=681 - _globals['_IDREQUEST']._serialized_start=683 - _globals['_IDREQUEST']._serialized_end=706 - _globals['_NODEREQUEST']._serialized_start=708 - _globals['_NODEREQUEST']._serialized_end=791 - _globals['_DELETEREQUEST']._serialized_start=793 - _globals['_DELETEREQUEST']._serialized_end=839 - _globals['_MESSAGEVALUE']._serialized_start=841 - _globals['_MESSAGEVALUE']._serialized_end=870 - _globals['_MOVEREQUEST']._serialized_start=872 - _globals['_MOVEREQUEST']._serialized_end=931 - _globals['_BULKREQUEST']._serialized_start=933 - _globals['_BULKREQUEST']._serialized_end=1020 - _globals['_STRUCTRESPONSE']._serialized_start=1022 - _globals['_STRUCTRESPONSE']._serialized_end=1077 - _globals['_PROFILEREQUEST']._serialized_start=1079 - _globals['_PROFILEREQUEST']._serialized_end=1126 - _globals['_STRUCTREQUEST']._serialized_start=1128 - _globals['_STRUCTREQUEST']._serialized_end=1182 - _globals['_STRINGREQUEST']._serialized_start=1184 - _globals['_STRINGREQUEST']._serialized_end=1214 - _globals['_STRINGRESPONSE']._serialized_start=1216 - _globals['_STRINGRESPONSE']._serialized_end=1247 - _globals['_UPDATEREQUEST']._serialized_start=1249 - _globals['_UPDATEREQUEST']._serialized_end=1316 - _globals['_PLUGINREQUEST']._serialized_start=1318 - _globals['_PLUGINREQUEST']._serialized_end=1384 - _globals['_RUNREQUEST']._serialized_start=1387 - _globals['_RUNREQUEST']._serialized_end=1552 - _globals['_TESTREQUEST']._serialized_start=1555 - _globals['_TESTREQUEST']._serialized_end=1739 - _globals['_SCRIPTREQUEST']._serialized_start=1741 - _globals['_SCRIPTREQUEST']._serialized_end=1806 - _globals['_EXPORTREQUEST']._serialized_start=1808 - _globals['_EXPORTREQUEST']._serialized_end=1859 - _globals['_LISTREQUEST']._serialized_start=1861 - _globals['_LISTREQUEST']._serialized_end=1889 - _globals['_ASKREQUEST']._serialized_start=1892 - _globals['_ASKREQUEST']._serialized_end=2186 - _globals['_AIRESPONSE']._serialized_start=2189 - _globals['_AIRESPONSE']._serialized_end=2389 - _globals['_BOOLRESPONSE']._serialized_start=2391 - _globals['_BOOLRESPONSE']._serialized_end=2420 - _globals['_PROVIDERREQUEST']._serialized_start=2422 - _globals['_PROVIDERREQUEST']._serialized_end=2489 - _globals['_INTREQUEST']._serialized_start=2491 - _globals['_INTREQUEST']._serialized_end=2518 - _globals['_NODERUNRESULT']._serialized_start=2520 - _globals['_NODERUNRESULT']._serialized_end=2632 - _globals['_FULLREPLACEREQUEST']._serialized_start=2634 - _globals['_FULLREPLACEREQUEST']._serialized_end=2743 - _globals['_COPILOTREQUEST']._serialized_start=2745 - _globals['_COPILOTREQUEST']._serialized_end=2833 - _globals['_COPILOTRESPONSE']._serialized_start=2835 - _globals['_COPILOTRESPONSE']._serialized_end=2920 - _globals['_NODESERVICE']._serialized_start=2923 - _globals['_NODESERVICE']._serialized_end=3916 - _globals['_PROFILESERVICE']._serialized_start=3919 - _globals['_PROFILESERVICE']._serialized_end=4325 - _globals['_CONFIGSERVICE']._serialized_start=4328 - _globals['_CONFIGSERVICE']._serialized_end=4758 - _globals['_PLUGINSERVICE']._serialized_start=4761 - _globals['_PLUGINSERVICE']._serialized_end=5091 - _globals['_EXECUTIONSERVICE']._serialized_start=5094 - _globals['_EXECUTIONSERVICE']._serialized_end=5377 - _globals['_IMPORTEXPORTSERVICE']._serialized_start=5380 - _globals['_IMPORTEXPORTSERVICE']._serialized_end=5606 - _globals['_AISERVICE']._serialized_start=5609 - _globals['_AISERVICE']._serialized_end=6073 - _globals['_SYSTEMSERVICE']._serialized_start=6076 - _globals['_SYSTEMSERVICE']._serialized_end=6398 + _globals['_INTERACTREQUEST']._serialized_start=97 + _globals['_INTERACTREQUEST']._serialized_end=317 + _globals['_INTERACTRESPONSE']._serialized_start=320 + _globals['_INTERACTRESPONSE']._serialized_end=582 + _globals['_FILTERREQUEST']._serialized_start=584 + _globals['_FILTERREQUEST']._serialized_end=639 + _globals['_VALUERESPONSE']._serialized_start=641 + _globals['_VALUERESPONSE']._serialized_end=694 + _globals['_IDREQUEST']._serialized_start=696 + _globals['_IDREQUEST']._serialized_end=719 + _globals['_NODEREQUEST']._serialized_start=721 + _globals['_NODEREQUEST']._serialized_end=804 + _globals['_DELETEREQUEST']._serialized_start=806 + _globals['_DELETEREQUEST']._serialized_end=852 + _globals['_MESSAGEVALUE']._serialized_start=854 + _globals['_MESSAGEVALUE']._serialized_end=883 + _globals['_MOVEREQUEST']._serialized_start=885 + _globals['_MOVEREQUEST']._serialized_end=944 + _globals['_BULKREQUEST']._serialized_start=946 + _globals['_BULKREQUEST']._serialized_end=1033 + _globals['_STRUCTRESPONSE']._serialized_start=1035 + _globals['_STRUCTRESPONSE']._serialized_end=1090 + _globals['_PROFILEREQUEST']._serialized_start=1092 + _globals['_PROFILEREQUEST']._serialized_end=1139 + _globals['_STRUCTREQUEST']._serialized_start=1141 + _globals['_STRUCTREQUEST']._serialized_end=1195 + _globals['_STRINGREQUEST']._serialized_start=1197 + _globals['_STRINGREQUEST']._serialized_end=1227 + _globals['_STRINGRESPONSE']._serialized_start=1229 + _globals['_STRINGRESPONSE']._serialized_end=1260 + _globals['_UPDATEREQUEST']._serialized_start=1262 + _globals['_UPDATEREQUEST']._serialized_end=1329 + _globals['_PLUGINREQUEST']._serialized_start=1331 + _globals['_PLUGINREQUEST']._serialized_end=1397 + _globals['_RUNREQUEST']._serialized_start=1400 + _globals['_RUNREQUEST']._serialized_end=1565 + _globals['_TESTREQUEST']._serialized_start=1568 + _globals['_TESTREQUEST']._serialized_end=1752 + _globals['_SCRIPTREQUEST']._serialized_start=1754 + _globals['_SCRIPTREQUEST']._serialized_end=1819 + _globals['_EXPORTREQUEST']._serialized_start=1821 + _globals['_EXPORTREQUEST']._serialized_end=1872 + _globals['_LISTREQUEST']._serialized_start=1874 + _globals['_LISTREQUEST']._serialized_end=1902 + _globals['_ASKREQUEST']._serialized_start=1905 + _globals['_ASKREQUEST']._serialized_end=2199 + _globals['_AIRESPONSE']._serialized_start=2202 + _globals['_AIRESPONSE']._serialized_end=2402 + _globals['_BOOLRESPONSE']._serialized_start=2404 + _globals['_BOOLRESPONSE']._serialized_end=2433 + _globals['_PROVIDERREQUEST']._serialized_start=2435 + _globals['_PROVIDERREQUEST']._serialized_end=2502 + _globals['_INTREQUEST']._serialized_start=2504 + _globals['_INTREQUEST']._serialized_end=2531 + _globals['_NODERUNRESULT']._serialized_start=2533 + _globals['_NODERUNRESULT']._serialized_end=2645 + _globals['_FULLREPLACEREQUEST']._serialized_start=2647 + _globals['_FULLREPLACEREQUEST']._serialized_end=2756 + _globals['_COPILOTREQUEST']._serialized_start=2758 + _globals['_COPILOTREQUEST']._serialized_end=2846 + _globals['_COPILOTRESPONSE']._serialized_start=2848 + _globals['_COPILOTRESPONSE']._serialized_end=2933 + _globals['_MCPREQUEST']._serialized_start=2935 + _globals['_MCPREQUEST']._serialized_end=3032 + _globals['_NODESERVICE']._serialized_start=3035 + _globals['_NODESERVICE']._serialized_end=4028 + _globals['_PROFILESERVICE']._serialized_start=4031 + _globals['_PROFILESERVICE']._serialized_end=4437 + _globals['_CONFIGSERVICE']._serialized_start=4440 + _globals['_CONFIGSERVICE']._serialized_end=4870 + _globals['_PLUGINSERVICE']._serialized_start=4873 + _globals['_PLUGINSERVICE']._serialized_end=5203 + _globals['_EXECUTIONSERVICE']._serialized_start=5206 + _globals['_EXECUTIONSERVICE']._serialized_end=5489 + _globals['_IMPORTEXPORTSERVICE']._serialized_start=5492 + _globals['_IMPORTEXPORTSERVICE']._serialized_end=5718 + _globals['_AISERVICE']._serialized_start=5721 + _globals['_AISERVICE']._serialized_end=6248 + _globals['_SYSTEMSERVICE']._serialized_start=6251 + _globals['_SYSTEMSERVICE']._serialized_end=6573 # @@protoc_insertion_point(module_scope) diff --git a/connpy/grpc_layer/connpy_pb2_grpc.py b/connpy/grpc_layer/connpy_pb2_grpc.py index dd9b5e7..30d99c4 100644 --- a/connpy/grpc_layer/connpy_pb2_grpc.py +++ b/connpy/grpc_layer/connpy_pb2_grpc.py @@ -3,7 +3,7 @@ import grpc import warnings -import connpy_pb2 as connpy__pb2 +from . import connpy_pb2 as connpy_dot_proto_dot_connpy__pb2 from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 GRPC_GENERATED_VERSION = '1.80.0' @@ -19,7 +19,7 @@ except ImportError: if _version_not_supported: raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in connpy_pb2_grpc.py depends on' + + ' but the generated code in connpy/proto/connpy_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' @@ -37,23 +37,23 @@ class NodeServiceStub(object): """ self.list_nodes = channel.unary_unary( '/connpy.NodeService/list_nodes', - request_serializer=connpy__pb2.FilterRequest.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.list_folders = channel.unary_unary( '/connpy.NodeService/list_folders', - request_serializer=connpy__pb2.FilterRequest.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.get_node_details = channel.unary_unary( '/connpy.NodeService/get_node_details', - request_serializer=connpy__pb2.IdRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) self.explode_unique = channel.unary_unary( '/connpy.NodeService/explode_unique', - request_serializer=connpy__pb2.IdRequest.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.generate_cache = channel.unary_unary( '/connpy.NodeService/generate_cache', @@ -62,53 +62,53 @@ class NodeServiceStub(object): _registered_method=True) self.add_node = channel.unary_unary( '/connpy.NodeService/add_node', - request_serializer=connpy__pb2.NodeRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.update_node = channel.unary_unary( '/connpy.NodeService/update_node', - request_serializer=connpy__pb2.NodeRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.delete_node = channel.unary_unary( '/connpy.NodeService/delete_node', - request_serializer=connpy__pb2.DeleteRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.DeleteRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.move_node = channel.unary_unary( '/connpy.NodeService/move_node', - request_serializer=connpy__pb2.MoveRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.MoveRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.bulk_add = channel.unary_unary( '/connpy.NodeService/bulk_add', - request_serializer=connpy__pb2.BulkRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.BulkRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.validate_parent_folder = channel.unary_unary( '/connpy.NodeService/validate_parent_folder', - request_serializer=connpy__pb2.IdRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.set_reserved_names = channel.unary_unary( '/connpy.NodeService/set_reserved_names', - request_serializer=connpy__pb2.ListRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ListRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.interact_node = channel.stream_stream( '/connpy.NodeService/interact_node', - request_serializer=connpy__pb2.InteractRequest.SerializeToString, - response_deserializer=connpy__pb2.InteractResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.InteractRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.InteractResponse.FromString, _registered_method=True) self.full_replace = channel.unary_unary( '/connpy.NodeService/full_replace', - request_serializer=connpy__pb2.FullReplaceRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.get_inventory = channel.unary_unary( '/connpy.NodeService/get_inventory', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.FullReplaceRequest.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.FromString, _registered_method=True) @@ -210,23 +210,23 @@ def add_NodeServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'list_nodes': grpc.unary_unary_rpc_method_handler( servicer.list_nodes, - request_deserializer=connpy__pb2.FilterRequest.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'list_folders': grpc.unary_unary_rpc_method_handler( servicer.list_folders, - request_deserializer=connpy__pb2.FilterRequest.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'get_node_details': grpc.unary_unary_rpc_method_handler( servicer.get_node_details, - request_deserializer=connpy__pb2.IdRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), 'explode_unique': grpc.unary_unary_rpc_method_handler( servicer.explode_unique, - request_deserializer=connpy__pb2.IdRequest.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'generate_cache': grpc.unary_unary_rpc_method_handler( servicer.generate_cache, @@ -235,53 +235,53 @@ def add_NodeServiceServicer_to_server(servicer, server): ), 'add_node': grpc.unary_unary_rpc_method_handler( servicer.add_node, - request_deserializer=connpy__pb2.NodeRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'update_node': grpc.unary_unary_rpc_method_handler( servicer.update_node, - request_deserializer=connpy__pb2.NodeRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'delete_node': grpc.unary_unary_rpc_method_handler( servicer.delete_node, - request_deserializer=connpy__pb2.DeleteRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.DeleteRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'move_node': grpc.unary_unary_rpc_method_handler( servicer.move_node, - request_deserializer=connpy__pb2.MoveRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.MoveRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'bulk_add': grpc.unary_unary_rpc_method_handler( servicer.bulk_add, - request_deserializer=connpy__pb2.BulkRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.BulkRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'validate_parent_folder': grpc.unary_unary_rpc_method_handler( servicer.validate_parent_folder, - request_deserializer=connpy__pb2.IdRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'set_reserved_names': grpc.unary_unary_rpc_method_handler( servicer.set_reserved_names, - request_deserializer=connpy__pb2.ListRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ListRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'interact_node': grpc.stream_stream_rpc_method_handler( servicer.interact_node, - request_deserializer=connpy__pb2.InteractRequest.FromString, - response_serializer=connpy__pb2.InteractResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.InteractRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.InteractResponse.SerializeToString, ), 'full_replace': grpc.unary_unary_rpc_method_handler( servicer.full_replace, - request_deserializer=connpy__pb2.FullReplaceRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'get_inventory': grpc.unary_unary_rpc_method_handler( servicer.get_inventory, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.FullReplaceRequest.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -309,8 +309,8 @@ class NodeService(object): request, target, '/connpy.NodeService/list_nodes', - connpy__pb2.FilterRequest.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -336,8 +336,8 @@ class NodeService(object): request, target, '/connpy.NodeService/list_folders', - connpy__pb2.FilterRequest.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -363,8 +363,8 @@ class NodeService(object): request, target, '/connpy.NodeService/get_node_details', - connpy__pb2.IdRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -390,8 +390,8 @@ class NodeService(object): request, target, '/connpy.NodeService/explode_unique', - connpy__pb2.IdRequest.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -444,7 +444,7 @@ class NodeService(object): request, target, '/connpy.NodeService/add_node', - connpy__pb2.NodeRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -471,7 +471,7 @@ class NodeService(object): request, target, '/connpy.NodeService/update_node', - connpy__pb2.NodeRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -498,7 +498,7 @@ class NodeService(object): request, target, '/connpy.NodeService/delete_node', - connpy__pb2.DeleteRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.DeleteRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -525,7 +525,7 @@ class NodeService(object): request, target, '/connpy.NodeService/move_node', - connpy__pb2.MoveRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.MoveRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -552,7 +552,7 @@ class NodeService(object): request, target, '/connpy.NodeService/bulk_add', - connpy__pb2.BulkRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.BulkRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -579,7 +579,7 @@ class NodeService(object): request, target, '/connpy.NodeService/validate_parent_folder', - connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -606,7 +606,7 @@ class NodeService(object): request, target, '/connpy.NodeService/set_reserved_names', - connpy__pb2.ListRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ListRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -633,8 +633,8 @@ class NodeService(object): request_iterator, target, '/connpy.NodeService/interact_node', - connpy__pb2.InteractRequest.SerializeToString, - connpy__pb2.InteractResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.InteractRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.InteractResponse.FromString, options, channel_credentials, insecure, @@ -660,7 +660,7 @@ class NodeService(object): request, target, '/connpy.NodeService/full_replace', - connpy__pb2.FullReplaceRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -688,7 +688,7 @@ class NodeService(object): target, '/connpy.NodeService/get_inventory', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.FullReplaceRequest.FromString, + connpy_dot_proto_dot_connpy__pb2.FullReplaceRequest.FromString, options, channel_credentials, insecure, @@ -711,32 +711,32 @@ class ProfileServiceStub(object): """ self.list_profiles = channel.unary_unary( '/connpy.ProfileService/list_profiles', - request_serializer=connpy__pb2.FilterRequest.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.get_profile = channel.unary_unary( '/connpy.ProfileService/get_profile', - request_serializer=connpy__pb2.ProfileRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ProfileRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) self.add_profile = channel.unary_unary( '/connpy.ProfileService/add_profile', - request_serializer=connpy__pb2.NodeRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.resolve_node_data = channel.unary_unary( '/connpy.ProfileService/resolve_node_data', - request_serializer=connpy__pb2.StructRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StructRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) self.delete_profile = channel.unary_unary( '/connpy.ProfileService/delete_profile', - request_serializer=connpy__pb2.IdRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.update_profile = channel.unary_unary( '/connpy.ProfileService/update_profile', - request_serializer=connpy__pb2.NodeRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) @@ -785,32 +785,32 @@ def add_ProfileServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'list_profiles': grpc.unary_unary_rpc_method_handler( servicer.list_profiles, - request_deserializer=connpy__pb2.FilterRequest.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.FilterRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'get_profile': grpc.unary_unary_rpc_method_handler( servicer.get_profile, - request_deserializer=connpy__pb2.ProfileRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ProfileRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), 'add_profile': grpc.unary_unary_rpc_method_handler( servicer.add_profile, - request_deserializer=connpy__pb2.NodeRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'resolve_node_data': grpc.unary_unary_rpc_method_handler( servicer.resolve_node_data, - request_deserializer=connpy__pb2.StructRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StructRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), 'delete_profile': grpc.unary_unary_rpc_method_handler( servicer.delete_profile, - request_deserializer=connpy__pb2.IdRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'update_profile': grpc.unary_unary_rpc_method_handler( servicer.update_profile, - request_deserializer=connpy__pb2.NodeRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), } @@ -839,8 +839,8 @@ class ProfileService(object): request, target, '/connpy.ProfileService/list_profiles', - connpy__pb2.FilterRequest.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.FilterRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -866,8 +866,8 @@ class ProfileService(object): request, target, '/connpy.ProfileService/get_profile', - connpy__pb2.ProfileRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.ProfileRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -893,7 +893,7 @@ class ProfileService(object): request, target, '/connpy.ProfileService/add_profile', - connpy__pb2.NodeRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -920,8 +920,8 @@ class ProfileService(object): request, target, '/connpy.ProfileService/resolve_node_data', - connpy__pb2.StructRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StructRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -947,7 +947,7 @@ class ProfileService(object): request, target, '/connpy.ProfileService/delete_profile', - connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -974,7 +974,7 @@ class ProfileService(object): request, target, '/connpy.ProfileService/update_profile', - connpy__pb2.NodeRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -999,32 +999,32 @@ class ConfigServiceStub(object): self.get_settings = channel.unary_unary( '/connpy.ConfigService/get_settings', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) self.get_default_dir = channel.unary_unary( '/connpy.ConfigService/get_default_dir', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.StringResponse.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StringResponse.FromString, _registered_method=True) self.set_config_folder = channel.unary_unary( '/connpy.ConfigService/set_config_folder', - request_serializer=connpy__pb2.StringRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.update_setting = channel.unary_unary( '/connpy.ConfigService/update_setting', - request_serializer=connpy__pb2.UpdateRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.UpdateRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.encrypt_password = channel.unary_unary( '/connpy.ConfigService/encrypt_password', - request_serializer=connpy__pb2.StringRequest.SerializeToString, - response_deserializer=connpy__pb2.StringResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StringResponse.FromString, _registered_method=True) self.apply_theme_from_file = channel.unary_unary( '/connpy.ConfigService/apply_theme_from_file', - request_serializer=connpy__pb2.StringRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) @@ -1073,32 +1073,32 @@ def add_ConfigServiceServicer_to_server(servicer, server): 'get_settings': grpc.unary_unary_rpc_method_handler( servicer.get_settings, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), 'get_default_dir': grpc.unary_unary_rpc_method_handler( servicer.get_default_dir, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.StringResponse.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StringResponse.SerializeToString, ), 'set_config_folder': grpc.unary_unary_rpc_method_handler( servicer.set_config_folder, - request_deserializer=connpy__pb2.StringRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'update_setting': grpc.unary_unary_rpc_method_handler( servicer.update_setting, - request_deserializer=connpy__pb2.UpdateRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.UpdateRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'encrypt_password': grpc.unary_unary_rpc_method_handler( servicer.encrypt_password, - request_deserializer=connpy__pb2.StringRequest.FromString, - response_serializer=connpy__pb2.StringResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StringResponse.SerializeToString, ), 'apply_theme_from_file': grpc.unary_unary_rpc_method_handler( servicer.apply_theme_from_file, - request_deserializer=connpy__pb2.StringRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -1127,7 +1127,7 @@ class ConfigService(object): target, '/connpy.ConfigService/get_settings', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -1154,7 +1154,7 @@ class ConfigService(object): target, '/connpy.ConfigService/get_default_dir', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.StringResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StringResponse.FromString, options, channel_credentials, insecure, @@ -1180,7 +1180,7 @@ class ConfigService(object): request, target, '/connpy.ConfigService/set_config_folder', - connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1207,7 +1207,7 @@ class ConfigService(object): request, target, '/connpy.ConfigService/update_setting', - connpy__pb2.UpdateRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.UpdateRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1234,8 +1234,8 @@ class ConfigService(object): request, target, '/connpy.ConfigService/encrypt_password', - connpy__pb2.StringRequest.SerializeToString, - connpy__pb2.StringResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StringResponse.FromString, options, channel_credentials, insecure, @@ -1261,8 +1261,8 @@ class ConfigService(object): request, target, '/connpy.ConfigService/apply_theme_from_file', - connpy__pb2.StringRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -1286,26 +1286,26 @@ class PluginServiceStub(object): self.list_plugins = channel.unary_unary( '/connpy.PluginService/list_plugins', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.add_plugin = channel.unary_unary( '/connpy.PluginService/add_plugin', - request_serializer=connpy__pb2.PluginRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.PluginRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.delete_plugin = channel.unary_unary( '/connpy.PluginService/delete_plugin', - request_serializer=connpy__pb2.IdRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.enable_plugin = channel.unary_unary( '/connpy.PluginService/enable_plugin', - request_serializer=connpy__pb2.IdRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.disable_plugin = channel.unary_unary( '/connpy.PluginService/disable_plugin', - request_serializer=connpy__pb2.IdRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) @@ -1349,26 +1349,26 @@ def add_PluginServiceServicer_to_server(servicer, server): 'list_plugins': grpc.unary_unary_rpc_method_handler( servicer.list_plugins, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'add_plugin': grpc.unary_unary_rpc_method_handler( servicer.add_plugin, - request_deserializer=connpy__pb2.PluginRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.PluginRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'delete_plugin': grpc.unary_unary_rpc_method_handler( servicer.delete_plugin, - request_deserializer=connpy__pb2.IdRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'enable_plugin': grpc.unary_unary_rpc_method_handler( servicer.enable_plugin, - request_deserializer=connpy__pb2.IdRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'disable_plugin': grpc.unary_unary_rpc_method_handler( servicer.disable_plugin, - request_deserializer=connpy__pb2.IdRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), } @@ -1398,7 +1398,7 @@ class PluginService(object): target, '/connpy.PluginService/list_plugins', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -1424,7 +1424,7 @@ class PluginService(object): request, target, '/connpy.PluginService/add_plugin', - connpy__pb2.PluginRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.PluginRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1451,7 +1451,7 @@ class PluginService(object): request, target, '/connpy.PluginService/delete_plugin', - connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1478,7 +1478,7 @@ class PluginService(object): request, target, '/connpy.PluginService/enable_plugin', - connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1505,7 +1505,7 @@ class PluginService(object): request, target, '/connpy.PluginService/disable_plugin', - connpy__pb2.IdRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1529,23 +1529,23 @@ class ExecutionServiceStub(object): """ self.run_commands = channel.unary_stream( '/connpy.ExecutionService/run_commands', - request_serializer=connpy__pb2.RunRequest.SerializeToString, - response_deserializer=connpy__pb2.NodeRunResult.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.RunRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRunResult.FromString, _registered_method=True) self.test_commands = channel.unary_stream( '/connpy.ExecutionService/test_commands', - request_serializer=connpy__pb2.TestRequest.SerializeToString, - response_deserializer=connpy__pb2.NodeRunResult.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.TestRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.NodeRunResult.FromString, _registered_method=True) self.run_cli_script = channel.unary_unary( '/connpy.ExecutionService/run_cli_script', - request_serializer=connpy__pb2.ScriptRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ScriptRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) self.run_yaml_playbook = channel.unary_unary( '/connpy.ExecutionService/run_yaml_playbook', - request_serializer=connpy__pb2.ScriptRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ScriptRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) @@ -1581,23 +1581,23 @@ def add_ExecutionServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'run_commands': grpc.unary_stream_rpc_method_handler( servicer.run_commands, - request_deserializer=connpy__pb2.RunRequest.FromString, - response_serializer=connpy__pb2.NodeRunResult.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.RunRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRunResult.SerializeToString, ), 'test_commands': grpc.unary_stream_rpc_method_handler( servicer.test_commands, - request_deserializer=connpy__pb2.TestRequest.FromString, - response_serializer=connpy__pb2.NodeRunResult.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.TestRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.NodeRunResult.SerializeToString, ), 'run_cli_script': grpc.unary_unary_rpc_method_handler( servicer.run_cli_script, - request_deserializer=connpy__pb2.ScriptRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ScriptRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), 'run_yaml_playbook': grpc.unary_unary_rpc_method_handler( servicer.run_yaml_playbook, - request_deserializer=connpy__pb2.ScriptRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ScriptRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -1625,8 +1625,8 @@ class ExecutionService(object): request, target, '/connpy.ExecutionService/run_commands', - connpy__pb2.RunRequest.SerializeToString, - connpy__pb2.NodeRunResult.FromString, + connpy_dot_proto_dot_connpy__pb2.RunRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRunResult.FromString, options, channel_credentials, insecure, @@ -1652,8 +1652,8 @@ class ExecutionService(object): request, target, '/connpy.ExecutionService/test_commands', - connpy__pb2.TestRequest.SerializeToString, - connpy__pb2.NodeRunResult.FromString, + connpy_dot_proto_dot_connpy__pb2.TestRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.NodeRunResult.FromString, options, channel_credentials, insecure, @@ -1679,8 +1679,8 @@ class ExecutionService(object): request, target, '/connpy.ExecutionService/run_cli_script', - connpy__pb2.ScriptRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.ScriptRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -1706,8 +1706,8 @@ class ExecutionService(object): request, target, '/connpy.ExecutionService/run_yaml_playbook', - connpy__pb2.ScriptRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.ScriptRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -1730,17 +1730,17 @@ class ImportExportServiceStub(object): """ self.export_to_file = channel.unary_unary( '/connpy.ImportExportService/export_to_file', - request_serializer=connpy__pb2.ExportRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ExportRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.import_from_file = channel.unary_unary( '/connpy.ImportExportService/import_from_file', - request_serializer=connpy__pb2.StringRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.set_reserved_names = channel.unary_unary( '/connpy.ImportExportService/set_reserved_names', - request_serializer=connpy__pb2.ListRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ListRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) @@ -1771,17 +1771,17 @@ def add_ImportExportServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'export_to_file': grpc.unary_unary_rpc_method_handler( servicer.export_to_file, - request_deserializer=connpy__pb2.ExportRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ExportRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'import_from_file': grpc.unary_unary_rpc_method_handler( servicer.import_from_file, - request_deserializer=connpy__pb2.StringRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'set_reserved_names': grpc.unary_unary_rpc_method_handler( servicer.set_reserved_names, - request_deserializer=connpy__pb2.ListRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ListRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), } @@ -1810,7 +1810,7 @@ class ImportExportService(object): request, target, '/connpy.ImportExportService/export_to_file', - connpy__pb2.ExportRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ExportRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1837,7 +1837,7 @@ class ImportExportService(object): request, target, '/connpy.ImportExportService/import_from_file', - connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1864,7 +1864,7 @@ class ImportExportService(object): request, target, '/connpy.ImportExportService/set_reserved_names', - connpy__pb2.ListRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ListRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -1888,38 +1888,43 @@ class AIServiceStub(object): """ self.ask = channel.stream_stream( '/connpy.AIService/ask', - request_serializer=connpy__pb2.AskRequest.SerializeToString, - response_deserializer=connpy__pb2.AIResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.AskRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.AIResponse.FromString, _registered_method=True) self.confirm = channel.unary_unary( '/connpy.AIService/confirm', - request_serializer=connpy__pb2.StringRequest.SerializeToString, - response_deserializer=connpy__pb2.BoolResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.BoolResponse.FromString, _registered_method=True) self.ask_copilot = channel.unary_unary( '/connpy.AIService/ask_copilot', - request_serializer=connpy__pb2.CopilotRequest.SerializeToString, - response_deserializer=connpy__pb2.CopilotResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.CopilotRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.CopilotResponse.FromString, _registered_method=True) self.list_sessions = channel.unary_unary( '/connpy.AIService/list_sessions', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.ValueResponse.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, _registered_method=True) self.delete_session = channel.unary_unary( '/connpy.AIService/delete_session', - request_serializer=connpy__pb2.StringRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.configure_provider = channel.unary_unary( '/connpy.AIService/configure_provider', - request_serializer=connpy__pb2.ProviderRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.ProviderRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + _registered_method=True) + self.configure_mcp = channel.unary_unary( + '/connpy.AIService/configure_mcp', + request_serializer=connpy_dot_proto_dot_connpy__pb2.MCPRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.load_session_data = channel.unary_unary( '/connpy.AIService/load_session_data', - request_serializer=connpy__pb2.StringRequest.SerializeToString, - response_deserializer=connpy__pb2.StructResponse.FromString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, _registered_method=True) @@ -1962,6 +1967,12 @@ class AIServiceServicer(object): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def configure_mcp(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def load_session_data(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -1973,38 +1984,43 @@ def add_AIServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'ask': grpc.stream_stream_rpc_method_handler( servicer.ask, - request_deserializer=connpy__pb2.AskRequest.FromString, - response_serializer=connpy__pb2.AIResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.AskRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.AIResponse.SerializeToString, ), 'confirm': grpc.unary_unary_rpc_method_handler( servicer.confirm, - request_deserializer=connpy__pb2.StringRequest.FromString, - response_serializer=connpy__pb2.BoolResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.BoolResponse.SerializeToString, ), 'ask_copilot': grpc.unary_unary_rpc_method_handler( servicer.ask_copilot, - request_deserializer=connpy__pb2.CopilotRequest.FromString, - response_serializer=connpy__pb2.CopilotResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.CopilotRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.CopilotResponse.SerializeToString, ), 'list_sessions': grpc.unary_unary_rpc_method_handler( servicer.list_sessions, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.ValueResponse.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.ValueResponse.SerializeToString, ), 'delete_session': grpc.unary_unary_rpc_method_handler( servicer.delete_session, - request_deserializer=connpy__pb2.StringRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'configure_provider': grpc.unary_unary_rpc_method_handler( servicer.configure_provider, - request_deserializer=connpy__pb2.ProviderRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.ProviderRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), + 'configure_mcp': grpc.unary_unary_rpc_method_handler( + servicer.configure_mcp, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.MCPRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'load_session_data': grpc.unary_unary_rpc_method_handler( servicer.load_session_data, - request_deserializer=connpy__pb2.StringRequest.FromString, - response_serializer=connpy__pb2.StructResponse.SerializeToString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.StringRequest.FromString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.StructResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -2032,8 +2048,8 @@ class AIService(object): request_iterator, target, '/connpy.AIService/ask', - connpy__pb2.AskRequest.SerializeToString, - connpy__pb2.AIResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.AskRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.AIResponse.FromString, options, channel_credentials, insecure, @@ -2059,8 +2075,8 @@ class AIService(object): request, target, '/connpy.AIService/confirm', - connpy__pb2.StringRequest.SerializeToString, - connpy__pb2.BoolResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.BoolResponse.FromString, options, channel_credentials, insecure, @@ -2086,8 +2102,8 @@ class AIService(object): request, target, '/connpy.AIService/ask_copilot', - connpy__pb2.CopilotRequest.SerializeToString, - connpy__pb2.CopilotResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.CopilotRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.CopilotResponse.FromString, options, channel_credentials, insecure, @@ -2114,7 +2130,7 @@ class AIService(object): target, '/connpy.AIService/list_sessions', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.ValueResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.ValueResponse.FromString, options, channel_credentials, insecure, @@ -2140,7 +2156,7 @@ class AIService(object): request, target, '/connpy.AIService/delete_session', - connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -2167,7 +2183,34 @@ class AIService(object): request, target, '/connpy.AIService/configure_provider', - connpy__pb2.ProviderRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.ProviderRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def configure_mcp(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/connpy.AIService/configure_mcp', + connpy_dot_proto_dot_connpy__pb2.MCPRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -2194,8 +2237,8 @@ class AIService(object): request, target, '/connpy.AIService/load_session_data', - connpy__pb2.StringRequest.SerializeToString, - connpy__pb2.StructResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.StringRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.StructResponse.FromString, options, channel_credentials, insecure, @@ -2218,12 +2261,12 @@ class SystemServiceStub(object): """ self.start_api = channel.unary_unary( '/connpy.SystemService/start_api', - request_serializer=connpy__pb2.IntRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.debug_api = channel.unary_unary( '/connpy.SystemService/debug_api', - request_serializer=connpy__pb2.IntRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.stop_api = channel.unary_unary( @@ -2233,13 +2276,13 @@ class SystemServiceStub(object): _registered_method=True) self.restart_api = channel.unary_unary( '/connpy.SystemService/restart_api', - request_serializer=connpy__pb2.IntRequest.SerializeToString, + request_serializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, _registered_method=True) self.get_api_status = channel.unary_unary( '/connpy.SystemService/get_api_status', request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=connpy__pb2.BoolResponse.FromString, + response_deserializer=connpy_dot_proto_dot_connpy__pb2.BoolResponse.FromString, _registered_method=True) @@ -2281,12 +2324,12 @@ def add_SystemServiceServicer_to_server(servicer, server): rpc_method_handlers = { 'start_api': grpc.unary_unary_rpc_method_handler( servicer.start_api, - request_deserializer=connpy__pb2.IntRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'debug_api': grpc.unary_unary_rpc_method_handler( servicer.debug_api, - request_deserializer=connpy__pb2.IntRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'stop_api': grpc.unary_unary_rpc_method_handler( @@ -2296,13 +2339,13 @@ def add_SystemServiceServicer_to_server(servicer, server): ), 'restart_api': grpc.unary_unary_rpc_method_handler( servicer.restart_api, - request_deserializer=connpy__pb2.IntRequest.FromString, + request_deserializer=connpy_dot_proto_dot_connpy__pb2.IntRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), 'get_api_status': grpc.unary_unary_rpc_method_handler( servicer.get_api_status, request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=connpy__pb2.BoolResponse.SerializeToString, + response_serializer=connpy_dot_proto_dot_connpy__pb2.BoolResponse.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( @@ -2330,7 +2373,7 @@ class SystemService(object): request, target, '/connpy.SystemService/start_api', - connpy__pb2.IntRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -2357,7 +2400,7 @@ class SystemService(object): request, target, '/connpy.SystemService/debug_api', - connpy__pb2.IntRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -2411,7 +2454,7 @@ class SystemService(object): request, target, '/connpy.SystemService/restart_api', - connpy__pb2.IntRequest.SerializeToString, + connpy_dot_proto_dot_connpy__pb2.IntRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, @@ -2439,7 +2482,7 @@ class SystemService(object): target, '/connpy.SystemService/get_api_status', google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - connpy__pb2.BoolResponse.FromString, + connpy_dot_proto_dot_connpy__pb2.BoolResponse.FromString, options, channel_credentials, insecure, diff --git a/connpy/grpc_layer/server.py b/connpy/grpc_layer/server.py index 7b1911b..0761943 100644 --- a/connpy/grpc_layer/server.py +++ b/connpy/grpc_layer/server.py @@ -207,59 +207,19 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer): import json import asyncio import os - import re - - # Build context blocks like local CLI does - blocks = [] - raw_bytes = n.mylog.getvalue() if hasattr(n, 'mylog') else b'' - - if cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes: - default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' - device_prompt = node_info.get("prompt", default_prompt) if isinstance(node_info, dict) else default_prompt - prompt_re_str = re.sub(r'(?= 2 and raw_bytes: - default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' - device_prompt = node_info.get("prompt", default_prompt) - prompt_re_str = re.sub(r'(?= total_lines: - context_lines[0] = min(50, total_lines) - else: - context_lines[0] = min(context_lines[0] + 50, total_lines) - else: - if context_cmd[0] < total_cmds: - context_cmd[0] += 1 - else: - context_cmd[0] = 1 - event.app.invalidate() - - @bindings.add('c-down') - def _(event): - if context_mode[0] == MODE_LINES: - if context_lines[0] <= min(50, total_lines): - context_lines[0] = total_lines - else: - context_lines[0] = max(context_lines[0] - 50, min(50, total_lines)) - else: - if context_cmd[0] > 1: - context_cmd[0] -= 1 - else: - context_cmd[0] = total_cmds - event.app.invalidate() - - @bindings.add('tab') - def _(event): - context_mode[0] = (context_mode[0] + 1) % 3 - event.app.invalidate() - - @bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='') - - def get_current_block(): - idx = max(0, total_cmds - context_cmd[0]) - return idx, blocks[idx] - - def get_active_buffer(): - if context_mode[0] == MODE_LINES: - buffer_lines = clean_buffer.split('\n') - return '\n'.join(buffer_lines[-context_lines[0]:]) - - idx, (start, preview) = get_current_block() - if context_mode[0] == MODE_SINGLE and idx + 1 < total_cmds: - end = blocks[idx + 1][0] - active_raw = raw_bytes[start:end] - else: - active_raw = raw_bytes[start:] - return preview + "\n" + dummy_node._logclean(active_raw.decode(errors='replace'), var=True) - - def get_prompt_text(): - if context_mode[0] == MODE_LINES: - return HTML(f"Ask [Ctx: {context_lines[0]}/{total_lines}L]: ") - - lines_count = len(get_active_buffer().split('\n')) - if context_mode[0] == MODE_SINGLE: - return HTML(f"Ask [Cmd {context_cmd[0]} ~{lines_count}L]: ") - else: - return HTML(f"Ask [Cmd {context_cmd[0]}\u2192END ~{lines_count}L]: ") - - def get_toolbar(): - mode_labels = {MODE_RANGE: "RANGE", MODE_SINGLE: "SINGLE", MODE_LINES: "LINES"} - mode_label = mode_labels[context_mode[0]] - if context_mode[0] == MODE_LINES: - return HTML(f"\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {mode_label}]") - _, (_, preview) = get_current_block() - return HTML(f"\u25b6 {preview} [Tab: {mode_label}]") - - try: - session = PromptSession(history=self.copilot_history) - question = session.prompt(get_prompt_text, key_bindings=bindings, bottom_toolbar=get_toolbar) - except KeyboardInterrupt: - question = "" - - if not question or not question.strip() or question.strip() == "CANCEL": - console.print("\n[dim]Copilot cancelled.[/dim]") - request_queue.put(connpy_pb2.InteractRequest(copilot_question="CANCEL")) - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - active_buffer = get_active_buffer() - # Enrich question with history (same as local CLI) - past_questions = self.copilot_history.get_strings() - if len(past_questions) > 1: - # Limit history to last 5 questions to save tokens, excluding current - recent_history = past_questions[-6:-1] - history_text = "\n".join(f"- {q}" for q in recent_history) - enriched_question = f"Previous questions in this session:\n{history_text}\n\nCurrent Question:\n{question}" - else: - enriched_question = question - - request_queue.put(connpy_pb2.InteractRequest(copilot_question=enriched_question, copilot_context_buffer=active_buffer)) - - from rich.live import Live - live_text = "Thinking..." - panel = Panel(live_text, title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan") - result = {} - cancelled = False - - with copilot_terminal_mode(), Live(panel, console=console, refresh_per_second=10) as live: - # Make stdin non-blocking to check for Ctrl+C locally - import fcntl - flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK) - - while True: - # 1. Read input for Ctrl+C - try: - key = os.read(sys.stdin.fileno(), 1024) - if b'\x03' in key or b'\x1b' in key: - cancelled = True - request_queue.put(connpy_pb2.InteractRequest(copilot_question="CANCEL")) - msg = "Ctrl+C" if b'\x03' in key else "Esc" - console.print(f"\n[dim]Copilot cancelled via {msg}.[/dim]") - break - except OSError: - pass - - # 2. Wait for response chunk - try: - chunk_res = response_queue.get(timeout=0.1) - if chunk_res is None: - break - - if chunk_res.copilot_stream_chunk: - if live_text == "Thinking...": live_text = "" - live_text += chunk_res.copilot_stream_chunk - live.update(Panel(Markdown(live_text), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) - elif chunk_res.copilot_response_json: - result = json.loads(chunk_res.copilot_response_json) - break - except queue.Empty: - continue - - # Restore blocking mode - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags) - - if cancelled: - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - if result.get("error"): - console.print(f"[red]Error: {result['error']}[/red]") - request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel")) - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - if live_text == "Thinking..." and result.get("guide"): - console.print(Panel(Markdown(result["guide"]), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) - - commands = result.get("commands", []) - risk = result.get("risk_level", "low") - risk_style = {"low": "green", "high": "yellow", "destructive": "red"}.get(risk, "green") - - action_sent = "cancel" - if commands: - cmd_text = "\n".join(f" {i+1}. {cmd}" for i, cmd in enumerate(commands)) - console.print(Panel( - cmd_text, - title=f"[bold {risk_style}]Suggested Commands [{risk.upper()}][/bold {risk_style}]", - border_style=risk_style - )) - - try: - confirm_session = PromptSession() - confirm_bindings = KeyBindings() - @confirm_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='n') - - pt_color = "ansi" + risk_style - action = confirm_session.prompt( - HTML(f"<{pt_color}>Send commands? (y/n/e/number/range) [n]: "), - key_bindings=confirm_bindings - ) - except KeyboardInterrupt: - action = "n" - - if not action.strip(): - action = "n" - - action_l = action.lower().strip() - if action_l in ('y', 'yes', 'all'): - action_sent = "send_all" - elif action_l.startswith('e'): - action_sent = f"edit_{action_l[1:]}" if len(action_l) > 1 else "edit_all" - # For remote editing, the client edits and sends back as custom action - edit_session = PromptSession() - cmds_to_edit = [] - if action_sent.startswith("edit_") and action_sent[5:].isdigit(): - idx = int(action_sent[5:]) - 1 - if 0 <= idx < len(commands): - cmds_to_edit = [commands[idx]] - else: - cmds_to_edit = commands - - if cmds_to_edit: - target_cmd = "\n".join(cmds_to_edit) - try: - edit_bindings = KeyBindings() - @edit_bindings.add('c-j') - def _(event): - event.app.exit(result=event.app.current_buffer.text) - @edit_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='') - - edited_cmd = edit_session.prompt( - HTML("Edit commands (Ctrl+Enter to submit, Esc to cancel):\n"), - default=target_cmd, - multiline=True, - key_bindings=edit_bindings - ) - if edited_cmd.strip(): - action_sent = "custom:" + edited_cmd.strip() - else: - action_sent = "cancel" - except KeyboardInterrupt: - action_sent = "cancel" - elif action_l not in ('n', 'no', ''): - action_sent = action_l - - console.print("[dim]Returning to session...[/dim]\n") - request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent)) - resume_generator() - tty.setraw(sys.stdin.fileno()) + self._handle_remote_copilot( + res, request_queue, response_queue, + client_buffer_bytes, cmd_byte_positions, + pause_generator, resume_generator, old_tty + ) continue if res.copilot_injected_command: @@ -638,321 +379,11 @@ class NodeStub: if res is None: break if res.copilot_prompt: - pause_generator() - import json - import asyncio - import re - from rich.console import Console - 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.formatted_text import HTML - from prompt_toolkit.history import InMemoryHistory - from ..printer import connpy_theme - from ..core import copilot_terminal_mode - - if not hasattr(self, 'copilot_history'): - self.copilot_history = InMemoryHistory() - - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) - import fcntl - flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags & ~os.O_NONBLOCK) - console = Console(theme=connpy_theme) - console.print("\n") - console.print(Panel( - "[bold cyan]AI Terminal Copilot[/bold cyan]\n" - "[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n" - "Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]", - border_style="cyan" - )) - - node_info = json.loads(res.copilot_node_info_json) if res.copilot_node_info_json else {} - - # Logic for context selection - blocks = [] - raw_bytes = client_buffer_bytes - from ..core import node - dummy_node = node("dummy", "dummy") # For logclean - - if cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes: - default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$' - device_prompt = node_info.get("prompt", default_prompt) - prompt_re_str = re.sub(r'(?= total_lines: - context_lines[0] = min(50, total_lines) - else: - context_lines[0] = min(context_lines[0] + 50, total_lines) - else: - if context_cmd[0] < total_cmds: - context_cmd[0] += 1 - else: - context_cmd[0] = 1 - event.app.invalidate() - - @bindings.add('c-down') - def _(event): - if context_mode[0] == MODE_LINES: - if context_lines[0] <= min(50, total_lines): - context_lines[0] = total_lines - else: - context_lines[0] = max(context_lines[0] - 50, min(50, total_lines)) - else: - if context_cmd[0] > 1: - context_cmd[0] -= 1 - else: - context_cmd[0] = total_cmds - event.app.invalidate() - - @bindings.add('tab') - def _(event): - context_mode[0] = (context_mode[0] + 1) % 3 - event.app.invalidate() - - @bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='') - - def get_current_block(): - idx = max(0, total_cmds - context_cmd[0]) - return idx, blocks[idx] - - def get_active_buffer(): - if context_mode[0] == MODE_LINES: - buffer_lines = clean_buffer.split('\n') - return '\n'.join(buffer_lines[-context_lines[0]:]) - - idx, (start, preview) = get_current_block() - if context_mode[0] == MODE_SINGLE and idx + 1 < total_cmds: - end = blocks[idx + 1][0] - active_raw = raw_bytes[start:end] - else: - active_raw = raw_bytes[start:] - return preview + "\n" + dummy_node._logclean(active_raw.decode(errors='replace'), var=True) - - def get_prompt_text(): - if context_mode[0] == MODE_LINES: - return HTML(f"Ask [Ctx: {context_lines[0]}/{total_lines}L]: ") - - lines_count = len(get_active_buffer().split('\n')) - if context_mode[0] == MODE_SINGLE: - return HTML(f"Ask [Cmd {context_cmd[0]} ~{lines_count}L]: ") - else: - return HTML(f"Ask [Cmd {context_cmd[0]}\u2192END ~{lines_count}L]: ") - - def get_toolbar(): - mode_labels = {MODE_RANGE: "RANGE", MODE_SINGLE: "SINGLE", MODE_LINES: "LINES"} - mode_label = mode_labels[context_mode[0]] - if context_mode[0] == MODE_LINES: - return HTML(f"\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {mode_label}]") - _, (_, preview) = get_current_block() - return HTML(f"\u25b6 {preview} [Tab: {mode_label}]") - - try: - session = PromptSession(history=self.copilot_history) - question = session.prompt(get_prompt_text, key_bindings=bindings, bottom_toolbar=get_toolbar) - except KeyboardInterrupt: - question = "" - - if not question or not question.strip() or question.strip() == "CANCEL": - console.print("\n[dim]Copilot cancelled.[/dim]") - request_queue.put(connpy_pb2.InteractRequest(copilot_question="CANCEL")) - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - active_buffer = get_active_buffer() - # Enrich question with history (same as local CLI) - past_questions = self.copilot_history.get_strings() - if len(past_questions) > 1: - # Limit history to last 5 questions to save tokens, excluding current - recent_history = past_questions[-6:-1] - history_text = "\n".join(f"- {q}" for q in recent_history) - enriched_question = f"Previous questions in this session:\n{history_text}\n\nCurrent Question:\n{question}" - else: - enriched_question = question - - request_queue.put(connpy_pb2.InteractRequest(copilot_question=enriched_question, copilot_context_buffer=active_buffer)) - - from rich.live import Live - live_text = "Thinking..." - panel = Panel(live_text, title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan") - result = {} - cancelled = False - - with copilot_terminal_mode(), Live(panel, console=console, refresh_per_second=10) as live: - import fcntl - flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK) - - while True: - try: - key = os.read(sys.stdin.fileno(), 1024) - if b'\x03' in key or b'\x1b' in key: - cancelled = True - request_queue.put(connpy_pb2.InteractRequest(copilot_question="CANCEL")) - msg = "Ctrl+C" if b'\x03' in key else "Esc" - console.print(f"\n[dim]Copilot cancelled via {msg}.[/dim]") - break - except OSError: - pass - - try: - chunk_res = response_queue.get(timeout=0.1) - if chunk_res is None: - break - - if chunk_res.copilot_stream_chunk: - if live_text == "Thinking...": live_text = "" - live_text += chunk_res.copilot_stream_chunk - live.update(Panel(Markdown(live_text), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) - elif chunk_res.copilot_response_json: - result = json.loads(chunk_res.copilot_response_json) - break - except queue.Empty: - continue - - fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flags) - - if cancelled: - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - if result.get("error"): - console.print(f"[red]Error: {result['error']}[/red]") - request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel")) - resume_generator() - tty.setraw(sys.stdin.fileno()) - continue - - if live_text == "Thinking..." and result.get("guide"): - console.print(Panel(Markdown(result["guide"]), title="[bold cyan]Copilot Guide[/bold cyan]", border_style="cyan")) - - commands = result.get("commands", []) - risk = result.get("risk_level", "low") - risk_style = {"low": "green", "high": "yellow", "destructive": "red"}.get(risk, "green") - - action_sent = "cancel" - if commands: - cmd_text = "\n".join(f" {i+1}. {cmd}" for i, cmd in enumerate(commands)) - console.print(Panel( - cmd_text, - title=f"[bold {risk_style}]Suggested Commands [{risk.upper()}][/bold {risk_style}]", - border_style=risk_style - )) - - try: - confirm_session = PromptSession() - confirm_bindings = KeyBindings() - @confirm_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='n') - - pt_color = "ansi" + risk_style - action = confirm_session.prompt( - HTML(f"<{pt_color}>Send commands? (y/n/e/number/range) [n]: "), - key_bindings=confirm_bindings - ) - except KeyboardInterrupt: - action = "n" - - if not action.strip(): - action = "n" - - action_l = action.lower().strip() - if action_l in ('y', 'yes', 'all'): - action_sent = "send_all" - elif action_l.startswith('e'): - action_sent = f"edit_{action_l[1:]}" if len(action_l) > 1 else "edit_all" - # For remote editing, the client edits and sends back as custom action - edit_session = PromptSession() - cmds_to_edit = [] - if action_sent.startswith("edit_") and action_sent[5:].isdigit(): - idx = int(action_sent[5:]) - 1 - if 0 <= idx < len(commands): - cmds_to_edit = [commands[idx]] - else: - cmds_to_edit = commands - - if cmds_to_edit: - target_cmd = "\n".join(cmds_to_edit) - try: - edit_bindings = KeyBindings() - @edit_bindings.add('c-j') - def _(event): - event.app.exit(result=event.app.current_buffer.text) - @edit_bindings.add('escape', eager=True) - def _(event): - event.app.exit(result='') - - edited_cmd = edit_session.prompt( - HTML("Edit commands (Ctrl+Enter to submit, Esc to cancel):\n"), - default=target_cmd, - multiline=True, - key_bindings=edit_bindings - ) - if edited_cmd.strip(): - action_sent = "custom:" + edited_cmd.strip() - else: - action_sent = "cancel" - except KeyboardInterrupt: - action_sent = "cancel" - elif action_l not in ('n', 'no', ''): - action_sent = action_l - - console.print("[dim]Returning to session...[/dim]\n") - request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent)) - resume_generator() - tty.setraw(sys.stdin.fileno()) + self._handle_remote_copilot( + res, request_queue, response_queue, + client_buffer_bytes, cmd_byte_positions, + pause_generator, resume_generator, old_tty + ) continue if res.copilot_injected_command: @@ -1496,6 +927,17 @@ class AIStub: req = connpy_pb2.ProviderRequest(provider=provider, model=model or "", api_key=api_key or "") self.stub.configure_provider(req) + @handle_errors + def configure_mcp(self, name, url=None, enabled=True, auto_load_on_os=None, remove=False): + req = connpy_pb2.MCPRequest( + name=name, + url=url or "", + enabled=enabled, + auto_load_on_os=auto_load_on_os or "", + remove=remove + ) + self.stub.configure_mcp(req) + @handle_errors def load_session_data(self, session_id): return from_struct(self.stub.load_session_data(connpy_pb2.StringRequest(value=session_id)).data) diff --git a/connpy/mcp_client.py b/connpy/mcp_client.py new file mode 100644 index 0000000..2bce2cb --- /dev/null +++ b/connpy/mcp_client.py @@ -0,0 +1,171 @@ +import asyncio +import json +import os +import threading +from typing import Any, Dict, List, Optional +import logging + +try: + from mcp import ClientSession + from mcp.client.sse import sse_client + MCP_AVAILABLE = True +except ImportError: + MCP_AVAILABLE = False + +# Silence noisy MCP and HTTP internal logging +logging.getLogger("mcp").setLevel(logging.CRITICAL) +logging.getLogger("httpx").setLevel(logging.CRITICAL) +logging.getLogger("httpcore").setLevel(logging.CRITICAL) + +class MCPClientManager: + """Manages MCP SSE client connections for connpy.""" + + _instance = None + _lock = threading.Lock() + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super(MCPClientManager, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, config=None): + if self._initialized: + return + self.config = config + self.sessions: Dict[str, Dict[str, Any]] = {} # name -> {session, stack} + self.tool_cache: Dict[str, List[Dict[str, Any]]] = {} + self._connecting: Dict[str, asyncio.Future] = {} + self._initialized = True + + async def get_tools_for_llm(self, os_filter: Optional[str] = None) -> List[Dict[str, Any]]: + """ + Fetches tools from enabled MCP servers that match the OS filter. + """ + if not MCP_AVAILABLE: + return [] + + all_llm_tools = [] + try: + mcp_config = self.config.config.get("ai", {}).get("mcp_servers", {}) + except Exception: + return [] + + async def _fetch(name, cfg): + if not cfg.get("enabled", True): return [] + + # Filter by OS if specified in config (primarily used for copilot strict matching) + auto_os = cfg.get("auto_load_on_os") + if os_filter is not None and auto_os and os_filter.lower() != auto_os.lower(): + return [] + + try: + session = await self._ensure_connected(name, cfg) + if session: + if name in self.tool_cache: return self.tool_cache[name] + llm_tools = await self._fetch_tools_as_openai(name, session) + self.tool_cache[name] = llm_tools + return llm_tools + except Exception: + pass + return [] + + tasks = [ _fetch(name, cfg) for name, cfg in mcp_config.items() ] + + if tasks: + results = await asyncio.gather(*tasks) + for tools in results: + all_llm_tools.extend(tools) + + return all_llm_tools + + async def _ensure_connected(self, name: str, cfg: Dict[str, Any]) -> Optional[Any]: + if not MCP_AVAILABLE: return None + + if name in self.sessions and self.sessions[name].get("session"): + return self.sessions[name]["session"] + + url = cfg.get("url") + if not url: + return None + + if name in self._connecting: + try: + return await asyncio.wait_for(asyncio.shield(self._connecting[name]), timeout=10.0) + except Exception: + return None + + loop = asyncio.get_running_loop() + fut = loop.create_future() + self._connecting[name] = fut + + try: + from contextlib import AsyncExitStack + stack = AsyncExitStack() + + async def _do_connect(): + read, write = await stack.enter_async_context(sse_client(url)) + session = await stack.enter_async_context(ClientSession(read, write)) + await session.initialize() + return session + + session = await asyncio.wait_for(_do_connect(), timeout=15.0) + self.sessions[name] = {"session": session, "stack": stack} + fut.set_result(session) + return session + except Exception: + fut.set_result(None) + return None + finally: + if name in self._connecting: + del self._connecting[name] + + async def _fetch_tools_as_openai(self, server_name: str, session: Any) -> List[Dict[str, Any]]: + try: + result = await asyncio.wait_for(session.list_tools(), timeout=5.0) + openai_tools = [] + for tool in result.tools: + # Use mcp_ prefix to ensure valid function name for LiteLLM/Gemini + prefixed_name = f"mcp_{server_name}__{tool.name}" + openai_tools.append({ + "type": "function", + "function": { + "name": prefixed_name, + "description": f"[{server_name}] {tool.description}", + "parameters": tool.inputSchema + } + }) + return openai_tools + except Exception: + return [] + + async def call_tool(self, full_tool_name: str, arguments: Dict[str, Any]) -> Any: + """Calls an MCP tool and returns text result.""" + if not MCP_AVAILABLE: + return "Error: MCP SDK is not installed." + + if "__" not in full_tool_name: + return f"Error: Tool {full_tool_name} is not a valid MCP tool." + + clean_name = full_tool_name[4:] if full_tool_name.startswith("mcp_") else full_tool_name + server_name, tool_name = clean_name.split("__", 1) + + if server_name not in self.sessions: + return f"Error: MCP server {server_name} is not connected." + + session = self.sessions[server_name]["session"] + try: + result = await asyncio.wait_for(session.call_tool(tool_name, arguments), timeout=60.0) + text_outputs = [content.text for content in result.content if hasattr(content, "text")] + return "\n".join(text_outputs) if text_outputs else str(result) + except Exception as e: + return f"Error calling tool {tool_name} on {server_name}: {str(e)}" + + async def shutdown(self): + """Close all SSE connections.""" + for name, data in self.sessions.items(): + stack = data.get("stack") + if stack: + await stack.aclose() + self.sessions = {} diff --git a/connpy/proto/connpy.proto b/connpy/proto/connpy.proto index 4304076..2c1a0ec 100644 --- a/connpy/proto/connpy.proto +++ b/connpy/proto/connpy.proto @@ -69,6 +69,7 @@ service AIService { rpc list_sessions (google.protobuf.Empty) returns (ValueResponse) {} rpc delete_session (StringRequest) returns (google.protobuf.Empty) {} rpc configure_provider (ProviderRequest) returns (google.protobuf.Empty) {} + rpc configure_mcp (MCPRequest) returns (google.protobuf.Empty) {} rpc load_session_data (StringRequest) returns (StructResponse) {} } @@ -282,3 +283,11 @@ message CopilotResponse { string risk_level = 3; string error = 4; } + +message MCPRequest { + string name = 1; + string url = 2; + bool enabled = 3; + string auto_load_on_os = 4; + bool remove = 5; +} diff --git a/connpy/services/ai_service.py b/connpy/services/ai_service.py index 73c5186..acd9715 100644 --- a/connpy/services/ai_service.py +++ b/connpy/services/ai_service.py @@ -60,6 +60,40 @@ class AIService(BaseService): self.config.config["ai"] = settings self.config._saveconfig(self.config.file) + def configure_mcp(self, name, url=None, enabled=None, auto_load_on_os=None, remove=False): + """Update MCP server settings in the configuration with smart merging.""" + ai_settings = self.config.config.get("ai", {}) + mcp_servers = ai_settings.get("mcp_servers", {}) + + if remove: + if name in mcp_servers: + del mcp_servers[name] + else: + # Get existing or new + server_cfg = mcp_servers.get(name, {}) + + # Partial updates + if url is not None: + server_cfg["url"] = url + + if enabled is not None: + server_cfg["enabled"] = bool(enabled) + elif "enabled" not in server_cfg: + server_cfg["enabled"] = True # Default for new entries + + if auto_load_on_os is not None: + if auto_load_on_os == "": # Explicit clear + if "auto_load_on_os" in server_cfg: + del server_cfg["auto_load_on_os"] + else: + server_cfg["auto_load_on_os"] = auto_load_on_os + + mcp_servers[name] = server_cfg + + ai_settings["mcp_servers"] = mcp_servers + self.config.config["ai"] = ai_settings + self.config._saveconfig(self.config.file) + def load_session_data(self, session_id): """Load a session's raw data by ID.""" from connpy.ai import ai diff --git a/connpy/services/execution_service.py b/connpy/services/execution_service.py index d8b4552..7588e16 100644 --- a/connpy/services/execution_service.py +++ b/connpy/services/execution_service.py @@ -14,7 +14,7 @@ class ExecutionService(BaseService): commands: List[str], variables: Optional[Dict[str, Any]] = None, parallel: int = 10, - timeout: int = 10, + timeout: int = 20, folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, @@ -62,7 +62,7 @@ class ExecutionService(BaseService): expected: List[str], variables: Optional[Dict[str, Any]] = None, parallel: int = 10, - timeout: int = 10, + timeout: int = 20, folder: Optional[str] = None, prompt: Optional[str] = None, on_node_complete: Optional[Callable] = None, @@ -139,7 +139,7 @@ class ExecutionService(BaseService): "commands": playbook["commands"], "variables": playbook.get("variables"), "parallel": options.get("parallel", parallel), - "timeout": playbook.get("timeout", options.get("timeout", 10)), + "timeout": playbook.get("timeout", options.get("timeout", 20)), "prompt": options.get("prompt"), "name": playbook.get("name", "Task") } diff --git a/connpy/tests/test_ai_copilot.py b/connpy/tests/test_ai_copilot.py index 524bcbe..9c2c874 100644 --- a/connpy/tests/test_ai_copilot.py +++ b/connpy/tests/test_ai_copilot.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, AsyncMock import json import asyncio @@ -11,12 +11,23 @@ class DummyConfig: self.config = {"ai": {"engineer_api_key": "test_key", "engineer_model": "test_model"}} self.defaultdir = "/tmp" +class MockAsyncIterator: + def __init__(self, items): + self.items = items + def __aiter__(self): + return self + async def __anext__(self): + if not self.items: + raise StopAsyncIteration + return self.items.pop(0) + @pytest.fixture -def mock_completion(): - with patch('connpy.ai.completion') as mock: +def mock_acompletion(): + # Patch acompletion inside connpy.ai.aask_copilot + with patch('litellm.acompletion') as mock: yield mock -def test_ask_copilot_tool_call(mock_completion): +def test_aask_copilot_tool_call(mock_acompletion): agent = ai(DummyConfig()) # Setup mock response for streaming @@ -32,13 +43,20 @@ def test_ask_copilot_tool_call(mock_completion): def __init__(self, content): self.choices = [MockChoice(content)] - mock_completion.return_value = [ - MockChunk("Check the interfaces and running config."), - MockChunk("\nshow ip int br\nshow run\n"), - MockChunk("low") - ] + # acompletion is awaited and returns an async iterator + async def mock_ac(*args, **kwargs): + return MockAsyncIterator([ + MockChunk("Check the interfaces and running config."), + MockChunk("\nshow ip int br\nshow run\n"), + MockChunk("low") + ]) - result = agent.ask_copilot("Router#", "What do I do?") + mock_acompletion.side_effect = mock_ac + + async def run_test(): + return await agent.aask_copilot("Router#", "What do I do?") + + result = asyncio.run(run_test()) if result["error"]: print(f"ERROR OCCURRED: {result['error']}") @@ -48,7 +66,7 @@ def test_ask_copilot_tool_call(mock_completion): assert result["risk_level"] == "low" assert result["commands"] == ["show ip int br", "show run"] -def test_ask_copilot_fallback(mock_completion): +def test_aask_copilot_fallback(mock_acompletion): agent = ai(DummyConfig()) # Setup mock response for streaming @@ -64,11 +82,17 @@ def test_ask_copilot_fallback(mock_completion): def __init__(self, content): self.choices = [MockChoice(content)] - mock_completion.return_value = [ - MockChunk("Here is some text response instead of tool call.") - ] + async def mock_ac(*args, **kwargs): + return MockAsyncIterator([ + MockChunk("Here is some text response instead of tool call.") + ]) - result = agent.ask_copilot("Router#", "What do I do?") + mock_acompletion.side_effect = mock_ac + + async def run_test(): + return await agent.aask_copilot("Router#", "What do I do?") + + result = asyncio.run(run_test()) if result["error"]: print(f"ERROR OCCURRED: {result['error']}") diff --git a/connpy/tunnels.py b/connpy/tunnels.py index e23fe35..a0ed142 100644 --- a/connpy/tunnels.py +++ b/connpy/tunnels.py @@ -47,6 +47,24 @@ class LocalStream: # signal handling not supported on some loops (e.g., Windows Proactor) pass + def stop_reading(self): + """Temporarily stop reading from stdin.""" + if self._loop and self.stdin_fd is not None: + try: + self._loop.remove_reader(self.stdin_fd) + except Exception: + pass + + def start_reading(self): + """Resume reading from stdin.""" + if self._loop and self.stdin_fd is not None: + try: + # Ensure we don't add it twice + self._loop.remove_reader(self.stdin_fd) + except Exception: + pass + self._loop.add_reader(self.stdin_fd, self._read_ready) + def teardown(self): if self._loop: try: