Compare commits

...
4 Commits
Author SHA1 Message Date
fluzzi32 01690c815f feat(grpc,copilot): add remote multi-turn mission execution loop, update pdoc and bump to v6.3.0
- Implement dynamic prompt settle detection and context block recalculation in gRPC server during Copilot execution.
- Enable multi-turn streaming interaction loop in NodeStub for remote Copilot and autonomous missions.
- Fix non-mission chat history logging in terminal UI.
- Add unit test coverage for remote Copilot command execution lifecycle.
- Regenerate HTML documentation in docs/ via pdoc.
- Bump version to v6.3.0.
2026-08-17 10:20:43 -03:00
fluzzi32 881d65b8a5 feat(copilot): implement autonomous mission mode with human
approval and prompt settle

    - Add /mission and /cancel slash commands in AIService
    - Implement autonomous multi-step execution loop with scratchpad in
  CopilotInterface
    - Add dynamic prompt settle detection in core.py after command injection
    - Add step limits with summary fallback and interactive feedback on pause
    - Add unit test coverage for mission lifecycle and state management
2026-08-14 16:31:03 -03:00
fluzzi32 3637d34f6f feat(copilot): implement 5-turn chat history & internal agent notes (<notes>) for CLI & Command Center (v6.2.0)
- Add multi-turn conversation memory (sliding window of last 5 turns / 10 messages) for Terminal Copilot.
- Introduce <notes> XML tag in Copilot response schema for tracking internal agent reasoning, tool usage, and key facts across turns.
- Instruct Copilot via System Prompt about the 5-turn pruning window and mandate summarizing critical facts in <notes>.
- Extend Command Center frontend (types.ts, useAISession.ts, AIPanel.tsx) with expandable "🧠 Internal Agent Notes / Memory" accordion and browser console response debug logging.
- Ensure 100% transparent serialization across local CLI execution, remote gRPC streams, and Web UI.
- Update documentation and bump version to v6.2.0.
2026-08-13 18:49:59 -03:00
fluzzi32 32fe18d27a perf(cli,core): lazy load Crypto and rich.markdown modules to boost startup latency and bump to v6.1.1
- Implement lazy `@property` getters for `configfile.privatekey` and `configfile.publickey` to defer RSA key loading and pycryptodome imports until explicitly accessed.
- Move top-level `Crypto` imports inside local methods in `configfile.py`, `core.py`, and `config_service.py`.
- Remove top-level `from rich.markdown import Markdown` across CLI handlers (`node_handler.py`, `ai_handler.py`, `terminal_ui.py`) and `ai.py`, deferring parser load to active rendering calls.
- Reduce CLI startup load latency from ~172 ms to ~49 ms (>3.5x speedup / ~71% latency reduction).
- Regenerate HTML documentation in `docs/` via `pdoc`.
- Bump version to v6.1.1.
2026-08-13 12:55:03 -03:00
65 changed files with 1549 additions and 475 deletions
+4 -2
View File
@@ -3,7 +3,7 @@
</p> </p>
# Connpy (v6.1.0) # Connpy (v6.3.0)
[![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/)
@@ -25,7 +25,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
### 1a. Terminal Copilot (Ctrl+Space) ### 1a. Terminal Copilot (Ctrl+Space)
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**. Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**. * **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
* **Slash Commands (`/`)**: Control the AI persona and safety settings: * **Slash Commands (`/`)**: Control the AI persona, safety settings, and mission mode:
* `/mission [objective]`: Start an autonomous multi-step investigation mission with human approval.
* `/cancel`: Abort active mission.
* `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor. * `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor.
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands. * `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
* `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`). * `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`).
+4 -2
View File
@@ -5,7 +5,7 @@
</p> </p>
# Connpy (v6.1.0) # Connpy (v6.3.0)
[![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/v/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square)](https://pypi.org/pypi/connpy/)
[![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/) [![](https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&cacheSeconds=86400)](https://pypi.org/pypi/connpy/)
@@ -27,7 +27,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
### 1a. Terminal Copilot (Ctrl+Space) ### 1a. Terminal Copilot (Ctrl+Space)
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**. Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**. * **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
* **Slash Commands (`/`)**: Control the AI persona and safety settings: * **Slash Commands (`/`)**: Control the AI persona, safety settings, and mission mode:
* `/mission [objective]`: Start an autonomous multi-step investigation mission with human approval.
* `/cancel`: Abort active mission.
* `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor. * `/architect` / `/engineer`: Swaps the agent between high-level strategist and technical executor.
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands. * `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
* `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`). * `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`).
+1 -1
View File
@@ -1 +1 @@
__version__ = "6.1.0" __version__ = "6.3.0"
+29 -5
View File
@@ -33,7 +33,6 @@ def stream_chunk_builder(*args, **kwargs):
return _stream_chunk_builder(*args, **kwargs) return _stream_chunk_builder(*args, **kwargs)
from .hooks import ClassHook, MethodHook from .hooks import ClassHook, MethodHook
from . import printer from . import printer
from rich.markdown import Markdown
from rich.panel import Panel from rich.panel import Panel
from rich.text import Text from rich.text import Text
from rich.console import Group from rich.console import Group
@@ -1183,6 +1182,7 @@ class ai:
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(resp_msg.content), title=f"[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]", border_style="architect" if current_brain == "architect" else "engineer")) self.console.print(Panel(Markdown(resp_msg.content), title=f"[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]", border_style="architect" if current_brain == "architect" else "engineer"))
if status: if status:
try: status.start() try: status.start()
@@ -1230,6 +1230,7 @@ class ai:
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect")) self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect"))
if status: if status:
try: status.start() try: status.start()
@@ -1393,6 +1394,9 @@ Rules:
5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices. 5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
6. Keep your guide concise and authoritative. 6. Keep your guide concise and authoritative.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
<notes>
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
</notes>
<guide> <guide>
Your brief tactical guide in markdown. Your brief tactical guide in markdown.
</guide> </guide>
@@ -1401,7 +1405,8 @@ Your brief tactical guide in markdown.
<risk> <risk>
low low
</risk> </risk>
8. Risk level is usually "low" for read-only/no commands. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your <notes> tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your <notes> are preserved in the assistant history, summarizing key facts in <notes> ensures you never lose vital context when older turns expire.
9. Risk level is usually "low" for read-only/no commands.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -1418,6 +1423,9 @@ Rules:
5. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block. 5. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block.
6. ULTRA-CONCISE. Keep your guide to the point. 6. ULTRA-CONCISE. Keep your guide to the point.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
<notes>
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
</notes>
<guide> <guide>
Your brief tactical guide in markdown. 3-4 sentences max. Your brief tactical guide in markdown. 3-4 sentences max.
</guide> </guide>
@@ -1428,7 +1436,8 @@ command 2
<risk> <risk>
low, high, or destructive low, high, or destructive
</risk> </risk>
8. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your <notes> tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your <notes> are preserved in the assistant history, summarizing key facts in <notes> ensures you never lose vital context when older turns expire.
9. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -1456,10 +1465,18 @@ Node: {node_name}"""
system_prompt += "\nUse these tools to validate syntax or find exact commands if needed before providing the final guide." system_prompt += "\nUse these tools to validate syntax or find exact commands if needed before providing the final guide."
messages = [ messages = [
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt}
{"role": "user", "content": user_question}
] ]
chat_history = node_info.get("chat_history", []) if node_info else []
if chat_history:
clean_history = chat_history[-10:]
for msg in clean_history:
if isinstance(msg, dict) and "role" in msg and "content" in msg:
messages.append({"role": msg["role"], "content": msg["content"]})
messages.append({"role": "user", "content": user_question})
iteration = 0 iteration = 0
max_iterations = 5 # Allow up to 5 iterations for tool usage max_iterations = 5 # Allow up to 5 iterations for tool usage
@@ -1581,6 +1598,11 @@ Node: {node_name}"""
chunk_callback(new_text) chunk_callback(new_text)
streamed_guide += new_text streamed_guide += new_text
notes = ""
notes_match = re.search(r"<notes>(.*?)</notes>", full_content, re.DOTALL)
if notes_match:
notes = notes_match.group(1).strip()
guide = "" guide = ""
commands = [] commands = []
risk_level = "low" risk_level = "low"
@@ -1605,6 +1627,7 @@ Node: {node_name}"""
return { return {
"commands": commands, "commands": commands,
"guide": guide, "guide": guide,
"notes": notes,
"risk_level": risk_level, "risk_level": risk_level,
"error": None "error": None
} }
@@ -1872,6 +1895,7 @@ class PlaybookBuilderAgent:
chunk_callback(resp_msg.content) chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls: elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown # In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content)) self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls: if not resp_msg.tool_calls:
+2 -1
View File
@@ -1,6 +1,5 @@
import sys import sys
from rich.panel import Panel from rich.panel import Panel
from rich.markdown import Markdown
from rich.rule import Rule from rich.rule import Rule
from rich.prompt import Prompt from rich.prompt import Prompt
@@ -102,6 +101,7 @@ class AIHandler:
title = "[architect][bold]Network Architect[/bold][/architect]" if responder == "architect" else "[engineer][bold]Network Engineer[/bold][/engineer]" title = "[architect][bold]Network Architect[/bold][/architect]" if responder == "architect" else "[engineer][bold]Network Engineer[/bold][/engineer]"
if not result.get("streamed"): if not result.get("streamed"):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False)) mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False))
if "usage" in result: if "usage" in result:
@@ -121,6 +121,7 @@ class AIHandler:
printer.info(f"Session '{session_id}' not found. Starting clean.") printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history: if not history:
from rich.markdown import Markdown
mdprint(Rule(style="engineer")) mdprint(Rule(style="engineer"))
mdprint(Markdown("**Networking Expert Agent**: Hi! I'm your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType 'exit' to quit.\n")) mdprint(Markdown("**Networking Expert Agent**: Hi! I'm your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType 'exit' to quit.\n"))
mdprint(Rule(style="engineer")) mdprint(Rule(style="engineer"))
+1 -1
View File
@@ -1,6 +1,5 @@
import sys import sys
import yaml import yaml
from rich.markdown import Markdown
from .. import printer from .. import printer
from ..services.exceptions import ConnpyError, InvalidConfigurationError from ..services.exceptions import ConnpyError, InvalidConfigurationError
@@ -156,6 +155,7 @@ class NodeHandler:
# Fast fail if parent folder does not exist # Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data) self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions())) printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques) new_node_data = self.forms.questions_nodes(args.data, uniques)
+208 -91
View File
@@ -11,7 +11,6 @@ from textwrap import dedent
from rich.console import Console from rich.console import Console
from rich.panel import Panel from rich.panel import Panel
from rich.markdown import Markdown
from prompt_toolkit import PromptSession from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.filters import has_completions from prompt_toolkit.filters import has_completions
@@ -37,6 +36,7 @@ class CopilotInterface:
self.session_state.setdefault('persona', 'engineer') self.session_state.setdefault('persona', 'engineer')
self.session_state.setdefault('trust_mode', False) self.session_state.setdefault('trust_mode', False)
self.session_state.setdefault('memories', []) self.session_state.setdefault('memories', [])
self.session_state.setdefault('copilot_chat_history', [])
self.session_state.setdefault('os', None) self.session_state.setdefault('os', None)
self.session_state.setdefault('prompt', None) self.session_state.setdefault('prompt', None)
self.session_state.setdefault('context_mode', self.mode_range) self.session_state.setdefault('context_mode', self.mode_range)
@@ -104,7 +104,8 @@ class CopilotInterface:
is_single = saved_mode in (self.mode_single, 1, 'SINGLE', 'single') is_single = saved_mode in (self.mode_single, 1, 'SINGLE', 'single')
if is_range or is_single: if is_range or is_single:
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1: is_mission = self.session_state.get('mission', {}).get('active', False)
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
new_cmds = total_cmds - last_total_cmds new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds initial_cmd = saved_cmd + new_cmds
else: else:
@@ -137,15 +138,17 @@ class CopilotInterface:
self.session_state['last_total_cmds'] = total_cmds self.session_state['last_total_cmds'] = total_cmds
self.session_state['last_total_lines'] = total_lines self.session_state['last_total_lines'] = total_lines
# 1. Visual Separation # 1. Visual Separation (Only show help banner on initial entry)
self.console.print("") # Real line break self.console.print("") # Real line break
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan")) self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
self.console.print(Panel( if not self.session_state.get('banner_shown', False):
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n" self.console.print(Panel(
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]", "[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n"
border_style="cyan" "Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
)) border_style="cyan"
self.console.print("\n") # Small space before the copilot prompt ))
self.session_state['banner_shown'] = True
self.console.print("") # Small space before the copilot prompt
bindings = KeyBindings() bindings = KeyBindings()
@bindings.add('c-up') @bindings.add('c-up')
@@ -227,7 +230,7 @@ class CopilotInterface:
text = app.current_buffer.text text = app.current_buffer.text
# Only show command help if typing the first command and there are no spaces # Only show command help if typing the first command and there are no spaces
if text.startswith('/') and ' ' not in text: if text.startswith('/') and ' ' not in text:
commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear'] commands = ['/os', '/prompt', '/architect', '/engineer', '/trust', '/untrust', '/memorize', '/clear', '/mission', '/cancel']
matches = [c for c in commands if c.startswith(text.lower())] matches = [c for c in commands if c.startswith(text.lower())]
if matches: if matches:
m_text = html.escape(f"Available: {' '.join(matches)}") m_text = html.escape(f"Available: {' '.join(matches)}")
@@ -235,7 +238,7 @@ class CopilotInterface:
m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']] m_label = {self.mode_range: "RANGE", self.mode_single: "SINGLE", self.mode_lines: "LINES"}[state['context_mode']]
if state['context_mode'] == self.mode_lines: if state['context_mode'] == self.mode_lines:
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]' base_str = f'\u25b6 [Tab: {m_label}] {state["context_lines"]}/{state["total_lines"]}L (Ctrl+\u2191/\u2193 adjusts lines)'
else: else:
idx = max(0, state['total_cmds'] - state['context_cmd']) idx = max(0, state['total_cmds'] - state['context_cmd'])
@@ -258,7 +261,7 @@ class CopilotInterface:
p = clean_preview(b[2]) p = clean_preview(b[2])
if p: if p:
# Truncar comandos individuales largos # Truncar comandos individuales largos
if len(p) > 25: p = p[:22] + "..." if len(p) > 25: p = p[:22].rstrip(' .,-_') + "..."
previews.append(p) previews.append(p)
if not previews: if not previews:
@@ -266,12 +269,12 @@ class CopilotInterface:
elif len(previews) <= 3: elif len(previews) <= 3:
desc = " + ".join(previews) desc = " + ".join(previews)
else: else:
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})" desc = f"{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})"
else: else:
# Modo SINGLE original # Modo SINGLE original
desc = clean_preview(blocks[idx][2]) desc = clean_preview(blocks[idx][2])
base_str = f'\u25b6 {desc} [Tab: {m_label}]' base_str = f'\u25b6 [Tab: {m_label}] {desc}'
# Wrap base_str in a style to maintain consistency and avoid glitches # Wrap base_str in a style to maintain consistency and avoid glitches
# The fg color will be inherited from bottom-toolbar global style if not specified here # The fg color will be inherited from bottom-toolbar global style if not specified here
@@ -305,7 +308,9 @@ class CopilotInterface:
('/trust', 'Enable auto-execute'), ('/trust', 'Enable auto-execute'),
('/untrust', 'Disable auto-execute'), ('/untrust', 'Disable auto-execute'),
('/memorize', 'Add fact to memory'), ('/memorize', 'Add fact to memory'),
('/clear', 'Clear memory') ('/clear', 'Clear memory'),
('/mission', 'Start autonomous mission'),
('/cancel', 'Cancel active mission')
] ]
for cmd, desc in commands: for cmd, desc in commands:
if cmd.startswith(cmd_part.lower()): if cmd.startswith(cmd_part.lower()):
@@ -313,68 +318,162 @@ class CopilotInterface:
copilot_completer = SlashCommandCompleter() copilot_completer = SlashCommandCompleter()
def _finalize_mission(reason="completed", final_guide=""):
mission = self.session_state.get('mission', {})
if not mission or not mission.get('active', False):
return
mission['active'] = False
if reason == "completed":
self.console.print("\n[bold green]🎉 Mission Completed[/bold green]")
elif reason == "user_cancelled":
self.console.print("\n[yellow]Mission cancelled by user.[/yellow]")
elif reason == "limit_reached":
self.console.print("\n[yellow]Mission step limit reached.[/yellow]")
final_notes = "\n".join(mission.get('scratchpad_notes', []))
guide = final_guide or mission.get('last_guide', '')
asst_msg = f"Notes: {final_notes}\nGuide: {guide}" if final_notes else guide
hist = self.session_state.setdefault("copilot_chat_history", [])
hist.append({"role": "user", "content": f"/mission {mission.get('goal', '')}"})
hist.append({"role": "assistant", "content": asst_msg})
self.session_state["copilot_chat_history"] = hist[-10:]
while True: while True:
# 2. Ask question overrides = {}
from prompt_toolkit.styles import Style # Check for active mission auto-looping
c_contrast = self._get_theme_color("contrast", "gray") mission = self.session_state.get('mission', {})
ui_style = Style.from_dict({ is_mission = mission.get('active', False) and not mission.get('paused', False)
'bottom-toolbar': f'fg:{c_contrast}',
})
session = PromptSession( if is_mission:
history=self.history, # Force mode_range for mission mode
input=self.pt_input, state['context_mode'] = self.mode_range
output=self.pt_output, self.session_state['context_mode'] = self.mode_range
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
# We use an internal try/finally to ensure that if something fails in prompt_async,
# we don't leave the terminal in a strange state.
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state['cancelled'] = True
question = ""
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']: if mission.get('start_block_idx') is None:
return "cancel", None, None mission['start_block_idx'] = state['total_cmds']
# 3. Process Input via AIService start_idx = mission.get('start_block_idx', state['total_cmds'])
directive = self.ai_service.process_copilot_input(question, self.session_state) cmds_since_start = max(1, (state['total_cmds'] - start_idx) + 1)
state['context_cmd'] = max(state.get('context_cmd', 1), cmds_since_start)
self.session_state['context_cmd'] = state['context_cmd']
if directive["action"] == "state_update": step = mission.get('step', 1)
msg = directive['message'] max_steps = mission.get('max_steps', 10)
state['toolbar_msg'] = msg
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh(): if step > max_steps:
await asyncio.sleep(3.1) ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
# Only invalidate if the message hasn't been replaced by a newer one c_warn = self._get_theme_color("warning", "yellow")
if state.get('toolbar_msg') == msg: import html
state['toolbar_msg'] = '' # Explicitly clear p_warn = html.escape(f"[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: ")
try: try:
from prompt_toolkit.application.current import get_app ext_ans = await ext_session.prompt_async(HTML(f'<style fg="{c_warn}" bold="true">{p_warn}</style>'))
app = get_app() except (KeyboardInterrupt, EOFError):
if app: app.invalidate() ext_ans = 'n'
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one if (ext_ans or 'y').lower().strip() in ('y', 'yes'):
sys.stdout.write('\x1b[1A\x1b[2K') mission['max_steps'] += 10
sys.stdout.flush() else:
continue goal = mission.get('goal', '')
question = f"[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}"
clean_question = question
mission['active'] = False
is_mission = False
self.console.print(f"\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]")
if is_mission:
goal = mission.get('goal', '')
step = mission.get('step', 1)
question = f"[MISSION STEP {step}]: Continue analysis of: {goal}"
scratchpad = mission.get('scratchpad_notes', [])
if scratchpad:
notes_text = "\n".join(f"- {n}" for n in scratchpad)
question += f"\n\nPast Mission Notes:\n{notes_text}"
clean_question = question
self.console.print(f"\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]")
elif not is_mission and 'clean_question' in locals() and clean_question.startswith("[MISSION SUMMARY]"):
# Pass-through to AI execution for generating final summary
pass
else: else:
# Clean the toolbar message when a real question is asked # 2. Ask question
state['toolbar_msg'] = '' from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color("contrast", "gray")
ui_style = Style.from_dict({
'bottom-toolbar': f'fg:{c_contrast}',
})
clean_question = directive.get("clean_prompt", question) session = PromptSession(
overrides = directive.get("overrides", {}) history=self.history,
input=self.pt_input,
output=self.pt_output,
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state['cancelled'] = True
question = ""
if state['cancelled'] or not question.strip() or question.strip().lower() in ['cancel', 'exit', 'quit']:
_finalize_mission("user_cancelled")
return "cancel", None, None
# 3. Process Input via AIService
directive = self.ai_service.process_copilot_input(question, self.session_state)
if directive["action"] == "mission_start":
mission = self.session_state.get('mission', {})
mission['start_block_idx'] = state['total_cmds']
state['context_mode'] = self.mode_range
self.session_state['context_mode'] = self.mode_range
state['context_cmd'] = 1
self.session_state['context_cmd'] = 1
clean_question = f"[MISSION STEP 1]: {directive['clean_prompt']}"
overrides = directive.get("overrides", {})
self.console.print(f"\n[bold cyan]🤖 Starting Mission: {directive['clean_prompt']}[/bold cyan]")
elif directive["action"] == "mission_cancel":
_finalize_mission("user_cancelled")
state['toolbar_msg'] = 'Mission cancelled'
continue
elif directive["action"] == "state_update":
msg = directive['message']
state['toolbar_msg'] = msg
state['msg_expiry'] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh():
await asyncio.sleep(3.1)
if state.get('toolbar_msg') == msg:
state['toolbar_msg'] = ''
try:
from prompt_toolkit.application.current import get_app
app = get_app()
if app: app.invalidate()
except: pass
asyncio.create_task(delayed_refresh())
sys.stdout.write('\x1b[1A\x1b[2K')
sys.stdout.flush()
continue
else:
state['toolbar_msg'] = ''
if mission.get('active', False):
mission['paused'] = False
step = mission.get('step', 1)
goal = mission.get('goal', '')
clean_question = f"[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}"
self.console.print(f"\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]")
overrides = directive.get("overrides", {})
else:
clean_question = directive.get("clean_prompt", question)
overrides = directive.get("overrides", {})
# Merge node_info with session_state and overrides # Merge node_info with session_state and overrides
merged_node_info = node_info.copy() merged_node_info = node_info.copy()
@@ -383,20 +482,12 @@ class CopilotInterface:
merged_node_info['persona'] = self.session_state['persona'] merged_node_info['persona'] = self.session_state['persona']
merged_node_info['trust'] = self.session_state['trust_mode'] merged_node_info['trust'] = self.session_state['trust_mode']
merged_node_info['memories'] = list(self.session_state['memories']) merged_node_info['memories'] = list(self.session_state['memories'])
merged_node_info['chat_history'] = list(self.session_state.get('copilot_chat_history', []))
for k, v in overrides.items(): for k, v in overrides.items():
merged_node_info[k] = v merged_node_info[k] = v
# Enrich question
past = self.history.get_strings()
if len(past) > 1:
clean_past = [q for q in past[-6:-1] if not q.startswith('/')]
if clean_past:
history_text = "\n".join(f"- {q}" for q in clean_past)
clean_question = f"Previous questions:\n{history_text}\n\nCurrent Question:\n{clean_question}"
# 3. AI Execution # 3. AI Execution
# Use persona from overrides (one-shot) or from session state
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer')) active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
persona_color = self._get_theme_color(active_persona, fallback="cyan") persona_color = self._get_theme_color(active_persona, fallback="cyan")
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer" persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
@@ -423,7 +514,6 @@ class CopilotInterface:
nonlocal live_text, first_chunk nonlocal live_text, first_chunk
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule( self.console.print(Rule(
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
style=persona_color style=persona_color
@@ -432,7 +522,6 @@ class CopilotInterface:
live_text += text live_text += text
md_parser.feed(text) md_parser.feed(text)
# Check for interruption during AI call
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info)) ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
try: try:
@@ -441,13 +530,12 @@ class CopilotInterface:
result = await ai_task result = await ai_task
except asyncio.CancelledError: except asyncio.CancelledError:
status_spinner.stop() status_spinner.stop()
_finalize_mission("user_cancelled")
return "cancel", None, None return "cancel", None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk: if not first_chunk:
md_parser.flush() md_parser.flush()
self.console.print(Rule(style=persona_color)) self.console.print(Rule(style=persona_color))
@@ -455,14 +543,35 @@ class CopilotInterface:
if not result or result.get("error"): if not result or result.get("error"):
if first_chunk and result and result.get("error"): if first_chunk and result and result.get("error"):
self.console.print(f"[red]Error: {result['error']}[/red]") self.console.print(f"[red]Error: {result['error']}[/red]")
_finalize_mission("user_cancelled")
return "cancel", None, None return "cancel", None, None
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get("guide"): if first_chunk and result and result.get("guide"):
from rich.markdown import Markdown
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color)) self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
# Update copilot_chat_history or mission scratchpad
if result and not result.get("error"):
guide = result.get("guide", "")
notes = result.get("notes", "")
mission = self.session_state.get('mission', {})
if mission.get('active', False):
if notes:
mission.setdefault('scratchpad_notes', []).append(notes)
if guide:
mission['last_guide'] = guide
else:
if guide or notes:
asst_msg = f"Notes: {notes}\nGuide: {guide}" if notes else guide
hist = self.session_state.setdefault("copilot_chat_history", [])
hist.append({"role": "user", "content": clean_question})
hist.append({"role": "assistant", "content": asst_msg})
self.session_state["copilot_chat_history"] = hist[-10:]
commands = result.get("commands", []) commands = result.get("commands", [])
mission = self.session_state.get('mission', {})
if not commands: if not commands:
if mission.get('active', False):
_finalize_mission("completed", result.get("guide", ""))
self.console.print("") self.console.print("")
return "continue", None, None return "continue", None, None
@@ -471,11 +580,12 @@ class CopilotInterface:
style_color = self._get_theme_color(risk_style, fallback="green") style_color = self._get_theme_color(risk_style, fallback="green")
cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands)) cmd_text = "\n".join(f" {i+1}. {c}" for i, c in enumerate(commands))
# Explicitly use 'bold style_color' for both TITLE and BORDER to ensure maximum consistency
self.console.print(Panel(cmd_text, title=f"[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]", border_style=f"bold {style_color}")) self.console.print(Panel(cmd_text, title=f"[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]", border_style=f"bold {style_color}"))
if merged_node_info.get('trust', False) and risk != "destructive": if merged_node_info.get('trust', False) and risk != "destructive":
self.console.print(f"[dim]⚙️ Auto-executing (Trust Mode)[/dim]") self.console.print(f"[dim]⚙️ Auto-executing (Trust Mode)[/dim]")
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
return "send_all", commands, None return "send_all", commands, None
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output) confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -487,41 +597,39 @@ class CopilotInterface:
import html import html
try: try:
p_text = html.escape(f"Send? (y/n/e/range) [n]: ") p_text = html.escape(f"Send? (y/n/e/range) [n]: ")
# Use the EXACT same style_color and force bold="true" for Prompt-Toolkit
action = await confirm_session.prompt_async(HTML(f'<style fg="{style_color}" bold="true">{p_text}</style>'), key_bindings=c_bindings) action = await confirm_session.prompt_async(HTML(f'<style fg="{style_color}" bold="true">{p_text}</style>'), key_bindings=c_bindings)
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
_finalize_mission("user_cancelled", result.get("guide", ""))
self.console.print("") self.console.print("")
return "continue", None, None return "continue", None, None
def parse_indices(text, max_len): def parse_indices(text, max_len):
"""Helper to parse '1-3, 5, 7' into [0, 1, 2, 4, 6]."""
indices = [] indices = []
# Replace commas with spaces and split
parts = text.replace(',', ' ').split() parts = text.replace(',', ' ').split()
for part in parts: for part in parts:
if '-' in part: if '-' in part:
try: try:
start, end = map(int, part.split('-')) start, end = map(int, part.split('-'))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end)) indices.extend(range(start-1, end))
except: continue except: continue
elif part.isdigit(): elif part.isdigit():
indices.append(int(part)-1) indices.append(int(part)-1)
# Filter valid indices and remove duplicates
return [i for i in sorted(set(indices)) if 0 <= i < max_len] return [i for i in sorted(set(indices)) if 0 <= i < max_len]
action_l = (action or "n").lower().strip() action_l = (action or "n").lower().strip()
if action_l in ('y', 'yes', 'all'): if action_l in ('y', 'yes', 'all'):
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
return "send_all", commands, None return "send_all", commands, None
# Check for numeric selection (e.g., "1, 2-4")
if re.match(r'^[0-9,\-\s]+$', action_l): if re.match(r'^[0-9,\-\s]+$', action_l):
selected_idxs = parse_indices(action_l, len(commands)) selected_idxs = parse_indices(action_l, len(commands))
if selected_idxs: if selected_idxs:
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
return "send_all", [commands[i] for i in selected_idxs], None return "send_all", [commands[i] for i in selected_idxs], None
elif action_l.startswith('e'): elif action_l.startswith('e'):
# Check if it's a selective edit like 'e1-2'
selection_str = action_l[1:].strip() selection_str = action_l[1:].strip()
if selection_str: if selection_str:
idxs = parse_indices(selection_str, len(commands)) idxs = parse_indices(selection_str, len(commands))
@@ -547,14 +655,23 @@ class CopilotInterface:
default=target, multiline=True, key_bindings=e_bindings default=target, multiline=True, key_bindings=e_bindings
) )
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
if mission.get('active', False):
mission['paused'] = True
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
self.console.print("") self.console.print("")
return "continue", None, None return "continue", None, None
if edited and edited.strip(): if edited and 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()] lines = [l.strip() for l in edited.split('\n') if l.strip()]
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
return "custom", None, lines return "custom", None, lines
# User rejected/cancelled the commands
if mission.get('active', False):
mission['paused'] = True
self.console.print("\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]")
self.console.print("") self.console.print("")
return "continue", None, None return "continue", None, None
+29 -5
View File
@@ -6,8 +6,6 @@ import re
import sys import sys
import yaml import yaml
import shutil import shutil
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from pathlib import Path from pathlib import Path
from copy import deepcopy from copy import deepcopy
from .hooks import MethodHook, ClassHook from .hooks import MethodHook, ClassHook
@@ -139,16 +137,39 @@ class configfile:
self.connections = config["connections"] self.connections = config["connections"]
self.profiles = config["profiles"] self.profiles = config["profiles"]
self._privatekey_obj = None
self._publickey_obj = None
if not os.path.exists(self.key): if not os.path.exists(self.key):
self._createkey(self.key) self._createkey(self.key)
with open(self.key) as f:
self.privatekey = RSA.import_key(f.read())
self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing # Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile): if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache() self._generate_nodes_cache()
@property
def privatekey(self):
if getattr(self, '_privatekey_obj', None) is None:
from Crypto.PublicKey import RSA
if not os.path.exists(self.key):
self._createkey(self.key)
with open(self.key) as f:
self._privatekey_obj = RSA.import_key(f.read())
return self._privatekey_obj
@privatekey.setter
def privatekey(self, value):
self._privatekey_obj = value
@property
def publickey(self):
if getattr(self, '_publickey_obj', None) is None:
self._publickey_obj = self.privatekey.publickey()
return self._publickey_obj
@publickey.setter
def publickey(self, value):
self._publickey_obj = value
def get_effective_setting(self, key, default=None): def get_effective_setting(self, key, default=None):
"""Get config setting with shared fallback for inheritable keys.""" """Get config setting with shared fallback for inheritable keys."""
@@ -297,6 +318,7 @@ class configfile:
def _createkey(self, keyfile): def _createkey(self, keyfile):
#Create key file #Create key file
from Crypto.PublicKey import RSA
key = RSA.generate(2048) key = RSA.generate(2048)
with open(keyfile,'wb') as f: with open(keyfile,'wb') as f:
f.write(key.export_key('PEM')) f.write(key.export_key('PEM'))
@@ -623,6 +645,8 @@ class configfile:
if keyfile is None: if keyfile is None:
keyfile = self.key keyfile = self.key
with open(keyfile) as f: with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read()) key = RSA.import_key(f.read())
f.close() f.close()
publickey = key.publickey() publickey = key.publickey()
+30 -7
View File
@@ -4,8 +4,6 @@ import os
import re import re
import pexpect import pexpect
import shlex import shlex
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import ast import ast
from time import sleep,time from time import sleep,time
import datetime import datetime
@@ -274,6 +272,8 @@ class node:
if keyfile is None: if keyfile is None:
keyfile = self.key keyfile = self.key
if keyfile is not None: if keyfile is not None:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f: with open(keyfile) as f:
key = RSA.import_key(f.read()) key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key) decryptor = PKCS1_OAEP.new(key)
@@ -791,6 +791,7 @@ class node:
# Save history back to stream for persistence in current session # Save history back to stream for persistence in current session
stream.copilot_history = interface.history stream.copilot_history = interface.history
stream.copilot_state = interface.session_state stream.copilot_state = interface.session_state
interface.session_state['banner_shown'] = False
ai_service = AIService(config) ai_service = AIService(config)
@@ -821,6 +822,32 @@ class node:
node_info=node_info, node_info=node_info,
on_ai_call=on_ai_call on_ai_call=on_ai_call
) )
if action in ("send_all", "custom"):
cmds_to_send = commands if action == "send_all" else custom_cmd
await self.inject_commands(cmds_to_send, child_fd)
# Dynamic Wait for Device Prompt Settle
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
start_t = time()
last_len = 0
quiet_count = 0
while time() - start_t < 60:
await asyncio.sleep(0.15)
cur_bytes = self.mylog.getvalue()
if len(cur_bytes) == last_len:
quiet_count += 1
else:
quiet_count = 0
last_len = len(cur_bytes)
clean_txt = self._logclean(cur_bytes.decode(errors='replace'), True).strip()
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
last_line = lines[-1] if lines else ""
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
break
raw_bytes = self.mylog.getvalue()
continue
if action == "continue": if action == "continue":
continue continue
break break
@@ -832,11 +859,7 @@ class node:
elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'): elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'):
stream._loop.add_reader(stream.stdin_fd, stream._read_ready) stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
if action in ("send_all", "custom"): os.write(child_fd, b'\x15\r')
cmds_to_send = commands if action == "send_all" else custom_cmd
await self.inject_commands(cmds_to_send, child_fd)
else:
os.write(child_fd, b'\x15\r')
except Exception as e: except Exception as e:
import traceback import traceback
print(f"\n[ERROR in Copilot Handler] {e}", flush=True) print(f"\n[ERROR in Copilot Handler] {e}", flush=True)
+70 -3
View File
@@ -277,7 +277,8 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
is_single = saved_mode in (1, 'SINGLE', 'single') is_single = saved_mode in (1, 'SINGLE', 'single')
if is_range or is_single: if is_range or is_single:
if last_total_cmds is not None and total_cmds > last_total_cmds and saved_cmd > 1: is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
new_cmds = total_cmds - last_total_cmds new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds initial_cmd = saved_cmd + new_cmds
else: else:
@@ -321,6 +322,14 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
)) ))
while True: while True:
# Refresh latest buffer from terminal log
raw_bytes = n.mylog.getvalue() if hasattr(n, 'mylog') else buffer
if not isinstance(raw_bytes, bytes):
raw_bytes = str(raw_bytes).encode()
from connpy.utils import log_cleaner
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
buffer = cleaned_buffer
# 0. Drain the queue of any stale messages before starting a new interaction # 0. Drain the queue of any stale messages before starting a new interaction
while not remote_stream.copilot_queue.empty(): while not remote_stream.copilot_queue.empty():
try: try:
@@ -454,16 +463,74 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
if action == "send_all": if action == "send_all":
commands = result.get("commands", []) commands = result.get("commands", [])
await n.inject_commands(commands, child_fd, on_inject=on_inject) await n.inject_commands(commands, child_fd, on_inject=on_inject)
return
elif action.startswith("custom:"): elif action.startswith("custom:"):
custom_cmds_raw = action[7:] custom_cmds_raw = action[7:]
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()] custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()]
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject) await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
return
else: else:
os.write(child_fd, b'\x15\r') os.write(child_fd, b'\x15\r')
return return
# Dynamic Wait for Device Prompt Settle
prompt_pattern = node_info.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$')
import time
import re
start_t = time.time()
last_len = 0
quiet_count = 0
while time.time() - start_t < 60:
await asyncio.sleep(0.15)
cur_bytes = n.mylog.getvalue()
if len(cur_bytes) == last_len:
quiet_count += 1
else:
quiet_count = 0
last_len = len(cur_bytes)
clean_txt = n._logclean(cur_bytes.decode(errors='replace'), True).strip()
lines = [l.strip() for l in clean_txt.split('\n') if l.strip()]
last_line = lines[-1] if lines else ""
if quiet_count >= 2 and re.search(prompt_pattern, last_line):
break
# Recalculate context blocks with updated buffer
raw_bytes = n.mylog.getvalue()
from connpy.utils import log_cleaner
cleaned_buffer = log_cleaner(raw_bytes.decode(errors='replace'))
last_line = cleaned_buffer.split('\n')[-1].strip() if cleaned_buffer.strip() else "(prompt)"
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
node_info["context_blocks"] = blocks
total_cmds = len(blocks)
total_lines = len(cleaned_buffer.split('\n'))
last_total_cmds = session_state.get('last_total_cmds', None)
saved_cmd = session_state.get('context_cmd', 1)
is_mission = session_state.get('mission', {}).get('active', False) or (isinstance(node_info, dict) and node_info.get('mission', {}).get('active', False))
if last_total_cmds is not None and total_cmds > last_total_cmds and (saved_cmd > 1 or is_mission):
new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds
else:
initial_cmd = saved_cmd
session_state['context_cmd'] = max(1, initial_cmd)
session_state['last_total_cmds'] = total_cmds
session_state['last_total_lines'] = total_lines
node_info['context_cmd'] = min(session_state['context_cmd'], max(1, total_cmds))
node_info['context_lines'] = min(session_state.get('context_lines', 50), max(1, total_lines))
node_info_json = json.dumps(node_info)
preview_str = raw_bytes[-200:].decode(errors='replace') if isinstance(raw_bytes, bytes) else str(raw_bytes)[-200:]
# Signal prompt settle and send updated blocks to client
response_queue.put(connpy_pb2.InteractResponse(
copilot_prompt=True,
copilot_buffer_preview=preview_str,
copilot_node_info_json=node_info_json
))
continue
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler)) asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
except Exception as e: except Exception as e:
import traceback import traceback
+30 -9
View File
@@ -44,7 +44,7 @@ class NodeStub:
self.config = config self.config = config
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty): def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty):
import json, asyncio, termios, sys, tty, queue import json, asyncio, termios, sys, tty, queue, os
from ..core import copilot_terminal_mode from ..core import copilot_terminal_mode
from . import connpy_pb2 from . import connpy_pb2
@@ -62,6 +62,7 @@ class NodeStub:
) )
self.copilot_history = interface.history self.copilot_history = interface.history
self.copilot_state = interface.session_state self.copilot_state = interface.session_state
interface.session_state['banner_shown'] = False
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info): async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
# Send request to server # Send request to server
@@ -84,6 +85,7 @@ class NodeStub:
# Wrap in async loop # Wrap in async loop
async def run_remote_copilot(): async def run_remote_copilot():
nonlocal blocks, node_info
while True: while True:
action, commands, custom_cmd = await interface.run_session( action, commands, custom_cmd = await interface.run_session(
raw_bytes=bytes(client_buffer_bytes), raw_bytes=bytes(client_buffer_bytes),
@@ -97,6 +99,32 @@ class NodeStub:
request_queue.put(connpy_pb2.InteractRequest(copilot_action="continue")) request_queue.put(connpy_pb2.InteractRequest(copilot_action="continue"))
continue continue
if action in ("send_all", "custom"):
action_sent = "cancel"
if action == "send_all" and commands:
action_sent = f"custom:{chr(10).join(commands)}"
elif action == "custom" and custom_cmd:
action_sent = f"custom:{chr(10).join(custom_cmd)}"
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
print("\r\n\033[2m [Remote] Ejecutando comandos y esperando al prompt...\033[0m\r\n", flush=True)
while True:
try:
prompt_res = response_queue.get(timeout=0.1)
if prompt_res is None:
return "cancel", None, None
if prompt_res.stdout_data:
os.write(sys.stdout.fileno(), prompt_res.stdout_data)
client_buffer_bytes.extend(prompt_res.stdout_data)
if prompt_res.copilot_prompt:
if prompt_res.copilot_node_info_json:
node_info = json.loads(prompt_res.copilot_node_info_json)
blocks = node_info.get("context_blocks", [])
break
except queue.Empty:
await asyncio.sleep(0.05)
continue
return action, commands, custom_cmd return action, commands, custom_cmd
with copilot_terminal_mode(): with copilot_terminal_mode():
@@ -104,14 +132,7 @@ class NodeStub:
print("\033[2m Returning to session...\033[0m", flush=True) print("\033[2m Returning to session...\033[0m", flush=True)
# Prepare final action for server # Prepare final action for server
action_sent = "cancel" request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel"))
if action == "send_all" and commands:
# In remote mode, send the selected commands as a custom block
# so the server executes exactly what the user picked (e.g., selection '1')
action_sent = f"custom:{chr(10).join(commands)}"
elif action == "custom" and custom_cmd:
action_sent = f"custom:{chr(10).join(custom_cmd)}"
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
resume_generator() resume_generator()
tty.setraw(sys.stdin.fileno()) tty.setraw(sys.stdin.fileno())
+20
View File
@@ -207,6 +207,26 @@ class AIService(BaseService):
else: else:
return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}} return {"action": "execute", "clean_prompt": args, "overrides": {"trust": False}}
elif cmd == "/mission":
if args:
session_state['mission'] = {
'active': True,
'goal': args,
'step': 1,
'max_steps': 10,
'start_block_idx': None,
'scratchpad_notes': []
}
return {"action": "mission_start", "clean_prompt": args, "overrides": {}}
else:
return {"action": "state_update", "message": "Usage: /mission <objective_description>"}
elif cmd in ("/cancel", "/abort"):
if session_state.get('mission', {}).get('active'):
session_state['mission']['active'] = False
return {"action": "mission_cancel", "message": "Mission cancelled"}
return {"action": "state_update", "message": "No active mission to cancel"}
# Unknown command, execute normally # Unknown command, execute normally
return {"action": "execute", "clean_prompt": text, "overrides": {}} return {"action": "execute", "clean_prompt": text, "overrides": {}}
-2
View File
@@ -2,8 +2,6 @@ import os
import shutil import shutil
import base64 import base64
from typing import Any, Dict from typing import Any, Dict
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from .base import BaseService from .base import BaseService
from .exceptions import ConnpyError, InvalidConfigurationError, NodeNotFoundError from .exceptions import ConnpyError, InvalidConfigurationError, NodeNotFoundError
+160 -13
View File
@@ -27,22 +27,21 @@ def mock_acompletion():
with patch('litellm.acompletion') as mock: with patch('litellm.acompletion') as mock:
yield mock yield mock
class MockDelta:
def __init__(self, content):
self.content = content
class MockChoice:
def __init__(self, content):
self.delta = MockDelta(content)
class MockChunk:
def __init__(self, content):
self.choices = [MockChoice(content)]
def test_aask_copilot_tool_call(mock_acompletion): def test_aask_copilot_tool_call(mock_acompletion):
agent = ai(DummyConfig()) agent = ai(DummyConfig())
# Setup mock response for streaming
class MockDelta:
def __init__(self, content):
self.content = content
class MockChoice:
def __init__(self, content):
self.delta = MockDelta(content)
class MockChunk:
def __init__(self, content):
self.choices = [MockChoice(content)]
# acompletion is awaited and returns an async iterator # acompletion is awaited and returns an async iterator
async def mock_ac(*args, **kwargs): async def mock_ac(*args, **kwargs):
return MockAsyncIterator([ return MockAsyncIterator([
@@ -557,7 +556,155 @@ def test_copilot_single_mode_retains_command_block():
assert interface_custom.session_state.get('context_cmd') == 4 assert interface_custom.session_state.get('context_cmd') == 4
def test_aask_copilot_notes_parsing(mock_acompletion):
agent = ai(DummyConfig())
async def mock_ac(*args, **kwargs):
return MockAsyncIterator([
MockChunk("<notes>Tool used: mcp_cisco__search. MTU mismatch suspected.</notes>"),
MockChunk("<guide>Check MTU on eth0.</guide>"),
MockChunk("<risk>low</risk>")
])
mock_acompletion.side_effect = mock_ac
async def run_test():
return await agent.aask_copilot("Router#", "Why is ping failing?", node_info={"chat_history": [{"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi"}]})
result = asyncio.run(run_test())
assert result["error"] is None
assert result["notes"] == "Tool used: mcp_cisco__search. MTU mismatch suspected."
assert result["guide"] == "Check MTU on eth0."
# Check that messages passed to acompletion included chat_history
call_args = mock_acompletion.call_args[1]
msgs = call_args["messages"]
assert len(msgs) == 4 # system, user(hello), asst(hi), current_user
assert msgs[1]["content"] == "hello"
assert msgs[2]["content"] == "hi"
assert msgs[3]["content"] == "Why is ping failing?"
def test_process_copilot_input_mission_command():
from connpy.services.ai_service import AIService
class MockConfig:
def __init__(self):
self.config = {"ai": {}}
self.defaultdir = "/tmp"
svc = AIService(MockConfig())
session_state = {}
# Test /mission command
res = svc.process_copilot_input("/mission check bgp neighbors", session_state)
assert res["action"] == "mission_start"
assert res["clean_prompt"] == "check bgp neighbors"
assert session_state["mission"]["active"] is True
assert session_state["mission"]["goal"] == "check bgp neighbors"
assert session_state["mission"]["step"] == 1
# Test /cancel command
res_cancel = svc.process_copilot_input("/cancel", session_state)
assert res_cancel["action"] == "mission_cancel"
assert session_state["mission"]["active"] is False
def test_copilot_mission_mode_lifecycle():
from connpy.cli.terminal_ui import CopilotInterface
class MockConfig:
def __init__(self):
self.config = {"ai": {}}
self.defaultdir = "/tmp"
raw_bytes = b"router# show ip bgp\r\nrouter# "
blocks = [
(0, 20, "router# show ip bgp"),
(20, 30, "router#")
]
async def mock_ai_call(active_buffer, question, on_chunk, node_info):
return {"guide": "BGP is UP", "notes": "Neighbor 10.0.0.1 established", "commands": [], "risk_level": "low"}
session_state = {
'persona': 'engineer',
'trust_mode': False,
'memories': [],
'copilot_chat_history': [],
'os': None,
'prompt': None,
'mission': {
'active': True,
'goal': 'verify bgp status',
'step': 1,
'max_steps': 10,
'start_block_idx': 0,
'scratchpad_notes': []
}
}
interface = CopilotInterface(MockConfig(), session_state=session_state)
asyncio.run(interface.run_session(raw_bytes=raw_bytes, node_info={"name": "test"}, on_ai_call=mock_ai_call, blocks=blocks))
# Mission completed (no commands), active flag should be set to False
assert interface.session_state['mission']['active'] is False
# Check that a single consolidated turn was added to copilot_chat_history
hist = interface.session_state.get('copilot_chat_history', [])
assert len(hist) == 2
assert hist[0]['role'] == 'user'
assert hist[0]['content'] == '/mission verify bgp status'
assert hist[1]['role'] == 'assistant'
assert 'Notes: Neighbor 10.0.0.1 established' in hist[1]['content']
assert 'Guide: BGP is UP' in hist[1]['content']
def test_copilot_mission_pause_feedback_and_cancel():
from connpy.cli.terminal_ui import CopilotInterface
from unittest.mock import patch
class MockConfig:
def __init__(self):
self.config = {"ai": {}}
self.defaultdir = "/tmp"
raw_bytes = b"router# show ip bgp\r\nrouter# "
blocks = [
(0, 20, "router# show ip bgp"),
(20, 30, "router#")
]
# AI returns commands to trigger rejection
async def mock_ai_call(active_buffer, question, on_chunk, node_info):
return {"guide": "Checking routes", "notes": "Need more info", "commands": ["clear ip bgp *"], "risk_level": "high"}
session_state = {
'persona': 'engineer',
'trust_mode': False,
'memories': [],
'copilot_chat_history': [],
'os': None,
'prompt': None,
'mission': {
'active': True,
'goal': 'troubleshoot routing',
'step': 2,
'max_steps': 10,
'start_block_idx': 0,
'scratchpad_notes': []
}
}
interface = CopilotInterface(MockConfig(), session_state=session_state)
# Simulate user rejecting the command ('n')
with patch('prompt_toolkit.PromptSession.prompt_async', return_value='n'):
action, commands, custom_cmd = asyncio.run(interface.run_session(
raw_bytes=raw_bytes, node_info={"name": "test"}, on_ai_call=mock_ai_call, blocks=blocks
))
# Mission should now be paused (active=True, paused=True)
assert interface.session_state['mission']['active'] is True
assert interface.session_state['mission']['paused'] is True
+77
View File
@@ -211,3 +211,80 @@ class TestGRPCIntegration:
finally: finally:
if os.path.exists(temp_path): if os.path.exists(temp_path):
os.remove(temp_path) os.remove(temp_path)
@patch("sys.stdin.fileno", return_value=0)
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("tty.setraw")
@patch("os.write")
def test_handle_remote_copilot_command_execution_loop(self, mock_os_write, mock_setraw, mock_getattr, mock_setattr, mock_fileno):
import queue
from connpy.grpc_layer.stubs import NodeStub
from connpy.cli.terminal_ui import CopilotInterface
mock_getattr.return_value = [0, 0, 0, 0, 0, 0, [0] * 32]
mock_channel = MagicMock()
stub = NodeStub(mock_channel, "localhost:8048")
initial_node_info = {
"name": "r1",
"context_blocks": [[0, 10, "router#"]]
}
res = connpy_pb2.InteractResponse(
copilot_prompt=True,
copilot_node_info_json=json.dumps(initial_node_info)
)
request_queue = queue.Queue()
response_queue = queue.Queue()
client_buffer_bytes = bytearray(b"router# ")
pause_gen = MagicMock()
resume_gen = MagicMock()
old_tty = [0] * 7
call_count = 0
received_blocks_per_call = []
async def mock_run_session(raw_bytes, node_info, on_ai_call, blocks):
nonlocal call_count
call_count += 1
received_blocks_per_call.append(list(blocks))
if call_count == 1:
# Step 1: User approves commands
# Simulate server putting stdout data and next copilot prompt
updated_node_info = {
"name": "r1",
"context_blocks": [[0, 10, "router#"], [10, 30, "router# show ip route"]]
}
response_queue.put(connpy_pb2.InteractResponse(stdout_data=b"show ip route\r\n10.0.0.0/24\r\nrouter# "))
response_queue.put(connpy_pb2.InteractResponse(
copilot_prompt=True,
copilot_node_info_json=json.dumps(updated_node_info)
))
return "send_all", ["show ip route"], None
else:
# Step 2: Second turn, mission completes or user cancels
return "cancel", None, None
with patch.object(CopilotInterface, "run_session", side_effect=mock_run_session):
stub._handle_remote_copilot(
res, request_queue, response_queue, client_buffer_bytes,
pause_gen, resume_gen, old_tty
)
# Assert that run_session was called twice
assert call_count == 2
# First call should have initial blocks
assert len(received_blocks_per_call[0]) == 1
# Second call should have updated blocks received from server
assert len(received_blocks_per_call[1]) == 2
assert received_blocks_per_call[1][1][2] == "router# show ip route"
# Assert client_buffer_bytes was updated with stdout_data
assert b"10.0.0.0/24" in bytes(client_buffer_bytes)
# Assert action sent to server
reqs = []
while not request_queue.empty():
reqs.append(request_queue.get_nowait())
assert reqs[0].copilot_action == "custom:show ip route"
assert reqs[1].copilot_action == "cancel"
+62 -12
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.ai API documentation</title> <title>connpy.ai API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -255,6 +255,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content) chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls: elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown # In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content)) self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls: if not resp_msg.tool_calls:
@@ -388,6 +389,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content) chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls: elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown # In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content)) self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls: if not resp_msg.tool_calls:
@@ -1621,6 +1623,7 @@ class ai:
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(resp_msg.content), title=f&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;)) self.console.print(Panel(Markdown(resp_msg.content), title=f&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;))
if status: if status:
try: status.start() try: status.start()
@@ -1668,6 +1671,7 @@ class ai:
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;)) self.console.print(Panel(Markdown(obs), title=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;))
if status: if status:
try: status.start() try: status.start()
@@ -1831,6 +1835,9 @@ Rules:
5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices. 5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
6. Keep your guide concise and authoritative. 6. Keep your guide concise and authoritative.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
&lt;notes&gt;
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
&lt;/notes&gt;
&lt;guide&gt; &lt;guide&gt;
Your brief tactical guide in markdown. Your brief tactical guide in markdown.
&lt;/guide&gt; &lt;/guide&gt;
@@ -1839,7 +1846,8 @@ Your brief tactical guide in markdown.
&lt;risk&gt; &lt;risk&gt;
low low
&lt;/risk&gt; &lt;/risk&gt;
8. Risk level is usually &#34;low&#34; for read-only/no commands. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your &lt;notes&gt; tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your &lt;notes&gt; are preserved in the assistant history, summarizing key facts in &lt;notes&gt; ensures you never lose vital context when older turns expire.
9. Risk level is usually &#34;low&#34; for read-only/no commands.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -1856,6 +1864,9 @@ Rules:
5. If the user wants to execute an action, provide the required CLI commands inside a &lt;commands&gt; block, one command per line. If no commands are needed, leave it empty or omit the block. 5. If the user wants to execute an action, provide the required CLI commands inside a &lt;commands&gt; block, one command per line. If no commands are needed, leave it empty or omit the block.
6. ULTRA-CONCISE. Keep your guide to the point. 6. ULTRA-CONCISE. Keep your guide to the point.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
&lt;notes&gt;
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
&lt;/notes&gt;
&lt;guide&gt; &lt;guide&gt;
Your brief tactical guide in markdown. 3-4 sentences max. Your brief tactical guide in markdown. 3-4 sentences max.
&lt;/guide&gt; &lt;/guide&gt;
@@ -1866,7 +1877,8 @@ command 2
&lt;risk&gt; &lt;risk&gt;
low, high, or destructive low, high, or destructive
&lt;/risk&gt; &lt;/risk&gt;
8. Risk level: &#34;low&#34; for read-only/no commands, &#34;high&#34; for config changes, &#34;destructive&#34; for potentially dangerous ops. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your &lt;notes&gt; tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your &lt;notes&gt; are preserved in the assistant history, summarizing key facts in &lt;notes&gt; ensures you never lose vital context when older turns expire.
9. Risk level: &#34;low&#34; for read-only/no commands, &#34;high&#34; for config changes, &#34;destructive&#34; for potentially dangerous ops.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -1894,10 +1906,18 @@ Node: {node_name}&#34;&#34;&#34;
system_prompt += &#34;\nUse these tools to validate syntax or find exact commands if needed before providing the final guide.&#34; system_prompt += &#34;\nUse these tools to validate syntax or find exact commands if needed before providing the final guide.&#34;
messages = [ messages = [
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}, {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}
{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question}
] ]
chat_history = node_info.get(&#34;chat_history&#34;, []) if node_info else []
if chat_history:
clean_history = chat_history[-10:]
for msg in clean_history:
if isinstance(msg, dict) and &#34;role&#34; in msg and &#34;content&#34; in msg:
messages.append({&#34;role&#34;: msg[&#34;role&#34;], &#34;content&#34;: msg[&#34;content&#34;]})
messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question})
iteration = 0 iteration = 0
max_iterations = 5 # Allow up to 5 iterations for tool usage max_iterations = 5 # Allow up to 5 iterations for tool usage
@@ -2019,6 +2039,11 @@ Node: {node_name}&#34;&#34;&#34;
chunk_callback(new_text) chunk_callback(new_text)
streamed_guide += new_text streamed_guide += new_text
notes = &#34;&#34;
notes_match = re.search(r&#34;&lt;notes&gt;(.*?)&lt;/notes&gt;&#34;, full_content, re.DOTALL)
if notes_match:
notes = notes_match.group(1).strip()
guide = &#34;&#34; guide = &#34;&#34;
commands = [] commands = []
risk_level = &#34;low&#34; risk_level = &#34;low&#34;
@@ -2043,6 +2068,7 @@ Node: {node_name}&#34;&#34;&#34;
return { return {
&#34;commands&#34;: commands, &#34;commands&#34;: commands,
&#34;guide&#34;: guide, &#34;guide&#34;: guide,
&#34;notes&#34;: notes,
&#34;risk_level&#34;: risk_level, &#34;risk_level&#34;: risk_level,
&#34;error&#34;: None &#34;error&#34;: None
} }
@@ -2075,11 +2101,11 @@ Node: {node_name}&#34;&#34;&#34;
<dl> <dl>
<dt id="connpy.ai.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt> <dt id="connpy.ai.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
<dt id="connpy.ai.ai.deferred_class_hooks"><code class="name">var <span class="ident">deferred_class_hooks</span></code></dt> <dt id="connpy.ai.ai.deferred_class_hooks"><code class="name">var <span class="ident">deferred_class_hooks</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
<h3>Instance variables</h3> <h3>Instance variables</h3>
@@ -2169,6 +2195,9 @@ Rules:
5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices. 5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
6. Keep your guide concise and authoritative. 6. Keep your guide concise and authoritative.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
&lt;notes&gt;
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
&lt;/notes&gt;
&lt;guide&gt; &lt;guide&gt;
Your brief tactical guide in markdown. Your brief tactical guide in markdown.
&lt;/guide&gt; &lt;/guide&gt;
@@ -2177,7 +2206,8 @@ Your brief tactical guide in markdown.
&lt;risk&gt; &lt;risk&gt;
low low
&lt;/risk&gt; &lt;/risk&gt;
8. Risk level is usually &#34;low&#34; for read-only/no commands. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your &lt;notes&gt; tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your &lt;notes&gt; are preserved in the assistant history, summarizing key facts in &lt;notes&gt; ensures you never lose vital context when older turns expire.
9. Risk level is usually &#34;low&#34; for read-only/no commands.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -2194,6 +2224,9 @@ Rules:
5. If the user wants to execute an action, provide the required CLI commands inside a &lt;commands&gt; block, one command per line. If no commands are needed, leave it empty or omit the block. 5. If the user wants to execute an action, provide the required CLI commands inside a &lt;commands&gt; block, one command per line. If no commands are needed, leave it empty or omit the block.
6. ULTRA-CONCISE. Keep your guide to the point. 6. ULTRA-CONCISE. Keep your guide to the point.
7. You MUST output your response in the following strict format: 7. You MUST output your response in the following strict format:
&lt;notes&gt;
Short internal notes for future context: tools used, key findings, IP addresses, or hypotheses.
&lt;/notes&gt;
&lt;guide&gt; &lt;guide&gt;
Your brief tactical guide in markdown. 3-4 sentences max. Your brief tactical guide in markdown. 3-4 sentences max.
&lt;/guide&gt; &lt;/guide&gt;
@@ -2204,7 +2237,8 @@ command 2
&lt;risk&gt; &lt;risk&gt;
low, high, or destructive low, high, or destructive
&lt;/risk&gt; &lt;/risk&gt;
8. Risk level: &#34;low&#34; for read-only/no commands, &#34;high&#34; for config changes, &#34;destructive&#34; for potentially dangerous ops. 8. CRITICAL CONVERSATION MEMORY: The chat history window retains ONLY the last 5 interactions. Messages older than 5 turns are automatically pruned. You MUST use your &lt;notes&gt; tag in EVERY response to record all critical facts, IP addresses, discoveries, and hypotheses. Because your &lt;notes&gt; are preserved in the assistant history, summarizing key facts in &lt;notes&gt; ensures you never lose vital context when older turns expire.
9. Risk level: &#34;low&#34; for read-only/no commands, &#34;high&#34; for config changes, &#34;destructive&#34; for potentially dangerous ops.
Terminal Context: Terminal Context:
{terminal_buffer} {terminal_buffer}
@@ -2232,10 +2266,18 @@ Node: {node_name}&#34;&#34;&#34;
system_prompt += &#34;\nUse these tools to validate syntax or find exact commands if needed before providing the final guide.&#34; system_prompt += &#34;\nUse these tools to validate syntax or find exact commands if needed before providing the final guide.&#34;
messages = [ messages = [
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}, {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}
{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question}
] ]
chat_history = node_info.get(&#34;chat_history&#34;, []) if node_info else []
if chat_history:
clean_history = chat_history[-10:]
for msg in clean_history:
if isinstance(msg, dict) and &#34;role&#34; in msg and &#34;content&#34; in msg:
messages.append({&#34;role&#34;: msg[&#34;role&#34;], &#34;content&#34;: msg[&#34;content&#34;]})
messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question})
iteration = 0 iteration = 0
max_iterations = 5 # Allow up to 5 iterations for tool usage max_iterations = 5 # Allow up to 5 iterations for tool usage
@@ -2357,6 +2399,11 @@ Node: {node_name}&#34;&#34;&#34;
chunk_callback(new_text) chunk_callback(new_text)
streamed_guide += new_text streamed_guide += new_text
notes = &#34;&#34;
notes_match = re.search(r&#34;&lt;notes&gt;(.*?)&lt;/notes&gt;&#34;, full_content, re.DOTALL)
if notes_match:
notes = notes_match.group(1).strip()
guide = &#34;&#34; guide = &#34;&#34;
commands = [] commands = []
risk_level = &#34;low&#34; risk_level = &#34;low&#34;
@@ -2381,6 +2428,7 @@ Node: {node_name}&#34;&#34;&#34;
return { return {
&#34;commands&#34;: commands, &#34;commands&#34;: commands,
&#34;guide&#34;: guide, &#34;guide&#34;: guide,
&#34;notes&#34;: notes,
&#34;risk_level&#34;: risk_level, &#34;risk_level&#34;: risk_level,
&#34;error&#34;: None &#34;error&#34;: None
} }
@@ -2579,6 +2627,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(resp_msg.content), title=f&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;)) self.console.print(Panel(Markdown(resp_msg.content), title=f&#34;[{current_brain}][bold]{label} Reasoning[/bold][/{current_brain}]&#34;, border_style=&#34;architect&#34; if current_brain == &#34;architect&#34; else &#34;engineer&#34;))
if status: if status:
try: status.start() try: status.start()
@@ -2626,6 +2675,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status: if status:
try: status.stop() try: status.stop()
except: pass except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;)) self.console.print(Panel(Markdown(obs), title=&#34;[architect]Architect Consultation[/architect]&#34;, border_style=&#34;architect&#34;))
if status: if status:
try: status.start() try: status.start()
@@ -3184,7 +3234,7 @@ def confirm(self, user_input): return True</code></pre>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+6 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.ai_handler API documentation</title> <title>connpy.cli.ai_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -148,6 +148,7 @@ el.replaceWith(d);
title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34; title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34;
if not result.get(&#34;streamed&#34;): if not result.get(&#34;streamed&#34;):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False)) mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False))
if &#34;usage&#34; in result: if &#34;usage&#34; in result:
@@ -167,6 +168,7 @@ el.replaceWith(d);
printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;) printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history: if not history:
from rich.markdown import Markdown
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;)) mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;))
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
@@ -573,6 +575,7 @@ el.replaceWith(d);
printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;) printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history: if not history:
from rich.markdown import Markdown
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;)) mdprint(Markdown(&#34;**Networking Expert Agent**: Hi! I&#39;m your assistant. I can help you diagnose issues, run commands, and manage your nodes.\nType &#39;exit&#39; to quit.\n&#34;))
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
@@ -626,6 +629,7 @@ el.replaceWith(d);
title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34; title = &#34;[architect][bold]Network Architect[/bold][/architect]&#34; if responder == &#34;architect&#34; else &#34;[engineer][bold]Network Engineer[/bold][/engineer]&#34;
if not result.get(&#34;streamed&#34;): if not result.get(&#34;streamed&#34;):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False)) mdprint(Panel(Markdown(result[&#34;response&#34;]), title=title, border_style=border, expand=False))
if &#34;usage&#34; in result: if &#34;usage&#34; in result:
@@ -666,7 +670,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.api_handler API documentation</title> <title>connpy.cli.api_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -193,7 +193,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.config_handler API documentation</title> <title>connpy.cli.config_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -600,7 +600,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.context_handler API documentation</title> <title>connpy.cli.context_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -249,7 +249,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.forms API documentation</title> <title>connpy.cli.forms API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -700,7 +700,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.help_text API documentation</title> <title>connpy.cli.help_text API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -303,7 +303,7 @@ tasks:
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.helpers API documentation</title> <title>connpy.cli.helpers API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.import_export_handler API documentation</title> <title>connpy.cli.import_export_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -304,7 +304,7 @@ def forms(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli API documentation</title> <title>connpy.cli API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -162,7 +162,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.login_handler API documentation</title> <title>connpy.cli.login_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -611,7 +611,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+4 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.node_handler API documentation</title> <title>connpy.cli.node_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -204,6 +204,7 @@ el.replaceWith(d);
# Fast fail if parent folder does not exist # Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data) self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions())) printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques) new_node_data = self.forms.questions_nodes(args.data, uniques)
@@ -374,6 +375,7 @@ def forms(self):
# Fast fail if parent folder does not exist # Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data) self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions())) printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques) new_node_data = self.forms.questions_nodes(args.data, uniques)
@@ -673,7 +675,7 @@ def forms(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.plugin_handler API documentation</title> <title>connpy.cli.plugin_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -397,7 +397,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.profile_handler API documentation</title> <title>connpy.cli.profile_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -346,7 +346,7 @@ def forms(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.run_handler API documentation</title> <title>connpy.cli.run_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -1163,7 +1163,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.shell_handler API documentation</title> <title>connpy.cli.shell_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -181,7 +181,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.sso_handler API documentation</title> <title>connpy.cli.sso_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -457,7 +457,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.sync_handler API documentation</title> <title>connpy.cli.sync_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -427,7 +427,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+417 -182
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.terminal_ui API documentation</title> <title>connpy.cli.terminal_ui API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -70,6 +70,7 @@ el.replaceWith(d);
self.session_state.setdefault(&#39;persona&#39;, &#39;engineer&#39;) self.session_state.setdefault(&#39;persona&#39;, &#39;engineer&#39;)
self.session_state.setdefault(&#39;trust_mode&#39;, False) self.session_state.setdefault(&#39;trust_mode&#39;, False)
self.session_state.setdefault(&#39;memories&#39;, []) self.session_state.setdefault(&#39;memories&#39;, [])
self.session_state.setdefault(&#39;copilot_chat_history&#39;, [])
self.session_state.setdefault(&#39;os&#39;, None) self.session_state.setdefault(&#39;os&#39;, None)
self.session_state.setdefault(&#39;prompt&#39;, None) self.session_state.setdefault(&#39;prompt&#39;, None)
self.session_state.setdefault(&#39;context_mode&#39;, self.mode_range) self.session_state.setdefault(&#39;context_mode&#39;, self.mode_range)
@@ -137,7 +138,8 @@ el.replaceWith(d);
is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;) is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single: if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1: is_mission = self.session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False)
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and (saved_cmd &gt; 1 or is_mission):
new_cmds = total_cmds - last_total_cmds new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds initial_cmd = saved_cmd + new_cmds
else: else:
@@ -170,15 +172,17 @@ el.replaceWith(d);
self.session_state[&#39;last_total_cmds&#39;] = total_cmds self.session_state[&#39;last_total_cmds&#39;] = total_cmds
self.session_state[&#39;last_total_lines&#39;] = total_lines self.session_state[&#39;last_total_lines&#39;] = total_lines
# 1. Visual Separation # 1. Visual Separation (Only show help banner on initial entry)
self.console.print(&#34;&#34;) # Real line break self.console.print(&#34;&#34;) # Real line break
self.console.print(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;)) self.console.print(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;))
self.console.print(Panel( if not self.session_state.get(&#39;banner_shown&#39;, False):
&#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#34; self.console.print(Panel(
&#34;Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]&#34;, &#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#34;
border_style=&#34;cyan&#34; &#34;Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]&#34;,
)) border_style=&#34;cyan&#34;
self.console.print(&#34;\n&#34;) # Small space before the copilot prompt ))
self.session_state[&#39;banner_shown&#39;] = True
self.console.print(&#34;&#34;) # Small space before the copilot prompt
bindings = KeyBindings() bindings = KeyBindings()
@bindings.add(&#39;c-up&#39;) @bindings.add(&#39;c-up&#39;)
@@ -260,7 +264,7 @@ el.replaceWith(d);
text = app.current_buffer.text text = app.current_buffer.text
# Only show command help if typing the first command and there are no spaces # Only show command help if typing the first command and there are no spaces
if text.startswith(&#39;/&#39;) and &#39; &#39; not in text: if text.startswith(&#39;/&#39;) and &#39; &#39; not in text:
commands = [&#39;/os&#39;, &#39;/prompt&#39;, &#39;/architect&#39;, &#39;/engineer&#39;, &#39;/trust&#39;, &#39;/untrust&#39;, &#39;/memorize&#39;, &#39;/clear&#39;] commands = [&#39;/os&#39;, &#39;/prompt&#39;, &#39;/architect&#39;, &#39;/engineer&#39;, &#39;/trust&#39;, &#39;/untrust&#39;, &#39;/memorize&#39;, &#39;/clear&#39;, &#39;/mission&#39;, &#39;/cancel&#39;]
matches = [c for c in commands if c.startswith(text.lower())] matches = [c for c in commands if c.startswith(text.lower())]
if matches: if matches:
m_text = html.escape(f&#34;Available: {&#39; &#39;.join(matches)}&#34;) m_text = html.escape(f&#34;Available: {&#39; &#39;.join(matches)}&#34;)
@@ -268,7 +272,7 @@ el.replaceWith(d);
m_label = {self.mode_range: &#34;RANGE&#34;, self.mode_single: &#34;SINGLE&#34;, self.mode_lines: &#34;LINES&#34;}[state[&#39;context_mode&#39;]] m_label = {self.mode_range: &#34;RANGE&#34;, self.mode_single: &#34;SINGLE&#34;, self.mode_lines: &#34;LINES&#34;}[state[&#39;context_mode&#39;]]
if state[&#39;context_mode&#39;] == self.mode_lines: if state[&#39;context_mode&#39;] == self.mode_lines:
base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 [Tab: {m_label}] {state[&#34;context_lines&#34;]}/{state[&#34;total_lines&#34;]}L (Ctrl+\u2191/\u2193 adjusts lines)&#39;
else: else:
idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;]) idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;])
@@ -291,7 +295,7 @@ el.replaceWith(d);
p = clean_preview(b[2]) p = clean_preview(b[2])
if p: if p:
# Truncar comandos individuales largos # Truncar comandos individuales largos
if len(p) &gt; 25: p = p[:22] + &#34;...&#34; if len(p) &gt; 25: p = p[:22].rstrip(&#39; .,-_&#39;) + &#34;...&#34;
previews.append(p) previews.append(p)
if not previews: if not previews:
@@ -299,12 +303,12 @@ el.replaceWith(d);
elif len(previews) &lt;= 3: elif len(previews) &lt;= 3:
desc = &#34; + &#34;.join(previews) desc = &#34; + &#34;.join(previews)
else: else:
desc = f&#34;{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})&#34; desc = f&#34;{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})&#34;
else: else:
# Modo SINGLE original # Modo SINGLE original
desc = clean_preview(blocks[idx][2]) desc = clean_preview(blocks[idx][2])
base_str = f&#39;\u25b6 {desc} [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 [Tab: {m_label}] {desc}&#39;
# Wrap base_str in a style to maintain consistency and avoid glitches # Wrap base_str in a style to maintain consistency and avoid glitches
# The fg color will be inherited from bottom-toolbar global style if not specified here # The fg color will be inherited from bottom-toolbar global style if not specified here
@@ -338,7 +342,9 @@ el.replaceWith(d);
(&#39;/trust&#39;, &#39;Enable auto-execute&#39;), (&#39;/trust&#39;, &#39;Enable auto-execute&#39;),
(&#39;/untrust&#39;, &#39;Disable auto-execute&#39;), (&#39;/untrust&#39;, &#39;Disable auto-execute&#39;),
(&#39;/memorize&#39;, &#39;Add fact to memory&#39;), (&#39;/memorize&#39;, &#39;Add fact to memory&#39;),
(&#39;/clear&#39;, &#39;Clear memory&#39;) (&#39;/clear&#39;, &#39;Clear memory&#39;),
(&#39;/mission&#39;, &#39;Start autonomous mission&#39;),
(&#39;/cancel&#39;, &#39;Cancel active mission&#39;)
] ]
for cmd, desc in commands: for cmd, desc in commands:
if cmd.startswith(cmd_part.lower()): if cmd.startswith(cmd_part.lower()):
@@ -346,68 +352,162 @@ el.replaceWith(d);
copilot_completer = SlashCommandCompleter() copilot_completer = SlashCommandCompleter()
def _finalize_mission(reason=&#34;completed&#34;, final_guide=&#34;&#34;):
mission = self.session_state.get(&#39;mission&#39;, {})
if not mission or not mission.get(&#39;active&#39;, False):
return
mission[&#39;active&#39;] = False
if reason == &#34;completed&#34;:
self.console.print(&#34;\n[bold green]🎉 Mission Completed[/bold green]&#34;)
elif reason == &#34;user_cancelled&#34;:
self.console.print(&#34;\n[yellow]Mission cancelled by user.[/yellow]&#34;)
elif reason == &#34;limit_reached&#34;:
self.console.print(&#34;\n[yellow]Mission step limit reached.[/yellow]&#34;)
final_notes = &#34;\n&#34;.join(mission.get(&#39;scratchpad_notes&#39;, []))
guide = final_guide or mission.get(&#39;last_guide&#39;, &#39;&#39;)
asst_msg = f&#34;Notes: {final_notes}\nGuide: {guide}&#34; if final_notes else guide
hist = self.session_state.setdefault(&#34;copilot_chat_history&#34;, [])
hist.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: f&#34;/mission {mission.get(&#39;goal&#39;, &#39;&#39;)}&#34;})
hist.append({&#34;role&#34;: &#34;assistant&#34;, &#34;content&#34;: asst_msg})
self.session_state[&#34;copilot_chat_history&#34;] = hist[-10:]
while True: while True:
# 2. Ask question overrides = {}
from prompt_toolkit.styles import Style # Check for active mission auto-looping
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;) mission = self.session_state.get(&#39;mission&#39;, {})
ui_style = Style.from_dict({ is_mission = mission.get(&#39;active&#39;, False) and not mission.get(&#39;paused&#39;, False)
&#39;bottom-toolbar&#39;: f&#39;fg:{c_contrast}&#39;,
})
session = PromptSession( if is_mission:
history=self.history, # Force mode_range for mission mode
input=self.pt_input, state[&#39;context_mode&#39;] = self.mode_range
output=self.pt_output, self.session_state[&#39;context_mode&#39;] = self.mode_range
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
# We use an internal try/finally to ensure that if something fails in prompt_async,
# we don&#39;t leave the terminal in a strange state.
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state[&#39;cancelled&#39;] = True
question = &#34;&#34;
if state[&#39;cancelled&#39;] or not question.strip() or question.strip().lower() in [&#39;cancel&#39;, &#39;exit&#39;, &#39;quit&#39;]: if mission.get(&#39;start_block_idx&#39;) is None:
return &#34;cancel&#34;, None, None mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
# 3. Process Input via AIService start_idx = mission.get(&#39;start_block_idx&#39;, state[&#39;total_cmds&#39;])
directive = self.ai_service.process_copilot_input(question, self.session_state) cmds_since_start = max(1, (state[&#39;total_cmds&#39;] - start_idx) + 1)
state[&#39;context_cmd&#39;] = max(state.get(&#39;context_cmd&#39;, 1), cmds_since_start)
self.session_state[&#39;context_cmd&#39;] = state[&#39;context_cmd&#39;]
if directive[&#34;action&#34;] == &#34;state_update&#34;: step = mission.get(&#39;step&#39;, 1)
msg = directive[&#39;message&#39;] max_steps = mission.get(&#39;max_steps&#39;, 10)
state[&#39;toolbar_msg&#39;] = msg
state[&#39;msg_expiry&#39;] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh(): if step &gt; max_steps:
await asyncio.sleep(3.1) ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
# Only invalidate if the message hasn&#39;t been replaced by a newer one c_warn = self._get_theme_color(&#34;warning&#34;, &#34;yellow&#34;)
if state.get(&#39;toolbar_msg&#39;) == msg: import html
state[&#39;toolbar_msg&#39;] = &#39;&#39; # Explicitly clear p_warn = html.escape(f&#34;[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: &#34;)
try: try:
from prompt_toolkit.application.current import get_app ext_ans = await ext_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{c_warn}&#34; bold=&#34;true&#34;&gt;{p_warn}&lt;/style&gt;&#39;))
app = get_app() except (KeyboardInterrupt, EOFError):
if app: app.invalidate() ext_ans = &#39;n&#39;
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one if (ext_ans or &#39;y&#39;).lower().strip() in (&#39;y&#39;, &#39;yes&#39;):
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;) mission[&#39;max_steps&#39;] += 10
sys.stdout.flush() else:
continue goal = mission.get(&#39;goal&#39;, &#39;&#39;)
question = f&#34;[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}&#34;
clean_question = question
mission[&#39;active&#39;] = False
is_mission = False
self.console.print(f&#34;\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]&#34;)
if is_mission:
goal = mission.get(&#39;goal&#39;, &#39;&#39;)
step = mission.get(&#39;step&#39;, 1)
question = f&#34;[MISSION STEP {step}]: Continue analysis of: {goal}&#34;
scratchpad = mission.get(&#39;scratchpad_notes&#39;, [])
if scratchpad:
notes_text = &#34;\n&#34;.join(f&#34;- {n}&#34; for n in scratchpad)
question += f&#34;\n\nPast Mission Notes:\n{notes_text}&#34;
clean_question = question
self.console.print(f&#34;\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]&#34;)
elif not is_mission and &#39;clean_question&#39; in locals() and clean_question.startswith(&#34;[MISSION SUMMARY]&#34;):
# Pass-through to AI execution for generating final summary
pass
else: else:
# Clean the toolbar message when a real question is asked # 2. Ask question
state[&#39;toolbar_msg&#39;] = &#39;&#39; from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;)
ui_style = Style.from_dict({
&#39;bottom-toolbar&#39;: f&#39;fg:{c_contrast}&#39;,
})
clean_question = directive.get(&#34;clean_prompt&#34;, question) session = PromptSession(
overrides = directive.get(&#34;overrides&#34;, {}) history=self.history,
input=self.pt_input,
output=self.pt_output,
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state[&#39;cancelled&#39;] = True
question = &#34;&#34;
if state[&#39;cancelled&#39;] or not question.strip() or question.strip().lower() in [&#39;cancel&#39;, &#39;exit&#39;, &#39;quit&#39;]:
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None
# 3. Process Input via AIService
directive = self.ai_service.process_copilot_input(question, self.session_state)
if directive[&#34;action&#34;] == &#34;mission_start&#34;:
mission = self.session_state.get(&#39;mission&#39;, {})
mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
state[&#39;context_mode&#39;] = self.mode_range
self.session_state[&#39;context_mode&#39;] = self.mode_range
state[&#39;context_cmd&#39;] = 1
self.session_state[&#39;context_cmd&#39;] = 1
clean_question = f&#34;[MISSION STEP 1]: {directive[&#39;clean_prompt&#39;]}&#34;
overrides = directive.get(&#34;overrides&#34;, {})
self.console.print(f&#34;\n[bold cyan]🤖 Starting Mission: {directive[&#39;clean_prompt&#39;]}[/bold cyan]&#34;)
elif directive[&#34;action&#34;] == &#34;mission_cancel&#34;:
_finalize_mission(&#34;user_cancelled&#34;)
state[&#39;toolbar_msg&#39;] = &#39;Mission cancelled&#39;
continue
elif directive[&#34;action&#34;] == &#34;state_update&#34;:
msg = directive[&#39;message&#39;]
state[&#39;toolbar_msg&#39;] = msg
state[&#39;msg_expiry&#39;] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh():
await asyncio.sleep(3.1)
if state.get(&#39;toolbar_msg&#39;) == msg:
state[&#39;toolbar_msg&#39;] = &#39;&#39;
try:
from prompt_toolkit.application.current import get_app
app = get_app()
if app: app.invalidate()
except: pass
asyncio.create_task(delayed_refresh())
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;)
sys.stdout.flush()
continue
else:
state[&#39;toolbar_msg&#39;] = &#39;&#39;
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = False
step = mission.get(&#39;step&#39;, 1)
goal = mission.get(&#39;goal&#39;, &#39;&#39;)
clean_question = f&#34;[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}&#34;
self.console.print(f&#34;\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]&#34;)
overrides = directive.get(&#34;overrides&#34;, {})
else:
clean_question = directive.get(&#34;clean_prompt&#34;, question)
overrides = directive.get(&#34;overrides&#34;, {})
# Merge node_info with session_state and overrides # Merge node_info with session_state and overrides
merged_node_info = node_info.copy() merged_node_info = node_info.copy()
@@ -416,20 +516,12 @@ el.replaceWith(d);
merged_node_info[&#39;persona&#39;] = self.session_state[&#39;persona&#39;] merged_node_info[&#39;persona&#39;] = self.session_state[&#39;persona&#39;]
merged_node_info[&#39;trust&#39;] = self.session_state[&#39;trust_mode&#39;] merged_node_info[&#39;trust&#39;] = self.session_state[&#39;trust_mode&#39;]
merged_node_info[&#39;memories&#39;] = list(self.session_state[&#39;memories&#39;]) merged_node_info[&#39;memories&#39;] = list(self.session_state[&#39;memories&#39;])
merged_node_info[&#39;chat_history&#39;] = list(self.session_state.get(&#39;copilot_chat_history&#39;, []))
for k, v in overrides.items(): for k, v in overrides.items():
merged_node_info[k] = v merged_node_info[k] = v
# Enrich question
past = self.history.get_strings()
if len(past) &gt; 1:
clean_past = [q for q in past[-6:-1] if not q.startswith(&#39;/&#39;)]
if clean_past:
history_text = &#34;\n&#34;.join(f&#34;- {q}&#34; for q in clean_past)
clean_question = f&#34;Previous questions:\n{history_text}\n\nCurrent Question:\n{clean_question}&#34;
# 3. AI Execution # 3. AI Execution
# Use persona from overrides (one-shot) or from session state
active_persona = merged_node_info.get(&#39;persona&#39;, self.session_state.get(&#39;persona&#39;, &#39;engineer&#39;)) active_persona = merged_node_info.get(&#39;persona&#39;, self.session_state.get(&#39;persona&#39;, &#39;engineer&#39;))
persona_color = self._get_theme_color(active_persona, fallback=&#34;cyan&#34;) persona_color = self._get_theme_color(active_persona, fallback=&#34;cyan&#34;)
persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34; persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34;
@@ -456,7 +548,6 @@ el.replaceWith(d);
nonlocal live_text, first_chunk nonlocal live_text, first_chunk
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule( self.console.print(Rule(
f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;,
style=persona_color style=persona_color
@@ -465,7 +556,6 @@ el.replaceWith(d);
live_text += text live_text += text
md_parser.feed(text) md_parser.feed(text)
# Check for interruption during AI call
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info)) ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
try: try:
@@ -474,13 +564,12 @@ el.replaceWith(d);
result = await ai_task result = await ai_task
except asyncio.CancelledError: except asyncio.CancelledError:
status_spinner.stop() status_spinner.stop()
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None return &#34;cancel&#34;, None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk: if not first_chunk:
md_parser.flush() md_parser.flush()
self.console.print(Rule(style=persona_color)) self.console.print(Rule(style=persona_color))
@@ -488,14 +577,35 @@ el.replaceWith(d);
if not result or result.get(&#34;error&#34;): if not result or result.get(&#34;error&#34;):
if first_chunk and result and result.get(&#34;error&#34;): if first_chunk and result and result.get(&#34;error&#34;):
self.console.print(f&#34;[red]Error: {result[&#39;error&#39;]}[/red]&#34;) self.console.print(f&#34;[red]Error: {result[&#39;error&#39;]}[/red]&#34;)
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None return &#34;cancel&#34;, None, None
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get(&#34;guide&#34;): if first_chunk and result and result.get(&#34;guide&#34;):
from rich.markdown import Markdown
self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color)) self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color))
# Update copilot_chat_history or mission scratchpad
if result and not result.get(&#34;error&#34;):
guide = result.get(&#34;guide&#34;, &#34;&#34;)
notes = result.get(&#34;notes&#34;, &#34;&#34;)
mission = self.session_state.get(&#39;mission&#39;, {})
if mission.get(&#39;active&#39;, False):
if notes:
mission.setdefault(&#39;scratchpad_notes&#39;, []).append(notes)
if guide:
mission[&#39;last_guide&#39;] = guide
else:
if guide or notes:
asst_msg = f&#34;Notes: {notes}\nGuide: {guide}&#34; if notes else guide
hist = self.session_state.setdefault(&#34;copilot_chat_history&#34;, [])
hist.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: clean_question})
hist.append({&#34;role&#34;: &#34;assistant&#34;, &#34;content&#34;: asst_msg})
self.session_state[&#34;copilot_chat_history&#34;] = hist[-10:]
commands = result.get(&#34;commands&#34;, []) commands = result.get(&#34;commands&#34;, [])
mission = self.session_state.get(&#39;mission&#39;, {})
if not commands: if not commands:
if mission.get(&#39;active&#39;, False):
_finalize_mission(&#34;completed&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
@@ -504,11 +614,12 @@ el.replaceWith(d);
style_color = self._get_theme_color(risk_style, fallback=&#34;green&#34;) style_color = self._get_theme_color(risk_style, fallback=&#34;green&#34;)
cmd_text = &#34;\n&#34;.join(f&#34; {i+1}. {c}&#34; for i, c in enumerate(commands)) cmd_text = &#34;\n&#34;.join(f&#34; {i+1}. {c}&#34; for i, c in enumerate(commands))
# Explicitly use &#39;bold style_color&#39; for both TITLE and BORDER to ensure maximum consistency
self.console.print(Panel(cmd_text, title=f&#34;[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]&#34;, border_style=f&#34;bold {style_color}&#34;)) self.console.print(Panel(cmd_text, title=f&#34;[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]&#34;, border_style=f&#34;bold {style_color}&#34;))
if merged_node_info.get(&#39;trust&#39;, False) and risk != &#34;destructive&#34;: if merged_node_info.get(&#39;trust&#39;, False) and risk != &#34;destructive&#34;:
self.console.print(f&#34;[dim]⚙️ Auto-executing (Trust Mode)[/dim]&#34;) self.console.print(f&#34;[dim]⚙️ Auto-executing (Trust Mode)[/dim]&#34;)
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, commands, None return &#34;send_all&#34;, commands, None
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output) confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -520,41 +631,39 @@ el.replaceWith(d);
import html import html
try: try:
p_text = html.escape(f&#34;Send? (y/n/e/range) [n]: &#34;) p_text = html.escape(f&#34;Send? (y/n/e/range) [n]: &#34;)
# Use the EXACT same style_color and force bold=&#34;true&#34; for Prompt-Toolkit
action = await confirm_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{style_color}&#34; bold=&#34;true&#34;&gt;{p_text}&lt;/style&gt;&#39;), key_bindings=c_bindings) action = await confirm_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{style_color}&#34; bold=&#34;true&#34;&gt;{p_text}&lt;/style&gt;&#39;), key_bindings=c_bindings)
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
_finalize_mission(&#34;user_cancelled&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
def parse_indices(text, max_len): def parse_indices(text, max_len):
&#34;&#34;&#34;Helper to parse &#39;1-3, 5, 7&#39; into [0, 1, 2, 4, 6].&#34;&#34;&#34;
indices = [] indices = []
# Replace commas with spaces and split
parts = text.replace(&#39;,&#39;, &#39; &#39;).split() parts = text.replace(&#39;,&#39;, &#39; &#39;).split()
for part in parts: for part in parts:
if &#39;-&#39; in part: if &#39;-&#39; in part:
try: try:
start, end = map(int, part.split(&#39;-&#39;)) start, end = map(int, part.split(&#39;-&#39;))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end)) indices.extend(range(start-1, end))
except: continue except: continue
elif part.isdigit(): elif part.isdigit():
indices.append(int(part)-1) indices.append(int(part)-1)
# Filter valid indices and remove duplicates
return [i for i in sorted(set(indices)) if 0 &lt;= i &lt; max_len] return [i for i in sorted(set(indices)) if 0 &lt;= i &lt; max_len]
action_l = (action or &#34;n&#34;).lower().strip() action_l = (action or &#34;n&#34;).lower().strip()
if action_l in (&#39;y&#39;, &#39;yes&#39;, &#39;all&#39;): if action_l in (&#39;y&#39;, &#39;yes&#39;, &#39;all&#39;):
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, commands, None return &#34;send_all&#34;, commands, None
# Check for numeric selection (e.g., &#34;1, 2-4&#34;)
if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l): if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l):
selected_idxs = parse_indices(action_l, len(commands)) selected_idxs = parse_indices(action_l, len(commands))
if selected_idxs: if selected_idxs:
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, [commands[i] for i in selected_idxs], None return &#34;send_all&#34;, [commands[i] for i in selected_idxs], None
elif action_l.startswith(&#39;e&#39;): elif action_l.startswith(&#39;e&#39;):
# Check if it&#39;s a selective edit like &#39;e1-2&#39;
selection_str = action_l[1:].strip() selection_str = action_l[1:].strip()
if selection_str: if selection_str:
idxs = parse_indices(selection_str, len(commands)) idxs = parse_indices(selection_str, len(commands))
@@ -580,14 +689,23 @@ el.replaceWith(d);
default=target, multiline=True, key_bindings=e_bindings default=target, multiline=True, key_bindings=e_bindings
) )
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = True
self.console.print(&#34;\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]&#34;)
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
if edited and edited.strip(): if edited and edited.strip():
# Split by lines to ensure core.py applies delay between each command
lines = [l.strip() for l in edited.split(&#39;\n&#39;) if l.strip()] lines = [l.strip() for l in edited.split(&#39;\n&#39;) if l.strip()]
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;custom&#34;, None, lines return &#34;custom&#34;, None, lines
# User rejected/cancelled the commands
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = True
self.console.print(&#34;\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]&#34;)
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
@@ -642,7 +760,8 @@ el.replaceWith(d);
is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;) is_single = saved_mode in (self.mode_single, 1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single: if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1: is_mission = self.session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False)
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and (saved_cmd &gt; 1 or is_mission):
new_cmds = total_cmds - last_total_cmds new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds initial_cmd = saved_cmd + new_cmds
else: else:
@@ -675,15 +794,17 @@ el.replaceWith(d);
self.session_state[&#39;last_total_cmds&#39;] = total_cmds self.session_state[&#39;last_total_cmds&#39;] = total_cmds
self.session_state[&#39;last_total_lines&#39;] = total_lines self.session_state[&#39;last_total_lines&#39;] = total_lines
# 1. Visual Separation # 1. Visual Separation (Only show help banner on initial entry)
self.console.print(&#34;&#34;) # Real line break self.console.print(&#34;&#34;) # Real line break
self.console.print(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;)) self.console.print(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;))
self.console.print(Panel( if not self.session_state.get(&#39;banner_shown&#39;, False):
&#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#34; self.console.print(Panel(
&#34;Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]&#34;, &#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#34;
border_style=&#34;cyan&#34; &#34;Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]&#34;,
)) border_style=&#34;cyan&#34;
self.console.print(&#34;\n&#34;) # Small space before the copilot prompt ))
self.session_state[&#39;banner_shown&#39;] = True
self.console.print(&#34;&#34;) # Small space before the copilot prompt
bindings = KeyBindings() bindings = KeyBindings()
@bindings.add(&#39;c-up&#39;) @bindings.add(&#39;c-up&#39;)
@@ -765,7 +886,7 @@ el.replaceWith(d);
text = app.current_buffer.text text = app.current_buffer.text
# Only show command help if typing the first command and there are no spaces # Only show command help if typing the first command and there are no spaces
if text.startswith(&#39;/&#39;) and &#39; &#39; not in text: if text.startswith(&#39;/&#39;) and &#39; &#39; not in text:
commands = [&#39;/os&#39;, &#39;/prompt&#39;, &#39;/architect&#39;, &#39;/engineer&#39;, &#39;/trust&#39;, &#39;/untrust&#39;, &#39;/memorize&#39;, &#39;/clear&#39;] commands = [&#39;/os&#39;, &#39;/prompt&#39;, &#39;/architect&#39;, &#39;/engineer&#39;, &#39;/trust&#39;, &#39;/untrust&#39;, &#39;/memorize&#39;, &#39;/clear&#39;, &#39;/mission&#39;, &#39;/cancel&#39;]
matches = [c for c in commands if c.startswith(text.lower())] matches = [c for c in commands if c.startswith(text.lower())]
if matches: if matches:
m_text = html.escape(f&#34;Available: {&#39; &#39;.join(matches)}&#34;) m_text = html.escape(f&#34;Available: {&#39; &#39;.join(matches)}&#34;)
@@ -773,7 +894,7 @@ el.replaceWith(d);
m_label = {self.mode_range: &#34;RANGE&#34;, self.mode_single: &#34;SINGLE&#34;, self.mode_lines: &#34;LINES&#34;}[state[&#39;context_mode&#39;]] m_label = {self.mode_range: &#34;RANGE&#34;, self.mode_single: &#34;SINGLE&#34;, self.mode_lines: &#34;LINES&#34;}[state[&#39;context_mode&#39;]]
if state[&#39;context_mode&#39;] == self.mode_lines: if state[&#39;context_mode&#39;] == self.mode_lines:
base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 [Tab: {m_label}] {state[&#34;context_lines&#34;]}/{state[&#34;total_lines&#34;]}L (Ctrl+\u2191/\u2193 adjusts lines)&#39;
else: else:
idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;]) idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;])
@@ -796,7 +917,7 @@ el.replaceWith(d);
p = clean_preview(b[2]) p = clean_preview(b[2])
if p: if p:
# Truncar comandos individuales largos # Truncar comandos individuales largos
if len(p) &gt; 25: p = p[:22] + &#34;...&#34; if len(p) &gt; 25: p = p[:22].rstrip(&#39; .,-_&#39;) + &#34;...&#34;
previews.append(p) previews.append(p)
if not previews: if not previews:
@@ -804,12 +925,12 @@ el.replaceWith(d);
elif len(previews) &lt;= 3: elif len(previews) &lt;= 3:
desc = &#34; + &#34;.join(previews) desc = &#34; + &#34;.join(previews)
else: else:
desc = f&#34;{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})&#34; desc = f&#34;{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})&#34;
else: else:
# Modo SINGLE original # Modo SINGLE original
desc = clean_preview(blocks[idx][2]) desc = clean_preview(blocks[idx][2])
base_str = f&#39;\u25b6 {desc} [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 [Tab: {m_label}] {desc}&#39;
# Wrap base_str in a style to maintain consistency and avoid glitches # Wrap base_str in a style to maintain consistency and avoid glitches
# The fg color will be inherited from bottom-toolbar global style if not specified here # The fg color will be inherited from bottom-toolbar global style if not specified here
@@ -843,7 +964,9 @@ el.replaceWith(d);
(&#39;/trust&#39;, &#39;Enable auto-execute&#39;), (&#39;/trust&#39;, &#39;Enable auto-execute&#39;),
(&#39;/untrust&#39;, &#39;Disable auto-execute&#39;), (&#39;/untrust&#39;, &#39;Disable auto-execute&#39;),
(&#39;/memorize&#39;, &#39;Add fact to memory&#39;), (&#39;/memorize&#39;, &#39;Add fact to memory&#39;),
(&#39;/clear&#39;, &#39;Clear memory&#39;) (&#39;/clear&#39;, &#39;Clear memory&#39;),
(&#39;/mission&#39;, &#39;Start autonomous mission&#39;),
(&#39;/cancel&#39;, &#39;Cancel active mission&#39;)
] ]
for cmd, desc in commands: for cmd, desc in commands:
if cmd.startswith(cmd_part.lower()): if cmd.startswith(cmd_part.lower()):
@@ -851,68 +974,162 @@ el.replaceWith(d);
copilot_completer = SlashCommandCompleter() copilot_completer = SlashCommandCompleter()
def _finalize_mission(reason=&#34;completed&#34;, final_guide=&#34;&#34;):
mission = self.session_state.get(&#39;mission&#39;, {})
if not mission or not mission.get(&#39;active&#39;, False):
return
mission[&#39;active&#39;] = False
if reason == &#34;completed&#34;:
self.console.print(&#34;\n[bold green]🎉 Mission Completed[/bold green]&#34;)
elif reason == &#34;user_cancelled&#34;:
self.console.print(&#34;\n[yellow]Mission cancelled by user.[/yellow]&#34;)
elif reason == &#34;limit_reached&#34;:
self.console.print(&#34;\n[yellow]Mission step limit reached.[/yellow]&#34;)
final_notes = &#34;\n&#34;.join(mission.get(&#39;scratchpad_notes&#39;, []))
guide = final_guide or mission.get(&#39;last_guide&#39;, &#39;&#39;)
asst_msg = f&#34;Notes: {final_notes}\nGuide: {guide}&#34; if final_notes else guide
hist = self.session_state.setdefault(&#34;copilot_chat_history&#34;, [])
hist.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: f&#34;/mission {mission.get(&#39;goal&#39;, &#39;&#39;)}&#34;})
hist.append({&#34;role&#34;: &#34;assistant&#34;, &#34;content&#34;: asst_msg})
self.session_state[&#34;copilot_chat_history&#34;] = hist[-10:]
while True: while True:
# 2. Ask question overrides = {}
from prompt_toolkit.styles import Style # Check for active mission auto-looping
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;) mission = self.session_state.get(&#39;mission&#39;, {})
ui_style = Style.from_dict({ is_mission = mission.get(&#39;active&#39;, False) and not mission.get(&#39;paused&#39;, False)
&#39;bottom-toolbar&#39;: f&#39;fg:{c_contrast}&#39;,
})
session = PromptSession( if is_mission:
history=self.history, # Force mode_range for mission mode
input=self.pt_input, state[&#39;context_mode&#39;] = self.mode_range
output=self.pt_output, self.session_state[&#39;context_mode&#39;] = self.mode_range
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
# We use an internal try/finally to ensure that if something fails in prompt_async,
# we don&#39;t leave the terminal in a strange state.
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state[&#39;cancelled&#39;] = True
question = &#34;&#34;
if state[&#39;cancelled&#39;] or not question.strip() or question.strip().lower() in [&#39;cancel&#39;, &#39;exit&#39;, &#39;quit&#39;]: if mission.get(&#39;start_block_idx&#39;) is None:
return &#34;cancel&#34;, None, None mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
# 3. Process Input via AIService start_idx = mission.get(&#39;start_block_idx&#39;, state[&#39;total_cmds&#39;])
directive = self.ai_service.process_copilot_input(question, self.session_state) cmds_since_start = max(1, (state[&#39;total_cmds&#39;] - start_idx) + 1)
state[&#39;context_cmd&#39;] = max(state.get(&#39;context_cmd&#39;, 1), cmds_since_start)
self.session_state[&#39;context_cmd&#39;] = state[&#39;context_cmd&#39;]
if directive[&#34;action&#34;] == &#34;state_update&#34;: step = mission.get(&#39;step&#39;, 1)
msg = directive[&#39;message&#39;] max_steps = mission.get(&#39;max_steps&#39;, 10)
state[&#39;toolbar_msg&#39;] = msg
state[&#39;msg_expiry&#39;] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh(): if step &gt; max_steps:
await asyncio.sleep(3.1) ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
# Only invalidate if the message hasn&#39;t been replaced by a newer one c_warn = self._get_theme_color(&#34;warning&#34;, &#34;yellow&#34;)
if state.get(&#39;toolbar_msg&#39;) == msg: import html
state[&#39;toolbar_msg&#39;] = &#39;&#39; # Explicitly clear p_warn = html.escape(f&#34;[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: &#34;)
try: try:
from prompt_toolkit.application.current import get_app ext_ans = await ext_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{c_warn}&#34; bold=&#34;true&#34;&gt;{p_warn}&lt;/style&gt;&#39;))
app = get_app() except (KeyboardInterrupt, EOFError):
if app: app.invalidate() ext_ans = &#39;n&#39;
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one if (ext_ans or &#39;y&#39;).lower().strip() in (&#39;y&#39;, &#39;yes&#39;):
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;) mission[&#39;max_steps&#39;] += 10
sys.stdout.flush() else:
continue goal = mission.get(&#39;goal&#39;, &#39;&#39;)
question = f&#34;[MISSION SUMMARY]: Step limit reached ({max_steps} steps). Provide a concise summary of all findings and current status for: {goal}&#34;
clean_question = question
mission[&#39;active&#39;] = False
is_mission = False
self.console.print(f&#34;\n[bold cyan]🤖 Generating Final Mission Summary ({max_steps} steps reached)...[/bold cyan]&#34;)
if is_mission:
goal = mission.get(&#39;goal&#39;, &#39;&#39;)
step = mission.get(&#39;step&#39;, 1)
question = f&#34;[MISSION STEP {step}]: Continue analysis of: {goal}&#34;
scratchpad = mission.get(&#39;scratchpad_notes&#39;, [])
if scratchpad:
notes_text = &#34;\n&#34;.join(f&#34;- {n}&#34; for n in scratchpad)
question += f&#34;\n\nPast Mission Notes:\n{notes_text}&#34;
clean_question = question
self.console.print(f&#34;\n[bold cyan]🤖 Executing Mission Step {step}: {goal}[/bold cyan]&#34;)
elif not is_mission and &#39;clean_question&#39; in locals() and clean_question.startswith(&#34;[MISSION SUMMARY]&#34;):
# Pass-through to AI execution for generating final summary
pass
else: else:
# Clean the toolbar message when a real question is asked # 2. Ask question
state[&#39;toolbar_msg&#39;] = &#39;&#39; from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;)
ui_style = Style.from_dict({
&#39;bottom-toolbar&#39;: f&#39;fg:{c_contrast}&#39;,
})
clean_question = directive.get(&#34;clean_prompt&#34;, question) session = PromptSession(
overrides = directive.get(&#34;overrides&#34;, {}) history=self.history,
input=self.pt_input,
output=self.pt_output,
completer=copilot_completer,
reserve_space_for_menu=0,
style=ui_style
)
try:
question = await session.prompt_async(
get_prompt_text,
key_bindings=bindings,
bottom_toolbar=get_toolbar,
multiline=True
)
except (KeyboardInterrupt, EOFError):
state[&#39;cancelled&#39;] = True
question = &#34;&#34;
if state[&#39;cancelled&#39;] or not question.strip() or question.strip().lower() in [&#39;cancel&#39;, &#39;exit&#39;, &#39;quit&#39;]:
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None
# 3. Process Input via AIService
directive = self.ai_service.process_copilot_input(question, self.session_state)
if directive[&#34;action&#34;] == &#34;mission_start&#34;:
mission = self.session_state.get(&#39;mission&#39;, {})
mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
state[&#39;context_mode&#39;] = self.mode_range
self.session_state[&#39;context_mode&#39;] = self.mode_range
state[&#39;context_cmd&#39;] = 1
self.session_state[&#39;context_cmd&#39;] = 1
clean_question = f&#34;[MISSION STEP 1]: {directive[&#39;clean_prompt&#39;]}&#34;
overrides = directive.get(&#34;overrides&#34;, {})
self.console.print(f&#34;\n[bold cyan]🤖 Starting Mission: {directive[&#39;clean_prompt&#39;]}[/bold cyan]&#34;)
elif directive[&#34;action&#34;] == &#34;mission_cancel&#34;:
_finalize_mission(&#34;user_cancelled&#34;)
state[&#39;toolbar_msg&#39;] = &#39;Mission cancelled&#39;
continue
elif directive[&#34;action&#34;] == &#34;state_update&#34;:
msg = directive[&#39;message&#39;]
state[&#39;toolbar_msg&#39;] = msg
state[&#39;msg_expiry&#39;] = time.time() + 3 # 3 seconds timeout
async def delayed_refresh():
await asyncio.sleep(3.1)
if state.get(&#39;toolbar_msg&#39;) == msg:
state[&#39;toolbar_msg&#39;] = &#39;&#39;
try:
from prompt_toolkit.application.current import get_app
app = get_app()
if app: app.invalidate()
except: pass
asyncio.create_task(delayed_refresh())
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;)
sys.stdout.flush()
continue
else:
state[&#39;toolbar_msg&#39;] = &#39;&#39;
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = False
step = mission.get(&#39;step&#39;, 1)
goal = mission.get(&#39;goal&#39;, &#39;&#39;)
clean_question = f&#34;[MISSION FEEDBACK (Step {step})]: User provided guidance: {question}. Continue the mission for goal: {goal}&#34;
self.console.print(f&#34;\n[bold cyan]🤖 Continuing Mission with User Feedback: {question}[/bold cyan]&#34;)
overrides = directive.get(&#34;overrides&#34;, {})
else:
clean_question = directive.get(&#34;clean_prompt&#34;, question)
overrides = directive.get(&#34;overrides&#34;, {})
# Merge node_info with session_state and overrides # Merge node_info with session_state and overrides
merged_node_info = node_info.copy() merged_node_info = node_info.copy()
@@ -921,20 +1138,12 @@ el.replaceWith(d);
merged_node_info[&#39;persona&#39;] = self.session_state[&#39;persona&#39;] merged_node_info[&#39;persona&#39;] = self.session_state[&#39;persona&#39;]
merged_node_info[&#39;trust&#39;] = self.session_state[&#39;trust_mode&#39;] merged_node_info[&#39;trust&#39;] = self.session_state[&#39;trust_mode&#39;]
merged_node_info[&#39;memories&#39;] = list(self.session_state[&#39;memories&#39;]) merged_node_info[&#39;memories&#39;] = list(self.session_state[&#39;memories&#39;])
merged_node_info[&#39;chat_history&#39;] = list(self.session_state.get(&#39;copilot_chat_history&#39;, []))
for k, v in overrides.items(): for k, v in overrides.items():
merged_node_info[k] = v merged_node_info[k] = v
# Enrich question
past = self.history.get_strings()
if len(past) &gt; 1:
clean_past = [q for q in past[-6:-1] if not q.startswith(&#39;/&#39;)]
if clean_past:
history_text = &#34;\n&#34;.join(f&#34;- {q}&#34; for q in clean_past)
clean_question = f&#34;Previous questions:\n{history_text}\n\nCurrent Question:\n{clean_question}&#34;
# 3. AI Execution # 3. AI Execution
# Use persona from overrides (one-shot) or from session state
active_persona = merged_node_info.get(&#39;persona&#39;, self.session_state.get(&#39;persona&#39;, &#39;engineer&#39;)) active_persona = merged_node_info.get(&#39;persona&#39;, self.session_state.get(&#39;persona&#39;, &#39;engineer&#39;))
persona_color = self._get_theme_color(active_persona, fallback=&#34;cyan&#34;) persona_color = self._get_theme_color(active_persona, fallback=&#34;cyan&#34;)
persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34; persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34;
@@ -961,7 +1170,6 @@ el.replaceWith(d);
nonlocal live_text, first_chunk nonlocal live_text, first_chunk
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule( self.console.print(Rule(
f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;,
style=persona_color style=persona_color
@@ -970,7 +1178,6 @@ el.replaceWith(d);
live_text += text live_text += text
md_parser.feed(text) md_parser.feed(text)
# Check for interruption during AI call
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info)) ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
try: try:
@@ -979,13 +1186,12 @@ el.replaceWith(d);
result = await ai_task result = await ai_task
except asyncio.CancelledError: except asyncio.CancelledError:
status_spinner.stop() status_spinner.stop()
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None return &#34;cancel&#34;, None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk: if first_chunk:
status_spinner.stop() status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk: if not first_chunk:
md_parser.flush() md_parser.flush()
self.console.print(Rule(style=persona_color)) self.console.print(Rule(style=persona_color))
@@ -993,14 +1199,35 @@ el.replaceWith(d);
if not result or result.get(&#34;error&#34;): if not result or result.get(&#34;error&#34;):
if first_chunk and result and result.get(&#34;error&#34;): if first_chunk and result and result.get(&#34;error&#34;):
self.console.print(f&#34;[red]Error: {result[&#39;error&#39;]}[/red]&#34;) self.console.print(f&#34;[red]Error: {result[&#39;error&#39;]}[/red]&#34;)
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None return &#34;cancel&#34;, None, None
# If no chunks were streamed but we have a guide, print it as a panel
if first_chunk and result and result.get(&#34;guide&#34;): if first_chunk and result and result.get(&#34;guide&#34;):
from rich.markdown import Markdown
self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color)) self.console.print(Panel(Markdown(result[&#34;guide&#34;]), title=f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;, border_style=persona_color))
# Update copilot_chat_history or mission scratchpad
if result and not result.get(&#34;error&#34;):
guide = result.get(&#34;guide&#34;, &#34;&#34;)
notes = result.get(&#34;notes&#34;, &#34;&#34;)
mission = self.session_state.get(&#39;mission&#39;, {})
if mission.get(&#39;active&#39;, False):
if notes:
mission.setdefault(&#39;scratchpad_notes&#39;, []).append(notes)
if guide:
mission[&#39;last_guide&#39;] = guide
else:
if guide or notes:
asst_msg = f&#34;Notes: {notes}\nGuide: {guide}&#34; if notes else guide
hist = self.session_state.setdefault(&#34;copilot_chat_history&#34;, [])
hist.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: clean_question})
hist.append({&#34;role&#34;: &#34;assistant&#34;, &#34;content&#34;: asst_msg})
self.session_state[&#34;copilot_chat_history&#34;] = hist[-10:]
commands = result.get(&#34;commands&#34;, []) commands = result.get(&#34;commands&#34;, [])
mission = self.session_state.get(&#39;mission&#39;, {})
if not commands: if not commands:
if mission.get(&#39;active&#39;, False):
_finalize_mission(&#34;completed&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
@@ -1009,11 +1236,12 @@ el.replaceWith(d);
style_color = self._get_theme_color(risk_style, fallback=&#34;green&#34;) style_color = self._get_theme_color(risk_style, fallback=&#34;green&#34;)
cmd_text = &#34;\n&#34;.join(f&#34; {i+1}. {c}&#34; for i, c in enumerate(commands)) cmd_text = &#34;\n&#34;.join(f&#34; {i+1}. {c}&#34; for i, c in enumerate(commands))
# Explicitly use &#39;bold style_color&#39; for both TITLE and BORDER to ensure maximum consistency
self.console.print(Panel(cmd_text, title=f&#34;[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]&#34;, border_style=f&#34;bold {style_color}&#34;)) self.console.print(Panel(cmd_text, title=f&#34;[bold {style_color}]Suggested Commands [{risk.upper()}][/bold {style_color}]&#34;, border_style=f&#34;bold {style_color}&#34;))
if merged_node_info.get(&#39;trust&#39;, False) and risk != &#34;destructive&#34;: if merged_node_info.get(&#39;trust&#39;, False) and risk != &#34;destructive&#34;:
self.console.print(f&#34;[dim]⚙️ Auto-executing (Trust Mode)[/dim]&#34;) self.console.print(f&#34;[dim]⚙️ Auto-executing (Trust Mode)[/dim]&#34;)
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, commands, None return &#34;send_all&#34;, commands, None
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output) confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -1025,41 +1253,39 @@ el.replaceWith(d);
import html import html
try: try:
p_text = html.escape(f&#34;Send? (y/n/e/range) [n]: &#34;) p_text = html.escape(f&#34;Send? (y/n/e/range) [n]: &#34;)
# Use the EXACT same style_color and force bold=&#34;true&#34; for Prompt-Toolkit
action = await confirm_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{style_color}&#34; bold=&#34;true&#34;&gt;{p_text}&lt;/style&gt;&#39;), key_bindings=c_bindings) action = await confirm_session.prompt_async(HTML(f&#39;&lt;style fg=&#34;{style_color}&#34; bold=&#34;true&#34;&gt;{p_text}&lt;/style&gt;&#39;), key_bindings=c_bindings)
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
_finalize_mission(&#34;user_cancelled&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
def parse_indices(text, max_len): def parse_indices(text, max_len):
&#34;&#34;&#34;Helper to parse &#39;1-3, 5, 7&#39; into [0, 1, 2, 4, 6].&#34;&#34;&#34;
indices = [] indices = []
# Replace commas with spaces and split
parts = text.replace(&#39;,&#39;, &#39; &#39;).split() parts = text.replace(&#39;,&#39;, &#39; &#39;).split()
for part in parts: for part in parts:
if &#39;-&#39; in part: if &#39;-&#39; in part:
try: try:
start, end = map(int, part.split(&#39;-&#39;)) start, end = map(int, part.split(&#39;-&#39;))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end)) indices.extend(range(start-1, end))
except: continue except: continue
elif part.isdigit(): elif part.isdigit():
indices.append(int(part)-1) indices.append(int(part)-1)
# Filter valid indices and remove duplicates
return [i for i in sorted(set(indices)) if 0 &lt;= i &lt; max_len] return [i for i in sorted(set(indices)) if 0 &lt;= i &lt; max_len]
action_l = (action or &#34;n&#34;).lower().strip() action_l = (action or &#34;n&#34;).lower().strip()
if action_l in (&#39;y&#39;, &#39;yes&#39;, &#39;all&#39;): if action_l in (&#39;y&#39;, &#39;yes&#39;, &#39;all&#39;):
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, commands, None return &#34;send_all&#34;, commands, None
# Check for numeric selection (e.g., &#34;1, 2-4&#34;)
if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l): if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l):
selected_idxs = parse_indices(action_l, len(commands)) selected_idxs = parse_indices(action_l, len(commands))
if selected_idxs: if selected_idxs:
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;send_all&#34;, [commands[i] for i in selected_idxs], None return &#34;send_all&#34;, [commands[i] for i in selected_idxs], None
elif action_l.startswith(&#39;e&#39;): elif action_l.startswith(&#39;e&#39;):
# Check if it&#39;s a selective edit like &#39;e1-2&#39;
selection_str = action_l[1:].strip() selection_str = action_l[1:].strip()
if selection_str: if selection_str:
idxs = parse_indices(selection_str, len(commands)) idxs = parse_indices(selection_str, len(commands))
@@ -1085,14 +1311,23 @@ el.replaceWith(d);
default=target, multiline=True, key_bindings=e_bindings default=target, multiline=True, key_bindings=e_bindings
) )
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = True
self.console.print(&#34;\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]&#34;)
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
if edited and edited.strip(): if edited and edited.strip():
# Split by lines to ensure core.py applies delay between each command
lines = [l.strip() for l in edited.split(&#39;\n&#39;) if l.strip()] lines = [l.strip() for l in edited.split(&#39;\n&#39;) if l.strip()]
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
return &#34;custom&#34;, None, lines return &#34;custom&#34;, None, lines
# User rejected/cancelled the commands
if mission.get(&#39;active&#39;, False):
mission[&#39;paused&#39;] = True
self.console.print(&#34;\n[yellow]⏸️ Mission Paused — Provide feedback to redirect, or type /cancel to abort.[/yellow]&#34;)
self.console.print(&#34;&#34;) self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None return &#34;continue&#34;, None, None
@@ -1133,7 +1368,7 @@ on_ai_call: async function(active_buffer, question) -&gt; result_dict</p></div>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.user_handler API documentation</title> <title>connpy.cli.user_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -516,7 +516,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.validators API documentation</title> <title>connpy.cli.validators API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -508,7 +508,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.connpy_pb2 API documentation</title> <title>connpy.grpc_layer.connpy_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code."> <meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -61,7 +61,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.connpy_pb2_grpc API documentation</title> <title>connpy.grpc_layer.connpy_pb2_grpc API documentation</title>
<meta name="description" content="Client and server classes corresponding to protobuf-defined services."> <meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -7006,7 +7006,7 @@ def stop_api(request,
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer API documentation</title> <title>connpy.grpc_layer API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -107,7 +107,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title> <title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code."> <meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -62,7 +62,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -81,7 +81,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -100,7 +100,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -119,7 +119,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -168,7 +168,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title> <title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title>
<meta name="description" content="Client and server classes corresponding to protobuf-defined services."> <meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -366,7 +366,7 @@ def invoke_plugin(request,
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+73 -6
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.server API documentation</title> <title>connpy.grpc_layer.server API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -604,7 +604,7 @@ def service(self):
<dl> <dl>
<dt id="connpy.grpc_layer.server.AuthInterceptor.OPEN_METHODS"><code class="name">var <span class="ident">OPEN_METHODS</span></code></dt> <dt id="connpy.grpc_layer.server.AuthInterceptor.OPEN_METHODS"><code class="name">var <span class="ident">OPEN_METHODS</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
<h3>Methods</h3> <h3>Methods</h3>
@@ -1583,7 +1583,8 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
is_single = saved_mode in (1, &#39;SINGLE&#39;, &#39;single&#39;) is_single = saved_mode in (1, &#39;SINGLE&#39;, &#39;single&#39;)
if is_range or is_single: if is_range or is_single:
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and saved_cmd &gt; 1: is_mission = session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False) or (isinstance(node_info, dict) and node_info.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False))
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and (saved_cmd &gt; 1 or is_mission):
new_cmds = total_cmds - last_total_cmds new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds initial_cmd = saved_cmd + new_cmds
else: else:
@@ -1627,6 +1628,14 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
)) ))
while True: while True:
# Refresh latest buffer from terminal log
raw_bytes = n.mylog.getvalue() if hasattr(n, &#39;mylog&#39;) else buffer
if not isinstance(raw_bytes, bytes):
raw_bytes = str(raw_bytes).encode()
from connpy.utils import log_cleaner
cleaned_buffer = log_cleaner(raw_bytes.decode(errors=&#39;replace&#39;))
buffer = cleaned_buffer
# 0. Drain the queue of any stale messages before starting a new interaction # 0. Drain the queue of any stale messages before starting a new interaction
while not remote_stream.copilot_queue.empty(): while not remote_stream.copilot_queue.empty():
try: try:
@@ -1760,16 +1769,74 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
if action == &#34;send_all&#34;: if action == &#34;send_all&#34;:
commands = result.get(&#34;commands&#34;, []) commands = result.get(&#34;commands&#34;, [])
await n.inject_commands(commands, child_fd, on_inject=on_inject) await n.inject_commands(commands, child_fd, on_inject=on_inject)
return
elif action.startswith(&#34;custom:&#34;): elif action.startswith(&#34;custom:&#34;):
custom_cmds_raw = action[7:] custom_cmds_raw = action[7:]
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split(&#39;\n&#39;) if cmd.strip()] custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split(&#39;\n&#39;) if cmd.strip()]
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject) await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
return
else: else:
os.write(child_fd, b&#39;\x15\r&#39;) os.write(child_fd, b&#39;\x15\r&#39;)
return return
# Dynamic Wait for Device Prompt Settle
prompt_pattern = node_info.get(&#34;prompt&#34;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;)
import time
import re
start_t = time.time()
last_len = 0
quiet_count = 0
while time.time() - start_t &lt; 60:
await asyncio.sleep(0.15)
cur_bytes = n.mylog.getvalue()
if len(cur_bytes) == last_len:
quiet_count += 1
else:
quiet_count = 0
last_len = len(cur_bytes)
clean_txt = n._logclean(cur_bytes.decode(errors=&#39;replace&#39;), True).strip()
lines = [l.strip() for l in clean_txt.split(&#39;\n&#39;) if l.strip()]
last_line = lines[-1] if lines else &#34;&#34;
if quiet_count &gt;= 2 and re.search(prompt_pattern, last_line):
break
# Recalculate context blocks with updated buffer
raw_bytes = n.mylog.getvalue()
from connpy.utils import log_cleaner
cleaned_buffer = log_cleaner(raw_bytes.decode(errors=&#39;replace&#39;))
last_line = cleaned_buffer.split(&#39;\n&#39;)[-1].strip() if cleaned_buffer.strip() else &#34;(prompt)&#34;
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
node_info[&#34;context_blocks&#34;] = blocks
total_cmds = len(blocks)
total_lines = len(cleaned_buffer.split(&#39;\n&#39;))
last_total_cmds = session_state.get(&#39;last_total_cmds&#39;, None)
saved_cmd = session_state.get(&#39;context_cmd&#39;, 1)
is_mission = session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False) or (isinstance(node_info, dict) and node_info.get(&#39;mission&#39;, {}).get(&#39;active&#39;, False))
if last_total_cmds is not None and total_cmds &gt; last_total_cmds and (saved_cmd &gt; 1 or is_mission):
new_cmds = total_cmds - last_total_cmds
initial_cmd = saved_cmd + new_cmds
else:
initial_cmd = saved_cmd
session_state[&#39;context_cmd&#39;] = max(1, initial_cmd)
session_state[&#39;last_total_cmds&#39;] = total_cmds
session_state[&#39;last_total_lines&#39;] = total_lines
node_info[&#39;context_cmd&#39;] = min(session_state[&#39;context_cmd&#39;], max(1, total_cmds))
node_info[&#39;context_lines&#39;] = min(session_state.get(&#39;context_lines&#39;, 50), max(1, total_lines))
node_info_json = json.dumps(node_info)
preview_str = raw_bytes[-200:].decode(errors=&#39;replace&#39;) if isinstance(raw_bytes, bytes) else str(raw_bytes)[-200:]
# Signal prompt settle and send updated blocks to client
response_queue.put(connpy_pb2.InteractResponse(
copilot_prompt=True,
copilot_buffer_preview=preview_str,
copilot_node_info_json=node_info_json
))
continue
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler)) asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
except Exception as e: except Exception as e:
import traceback import traceback
@@ -2516,7 +2583,7 @@ def service(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+32 -11
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.stubs API documentation</title> <title>connpy.grpc_layer.stubs API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -1373,7 +1373,7 @@ def set_reserved_names(self, names):
self.config = config self.config = config
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty): def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty):
import json, asyncio, termios, sys, tty, queue import json, asyncio, termios, sys, tty, queue, os
from ..core import copilot_terminal_mode from ..core import copilot_terminal_mode
from . import connpy_pb2 from . import connpy_pb2
@@ -1391,6 +1391,7 @@ def set_reserved_names(self, names):
) )
self.copilot_history = interface.history self.copilot_history = interface.history
self.copilot_state = interface.session_state self.copilot_state = interface.session_state
interface.session_state[&#39;banner_shown&#39;] = False
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info): async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
# Send request to server # Send request to server
@@ -1413,6 +1414,7 @@ def set_reserved_names(self, names):
# Wrap in async loop # Wrap in async loop
async def run_remote_copilot(): async def run_remote_copilot():
nonlocal blocks, node_info
while True: while True:
action, commands, custom_cmd = await interface.run_session( action, commands, custom_cmd = await interface.run_session(
raw_bytes=bytes(client_buffer_bytes), raw_bytes=bytes(client_buffer_bytes),
@@ -1426,6 +1428,32 @@ def set_reserved_names(self, names):
request_queue.put(connpy_pb2.InteractRequest(copilot_action=&#34;continue&#34;)) request_queue.put(connpy_pb2.InteractRequest(copilot_action=&#34;continue&#34;))
continue continue
if action in (&#34;send_all&#34;, &#34;custom&#34;):
action_sent = &#34;cancel&#34;
if action == &#34;send_all&#34; and commands:
action_sent = f&#34;custom:{chr(10).join(commands)}&#34;
elif action == &#34;custom&#34; and custom_cmd:
action_sent = f&#34;custom:{chr(10).join(custom_cmd)}&#34;
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
print(&#34;\r\n\033[2m [Remote] Ejecutando comandos y esperando al prompt...\033[0m\r\n&#34;, flush=True)
while True:
try:
prompt_res = response_queue.get(timeout=0.1)
if prompt_res is None:
return &#34;cancel&#34;, None, None
if prompt_res.stdout_data:
os.write(sys.stdout.fileno(), prompt_res.stdout_data)
client_buffer_bytes.extend(prompt_res.stdout_data)
if prompt_res.copilot_prompt:
if prompt_res.copilot_node_info_json:
node_info = json.loads(prompt_res.copilot_node_info_json)
blocks = node_info.get(&#34;context_blocks&#34;, [])
break
except queue.Empty:
await asyncio.sleep(0.05)
continue
return action, commands, custom_cmd return action, commands, custom_cmd
with copilot_terminal_mode(): with copilot_terminal_mode():
@@ -1433,14 +1461,7 @@ def set_reserved_names(self, names):
print(&#34;\033[2m Returning to session...\033[0m&#34;, flush=True) print(&#34;\033[2m Returning to session...\033[0m&#34;, flush=True)
# Prepare final action for server # Prepare final action for server
action_sent = &#34;cancel&#34; request_queue.put(connpy_pb2.InteractRequest(copilot_action=&#34;cancel&#34;))
if action == &#34;send_all&#34; and commands:
# In remote mode, send the selected commands as a custom block
# so the server executes exactly what the user picked (e.g., selection &#39;1&#39;)
action_sent = f&#34;custom:{chr(10).join(commands)}&#34;
elif action == &#34;custom&#34; and custom_cmd:
action_sent = f&#34;custom:{chr(10).join(custom_cmd)}&#34;
request_queue.put(connpy_pb2.InteractRequest(copilot_action=action_sent))
resume_generator() resume_generator()
tty.setraw(sys.stdin.fileno()) tty.setraw(sys.stdin.fileno())
@@ -2967,7 +2988,7 @@ def stop_api(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.user_registry API documentation</title> <title>connpy.grpc_layer.user_registry API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -312,7 +312,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.utils API documentation</title> <title>connpy.grpc_layer.utils API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -138,7 +138,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+105 -13
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy API documentation</title> <title>connpy API documentation</title>
<meta name="description" content="&lt;p align=&#34;center&#34;&gt; <meta name="description" content="&lt;p align=&#34;center&#34;&gt;
&lt;img src=&#34;https://nginx.gederico.dynu.net/images/CONNPY-resized.png&#34; alt=&#34;App Logo&#34;&gt; &lt;img src=&#34;https://nginx.gederico.dynu.net/images/CONNPY-resized.png&#34; alt=&#34;App Logo&#34;&gt;
@@ -41,7 +41,7 @@ el.replaceWith(d);
<p align="center"> <p align="center">
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo"> <img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
</p> </p>
<h1 id="connpy-v610">Connpy (v6.1.0)</h1> <h1 id="connpy-v630">Connpy (v6.3.0)</h1>
<p><a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/v/connpy.svg?style=flat-square"></a> <p><a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/v/connpy.svg?style=flat-square"></a>
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square"></a> <a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/pyversions/connpy.svg?style=flat-square"></a>
<a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&amp;cacheSeconds=86400"></a> <a href="https://pypi.org/pypi/connpy/"><img alt="" src="https://img.shields.io/pypi/dm/connpy.svg?style=flat-square&amp;cacheSeconds=86400"></a>
@@ -57,7 +57,9 @@ el.replaceWith(d);
<h3 id="1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</h3> <h3 id="1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</h3>
<p>Invoke the context-aware AI Copilot directly inside any active terminal session by pressing <strong><code>Ctrl + Space</code></strong>. <p>Invoke the context-aware AI Copilot directly inside any active terminal session by pressing <strong><code>Ctrl + Space</code></strong>.
* <strong>Context Modes</strong>: Cycles through <code>LINES</code> (sends raw scroll buffer), <code>SINGLE</code> (captures exactly one command + output block), and <code>RANGE</code> (logical group of recent commands) using <strong><code>Ctrl+Up/Down</code></strong>. * <strong>Context Modes</strong>: Cycles through <code>LINES</code> (sends raw scroll buffer), <code>SINGLE</code> (captures exactly one command + output block), and <code>RANGE</code> (logical group of recent commands) using <strong><code>Ctrl+Up/Down</code></strong>.
* <strong>Slash Commands (<code>/</code>)</strong>: Control the AI persona and safety settings: * <strong>Slash Commands (<code>/</code>)</strong>: Control the AI persona, safety settings, and mission mode:
* <code>/mission [objective]</code>: Start an autonomous multi-step investigation mission with human approval.
* <code>/cancel</code>: Abort active mission.
* <code>/architect</code> / <code>/engineer</code>: Swaps the agent between high-level strategist and technical executor. * <code>/architect</code> / <code>/engineer</code>: Swaps the agent between high-level strategist and technical executor.
* <code>/trust</code> / <code>/untrust</code>: Configures auto-run behavior for suggested non-destructive commands. * <code>/trust</code> / <code>/untrust</code>: Configures auto-run behavior for suggested non-destructive commands.
* <code>/os [system]</code>: Manually overrides target OS parsing rules (e.g. <code>/os cisco_ios</code>). * <code>/os [system]</code>: Manually overrides target OS parsing rules (e.g. <code>/os cisco_ios</code>).
@@ -819,16 +821,39 @@ class configfile:
self.connections = config[&#34;connections&#34;] self.connections = config[&#34;connections&#34;]
self.profiles = config[&#34;profiles&#34;] self.profiles = config[&#34;profiles&#34;]
self._privatekey_obj = None
self._publickey_obj = None
if not os.path.exists(self.key): if not os.path.exists(self.key):
self._createkey(self.key) self._createkey(self.key)
with open(self.key) as f:
self.privatekey = RSA.import_key(f.read())
self.publickey = self.privatekey.publickey()
# Self-heal text caches if they are missing # Self-heal text caches if they are missing
if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile): if not os.path.exists(self.fzf_cachefile) or not os.path.exists(self.folders_cachefile) or not os.path.exists(self.profiles_cachefile):
self._generate_nodes_cache() self._generate_nodes_cache()
@property
def privatekey(self):
if getattr(self, &#39;_privatekey_obj&#39;, None) is None:
from Crypto.PublicKey import RSA
if not os.path.exists(self.key):
self._createkey(self.key)
with open(self.key) as f:
self._privatekey_obj = RSA.import_key(f.read())
return self._privatekey_obj
@privatekey.setter
def privatekey(self, value):
self._privatekey_obj = value
@property
def publickey(self):
if getattr(self, &#39;_publickey_obj&#39;, None) is None:
self._publickey_obj = self.privatekey.publickey()
return self._publickey_obj
@publickey.setter
def publickey(self, value):
self._publickey_obj = value
def get_effective_setting(self, key, default=None): def get_effective_setting(self, key, default=None):
&#34;&#34;&#34;Get config setting with shared fallback for inheritable keys.&#34;&#34;&#34; &#34;&#34;&#34;Get config setting with shared fallback for inheritable keys.&#34;&#34;&#34;
@@ -977,6 +1002,7 @@ class configfile:
def _createkey(self, keyfile): def _createkey(self, keyfile):
#Create key file #Create key file
from Crypto.PublicKey import RSA
key = RSA.generate(2048) key = RSA.generate(2048)
with open(keyfile,&#39;wb&#39;) as f: with open(keyfile,&#39;wb&#39;) as f:
f.write(key.export_key(&#39;PEM&#39;)) f.write(key.export_key(&#39;PEM&#39;))
@@ -1303,6 +1329,8 @@ class configfile:
if keyfile is None: if keyfile is None:
keyfile = self.key keyfile = self.key
with open(keyfile) as f: with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read()) key = RSA.import_key(f.read())
f.close() f.close()
publickey = key.publickey() publickey = key.publickey()
@@ -1331,6 +1359,41 @@ class configfile:
- publickey (obj): Object containing the public key to decrypt - publickey (obj): Object containing the public key to decrypt
passwords. passwords.
</code></pre></div> </code></pre></div>
<h3>Instance variables</h3>
<dl>
<dt id="connpy.configfile.privatekey"><code class="name">prop <span class="ident">privatekey</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def privatekey(self):
if getattr(self, &#39;_privatekey_obj&#39;, None) is None:
from Crypto.PublicKey import RSA
if not os.path.exists(self.key):
self._createkey(self.key)
with open(self.key) as f:
self._privatekey_obj = RSA.import_key(f.read())
return self._privatekey_obj</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.configfile.publickey"><code class="name">prop <span class="ident">publickey</span></code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@property
def publickey(self):
if getattr(self, &#39;_publickey_obj&#39;, None) is None:
self._publickey_obj = self.privatekey.publickey()
return self._publickey_obj</code></pre>
</details>
<div class="desc"></div>
</dd>
</dl>
<h3>Methods</h3> <h3>Methods</h3>
<dl> <dl>
<dt id="connpy.configfile.encrypt"><code class="name flex"> <dt id="connpy.configfile.encrypt"><code class="name flex">
@@ -1363,6 +1426,8 @@ def encrypt(self, password, keyfile=None):
if keyfile is None: if keyfile is None:
keyfile = self.key keyfile = self.key
with open(keyfile) as f: with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read()) key = RSA.import_key(f.read())
f.close() f.close()
publickey = key.publickey() publickey = key.publickey()
@@ -1839,6 +1904,8 @@ class node:
if keyfile is None: if keyfile is None:
keyfile = self.key keyfile = self.key
if keyfile is not None: if keyfile is not None:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f: with open(keyfile) as f:
key = RSA.import_key(f.read()) key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key) decryptor = PKCS1_OAEP.new(key)
@@ -2356,6 +2423,7 @@ class node:
# Save history back to stream for persistence in current session # Save history back to stream for persistence in current session
stream.copilot_history = interface.history stream.copilot_history = interface.history
stream.copilot_state = interface.session_state stream.copilot_state = interface.session_state
interface.session_state[&#39;banner_shown&#39;] = False
ai_service = AIService(config) ai_service = AIService(config)
@@ -2386,6 +2454,32 @@ class node:
node_info=node_info, node_info=node_info,
on_ai_call=on_ai_call on_ai_call=on_ai_call
) )
if action in (&#34;send_all&#34;, &#34;custom&#34;):
cmds_to_send = commands if action == &#34;send_all&#34; else custom_cmd
await self.inject_commands(cmds_to_send, child_fd)
# Dynamic Wait for Device Prompt Settle
prompt_pattern = node_info.get(&#34;prompt&#34;, r&#39;&gt;$|#$|\$$|&gt;.$|#.$|\$.$&#39;)
start_t = time()
last_len = 0
quiet_count = 0
while time() - start_t &lt; 60:
await asyncio.sleep(0.15)
cur_bytes = self.mylog.getvalue()
if len(cur_bytes) == last_len:
quiet_count += 1
else:
quiet_count = 0
last_len = len(cur_bytes)
clean_txt = self._logclean(cur_bytes.decode(errors=&#39;replace&#39;), True).strip()
lines = [l.strip() for l in clean_txt.split(&#39;\n&#39;) if l.strip()]
last_line = lines[-1] if lines else &#34;&#34;
if quiet_count &gt;= 2 and re.search(prompt_pattern, last_line):
break
raw_bytes = self.mylog.getvalue()
continue
if action == &#34;continue&#34;: if action == &#34;continue&#34;:
continue continue
break break
@@ -2397,11 +2491,7 @@ class node:
elif hasattr(stream, &#39;_loop&#39;) and hasattr(stream, &#39;stdin_fd&#39;): elif hasattr(stream, &#39;_loop&#39;) and hasattr(stream, &#39;stdin_fd&#39;):
stream._loop.add_reader(stream.stdin_fd, stream._read_ready) stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
if action in (&#34;send_all&#34;, &#34;custom&#34;): os.write(child_fd, b&#39;\x15\r&#39;)
cmds_to_send = commands if action == &#34;send_all&#34; else custom_cmd
await self.inject_commands(cmds_to_send, child_fd)
else:
os.write(child_fd, b&#39;\x15\r&#39;)
except Exception as e: except Exception as e:
import traceback import traceback
print(f&#34;\n[ERROR in Copilot Handler] {e}&#34;, flush=True) print(f&#34;\n[ERROR in Copilot Handler] {e}&#34;, flush=True)
@@ -4022,7 +4112,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<nav id="sidebar"> <nav id="sidebar">
<div class="toc"> <div class="toc">
<ul> <ul>
<li><a href="#connpy-v610">Connpy (v6.1.0)</a><ul> <li><a href="#connpy-v630">Connpy (v6.3.0)</a><ul>
<li><a href="#1-ai-system">1. 🤖 AI System</a><ul> <li><a href="#1-ai-system">1. 🤖 AI System</a><ul>
<li><a href="#1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</a></li> <li><a href="#1a-terminal-copilot-ctrlspace">1a. Terminal Copilot (Ctrl+Space)</a></li>
<li><a href="#1b-ai-chat-conn-ai">1b. AI Chat (conn ai)</a></li> <li><a href="#1b-ai-chat-conn-ai">1b. AI Chat (conn ai)</a></li>
@@ -4104,6 +4194,8 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<li><code><a title="connpy.configfile.get_effective_setting" href="#connpy.configfile.get_effective_setting">get_effective_setting</a></code></li> <li><code><a title="connpy.configfile.get_effective_setting" href="#connpy.configfile.get_effective_setting">get_effective_setting</a></code></li>
<li><code><a title="connpy.configfile.getitem" href="#connpy.configfile.getitem">getitem</a></code></li> <li><code><a title="connpy.configfile.getitem" href="#connpy.configfile.getitem">getitem</a></code></li>
<li><code><a title="connpy.configfile.getitems" href="#connpy.configfile.getitems">getitems</a></code></li> <li><code><a title="connpy.configfile.getitems" href="#connpy.configfile.getitems">getitems</a></code></li>
<li><code><a title="connpy.configfile.privatekey" href="#connpy.configfile.privatekey">privatekey</a></code></li>
<li><code><a title="connpy.configfile.publickey" href="#connpy.configfile.publickey">publickey</a></code></li>
</ul> </ul>
</li> </li>
<li> <li>
@@ -4128,7 +4220,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.mcp_client API documentation</title> <title>connpy.mcp_client API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -349,7 +349,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.proto API documentation</title> <title>connpy.proto API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -60,7 +60,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+42 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.ai_service API documentation</title> <title>connpy.services.ai_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -259,6 +259,26 @@ el.replaceWith(d);
else: else:
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}}
elif cmd == &#34;/mission&#34;:
if args:
session_state[&#39;mission&#39;] = {
&#39;active&#39;: True,
&#39;goal&#39;: args,
&#39;step&#39;: 1,
&#39;max_steps&#39;: 10,
&#39;start_block_idx&#39;: None,
&#39;scratchpad_notes&#39;: []
}
return {&#34;action&#34;: &#34;mission_start&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {}}
else:
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;Usage: /mission &lt;objective_description&gt;&#34;}
elif cmd in (&#34;/cancel&#34;, &#34;/abort&#34;):
if session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;):
session_state[&#39;mission&#39;][&#39;active&#39;] = False
return {&#34;action&#34;: &#34;mission_cancel&#34;, &#34;message&#34;: &#34;Mission cancelled&#34;}
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;No active mission to cancel&#34;}
# Unknown command, execute normally # Unknown command, execute normally
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}
@@ -877,6 +897,26 @@ el.replaceWith(d);
else: else:
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}}
elif cmd == &#34;/mission&#34;:
if args:
session_state[&#39;mission&#39;] = {
&#39;active&#39;: True,
&#39;goal&#39;: args,
&#39;step&#39;: 1,
&#39;max_steps&#39;: 10,
&#39;start_block_idx&#39;: None,
&#39;scratchpad_notes&#39;: []
}
return {&#34;action&#34;: &#34;mission_start&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {}}
else:
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;Usage: /mission &lt;objective_description&gt;&#34;}
elif cmd in (&#34;/cancel&#34;, &#34;/abort&#34;):
if session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;):
session_state[&#39;mission&#39;][&#39;active&#39;] = False
return {&#34;action&#34;: &#34;mission_cancel&#34;, &#34;message&#34;: &#34;Mission cancelled&#34;}
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;No active mission to cancel&#34;}
# Unknown command, execute normally # Unknown command, execute normally
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre> return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre>
</details> </details>
@@ -933,7 +973,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.base API documentation</title> <title>connpy.services.base API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -152,7 +152,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.config_service API documentation</title> <title>connpy.services.config_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.context_service API documentation</title> <title>connpy.services.context_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -370,7 +370,7 @@ def current_context(self) -&gt; str:
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.exceptions API documentation</title> <title>connpy.services.exceptions API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -268,7 +268,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.execution_service API documentation</title> <title>connpy.services.execution_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -340,7 +340,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.import_export_service API documentation</title> <title>connpy.services.import_export_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -361,7 +361,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+42 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services API documentation</title> <title>connpy.services API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -318,6 +318,26 @@ el.replaceWith(d);
else: else:
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}}
elif cmd == &#34;/mission&#34;:
if args:
session_state[&#39;mission&#39;] = {
&#39;active&#39;: True,
&#39;goal&#39;: args,
&#39;step&#39;: 1,
&#39;max_steps&#39;: 10,
&#39;start_block_idx&#39;: None,
&#39;scratchpad_notes&#39;: []
}
return {&#34;action&#34;: &#34;mission_start&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {}}
else:
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;Usage: /mission &lt;objective_description&gt;&#34;}
elif cmd in (&#34;/cancel&#34;, &#34;/abort&#34;):
if session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;):
session_state[&#39;mission&#39;][&#39;active&#39;] = False
return {&#34;action&#34;: &#34;mission_cancel&#34;, &#34;message&#34;: &#34;Mission cancelled&#34;}
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;No active mission to cancel&#34;}
# Unknown command, execute normally # Unknown command, execute normally
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}
@@ -936,6 +956,26 @@ el.replaceWith(d);
else: else:
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}} return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {&#34;trust&#34;: False}}
elif cmd == &#34;/mission&#34;:
if args:
session_state[&#39;mission&#39;] = {
&#39;active&#39;: True,
&#39;goal&#39;: args,
&#39;step&#39;: 1,
&#39;max_steps&#39;: 10,
&#39;start_block_idx&#39;: None,
&#39;scratchpad_notes&#39;: []
}
return {&#34;action&#34;: &#34;mission_start&#34;, &#34;clean_prompt&#34;: args, &#34;overrides&#34;: {}}
else:
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;Usage: /mission &lt;objective_description&gt;&#34;}
elif cmd in (&#34;/cancel&#34;, &#34;/abort&#34;):
if session_state.get(&#39;mission&#39;, {}).get(&#39;active&#39;):
session_state[&#39;mission&#39;][&#39;active&#39;] = False
return {&#34;action&#34;: &#34;mission_cancel&#34;, &#34;message&#34;: &#34;Mission cancelled&#34;}
return {&#34;action&#34;: &#34;state_update&#34;, &#34;message&#34;: &#34;No active mission to cancel&#34;}
# Unknown command, execute normally # Unknown command, execute normally
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre> return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre>
</details> </details>
@@ -5858,7 +5898,7 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.node_service API documentation</title> <title>connpy.services.node_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -790,7 +790,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.plugin_service API documentation</title> <title>connpy.services.plugin_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -838,7 +838,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.profile_service API documentation</title> <title>connpy.services.profile_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -429,7 +429,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.provider API documentation</title> <title>connpy.services.provider API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -351,7 +351,7 @@ def users(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.sync_service API documentation</title> <title>connpy.services.sync_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -978,7 +978,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.system_service API documentation</title> <title>connpy.services.system_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -325,7 +325,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.user_service API documentation</title> <title>connpy.services.user_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -894,7 +894,7 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.tunnels API documentation</title> <title>connpy.tunnels API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -549,7 +549,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.utils API documentation</title> <title>connpy.utils API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -147,7 +147,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>