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>
# 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/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/)
@@ -25,7 +25,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
### 1a. Terminal Copilot (Ctrl+Space)
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
* **Slash Commands (`/`)**: Control the AI persona and safety settings:
* **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.
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
* `/os [system]`: Manually overrides target OS parsing rules (e.g. `/os cisco_ios`).
+4 -2
View File
@@ -5,7 +5,7 @@
</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/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/)
@@ -27,7 +27,9 @@ The v6 release introduces a comprehensive **AI Copilot** and **AI Playbook Engin
### 1a. Terminal Copilot (Ctrl+Space)
Invoke the context-aware AI Copilot directly inside any active terminal session by pressing **`Ctrl + Space`**.
* **Context Modes**: Cycles through `LINES` (sends raw scroll buffer), `SINGLE` (captures exactly one command + output block), and `RANGE` (logical group of recent commands) using **`Ctrl+Up/Down`**.
* **Slash Commands (`/`)**: Control the AI persona and safety settings:
* **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.
* `/trust` / `/untrust`: Configures auto-run behavior for suggested non-destructive commands.
* `/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)
from .hooks import ClassHook, MethodHook
from . import printer
from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text
from rich.console import Group
@@ -1183,6 +1182,7 @@ class ai:
if status:
try: status.stop()
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"))
if status:
try: status.start()
@@ -1230,6 +1230,7 @@ class ai:
if status:
try: status.stop()
except: pass
from rich.markdown import Markdown
self.console.print(Panel(Markdown(obs), title="[architect]Architect Consultation[/architect]", border_style="architect"))
if status:
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.
6. Keep your guide concise and authoritative.
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>
Your brief tactical guide in markdown.
</guide>
@@ -1401,7 +1405,8 @@ Your brief tactical guide in markdown.
<risk>
low
</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_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.
6. ULTRA-CONCISE. Keep your guide to the point.
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>
Your brief tactical guide in markdown. 3-4 sentences max.
</guide>
@@ -1428,7 +1436,8 @@ command 2
<risk>
low, high, or destructive
</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_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."
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_question}
{"role": "system", "content": system_prompt}
]
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
max_iterations = 5 # Allow up to 5 iterations for tool usage
@@ -1581,6 +1598,11 @@ Node: {node_name}"""
chunk_callback(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 = ""
commands = []
risk_level = "low"
@@ -1605,6 +1627,7 @@ Node: {node_name}"""
return {
"commands": commands,
"guide": guide,
"notes": notes,
"risk_level": risk_level,
"error": None
}
@@ -1872,6 +1895,7 @@ class PlaybookBuilderAgent:
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
+2 -1
View File
@@ -1,6 +1,5 @@
import sys
from rich.panel import Panel
from rich.markdown import Markdown
from rich.rule import Rule
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]"
if not result.get("streamed"):
from rich.markdown import Markdown
mdprint(Panel(Markdown(result["response"]), title=title, border_style=border, expand=False))
if "usage" in result:
@@ -121,6 +121,7 @@ class AIHandler:
printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history:
from rich.markdown import Markdown
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(Rule(style="engineer"))
+1 -1
View File
@@ -1,6 +1,5 @@
import sys
import yaml
from rich.markdown import Markdown
from .. import printer
from ..services.exceptions import ConnpyError, InvalidConfigurationError
@@ -156,6 +155,7 @@ class NodeHandler:
# Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques)
+158 -41
View File
@@ -11,7 +11,6 @@ from textwrap import dedent
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.filters import has_completions
@@ -37,6 +36,7 @@ class CopilotInterface:
self.session_state.setdefault('persona', 'engineer')
self.session_state.setdefault('trust_mode', False)
self.session_state.setdefault('memories', [])
self.session_state.setdefault('copilot_chat_history', [])
self.session_state.setdefault('os', None)
self.session_state.setdefault('prompt', None)
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')
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
initial_cmd = saved_cmd + new_cmds
else:
@@ -137,15 +138,17 @@ class CopilotInterface:
self.session_state['last_total_cmds'] = total_cmds
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(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
if not self.session_state.get('banner_shown', False):
self.console.print(Panel(
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n"
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
border_style="cyan"
))
self.console.print("\n") # Small space before the copilot prompt
self.session_state['banner_shown'] = True
self.console.print("") # Small space before the copilot prompt
bindings = KeyBindings()
@bindings.add('c-up')
@@ -227,7 +230,7 @@ class CopilotInterface:
text = app.current_buffer.text
# Only show command help if typing the first command and there are no spaces
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())]
if 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']]
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:
idx = max(0, state['total_cmds'] - state['context_cmd'])
@@ -258,7 +261,7 @@ class CopilotInterface:
p = clean_preview(b[2])
if p:
# Truncar comandos individuales largos
if len(p) > 25: p = p[:22] + "..."
if len(p) > 25: p = p[:22].rstrip(' .,-_') + "..."
previews.append(p)
if not previews:
@@ -266,12 +269,12 @@ class CopilotInterface:
elif len(previews) <= 3:
desc = " + ".join(previews)
else:
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
desc = f"{previews[0]} + {previews[1]} + {previews[2]} (+{len(previews)-3})"
else:
# Modo SINGLE original
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
# 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'),
('/untrust', 'Disable auto-execute'),
('/memorize', 'Add fact to memory'),
('/clear', 'Clear memory')
('/clear', 'Clear memory'),
('/mission', 'Start autonomous mission'),
('/cancel', 'Cancel active mission')
]
for cmd, desc in commands:
if cmd.startswith(cmd_part.lower()):
@@ -313,7 +318,84 @@ class CopilotInterface:
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:
overrides = {}
# Check for active mission auto-looping
mission = self.session_state.get('mission', {})
is_mission = mission.get('active', False) and not mission.get('paused', False)
if is_mission:
# Force mode_range for mission mode
state['context_mode'] = self.mode_range
self.session_state['context_mode'] = self.mode_range
if mission.get('start_block_idx') is None:
mission['start_block_idx'] = state['total_cmds']
start_idx = mission.get('start_block_idx', state['total_cmds'])
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']
step = mission.get('step', 1)
max_steps = mission.get('max_steps', 10)
if step > max_steps:
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
c_warn = self._get_theme_color("warning", "yellow")
import html
p_warn = html.escape(f"[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: ")
try:
ext_ans = await ext_session.prompt_async(HTML(f'<style fg="{c_warn}" bold="true">{p_warn}</style>'))
except (KeyboardInterrupt, EOFError):
ext_ans = 'n'
if (ext_ans or 'y').lower().strip() in ('y', 'yes'):
mission['max_steps'] += 10
else:
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:
# 2. Ask question
from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color("contrast", "gray")
@@ -330,8 +412,6 @@ class CopilotInterface:
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,
@@ -343,21 +423,35 @@ class CopilotInterface:
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"] == "state_update":
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)
# Only invalidate if the message hasn't been replaced by a newer one
if state.get('toolbar_msg') == msg:
state['toolbar_msg'] = '' # Explicitly clear
state['toolbar_msg'] = ''
try:
from prompt_toolkit.application.current import get_app
app = get_app()
@@ -365,14 +459,19 @@ class CopilotInterface:
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one
sys.stdout.write('\x1b[1A\x1b[2K')
sys.stdout.flush()
continue
else:
# Clean the toolbar message when a real question is asked
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", {})
@@ -383,20 +482,12 @@ class CopilotInterface:
merged_node_info['persona'] = self.session_state['persona']
merged_node_info['trust'] = self.session_state['trust_mode']
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():
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
# Use persona from overrides (one-shot) or from session state
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
persona_color = self._get_theme_color(active_persona, fallback="cyan")
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
@@ -423,7 +514,6 @@ class CopilotInterface:
nonlocal live_text, first_chunk
if first_chunk:
status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule(
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
style=persona_color
@@ -432,7 +522,6 @@ class CopilotInterface:
live_text += 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))
try:
@@ -441,13 +530,12 @@ class CopilotInterface:
result = await ai_task
except asyncio.CancelledError:
status_spinner.stop()
_finalize_mission("user_cancelled")
return "cancel", None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk:
status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk:
md_parser.flush()
self.console.print(Rule(style=persona_color))
@@ -455,14 +543,35 @@ class CopilotInterface:
if not result or result.get("error"):
if first_chunk and result and result.get("error"):
self.console.print(f"[red]Error: {result['error']}[/red]")
_finalize_mission("user_cancelled")
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"):
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))
# 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", [])
mission = self.session_state.get('mission', {})
if not commands:
if mission.get('active', False):
_finalize_mission("completed", result.get("guide", ""))
self.console.print("")
return "continue", None, None
@@ -471,11 +580,12 @@ class CopilotInterface:
style_color = self._get_theme_color(risk_style, fallback="green")
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}"))
if merged_node_info.get('trust', False) and risk != "destructive":
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
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -487,41 +597,39 @@ class CopilotInterface:
import html
try:
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)
except (KeyboardInterrupt, EOFError):
_finalize_mission("user_cancelled", result.get("guide", ""))
self.console.print("")
return "continue", None, None
def parse_indices(text, max_len):
"""Helper to parse '1-3, 5, 7' into [0, 1, 2, 4, 6]."""
indices = []
# Replace commas with spaces and split
parts = text.replace(',', ' ').split()
for part in parts:
if '-' in part:
try:
start, end = map(int, part.split('-'))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end))
except: continue
elif part.isdigit():
indices.append(int(part)-1)
# Filter valid indices and remove duplicates
return [i for i in sorted(set(indices)) if 0 <= i < max_len]
action_l = (action or "n").lower().strip()
if action_l in ('y', 'yes', 'all'):
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
return "send_all", commands, None
# Check for numeric selection (e.g., "1, 2-4")
if re.match(r'^[0-9,\-\s]+$', action_l):
selected_idxs = parse_indices(action_l, len(commands))
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
elif action_l.startswith('e'):
# Check if it's a selective edit like 'e1-2'
selection_str = action_l[1:].strip()
if selection_str:
idxs = parse_indices(selection_str, len(commands))
@@ -547,14 +655,23 @@ class CopilotInterface:
default=target, multiline=True, key_bindings=e_bindings
)
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("")
return "continue", None, None
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()]
if mission.get('active', False):
mission['step'] = mission.get('step', 1) + 1
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("")
return "continue", None, None
+29 -5
View File
@@ -6,8 +6,6 @@ import re
import sys
import yaml
import shutil
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from pathlib import Path
from copy import deepcopy
from .hooks import MethodHook, ClassHook
@@ -139,16 +137,39 @@ class configfile:
self.connections = config["connections"]
self.profiles = config["profiles"]
self._privatekey_obj = None
self._publickey_obj = None
if not os.path.exists(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
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()
@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):
"""Get config setting with shared fallback for inheritable keys."""
@@ -297,6 +318,7 @@ class configfile:
def _createkey(self, keyfile):
#Create key file
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
with open(keyfile,'wb') as f:
f.write(key.export_key('PEM'))
@@ -623,6 +645,8 @@ class configfile:
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
+29 -6
View File
@@ -4,8 +4,6 @@ import os
import re
import pexpect
import shlex
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
import ast
from time import sleep,time
import datetime
@@ -274,6 +272,8 @@ class node:
if keyfile is None:
keyfile = self.key
if keyfile is not None:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f:
key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key)
@@ -791,6 +791,7 @@ class node:
# Save history back to stream for persistence in current session
stream.copilot_history = interface.history
stream.copilot_state = interface.session_state
interface.session_state['banner_shown'] = False
ai_service = AIService(config)
@@ -821,6 +822,32 @@ class node:
node_info=node_info,
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":
continue
break
@@ -832,10 +859,6 @@ class node:
elif hasattr(stream, '_loop') and hasattr(stream, 'stdin_fd'):
stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
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)
else:
os.write(child_fd, b'\x15\r')
except Exception as e:
import traceback
+70 -3
View File
@@ -277,7 +277,8 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
is_single = saved_mode in (1, 'SINGLE', '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
initial_cmd = saved_cmd + new_cmds
else:
@@ -321,6 +322,14 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
))
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
while not remote_stream.copilot_queue.empty():
try:
@@ -454,16 +463,74 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
if action == "send_all":
commands = result.get("commands", [])
await n.inject_commands(commands, child_fd, on_inject=on_inject)
return
elif action.startswith("custom:"):
custom_cmds_raw = action[7:]
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)
return
else:
os.write(child_fd, b'\x15\r')
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))
except Exception as e:
import traceback
+30 -9
View File
@@ -44,7 +44,7 @@ class NodeStub:
self.config = config
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 . import connpy_pb2
@@ -62,6 +62,7 @@ class NodeStub:
)
self.copilot_history = interface.history
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):
# Send request to server
@@ -84,6 +85,7 @@ class NodeStub:
# Wrap in async loop
async def run_remote_copilot():
nonlocal blocks, node_info
while True:
action, commands, custom_cmd = await interface.run_session(
raw_bytes=bytes(client_buffer_bytes),
@@ -97,6 +99,32 @@ class NodeStub:
request_queue.put(connpy_pb2.InteractRequest(copilot_action="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
with copilot_terminal_mode():
@@ -104,14 +132,7 @@ class NodeStub:
print("\033[2m Returning to session...\033[0m", flush=True)
# Prepare final action for server
action_sent = "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))
request_queue.put(connpy_pb2.InteractRequest(copilot_action="cancel"))
resume_generator()
tty.setraw(sys.stdin.fileno())
+20
View File
@@ -207,6 +207,26 @@ class AIService(BaseService):
else:
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
return {"action": "execute", "clean_prompt": text, "overrides": {}}
-2
View File
@@ -2,8 +2,6 @@ import os
import shutil
import base64
from typing import Any, Dict
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from .base import BaseService
from .exceptions import ConnpyError, InvalidConfigurationError, NodeNotFoundError
+151 -4
View File
@@ -27,10 +27,6 @@ def mock_acompletion():
with patch('litellm.acompletion') as mock:
yield mock
def test_aask_copilot_tool_call(mock_acompletion):
agent = ai(DummyConfig())
# Setup mock response for streaming
class MockDelta:
def __init__(self, content):
self.content = content
@@ -43,6 +39,9 @@ def test_aask_copilot_tool_call(mock_acompletion):
def __init__(self, content):
self.choices = [MockChoice(content)]
def test_aask_copilot_tool_call(mock_acompletion):
agent = ai(DummyConfig())
# acompletion is awaited and returns an async iterator
async def mock_ac(*args, **kwargs):
return MockAsyncIterator([
@@ -557,7 +556,155 @@ def test_copilot_single_mode_retains_command_block():
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:
if os.path.exists(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>
<meta charset="utf-8">
<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>
<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>
@@ -255,6 +255,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
@@ -388,6 +389,7 @@ el.replaceWith(d);
chunk_callback(resp_msg.content)
elif not resp_msg.tool_calls:
# In direct non-streaming output, print markdown
from rich.markdown import Markdown
self.console.print(Markdown(resp_msg.content))
if not resp_msg.tool_calls:
@@ -1621,6 +1623,7 @@ class ai:
if status:
try: status.stop()
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;))
if status:
try: status.start()
@@ -1668,6 +1671,7 @@ class ai:
if status:
try: status.stop()
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;))
if status:
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.
6. Keep your guide concise and authoritative.
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;
Your brief tactical guide in markdown.
&lt;/guide&gt;
@@ -1839,7 +1846,8 @@ Your brief tactical guide in markdown.
&lt;risk&gt;
low
&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_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.
6. ULTRA-CONCISE. Keep your guide to the point.
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;
Your brief tactical guide in markdown. 3-4 sentences max.
&lt;/guide&gt;
@@ -1866,7 +1877,8 @@ command 2
&lt;risk&gt;
low, high, or destructive
&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_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;
messages = [
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt},
{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question}
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}
]
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
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)
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;
commands = []
risk_level = &#34;low&#34;
@@ -2043,6 +2068,7 @@ Node: {node_name}&#34;&#34;&#34;
return {
&#34;commands&#34;: commands,
&#34;guide&#34;: guide,
&#34;notes&#34;: notes,
&#34;risk_level&#34;: risk_level,
&#34;error&#34;: None
}
@@ -2075,11 +2101,11 @@ Node: {node_name}&#34;&#34;&#34;
<dl>
<dt id="connpy.ai.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
<dt id="connpy.ai.ai.deferred_class_hooks"><code class="name">var <span class="ident">deferred_class_hooks</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
<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.
6. Keep your guide concise and authoritative.
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;
Your brief tactical guide in markdown.
&lt;/guide&gt;
@@ -2177,7 +2206,8 @@ Your brief tactical guide in markdown.
&lt;risk&gt;
low
&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_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.
6. ULTRA-CONCISE. Keep your guide to the point.
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;
Your brief tactical guide in markdown. 3-4 sentences max.
&lt;/guide&gt;
@@ -2204,7 +2237,8 @@ command 2
&lt;risk&gt;
low, high, or destructive
&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_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;
messages = [
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt},
{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: user_question}
{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: system_prompt}
]
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
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)
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;
commands = []
risk_level = &#34;low&#34;
@@ -2381,6 +2428,7 @@ Node: {node_name}&#34;&#34;&#34;
return {
&#34;commands&#34;: commands,
&#34;guide&#34;: guide,
&#34;notes&#34;: notes,
&#34;risk_level&#34;: risk_level,
&#34;error&#34;: None
}
@@ -2579,6 +2627,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status:
try: status.stop()
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;))
if status:
try: status.start()
@@ -2626,6 +2675,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
if status:
try: status.stop()
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;))
if status:
try: status.start()
@@ -3184,7 +3234,7 @@ def confirm(self, user_input): return True</code></pre>
</nav>
</main>
<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>
</body>
</html>
+6 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -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;
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))
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;)
if not history:
from rich.markdown import Markdown
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(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;)
if not history:
from rich.markdown import Markdown
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(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;
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))
if &#34;usage&#34; in result:
@@ -666,7 +670,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -193,7 +193,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -600,7 +600,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -249,7 +249,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -700,7 +700,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -303,7 +303,7 @@ tasks:
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -304,7 +304,7 @@ def forms(self):
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -162,7 +162,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -611,7 +611,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+4 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -204,6 +204,7 @@ el.replaceWith(d);
# Fast fail if parent folder does not exist
self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
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
self.app.services.nodes.validate_parent_folder(args.data)
from rich.markdown import Markdown
printer.console.print(Markdown(get_instructions()))
new_node_data = self.forms.questions_nodes(args.data, uniques)
@@ -673,7 +675,7 @@ def forms(self):
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -397,7 +397,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -346,7 +346,7 @@ def forms(self):
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -1163,7 +1163,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -181,7 +181,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -457,7 +457,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -427,7 +427,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+317 -82
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -70,6 +70,7 @@ el.replaceWith(d);
self.session_state.setdefault(&#39;persona&#39;, &#39;engineer&#39;)
self.session_state.setdefault(&#39;trust_mode&#39;, False)
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;prompt&#39;, None)
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;)
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
initial_cmd = saved_cmd + new_cmds
else:
@@ -170,15 +172,17 @@ el.replaceWith(d);
self.session_state[&#39;last_total_cmds&#39;] = total_cmds
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(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;))
if not self.session_state.get(&#39;banner_shown&#39;, False):
self.console.print(Panel(
&#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#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.add(&#39;c-up&#39;)
@@ -260,7 +264,7 @@ el.replaceWith(d);
text = app.current_buffer.text
# 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:
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())]
if matches:
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;]]
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:
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])
if p:
# 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)
if not previews:
@@ -299,12 +303,12 @@ el.replaceWith(d);
elif len(previews) &lt;= 3:
desc = &#34; + &#34;.join(previews)
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:
# Modo SINGLE original
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
# 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;/untrust&#39;, &#39;Disable auto-execute&#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:
if cmd.startswith(cmd_part.lower()):
@@ -346,7 +352,84 @@ el.replaceWith(d);
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:
overrides = {}
# Check for active mission auto-looping
mission = self.session_state.get(&#39;mission&#39;, {})
is_mission = mission.get(&#39;active&#39;, False) and not mission.get(&#39;paused&#39;, False)
if is_mission:
# Force mode_range for mission mode
state[&#39;context_mode&#39;] = self.mode_range
self.session_state[&#39;context_mode&#39;] = self.mode_range
if mission.get(&#39;start_block_idx&#39;) is None:
mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
start_idx = mission.get(&#39;start_block_idx&#39;, state[&#39;total_cmds&#39;])
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;]
step = mission.get(&#39;step&#39;, 1)
max_steps = mission.get(&#39;max_steps&#39;, 10)
if step &gt; max_steps:
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
c_warn = self._get_theme_color(&#34;warning&#34;, &#34;yellow&#34;)
import html
p_warn = html.escape(f&#34;[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: &#34;)
try:
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;))
except (KeyboardInterrupt, EOFError):
ext_ans = &#39;n&#39;
if (ext_ans or &#39;y&#39;).lower().strip() in (&#39;y&#39;, &#39;yes&#39;):
mission[&#39;max_steps&#39;] += 10
else:
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:
# 2. Ask question
from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;)
@@ -363,8 +446,6 @@ el.replaceWith(d);
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,
@@ -376,21 +457,35 @@ el.replaceWith(d);
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;state_update&#34;:
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)
# Only invalidate if the message hasn&#39;t been replaced by a newer one
if state.get(&#39;toolbar_msg&#39;) == msg:
state[&#39;toolbar_msg&#39;] = &#39;&#39; # Explicitly clear
state[&#39;toolbar_msg&#39;] = &#39;&#39;
try:
from prompt_toolkit.application.current import get_app
app = get_app()
@@ -398,14 +493,19 @@ el.replaceWith(d);
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;)
sys.stdout.flush()
continue
else:
# Clean the toolbar message when a real question is asked
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;, {})
@@ -416,20 +516,12 @@ el.replaceWith(d);
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;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():
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
# 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;))
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;
@@ -456,7 +548,6 @@ el.replaceWith(d);
nonlocal live_text, first_chunk
if first_chunk:
status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule(
f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;,
style=persona_color
@@ -465,7 +556,6 @@ el.replaceWith(d);
live_text += 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))
try:
@@ -474,13 +564,12 @@ el.replaceWith(d);
result = await ai_task
except asyncio.CancelledError:
status_spinner.stop()
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk:
status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk:
md_parser.flush()
self.console.print(Rule(style=persona_color))
@@ -488,14 +577,35 @@ el.replaceWith(d);
if not result or 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;)
_finalize_mission(&#34;user_cancelled&#34;)
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;):
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))
# 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;, [])
mission = self.session_state.get(&#39;mission&#39;, {})
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;)
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;)
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;))
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;)
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
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -520,41 +631,39 @@ el.replaceWith(d);
import html
try:
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)
except (KeyboardInterrupt, EOFError):
_finalize_mission(&#34;user_cancelled&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None
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 = []
# Replace commas with spaces and split
parts = text.replace(&#39;,&#39;, &#39; &#39;).split()
for part in parts:
if &#39;-&#39; in part:
try:
start, end = map(int, part.split(&#39;-&#39;))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end))
except: continue
elif part.isdigit():
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]
action_l = (action or &#34;n&#34;).lower().strip()
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
# Check for numeric selection (e.g., &#34;1, 2-4&#34;)
if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l):
selected_idxs = parse_indices(action_l, len(commands))
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
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()
if selection_str:
idxs = parse_indices(selection_str, len(commands))
@@ -580,14 +689,23 @@ el.replaceWith(d);
default=target, multiline=True, key_bindings=e_bindings
)
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;)
return &#34;continue&#34;, None, None
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()]
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
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;)
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;)
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
initial_cmd = saved_cmd + new_cmds
else:
@@ -675,15 +794,17 @@ el.replaceWith(d);
self.session_state[&#39;last_total_cmds&#39;] = total_cmds
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(Rule(title=&#34;[bold cyan] AI TERMINAL COPILOT [/bold cyan]&#34;, style=&#34;cyan&#34;))
if not self.session_state.get(&#39;banner_shown&#39;, False):
self.console.print(Panel(
&#34;[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n&#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.add(&#39;c-up&#39;)
@@ -765,7 +886,7 @@ el.replaceWith(d);
text = app.current_buffer.text
# 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:
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())]
if matches:
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;]]
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:
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])
if p:
# 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)
if not previews:
@@ -804,12 +925,12 @@ el.replaceWith(d);
elif len(previews) &lt;= 3:
desc = &#34; + &#34;.join(previews)
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:
# Modo SINGLE original
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
# 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;/untrust&#39;, &#39;Disable auto-execute&#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:
if cmd.startswith(cmd_part.lower()):
@@ -851,7 +974,84 @@ el.replaceWith(d);
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:
overrides = {}
# Check for active mission auto-looping
mission = self.session_state.get(&#39;mission&#39;, {})
is_mission = mission.get(&#39;active&#39;, False) and not mission.get(&#39;paused&#39;, False)
if is_mission:
# Force mode_range for mission mode
state[&#39;context_mode&#39;] = self.mode_range
self.session_state[&#39;context_mode&#39;] = self.mode_range
if mission.get(&#39;start_block_idx&#39;) is None:
mission[&#39;start_block_idx&#39;] = state[&#39;total_cmds&#39;]
start_idx = mission.get(&#39;start_block_idx&#39;, state[&#39;total_cmds&#39;])
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;]
step = mission.get(&#39;step&#39;, 1)
max_steps = mission.get(&#39;max_steps&#39;, 10)
if step &gt; max_steps:
ext_session = PromptSession(input=self.pt_input, output=self.pt_output)
c_warn = self._get_theme_color(&#34;warning&#34;, &#34;yellow&#34;)
import html
p_warn = html.escape(f&#34;[Mission Limit Reached ({max_steps} steps)] Extend mission for 10 more steps? (y/n) [y]: &#34;)
try:
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;))
except (KeyboardInterrupt, EOFError):
ext_ans = &#39;n&#39;
if (ext_ans or &#39;y&#39;).lower().strip() in (&#39;y&#39;, &#39;yes&#39;):
mission[&#39;max_steps&#39;] += 10
else:
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:
# 2. Ask question
from prompt_toolkit.styles import Style
c_contrast = self._get_theme_color(&#34;contrast&#34;, &#34;gray&#34;)
@@ -868,8 +1068,6 @@ el.replaceWith(d);
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,
@@ -881,21 +1079,35 @@ el.replaceWith(d);
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;state_update&#34;:
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)
# Only invalidate if the message hasn&#39;t been replaced by a newer one
if state.get(&#39;toolbar_msg&#39;) == msg:
state[&#39;toolbar_msg&#39;] = &#39;&#39; # Explicitly clear
state[&#39;toolbar_msg&#39;] = &#39;&#39;
try:
from prompt_toolkit.application.current import get_app
app = get_app()
@@ -903,14 +1115,19 @@ el.replaceWith(d);
except: pass
asyncio.create_task(delayed_refresh())
# Move the cursor up and clean the line so the new prompt replaces the previous one
sys.stdout.write(&#39;\x1b[1A\x1b[2K&#39;)
sys.stdout.flush()
continue
else:
# Clean the toolbar message when a real question is asked
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;, {})
@@ -921,20 +1138,12 @@ el.replaceWith(d);
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;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():
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
# 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;))
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;
@@ -961,7 +1170,6 @@ el.replaceWith(d);
nonlocal live_text, first_chunk
if first_chunk:
status_spinner.stop()
# Print header rule before first chunk arrives
self.console.print(Rule(
f&#34;[bold {persona_color}]{persona_title}[/bold {persona_color}]&#34;,
style=persona_color
@@ -970,7 +1178,6 @@ el.replaceWith(d);
live_text += 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))
try:
@@ -979,13 +1186,12 @@ el.replaceWith(d);
result = await ai_task
except asyncio.CancelledError:
status_spinner.stop()
_finalize_mission(&#34;user_cancelled&#34;)
return &#34;cancel&#34;, None, None
# Ensure spinner is stopped if no chunks arrived
if first_chunk:
status_spinner.stop()
# Close the streamed output with a Rule
if not first_chunk:
md_parser.flush()
self.console.print(Rule(style=persona_color))
@@ -993,14 +1199,35 @@ el.replaceWith(d);
if not result or 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;)
_finalize_mission(&#34;user_cancelled&#34;)
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;):
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))
# 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;, [])
mission = self.session_state.get(&#39;mission&#39;, {})
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;)
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;)
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;))
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;)
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
confirm_session = PromptSession(input=self.pt_input, output=self.pt_output)
@@ -1025,41 +1253,39 @@ el.replaceWith(d);
import html
try:
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)
except (KeyboardInterrupt, EOFError):
_finalize_mission(&#34;user_cancelled&#34;, result.get(&#34;guide&#34;, &#34;&#34;))
self.console.print(&#34;&#34;)
return &#34;continue&#34;, None, None
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 = []
# Replace commas with spaces and split
parts = text.replace(&#39;,&#39;, &#39; &#39;).split()
for part in parts:
if &#39;-&#39; in part:
try:
start, end = map(int, part.split(&#39;-&#39;))
# Ensure inclusive and 0-indexed
indices.extend(range(start-1, end))
except: continue
elif part.isdigit():
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]
action_l = (action or &#34;n&#34;).lower().strip()
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
# Check for numeric selection (e.g., &#34;1, 2-4&#34;)
if re.match(r&#39;^[0-9,\-\s]+$&#39;, action_l):
selected_idxs = parse_indices(action_l, len(commands))
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
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()
if selection_str:
idxs = parse_indices(selection_str, len(commands))
@@ -1085,14 +1311,23 @@ el.replaceWith(d);
default=target, multiline=True, key_bindings=e_bindings
)
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;)
return &#34;continue&#34;, None, None
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()]
if mission.get(&#39;active&#39;, False):
mission[&#39;step&#39;] = mission.get(&#39;step&#39;, 1) + 1
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;)
return &#34;continue&#34;, None, None
@@ -1133,7 +1368,7 @@ on_ai_call: async function(active_buffer, question) -&gt; result_dict</p></div>
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -516,7 +516,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -508,7 +508,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -61,7 +61,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -7006,7 +7006,7 @@ def stop_api(request,
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -107,7 +107,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -62,7 +62,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
</dd>
@@ -81,7 +81,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
</dd>
@@ -100,7 +100,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
</dd>
@@ -119,7 +119,7 @@ el.replaceWith(d);
<dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
</dd>
@@ -168,7 +168,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -366,7 +366,7 @@ def invoke_plugin(request,
</nav>
</main>
<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>
</body>
</html>
+73 -6
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -604,7 +604,7 @@ def service(self):
<dl>
<dt id="connpy.grpc_layer.server.AuthInterceptor.OPEN_METHODS"><code class="name">var <span class="ident">OPEN_METHODS</span></code></dt>
<dd>
<div class="desc"></div>
<div class="desc"><p>The type of the None singleton.</p></div>
</dd>
</dl>
<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;)
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
initial_cmd = saved_cmd + new_cmds
else:
@@ -1627,6 +1628,14 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
))
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
while not remote_stream.copilot_queue.empty():
try:
@@ -1760,16 +1769,74 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
if action == &#34;send_all&#34;:
commands = result.get(&#34;commands&#34;, [])
await n.inject_commands(commands, child_fd, on_inject=on_inject)
return
elif action.startswith(&#34;custom:&#34;):
custom_cmds_raw = action[7:]
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)
return
else:
os.write(child_fd, b&#39;\x15\r&#39;)
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))
except Exception as e:
import traceback
@@ -2516,7 +2583,7 @@ def service(self):
</nav>
</main>
<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>
</body>
</html>
+32 -11
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -1373,7 +1373,7 @@ def set_reserved_names(self, names):
self.config = config
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 . import connpy_pb2
@@ -1391,6 +1391,7 @@ def set_reserved_names(self, names):
)
self.copilot_history = interface.history
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):
# Send request to server
@@ -1413,6 +1414,7 @@ def set_reserved_names(self, names):
# Wrap in async loop
async def run_remote_copilot():
nonlocal blocks, node_info
while True:
action, commands, custom_cmd = await interface.run_session(
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;))
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
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)
# Prepare final action for server
action_sent = &#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))
request_queue.put(connpy_pb2.InteractRequest(copilot_action=&#34;cancel&#34;))
resume_generator()
tty.setraw(sys.stdin.fileno())
@@ -2967,7 +2988,7 @@ def stop_api(self):
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -312,7 +312,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -138,7 +138,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+104 -12
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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;
@@ -41,7 +41,7 @@ el.replaceWith(d);
<p align="center">
<img src="https://nginx.gederico.dynu.net/images/CONNPY-resized.png" alt="App Logo">
</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>
<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>
@@ -57,7 +57,9 @@ el.replaceWith(d);
<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>.
* <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>/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>).
@@ -819,16 +821,39 @@ class configfile:
self.connections = config[&#34;connections&#34;]
self.profiles = config[&#34;profiles&#34;]
self._privatekey_obj = None
self._publickey_obj = None
if not os.path.exists(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
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()
@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):
&#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):
#Create key file
from Crypto.PublicKey import RSA
key = RSA.generate(2048)
with open(keyfile,&#39;wb&#39;) as f:
f.write(key.export_key(&#39;PEM&#39;))
@@ -1303,6 +1329,8 @@ class configfile:
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
@@ -1331,6 +1359,41 @@ class configfile:
- publickey (obj): Object containing the public key to decrypt
passwords.
</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>
<dl>
<dt id="connpy.configfile.encrypt"><code class="name flex">
@@ -1363,6 +1426,8 @@ def encrypt(self, password, keyfile=None):
if keyfile is None:
keyfile = self.key
with open(keyfile) as f:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
key = RSA.import_key(f.read())
f.close()
publickey = key.publickey()
@@ -1839,6 +1904,8 @@ class node:
if keyfile is None:
keyfile = self.key
if keyfile is not None:
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
with open(keyfile) as f:
key = RSA.import_key(f.read())
decryptor = PKCS1_OAEP.new(key)
@@ -2356,6 +2423,7 @@ class node:
# Save history back to stream for persistence in current session
stream.copilot_history = interface.history
stream.copilot_state = interface.session_state
interface.session_state[&#39;banner_shown&#39;] = False
ai_service = AIService(config)
@@ -2386,6 +2454,32 @@ class node:
node_info=node_info,
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;:
continue
break
@@ -2397,10 +2491,6 @@ class node:
elif hasattr(stream, &#39;_loop&#39;) and hasattr(stream, &#39;stdin_fd&#39;):
stream._loop.add_reader(stream.stdin_fd, stream._read_ready)
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)
else:
os.write(child_fd, b&#39;\x15\r&#39;)
except Exception as e:
import traceback
@@ -4022,7 +4112,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<nav id="sidebar">
<div class="toc">
<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="#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>
@@ -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.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.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>
</li>
<li>
@@ -4128,7 +4220,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -349,7 +349,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -60,7 +60,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+42 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -259,6 +259,26 @@ el.replaceWith(d);
else:
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
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}
@@ -877,6 +897,26 @@ el.replaceWith(d);
else:
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
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre>
</details>
@@ -933,7 +973,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -152,7 +152,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -370,7 +370,7 @@ def current_context(self) -&gt; str:
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -268,7 +268,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -340,7 +340,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -361,7 +361,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+42 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -318,6 +318,26 @@ el.replaceWith(d);
else:
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
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}
@@ -936,6 +956,26 @@ el.replaceWith(d);
else:
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
return {&#34;action&#34;: &#34;execute&#34;, &#34;clean_prompt&#34;: text, &#34;overrides&#34;: {}}</code></pre>
</details>
@@ -5858,7 +5898,7 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -790,7 +790,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -838,7 +838,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -429,7 +429,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -351,7 +351,7 @@ def users(self):
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -978,7 +978,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -325,7 +325,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -894,7 +894,7 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -549,7 +549,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
</nav>
</main>
<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>
</body>
</html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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>
<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>
@@ -147,7 +147,7 @@ el.replaceWith(d);
</nav>
</main>
<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>
</body>
</html>