Skip to content

Extensibility Guide

CLIver is designed with extensibility in mind, allowing you to customize its functionality, add new features, and integrate it seamlessly into your own Python applications. This guide covers how to extend CLIver and use it as a Python library.

Using CLIver as a Python Library

CLIver's core engine (TaskExecutor) is designed to be independent of the CLI layer. It has no dependencies on terminal I/O, prompt_toolkit, or Rich — making it suitable for embedding in web services, automation scripts, or other applications.

All features — LLM inference, tool calling, permissions, workflows, skills, and memory — work identically whether invoked from the CLI or from your own Python code.

Basic LLM Inference

from cliver import TaskExecutor
from cliver.config import ModelConfig, StdioMCPServerConfig
from cliver.model_capabilities import ModelCapability


# Example usage
async def run_example():
    # Create configuration for Qwen model
    llm_models = {
        "qwen": ModelConfig(
            name="qwen",
            provider="ollama",
            url="http://localhost:11434",
            name_in_provider="qwen2.5:latest",
            capabilities={ModelCapability.TEXT_TO_TEXT, ModelCapability.TOOL_CALLING},
        )
    }

    # Create MCP server configuration using the proper Pydantic model
    mcp_servers = {
        "time": StdioMCPServerConfig(
            name="time",
            transport="stdio",
            command="uvx",
            args=["mcp-server-time", "--local-timezone=Asia/Shanghai"],
        )
    }

    executor = TaskExecutor(
        llm_models=llm_models,
        mcp_servers={name: server.model_dump() for name, server in mcp_servers.items()},
        default_model="qwen",
    )

    # Process a user query
    user_input = "What time is it now ?"
    result = await executor.process_user_input(user_input)
    print("\n===\n")
    print(result)
    print("\n===\n")

    if hasattr(result, "content") and result.content:
        print("\nFinal answer:")
        print(str(result.content))
        print("\n===\n")


if __name__ == "__main__":
    import asyncio

    asyncio.run(run_example())

Streaming Responses

For real-time applications, you can stream responses from the LLM:

import asyncio

from cliver import TaskExecutor
from cliver.config import ModelConfig
from cliver.model_capabilities import ModelCapability

# Configure LLM models
llm_models = {
    "qwen": ModelConfig(
        name="qwen",
        provider="ollama",
        url="http://localhost:11434",
        name_in_provider="qwen2.5:latest",
        capabilities={ModelCapability.TEXT_TO_TEXT, ModelCapability.TOOL_CALLING},
    )
}

executor = TaskExecutor(llm_models=llm_models, mcp_servers={}, default_model="qwen")


# Stream the response
async def stream_query():
    async for chunk in executor.stream_user_input("Write a poem about programming"):
        if hasattr(chunk, "content") and chunk.content:
            print(chunk.content, end="", flush=True)


asyncio.run(stream_query())

Custom Commands

You can add custom commands to CLIver's CLI interface by creating command modules.

Creating a Custom Command

Create a Python file in the default config location, like: ~/.config/cliver/commands/my_command.py

import asyncio

import click
from langchain_core.messages import BaseMessage

from cliver.cli import Cliver, pass_cliver
from cliver.mcp_server_caller import MCPServersCaller


@click.group(name="my_command", help="My Command")
@click.pass_context
# here the function name is the same as the file name
def my_command(ctx: click.Context):
    """
    Tasks for My Command
    """
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())
        ctx.exit()


@my_command.command(name="sub_command", help="A sub command")
@click.argument("query", nargs=-1)
@pass_cliver
def sub_command(cliver: Cliver, query: str):
    task_executor = cliver.task_executor
    sentence = " ".join(query)
    response = task_executor.process_user_input_sync(
        user_input=sentence,
        filter_tools=lambda tn, tools: [tool for tool in tools if "my_command" in str(tool)],
        enhance_prompt=enhance_prompt,
    )
    if response:
        if isinstance(response, str):
            click.echo(response)
        else:
            if response.content:
                click.echo(response.content)


def enhance_prompt(query: str, mcp_caller: MCPServersCaller) -> list[BaseMessage]:
    return asyncio.run(mcp_caller.get_mcp_prompt("my-mcp-server", "auto_template", {"query": query}))
The command will be loaded automatically by CLIver

Next Steps

Now that you understand how to extend CLIver, explore Skills for creating specialized capabilities, Memory & Identity for agent personalization, or see the Roadmap for upcoming features.