fixing docs

This commit is contained in:
dullfig 2026-01-18 18:00:11 -08:00
parent 0fb35da5dd
commit 3ffab8a3dd
5 changed files with 725 additions and 10 deletions

View file

@ -3,6 +3,7 @@ Configuration management for xml-pipeline.
Handles: Handles:
- Agent configs (~/.xml-pipeline/agents/*.yaml) - Agent configs (~/.xml-pipeline/agents/*.yaml)
- Listener configs (~/.xml-pipeline/listeners/*.yaml)
- Organism config (organism.yaml) - Organism config (organism.yaml)
""" """
@ -13,11 +14,21 @@ from .agents import (
CONFIG_DIR, CONFIG_DIR,
AGENTS_DIR, AGENTS_DIR,
) )
from .listeners import (
ListenerConfigStore,
get_listener_config_store,
LISTENERS_DIR,
)
__all__ = [ __all__ = [
# Agent config
"AgentConfig", "AgentConfig",
"AgentConfigStore", "AgentConfigStore",
"get_agent_config_store", "get_agent_config_store",
"CONFIG_DIR", "CONFIG_DIR",
"AGENTS_DIR", "AGENTS_DIR",
# Listener config
"ListenerConfigStore",
"get_listener_config_store",
"LISTENERS_DIR",
] ]

View file

@ -23,6 +23,7 @@ FEATURES: dict[str, tuple[Callable[[], bool], str]] = {
"search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"), "search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"),
"auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"), "auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"),
"server": (lambda: _check_import("websockets"), "WebSocket server"), "server": (lambda: _check_import("websockets"), "WebSocket server"),
"lsp": (lambda: _check_import("lsp_client"), "LSP client for config editor"),
} }

View file

@ -2,31 +2,100 @@
Full-screen text editor using prompt_toolkit. Full-screen text editor using prompt_toolkit.
Provides a vim-like editing experience for configuration files. Provides a vim-like editing experience for configuration files.
Supports optional LSP integration for YAML files.
""" """
from typing import Optional, Tuple from __future__ import annotations
import asyncio
import logging
from pathlib import Path
from typing import Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from agentserver.console.lsp import YAMLLSPClient, ASLSClient
from typing import Union
LSPClientType = Union[YAMLLSPClient, ASLSClient]
logger = logging.getLogger(__name__)
try: try:
from prompt_toolkit import Application from prompt_toolkit import Application
from prompt_toolkit.buffer import Buffer from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout import Layout, HSplit, VSplit from prompt_toolkit.layout import Layout, HSplit, VSplit
from prompt_toolkit.layout.containers import Window, ConditionalContainer from prompt_toolkit.layout.containers import Window, ConditionalContainer, Float, FloatContainer
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.filters import Condition from prompt_toolkit.filters import Condition
from prompt_toolkit.styles import Style from prompt_toolkit.styles import Style
from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
PROMPT_TOOLKIT_AVAILABLE = True PROMPT_TOOLKIT_AVAILABLE = True
except ImportError: except ImportError:
PROMPT_TOOLKIT_AVAILABLE = False PROMPT_TOOLKIT_AVAILABLE = False
try: try:
from pygments.lexers.data import YamlLexer from pygments.lexers.data import YamlLexer
from pygments.lexers.javascript import TypeScriptLexer
PYGMENTS_AVAILABLE = True PYGMENTS_AVAILABLE = True
except ImportError: except ImportError:
PYGMENTS_AVAILABLE = False PYGMENTS_AVAILABLE = False
# Supported syntax types and their lexers
SYNTAX_LEXERS = {
"yaml": "YamlLexer",
"typescript": "TypeScriptLexer",
"assemblyscript": "TypeScriptLexer", # AS uses TS syntax
"ts": "TypeScriptLexer",
"as": "TypeScriptLexer",
}
def get_lexer_for_syntax(syntax: str) -> Optional[object]:
"""
Get a Pygments lexer for the given syntax type.
Args:
syntax: Syntax name ("yaml", "typescript", "ts", "as", "assemblyscript")
Returns:
PygmentsLexer instance or None
"""
if not PYGMENTS_AVAILABLE:
return None
syntax_lower = syntax.lower()
if syntax_lower in ("yaml", "yml"):
return PygmentsLexer(YamlLexer)
elif syntax_lower in ("typescript", "ts", "assemblyscript", "as"):
return PygmentsLexer(TypeScriptLexer)
else:
return None
def detect_syntax_from_path(path: str | Path) -> str:
"""
Detect syntax type from file extension.
Returns:
Syntax name for use with get_lexer_for_syntax()
"""
ext = Path(path).suffix.lower()
extension_map = {
".yaml": "yaml",
".yml": "yaml",
".ts": "typescript",
".as": "assemblyscript",
}
return extension_map.get(ext, "text")
def edit_text( def edit_text(
initial_text: str, initial_text: str,
title: str = "Editor", title: str = "Editor",
@ -38,7 +107,7 @@ def edit_text(
Args: Args:
initial_text: Text to edit initial_text: Text to edit
title: Title shown in header title: Title shown in header
syntax: Syntax highlighting ("yaml", "text") syntax: Syntax highlighting ("yaml", "typescript", "ts", "as", "text")
Returns: Returns:
(edited_text, saved) - edited_text is None if cancelled (edited_text, saved) - edited_text is None if cancelled
@ -79,9 +148,7 @@ def edit_text(
event.app.exit() event.app.exit()
# Syntax highlighting # Syntax highlighting
lexer = None lexer = get_lexer_for_syntax(syntax)
if PYGMENTS_AVAILABLE and syntax == "yaml":
lexer = PygmentsLexer(YamlLexer)
# Layout # Layout
header = Window( header = Window(
@ -225,3 +292,461 @@ def edit_with_system_editor(filepath: str) -> bool:
return mtime_before is None or mtime_after > mtime_before return mtime_before is None or mtime_after > mtime_before
return False return False
# =============================================================================
# LSP-Enhanced Editor
# =============================================================================
class LSPEditor:
"""
Full-screen editor with optional LSP integration.
Provides:
- Syntax highlighting (YAML, TypeScript/AssemblyScript via Pygments)
- Autocompletion (LSP when available)
- Inline diagnostics (LSP when available)
- Hover documentation on F1 (LSP when available)
Usage:
# YAML config editing
editor = LSPEditor(schema_type="listener")
edited_text, saved = await editor.edit(initial_text, title="greeter.yaml")
# AssemblyScript listener editing
editor = LSPEditor(syntax="assemblyscript")
edited_text, saved = await editor.edit(source_code, title="handler.ts")
"""
def __init__(
self,
schema_type: Optional[str] = None,
schema_uri: Optional[str] = None,
syntax: str = "yaml",
):
"""
Initialize the LSP editor.
Args:
schema_type: Schema type ("organism" or "listener") for YAML modeline
schema_uri: Explicit schema URI to use
syntax: Syntax highlighting ("yaml", "typescript", "assemblyscript", "ts", "as")
"""
self.schema_type = schema_type
self.schema_uri = schema_uri
self.syntax = syntax
self._lsp_client: Optional["LSPClientType"] = None
self._lsp_type: Optional[str] = None # "yaml" or "assemblyscript"
self._diagnostics_text = ""
self._hover_text = ""
self._show_hover = False
self._document_version = 0
async def edit(
self,
initial_text: str,
title: str = "Editor",
document_uri: Optional[str] = None,
) -> Tuple[Optional[str], bool]:
"""
Open the editor with LSP support.
Args:
initial_text: Initial content to edit
title: Title shown in header
document_uri: URI for LSP (auto-generated if not provided)
Returns:
(edited_text, saved) - edited_text is None if cancelled
"""
if not PROMPT_TOOLKIT_AVAILABLE:
print("Error: prompt_toolkit not installed")
return None, False
# Determine LSP type based on syntax
syntax_lower = self.syntax.lower()
if syntax_lower in ("yaml", "yml"):
self._lsp_type = "yaml"
elif syntax_lower in ("typescript", "ts", "assemblyscript", "as"):
self._lsp_type = "assemblyscript"
else:
self._lsp_type = None
# Try to get appropriate LSP client
try:
from agentserver.console.lsp import get_lsp_manager, LSPServerType
manager = get_lsp_manager()
if self._lsp_type == "yaml":
self._lsp_client = await manager.get_yaml_client()
elif self._lsp_type == "assemblyscript":
self._lsp_client = await manager.get_asls_client()
else:
self._lsp_client = None
except Exception as e:
logger.debug(f"LSP not available: {e}")
self._lsp_client = None
# Generate document URI with appropriate extension
if document_uri is None:
ext = ".yaml" if self._lsp_type == "yaml" else ".ts"
document_uri = f"file:///temp/{title.replace(' ', '_')}{ext}"
# Ensure schema modeline is present (YAML only)
if self._lsp_type == "yaml":
initial_text = self._ensure_modeline(initial_text)
# Open document in LSP
if self._lsp_client:
await self._lsp_client.did_open(document_uri, initial_text)
try:
result = await self._run_editor(initial_text, title, document_uri)
finally:
# Close document in LSP
if self._lsp_client:
await self._lsp_client.did_close(document_uri)
try:
from agentserver.console.lsp import get_lsp_manager, LSPServerType
manager = get_lsp_manager()
if self._lsp_type == "yaml":
await manager.release_client(LSPServerType.YAML)
elif self._lsp_type == "assemblyscript":
await manager.release_client(LSPServerType.ASSEMBLYSCRIPT)
except Exception:
pass
return result
def _ensure_modeline(self, text: str) -> str:
"""Ensure YAML has schema modeline if schema_type is set."""
if self.schema_type is None:
return text
modeline = f"# yaml-language-server: $schema=~/.xml-pipeline/schemas/{self.schema_type}.schema.json"
# Check if modeline already exists
lines = text.split("\n")
for line in lines[:3]: # Check first 3 lines
if "yaml-language-server" in line and "$schema" in line:
return text
# Add modeline at the top
return modeline + "\n" + text
async def _run_editor(
self,
initial_text: str,
title: str,
uri: str,
) -> Tuple[Optional[str], bool]:
"""Run the editor application."""
result = {"text": None, "saved": False}
# Create buffer
buffer = Buffer(multiline=True, name="editor")
buffer.text = initial_text
# Key bindings
kb = KeyBindings()
@kb.add("c-s") # Ctrl+S to save
def save(event):
result["text"] = buffer.text
result["saved"] = True
event.app.exit()
@kb.add("c-q") # Ctrl+Q to quit without saving
def quit_nosave(event):
result["text"] = None
result["saved"] = False
event.app.exit()
@kb.add("escape") # Escape to quit
def escape(event):
result["text"] = None
result["saved"] = False
event.app.exit()
@kb.add("c-space") # Ctrl+Space for completion
async def trigger_completion(event):
if self._lsp_client:
doc = buffer.document
line = doc.cursor_position_row
col = doc.cursor_position_col
completions = await self._lsp_client.completion(uri, line, col)
if completions:
# Show first completion as hint
self._diagnostics_text = f"Completions: {', '.join(c.label for c in completions[:5])}"
event.app.invalidate()
@kb.add("f1") # F1 for hover
async def show_hover(event):
if self._lsp_client:
doc = buffer.document
line = doc.cursor_position_row
col = doc.cursor_position_col
hover = await self._lsp_client.hover(uri, line, col)
if hover:
self._hover_text = hover.contents[:200] # Truncate
self._show_hover = True
else:
self._hover_text = ""
self._show_hover = False
event.app.invalidate()
@kb.add("escape", filter=Condition(lambda: self._show_hover))
def hide_hover(event):
self._show_hover = False
event.app.invalidate()
@kb.add("c-p") # Ctrl+P for signature help (ASLS only)
async def show_signature_help(event):
# Only available for ASLS
if self._lsp_client and self._lsp_type == "assemblyscript":
doc = buffer.document
line = doc.cursor_position_row
col = doc.cursor_position_col
try:
sig_help = await self._lsp_client.signature_help(uri, line, col)
if sig_help and sig_help.get("signatures"):
sig = sig_help["signatures"][0]
label = sig.get("label", "")
self._hover_text = f"Signature: {label}"
self._show_hover = True
else:
self._hover_text = ""
self._show_hover = False
except Exception:
pass
event.app.invalidate()
# Syntax highlighting
lexer = get_lexer_for_syntax(self.syntax)
# Header
def get_header():
if self._lsp_client:
if self._lsp_type == "yaml":
lsp_status = " [YAML LSP]"
elif self._lsp_type == "assemblyscript":
lsp_status = " [ASLS]"
else:
lsp_status = " [LSP]"
else:
lsp_status = ""
parts = [
("class:header", f" {title}{lsp_status} "),
("class:header.key", " Ctrl+S"),
("class:header", "=Save "),
("class:header.key", " Ctrl+Q"),
("class:header", "=Quit "),
("class:header.key", " F1"),
("class:header", "=Hover "),
]
# Add Ctrl+P hint for AssemblyScript
if self._lsp_type == "assemblyscript" and self._lsp_client:
parts.extend([
("class:header.key", " Ctrl+P"),
("class:header", "=Sig "),
])
return parts
header = Window(
height=1,
content=FormattedTextControl(get_header),
style="class:header",
)
# Editor window
editor_window = Window(
content=BufferControl(
buffer=buffer,
lexer=lexer,
),
)
# Status bar
def get_status():
row = buffer.document.cursor_position_row + 1
col = buffer.document.cursor_position_col + 1
lines = len(buffer.text.split("\n"))
parts = [("class:status", f" Line {row}/{lines}, Col {col} ")]
if self._diagnostics_text:
parts.append(("class:status.diag", f" | {self._diagnostics_text}"))
return parts
status_bar = Window(
height=1,
content=FormattedTextControl(get_status),
style="class:status",
)
# Hover popup (shown conditionally)
def get_hover_text():
if self._show_hover and self._hover_text:
return [("class:hover", self._hover_text)]
return []
hover_window = ConditionalContainer(
Window(
height=3,
content=FormattedTextControl(get_hover_text),
style="class:hover",
),
filter=Condition(lambda: self._show_hover and bool(self._hover_text)),
)
# Layout
layout = Layout(
HSplit([
header,
editor_window,
hover_window,
status_bar,
])
)
# Styles
style = Style.from_dict({
"header": "bg:#005f87 #ffffff",
"header.key": "bg:#005f87 #ffff00 bold",
"status": "bg:#444444 #ffffff",
"status.diag": "bg:#444444 #ff8800",
"hover": "bg:#333333 #ffffff italic",
"diagnostic.error": "bg:#5f0000 #ffffff",
"diagnostic.warning": "bg:#5f5f00 #ffffff",
})
# Set up diagnostics callback
async def on_text_changed(buff):
if self._lsp_client:
self._document_version += 1
diagnostics = await self._lsp_client.did_change(
uri, buff.text, self._document_version
)
if diagnostics:
errors = sum(1 for d in diagnostics if d.severity == "error")
warnings = sum(1 for d in diagnostics if d.severity == "warning")
parts = []
if errors:
parts.append(f"{errors} error{'s' if errors > 1 else ''}")
if warnings:
parts.append(f"{warnings} warning{'s' if warnings > 1 else ''}")
self._diagnostics_text = " | ".join(parts) if parts else ""
else:
self._diagnostics_text = ""
buffer.on_text_changed += lambda buff: asyncio.create_task(on_text_changed(buff))
# Create and run application
app: Application = Application(
layout=layout,
key_bindings=kb,
style=style,
full_screen=True,
mouse_support=True,
)
await app.run_async()
return result["text"], result["saved"]
async def edit_text_async(
initial_text: str,
title: str = "Editor",
schema_type: Optional[str] = None,
syntax: str = "yaml",
) -> Tuple[Optional[str], bool]:
"""
Async wrapper for LSP-enabled text editing.
Args:
initial_text: Text to edit
title: Title shown in header
schema_type: "organism" or "listener" for YAML schema modeline
syntax: Syntax highlighting ("yaml", "typescript", "assemblyscript", "ts", "as")
Returns:
(edited_text, saved) - edited_text is None if cancelled
"""
editor = LSPEditor(schema_type=schema_type, syntax=syntax)
return await editor.edit(initial_text, title=title)
async def edit_file_async(
filepath: str,
title: Optional[str] = None,
schema_type: Optional[str] = None,
syntax: Optional[str] = None,
) -> bool:
"""
Edit a file with LSP support.
Args:
filepath: Path to file
title: Optional title (defaults to filename)
schema_type: "organism" or "listener" for YAML schema modeline
syntax: Syntax highlighting (auto-detected from extension if not specified)
Returns:
True if saved, False if cancelled
"""
path = Path(filepath)
title = title or path.name
# Auto-detect syntax from extension if not specified
if syntax is None:
syntax = detect_syntax_from_path(path)
# Load existing content or empty
if path.exists():
initial_text = path.read_text()
else:
initial_text = ""
# Edit
edited_text, saved = await edit_text_async(
initial_text,
title=title,
schema_type=schema_type,
syntax=syntax,
)
# Save if requested
if saved and edited_text is not None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(edited_text)
return True
return False
async def edit_assemblyscript_source(
filepath: str,
title: Optional[str] = None,
) -> bool:
"""
Edit an AssemblyScript listener source file with ASLS support.
Args:
filepath: Path to .ts or .as file
title: Optional title (defaults to filename)
Returns:
True if saved, False if cancelled
"""
return await edit_file_async(
filepath,
title=title,
syntax="assemblyscript",
)

View file

@ -494,7 +494,12 @@ class SecureConsole:
cprint(" /buffer <thread> Inspect thread's context buffer", Colors.DIM) cprint(" /buffer <thread> Inspect thread's context buffer", Colors.DIM)
cprint(" /monitor <thread> Show recent messages from thread", Colors.DIM) cprint(" /monitor <thread> Show recent messages from thread", Colors.DIM)
cprint(" /monitor * Show recent messages from all threads", Colors.DIM) cprint(" /monitor * Show recent messages from all threads", Colors.DIM)
cprint(" /config View current configuration", Colors.DIM) cprint("")
cprint("Configuration:", Colors.CYAN)
cprint(" /config Show current config", Colors.DIM)
cprint(" /config -e Edit organism.yaml", Colors.DIM)
cprint(" /config @name Edit listener config", Colors.DIM)
cprint(" /config --list List listener configs", Colors.DIM)
cprint("") cprint("")
cprint("Protected (require password):", Colors.YELLOW) cprint("Protected (require password):", Colors.YELLOW)
cprint(" /restart Restart the pipeline", Colors.DIM) cprint(" /restart Restart the pipeline", Colors.DIM)
@ -670,16 +675,186 @@ class SecureConsole:
cprint(f" {payload_str}", Colors.DIM) cprint(f" {payload_str}", Colors.DIM)
async def _cmd_config(self, args: str) -> None: async def _cmd_config(self, args: str) -> None:
"""View current configuration (read-only).""" """
Edit configuration files.
/config - Edit organism.yaml
/config @name - Edit listener config (e.g., /config @greeter)
/config --list - List available listener configs
/config --show - Show current config (read-only)
"""
args = args.strip() if args else ""
if args == "--list":
await self._config_list()
elif args == "--show" or args == "":
await self._config_show()
elif args.startswith("@"):
listener_name = args[1:].strip()
if listener_name:
await self._config_edit_listener(listener_name)
else:
cprint("Usage: /config @listener_name", Colors.RED)
elif args == "--edit" or args == "-e":
await self._config_edit_organism()
else:
cprint(f"Unknown option: {args}", Colors.RED)
cprint("Usage:", Colors.DIM)
cprint(" /config Show current config", Colors.DIM)
cprint(" /config -e Edit organism.yaml", Colors.DIM)
cprint(" /config @name Edit listener config", Colors.DIM)
cprint(" /config --list List listener configs", Colors.DIM)
async def _config_show(self) -> None:
"""Show current configuration (read-only)."""
cprint(f"\nOrganism: {self.pump.config.name}", Colors.CYAN) cprint(f"\nOrganism: {self.pump.config.name}", Colors.CYAN)
cprint(f"Port: {self.pump.config.port}", Colors.DIM) cprint(f"Port: {self.pump.config.port}", Colors.DIM)
cprint(f"Thread scheduling: {self.pump.config.thread_scheduling}", Colors.DIM) cprint(f"Thread scheduling: {self.pump.config.thread_scheduling}", Colors.DIM)
cprint(f"Max concurrent pipelines: {self.pump.config.max_concurrent_pipelines}", Colors.DIM) cprint(f"Max concurrent pipelines: {self.pump.config.max_concurrent_pipelines}", Colors.DIM)
cprint(f"Max concurrent handlers: {self.pump.config.max_concurrent_handlers}", Colors.DIM) cprint(f"Max concurrent handlers: {self.pump.config.max_concurrent_handlers}", Colors.DIM)
cprint(f"Max concurrent per agent: {self.pump.config.max_concurrent_per_agent}", Colors.DIM) cprint(f"Max concurrent per agent: {self.pump.config.max_concurrent_per_agent}", Colors.DIM)
cprint("\nTo modify: /quit -> edit organism.yaml -> restart", Colors.DIM) cprint("\nUse /config -e to edit organism.yaml", Colors.DIM)
cprint("Use /config @listener to edit a listener config", Colors.DIM)
cprint("") cprint("")
async def _config_list(self) -> None:
"""List available listener configs."""
from agentserver.config import get_listener_config_store
store = get_listener_config_store()
listeners = store.list_listeners()
cprint("\nListener configurations:", Colors.CYAN)
cprint(f"Directory: {store.listeners_dir}", Colors.DIM)
cprint("")
if not listeners:
cprint(" No listener configs found.", Colors.DIM)
cprint(" Use /config @name to create one.", Colors.DIM)
else:
for name in sorted(listeners):
config = store.get(name)
agent_tag = "[agent]" if config.agent else "[tool]" if config.tool else ""
cprint(f" @{name:20} {agent_tag} {config.description or ''}", Colors.DIM)
# Also show registered listeners without config files
unconfigured = [
name for name in self.pump.listeners.keys()
if name not in listeners
]
if unconfigured:
cprint("\nRegistered listeners without config files:", Colors.YELLOW)
for name in sorted(unconfigured):
listener = self.pump.listeners[name]
agent_tag = "[agent]" if listener.is_agent else ""
cprint(f" @{name:20} {agent_tag} {listener.description}", Colors.DIM)
cprint("")
async def _config_edit_organism(self) -> None:
"""Edit organism.yaml in the full-screen editor."""
from agentserver.console.editor import edit_text_async
from agentserver.config.schema import ensure_schemas
from agentserver.config.split_loader import (
get_organism_yaml_path,
load_organism_yaml_content,
save_organism_yaml_content,
)
# Ensure schemas are written for LSP
try:
ensure_schemas()
except Exception as e:
cprint(f"Warning: Could not write schemas: {e}", Colors.YELLOW)
# Find organism.yaml
config_path = get_organism_yaml_path()
if config_path is None:
cprint("No organism.yaml found.", Colors.RED)
cprint("Searched in:", Colors.DIM)
cprint(" ~/.xml-pipeline/organism.yaml", Colors.DIM)
cprint(" ./organism.yaml", Colors.DIM)
cprint(" ./config/organism.yaml", Colors.DIM)
return
# Load content
try:
content = load_organism_yaml_content(config_path)
except Exception as e:
cprint(f"Failed to load config: {e}", Colors.RED)
return
# Edit
cprint(f"Editing: {config_path}", Colors.CYAN)
cprint("Press Ctrl+S to save, Ctrl+Q to cancel", Colors.DIM)
cprint("")
edited_text, saved = await edit_text_async(
content,
title=f"organism.yaml ({config_path.name})",
schema_type="organism",
)
if saved and edited_text is not None:
try:
save_organism_yaml_content(config_path, edited_text)
cprint("Configuration saved.", Colors.GREEN)
cprint("Note: Restart required for changes to take effect.", Colors.YELLOW)
except yaml.YAMLError as e:
cprint(f"Invalid YAML: {e}", Colors.RED)
except Exception as e:
cprint(f"Failed to save: {e}", Colors.RED)
else:
cprint("Edit cancelled.", Colors.DIM)
async def _config_edit_listener(self, name: str) -> None:
"""Edit a listener config in the full-screen editor."""
from agentserver.config import get_listener_config_store
from agentserver.console.editor import edit_text_async
from agentserver.config.schema import ensure_schemas
# Ensure schemas are written for LSP
try:
ensure_schemas()
except Exception as e:
cprint(f"Warning: Could not write schemas: {e}", Colors.YELLOW)
store = get_listener_config_store()
# Load or create content
if store.exists(name):
content = store.load_yaml(name)
cprint(f"Editing: {store.path_for(name)}", Colors.CYAN)
else:
# Check if it's a registered listener
if name in self.pump.listeners:
cprint(f"Creating new config for registered listener: {name}", Colors.CYAN)
else:
cprint(f"Creating new config for: {name}", Colors.CYAN)
content = store._default_template(name)
cprint("Press Ctrl+S to save, Ctrl+Q to cancel", Colors.DIM)
cprint("")
# Edit
edited_text, saved = await edit_text_async(
content,
title=f"{name}.yaml",
schema_type="listener",
)
if saved and edited_text is not None:
try:
path = store.save_yaml(name, edited_text)
cprint(f"Saved: {path}", Colors.GREEN)
cprint("Note: Restart required for changes to take effect.", Colors.YELLOW)
except yaml.YAMLError as e:
cprint(f"Invalid YAML: {e}", Colors.RED)
except Exception as e:
cprint(f"Failed to save: {e}", Colors.RED)
else:
cprint("Edit cancelled.", Colors.DIM)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Commands: Protected # Commands: Protected
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View file

@ -63,9 +63,12 @@ auth = [
# WebSocket server (for remote connections) # WebSocket server (for remote connections)
server = ["websockets"] server = ["websockets"]
# LSP support for config editor (requires yaml-language-server: npm install -g yaml-language-server)
lsp = ["lsp-client>=0.3.0"]
# All optional features # All optional features
all = [ all = [
"xml-pipeline[anthropic,openai,redis,search,auth,server]", "xml-pipeline[anthropic,openai,redis,search,auth,server,lsp]",
] ]
# Development # Development