Skip to content

Tools

Registry and base types

_base

Base types and registry for the tools package.

SearchResult dataclass

SearchResult(path, line_number=None, snippet='', score=0.0)

A single hit returned by a tool.

path instance-attribute

path

Absolute (or relative) path of the matching file.

line_number class-attribute instance-attribute

line_number = None

Line number of the match inside the file (content searches only).

snippet class-attribute instance-attribute

snippet = ''

Matching text excerpt (content searches only).

score class-attribute instance-attribute

score = 0.0

Optional relevance score (higher = more relevant).

ToolResult dataclass

ToolResult(tool_name, results=list(), error='', truncated=False)

Aggregated output from a single tool invocation.

truncated class-attribute instance-attribute

truncated = False

True when the result list was clipped to max_results.

to_text

to_text(max_display=20)

Format the result as a human-readable string for the AI/UI.

Source code in src/tools/_base.py
def to_text(self, max_display: int = 20) -> str:
    """Format the result as a human-readable string for the AI/UI."""
    if self.error:
        return f"[{self.tool_name}] Error: {self.error}"
    if not self.results:
        return f"[{self.tool_name}] No results found."
    lines = [f"[{self.tool_name}] {len(self.results)} result(s)"
             + (" (truncated)" if self.truncated else "") + ":"]
    for r in self.results[:max_display]:
        lines.append(f"  {r}")
    if len(self.results) > max_display:
        lines.append(f"  … and {len(self.results) - max_display} more")
    return "\n".join(lines)

Tool

Abstract base for all built-in tools.

schema_text

schema_text()

Return a compact description for inclusion in the system prompt.

Source code in src/tools/_base.py
def schema_text(self) -> str:
    """Return a compact description for inclusion in the system prompt."""
    params = ", ".join(
        f"{k}: {v.get('type', 'string')}"
        for k, v in self.parameters_schema.get("properties", {}).items()
    )
    return f"  {self.name}({params}) — {self.description}"

ToolPermissions

ToolPermissions(allowed=None, disallowed=None, requires_approval=None, approve_callback=None)

Encapsulates the allow/disallow/approval rules for tool dispatch.

Resolution order (highest to lowest precedence): 1. disallowed — tool is always blocked, no override. 2. allowed — if non-empty, only tools in this set may run. 3. requires_approval — tool may run, but only after the user confirms.

Parameters

allowed: Whitelist of tool names the AI may call. An empty set means all registered tools are allowed (subject to the disallowed list). disallowed: Blacklist of tool names that are completely blocked. Takes precedence over allowed. requires_approval: Tool names that need explicit user confirmation before executing. The approval callback receives the tool name and the argument dict and must return True to proceed. approve_callback: Called as approve_callback(tool_name, args) -> bool when a tool in requires_approval is invoked. Defaults to a terminal prompt.

Source code in src/tools/_base.py
def __init__(
    self,
    allowed: list[str] | None = None,
    disallowed: list[str] | None = None,
    requires_approval: list[str] | None = None,
    approve_callback: "Callable[[str, dict], bool] | None" = None,
) -> None:
    self._allowed: frozenset[str] = frozenset(allowed or [])
    self._disallowed: frozenset[str] = frozenset(disallowed or [])
    self._requires_approval: frozenset[str] = frozenset(requires_approval or [])
    self._approve = approve_callback or _terminal_approve

is_allowed

is_allowed(name)

Return True if name passes the whitelist check.

If no whitelist is configured (empty set) every tool passes.

Source code in src/tools/_base.py
def is_allowed(self, name: str) -> bool:
    """Return True if *name* passes the whitelist check.

    If no whitelist is configured (empty set) every tool passes.
    """
    if self._allowed:
        return name in self._allowed
    return True

check

check(tool_name, args)

Enforce permissions for tool_name with args.

Returns

ToolResult | None A ToolResult with an error message if the tool is blocked or the user declines. None means the tool may proceed.

Source code in src/tools/_base.py
def check(self, tool_name: str, args: dict) -> "ToolResult | None":
    """Enforce permissions for *tool_name* with *args*.

    Returns
    -------
    ToolResult | None
        A ``ToolResult`` with an error message if the tool is blocked or
        the user declines.  ``None`` means the tool **may proceed**.
    """
    if self.is_disallowed(tool_name):
        logger.info("Tool %r is disallowed by configuration.", tool_name)
        return ToolResult(
            tool_name=tool_name,
            error=f"Tool '{tool_name}' is disabled.",
        )

    if not self.is_allowed(tool_name):
        logger.info(
            "Tool %r is not in the allowed list %s.",
            tool_name,
            sorted(self._allowed),
        )
        return ToolResult(
            tool_name=tool_name,
            error=(
                f"Tool '{tool_name}' is not permitted. "
                f"Allowed tools: {', '.join(sorted(self._allowed)) or 'none'}."
            ),
        )

    if self.needs_approval(tool_name):
        if not self._approve(tool_name, args):
            logger.info("User declined approval for tool %r.", tool_name)
            return ToolResult(
                tool_name=tool_name,
                error=f"Tool '{tool_name}' was not approved by the user.",
            )

    return None  # all checks passed — proceed

visible_names

visible_names(all_names)

Return the subset of all_names that are advertised to the AI.

Disallowed tools and tools outside the whitelist are hidden from the system prompt so the AI doesn't even try to call them.

Source code in src/tools/_base.py
def visible_names(self, all_names: list[str]) -> list[str]:
    """Return the subset of *all_names* that are advertised to the AI.

    Disallowed tools and tools outside the whitelist are hidden from the
    system prompt so the AI doesn't even try to call them.
    """
    return [
        n for n in all_names
        if not self.is_disallowed(n) and self.is_allowed(n)
    ]

ToolDescriptor dataclass

ToolDescriptor(name, description, parameters_schema, factory, unload_after_use=False)

Metadata + factory for a single tool — the instance is created lazily.

The descriptor holds everything the :class:ToolRegistry needs to: - Advertise the tool to the AI (name, description, schema) without instantiating it. - Create the tool instance on first use via factory. - Release the instance after use when unload_after_use is True.

Lifecycle

.. code-block:: text

UNLOADED  ──(get_instance)──►  LOADED  ──(release)──►  UNLOADED
                                  │
                             run() called here

The transition back to UNLOADED is triggered automatically by :meth:ToolRegistry.dispatch when unload_after_use is True, or manually via :meth:ToolRegistry.unload / :meth:ToolRegistry.unload_all.

is_loaded property

is_loaded

True if the tool instance is currently in memory.

get_instance

get_instance()

Return the live tool instance, creating it on first call.

Source code in src/tools/_base.py
def get_instance(self) -> Tool:
    """Return the live tool instance, creating it on first call."""
    if self._instance is None:
        logger.debug("Lazy-loading tool: %s", self.name)
        self._instance = self.factory()
    return self._instance

release

release()

Release the tool instance so it can be garbage-collected.

Source code in src/tools/_base.py
def release(self) -> None:
    """Release the tool instance so it can be garbage-collected."""
    if self._instance is not None:
        logger.debug("Unloading tool: %s", self.name)
        self._instance = None

schema_text

schema_text()

Return a compact one-line description for the AI system prompt.

Reads only from the descriptor fields — never touches the instance.

Source code in src/tools/_base.py
def schema_text(self) -> str:
    """Return a compact one-line description for the AI system prompt.

    Reads only from the descriptor fields — never touches the instance.
    """
    props = self.parameters_schema.get("properties", {})
    params = ", ".join(
        f"{k}: {v.get('type', 'string')}"
        for k, v in props.items()
    )
    return f"  {self.name}({params}) — {self.description}"

ToolRegistry

ToolRegistry(permissions=None, unload_after_use=False)

Registry of all available tools.

Tools are stored as :class:ToolDescriptor objects and instantiated lazily — only when dispatch() actually calls them. After each call the instance can optionally be released (unloaded) so it is garbage-collected, keeping memory usage at a minimum.

Key properties
  • Zero startup cost: registering tools is free; nothing is imported or constructed until the first dispatch() for that tool.
  • Selective unloading: set unload_after_use=True per tool (or globally via config) to release instances between calls.
  • System-prompt generation uses descriptor metadata only — no tool is ever instantiated just to build the prompt.
  • Permission enforcement via :class:ToolPermissions happens before the tool instance is even loaded, so blocked tools cost nothing.
Source code in src/tools/_base.py
def __init__(
    self,
    permissions: ToolPermissions | None = None,
    unload_after_use: bool = False,
) -> None:
    self._descriptors: dict[str, ToolDescriptor] = {}
    self._permissions = permissions or ToolPermissions()
    self._default_unload = unload_after_use

register_lazy

register_lazy(name, description, schema, factory, *, unload_after_use=None)

Register a tool using a factory function (primary API).

The factory is only called on the first dispatch() for this tool. Subsequent calls reuse the cached instance unless unload_after_use is True, in which case the instance is released after every call.

Parameters

name: Tool name used in [TOOL: name {...}] markers. description: One-line description shown to the AI in the system prompt. schema: JSON Schema dict for the tool's parameters. factory: Zero-argument callable that returns a fresh Tool instance. unload_after_use: Override the registry's default unload policy for this tool. None → inherit the registry default.

Source code in src/tools/_base.py
def register_lazy(
    self,
    name: str,
    description: str,
    schema: dict,
    factory: Callable[[], Tool],
    *,
    unload_after_use: bool | None = None,
) -> None:
    """Register a tool using a factory function (primary API).

    The factory is only called on the first ``dispatch()`` for this tool.
    Subsequent calls reuse the cached instance unless *unload_after_use*
    is ``True``, in which case the instance is released after every call.

    Parameters
    ----------
    name:
        Tool name used in ``[TOOL: name {...}]`` markers.
    description:
        One-line description shown to the AI in the system prompt.
    schema:
        JSON Schema dict for the tool's parameters.
    factory:
        Zero-argument callable that returns a fresh ``Tool`` instance.
    unload_after_use:
        Override the registry's default unload policy for this tool.
        ``None`` → inherit the registry default.
    """
    unload = self._default_unload if unload_after_use is None else unload_after_use
    self._descriptors[name] = ToolDescriptor(
        name=name,
        description=description,
        parameters_schema=schema,
        factory=factory,
        unload_after_use=unload,
    )
    logger.debug("Registered tool (lazy): %s  unload_after_use=%s", name, unload)

register

register(tool)

Register an already-constructed tool instance (convenience API).

The instance is wrapped in a descriptor so it participates in the same lazy-load / unload lifecycle. The instance is considered pre-loaded (is_loaded == True) immediately after registration.

Source code in src/tools/_base.py
def register(self, tool: Tool) -> None:
    """Register an already-constructed tool instance (convenience API).

    The instance is wrapped in a descriptor so it participates in the
    same lazy-load / unload lifecycle.  The instance is considered
    pre-loaded (``is_loaded == True``) immediately after registration.
    """
    desc = ToolDescriptor(
        name=tool.name,
        description=getattr(tool, "description", ""),
        parameters_schema=getattr(tool, "parameters_schema", {}),
        factory=lambda t=tool: t,
        unload_after_use=self._default_unload,
    )
    desc._instance = tool  # already loaded
    self._descriptors[tool.name] = desc
    logger.debug("Registered tool (eager): %s", tool.name)

get

get(name)

Return the tool instance for name, loading it if needed.

Source code in src/tools/_base.py
def get(self, name: str) -> Tool | None:
    """Return the tool instance for *name*, loading it if needed."""
    desc = self._descriptors.get(name)
    if desc is None:
        return None
    return desc.get_instance()

get_descriptor

get_descriptor(name)

Return the :class:ToolDescriptor for name.

Source code in src/tools/_base.py
def get_descriptor(self, name: str) -> ToolDescriptor | None:
    """Return the :class:`ToolDescriptor` for *name*."""
    return self._descriptors.get(name)

names

names()

Return all registered tool names.

Source code in src/tools/_base.py
def names(self) -> list[str]:
    """Return all registered tool names."""
    return list(self._descriptors)

loaded_names

loaded_names()

Return names of tools whose instances are currently in memory.

Source code in src/tools/_base.py
def loaded_names(self) -> list[str]:
    """Return names of tools whose instances are currently in memory."""
    return [n for n, d in self._descriptors.items() if d.is_loaded]

unload

unload(name)

Release the instance for tool name.

Returns True if the tool was loaded and is now released, False if the tool is unknown or was already unloaded.

Source code in src/tools/_base.py
def unload(self, name: str) -> bool:
    """Release the instance for tool *name*.

    Returns ``True`` if the tool was loaded and is now released,
    ``False`` if the tool is unknown or was already unloaded.
    """
    desc = self._descriptors.get(name)
    if desc and desc.is_loaded:
        desc.release()
        return True
    return False

unload_all

unload_all()

Release all currently loaded tool instances.

Returns the list of tool names that were unloaded.

Source code in src/tools/_base.py
def unload_all(self) -> list[str]:
    """Release all currently loaded tool instances.

    Returns the list of tool names that were unloaded.
    """
    released = []
    for name, desc in self._descriptors.items():
        if desc.is_loaded:
            desc.release()
            released.append(name)
    if released:
        logger.debug("Unloaded %d tool(s): %s", len(released), released)
    return released

system_prompt_section

system_prompt_section()

Return the block injected into the AI system prompt.

Only lists tools the AI is permitted to call. No tool instance is created — reads descriptor metadata only.

Source code in src/tools/_base.py
def system_prompt_section(self) -> str:
    """Return the block injected into the AI system prompt.

    Only lists tools the AI is permitted to call.
    **No tool instance is created** — reads descriptor metadata only.
    """
    visible = self._permissions.visible_names(list(self._descriptors))
    if not visible:
        return ""

    lines = [
        "## Available tools",
        "",
        "You have access to the following tools. When you need to use one,",
        "output exactly one line in this format (valid JSON, on a single line):",
        "",
        '  [TOOL: tool_name {"arg": "value"}]',
        "",
        "Wait for the tool result before continuing your response.",
        "Only call one tool per response turn.",
        "",
        "Tools:",
    ]
    for name in visible:
        lines.append(self._descriptors[name].schema_text())
    lines.append("")
    return "\n".join(lines)

dispatch

dispatch(call_text)

Parse and execute a [TOOL: name {...}] call from the AI.

Lifecycle per call
  1. Parse call_text — return None if no marker found.
  2. Permission check (allow/disallow/approval) — return error result if blocked. No instance is created for blocked tools.
  3. Lazily load the tool instance via descriptor.get_instance().
  4. Run tool.run(args) and collect the result.
  5. If descriptor.unload_after_use is True, release the instance immediately so memory is reclaimed.
Parameters

call_text: Text containing a [TOOL: name {...}] marker.

Returns

ToolResult | None Result of the tool call, or None if no marker was found.

Source code in src/tools/_base.py
def dispatch(self, call_text: str) -> ToolResult | None:
    """Parse and execute a ``[TOOL: name {...}]`` call from the AI.

    Lifecycle per call
    ------------------
    1. Parse *call_text* — return ``None`` if no marker found.
    2. Permission check (allow/disallow/approval) — return error result
       if blocked.  **No instance is created for blocked tools.**
    3. Lazily load the tool instance via ``descriptor.get_instance()``.
    4. Run ``tool.run(args)`` and collect the result.
    5. If ``descriptor.unload_after_use`` is ``True``, release the
       instance immediately so memory is reclaimed.

    Parameters
    ----------
    call_text:
        Text containing a ``[TOOL: name {...}]`` marker.

    Returns
    -------
    ToolResult | None
        Result of the tool call, or ``None`` if no marker was found.
    """
    m = self._CALL_RE.search(call_text)
    if not m:
        return None

    tool_name = m.group(1).strip()
    args_str = m.group(2).strip()

    # 1. Unknown tool — report visible tools, not all tools
    desc = self._descriptors.get(tool_name)
    if desc is None:
        logger.warning("AI called unknown tool %r", tool_name)
        visible = self._permissions.visible_names(list(self._descriptors))
        return ToolResult(
            tool_name=tool_name,
            error=(
                f"Unknown tool: '{tool_name}'. "
                f"Available: {', '.join(visible) or 'none'}."
            ),
        )

    # 2. Parse JSON args
    try:
        args = json.loads(args_str)
    except json.JSONDecodeError as exc:
        return ToolResult(
            tool_name=tool_name,
            error=f"Invalid tool arguments (not valid JSON): {exc}",
        )

    # 3. Permission gate — no instance created if blocked
    blocked = self._permissions.check(tool_name, args)
    if blocked is not None:
        return blocked

    # 4. Validate and sanitise AI-supplied arguments before dispatch
    try:
        from src.security import validate_tool_args
        args = validate_tool_args(args, schema=desc.parameters_schema)
    except (ValueError, TypeError) as exc:
        return ToolResult(
            tool_name=tool_name,
            error=f"Tool argument validation failed: {exc}",
        )

    # 5. Lazy-load and run
    tool = desc.get_instance()
    logger.info("Dispatching tool '%s' args=%s", tool_name, args)
    result = tool.run(args)

    # 6. Optional unload after use
    if desc.unload_after_use:
        desc.release()

    return result

find_calls

find_calls(text)

Return all [TOOL: ...] substrings found in text.

Source code in src/tools/_base.py
def find_calls(self, text: str) -> list[str]:
    """Return all ``[TOOL: ...]`` substrings found in *text*."""
    return [m.group(0) for m in self._CALL_RE.finditer(text)]

Available tools

find_files

Find files tool — search for files by name or glob pattern.

FindFilesTool

FindFilesTool(default_search_path=None)

Bases: Tool

Search for files by name or glob pattern.

Backend priority
  1. plocate – fastest; uses an mmap-based index updated by a daemon.
  2. locate – compatible but slightly slower index format.
  3. find – always available; slower because it walks the filesystem.

The tool automatically chooses the fastest backend present on the system.

Source code in src/tools/find_files.py
def __init__(self, default_search_path: str | Path | None = None) -> None:
    self._default_path = Path(default_search_path or Path.home())
    self._backend: str | None = None  # cached after first call

search_in_files

Search in files tool — find text patterns inside files.

SearchInFilesTool

SearchInFilesTool(default_search_path=None, blocked_paths=None)

Bases: Tool

Search for text inside files.

Backend priority
  1. rg (ripgrep) – fastest; respects .gitignore; written in Rust.
  2. grep -r – always available; slower on large trees.
Source code in src/tools/search_in_files.py
def __init__(self, default_search_path: str | Path | None = None,
             blocked_paths: list[str] | None = None) -> None:
    self._default_path = Path(default_search_path or Path.home())
    self._backend: str | None = None
    # Expand and resolve blocked paths at init time so the check is fast
    self._blocked: list[Path] = [
        Path(p).expanduser().resolve()
        for p in (blocked_paths or [])
    ]

web_fetch

Web fetch tool — retrieve text content from a URL.

WebFetchTool

WebFetchTool(max_response_chars=8000, allowed_content_types=None, domain_allowlist=None, domain_blocklist=None, max_redirects=5, connect_timeout=5.0, read_timeout=15.0)

Bases: Tool

Fetch a URL from the internet and return its text content.

Security model
  • TLS always verifiedverify=True is non-negotiable; there is no way to disable certificate checking through config.
  • SSRF protection — requests to loopback (127.x, ::1, localhost) and link-local addresses are blocked so the AI cannot use this tool to probe internal services.
  • Domain filtering — optional allow-list and block-list. If domain_allowlist is non-empty, only those domains are reachable.
  • Content-type whitelist — only text/* and safe application/* responses are returned; binary files and executables are rejected.
  • Size cap — responses larger than max_response_chars are truncated; the raw body is never buffered beyond that limit.
  • No session / cookie persistence — each request uses a fresh requests.Session destroyed immediately after the call.
  • Connection: close — the TCP socket is released right after the response is consumed.

This tool is disabled by default. Enable it via config.yaml:

.. code-block:: yaml

tools: web_fetch: enabled: true

And ensure it stays in requires_approval so every fetch is confirmed by the user before it happens.

Source code in src/tools/web_fetch.py
def __init__(
    self,
    max_response_chars: int = 8_000,
    allowed_content_types: list[str] | None = None,
    domain_allowlist: list[str] | None = None,
    domain_blocklist: list[str] | None = None,
    max_redirects: int = 5,
    connect_timeout: float = 5.0,
    read_timeout: float = 15.0,
) -> None:
    self._max_chars = min(max_response_chars, _FETCH_ABSOLUTE_MAX)
    self._allowed_types: frozenset[str] = (
        frozenset(t.lower() for t in allowed_content_types)
        if allowed_content_types is not None
        else _SAFE_CONTENT_TYPES
    )
    self._domain_allowlist: frozenset[str] = frozenset(
        d.lower().lstrip("*.") for d in (domain_allowlist or [])
    )
    self._domain_blocklist: frozenset[str] = frozenset(
        d.lower().lstrip("*.") for d in (domain_blocklist or [])
    )
    self._max_redirects = max_redirects
    self._connect_timeout = connect_timeout
    self._read_timeout = read_timeout

Web search tool — open the user's browser with a search query.

WebSearchTool

WebSearchTool(default_engine='duckduckgo', engines=None)

Bases: Tool

Open the user's default browser to search the web.

Privacy model

This tool never makes any HTTP request itself. It builds a search URL and hands it to xdg-open, which opens the URL in whatever browser the user has set as their default. The assistant has no visibility into what the browser does after that point.

The tool is useful for: - Letting the AI suggest search queries based on conversation context. - Giving the user a one-click way to look something up without the assistant having to access the internet itself.

Source code in src/tools/web_search.py
def __init__(
    self,
    default_engine: str = "duckduckgo",
    engines: dict[str, str] | None = None,
) -> None:
    self._default_engine = default_engine
    # Merge user-defined engines over the built-in set
    self._engines: dict[str, str] = {**_DEFAULT_ENGINES, **(engines or {})}

man_reader

Man page reader tool — read Linux manual pages.

ManPageTool

ManPageTool(max_chars=8000, default_sections=None)

Bases: Tool

Read the Linux manual page for a command.

The tool runs man <command> locally with plain-text output (no pager, no ANSI codes), optionally extracts specific sections, and truncates the result to keep it within the model's context window.

This lets the AI look up the exact flags, syntax, and examples for any installed command before crafting a shell instruction — ensuring accuracy especially across distros where option names differ.

The tool is entirely offline: it reads man pages from the local system and never contacts any network resource.

Parameters

max_chars: Maximum characters returned per call. Defaults to 8 000, which is enough for SYNOPSIS + OPTIONS of most commands. default_sections: Section names to extract when the caller doesn't specify any. Use [] to return the full man page (up to max_chars).

Source code in src/tools/man_reader.py
def __init__(
    self,
    max_chars: int = 8_000,
    default_sections: list[str] | None = None,
) -> None:
    self._max_chars = min(max_chars, _ABSOLUTE_MAX_CHARS)
    self._default_sections: list[str] = (
        default_sections
        if default_sections is not None
        else list(_USEFUL_SECTIONS)
    )

system_control

System control tool — control audio, bluetooth, wifi, brightness, etc.

SystemControlTool

Bases: Tool

Control system resources: audio, microphone, Bluetooth, Wi-Fi, power mode, and display brightness.

Backend detection

For each resource the tool tries available backends in order:

  • Audio / Microphone: WirePlumber (wpctl) → PulseAudio (pactl) → ALSA (amixer)
  • Bluetooth: bluetoothctlrfkill
  • Wi-Fi: nmcli (NetworkManager) → rfkill
  • Power mode: powerprofilesctl (power-profiles-daemon) → tuned-adm (TuneD)
  • Brightness: brightnessctlxbacklightlight

The first backend whose executable is on $PATH is used. All subprocess imports and shutil.which checks are deferred to the first run() call so loading this module is free.

Safety

This tool is in requires_approval by default. The user sees exactly which resource and action the AI is requesting before anything changes. Read-only get queries skip approval.

system_info

System information tool — answer questions about the user's system.

Queries answered

time Current local date and time. uptime How long the system has been running. battery Battery percentage, charging status, and time remaining. battery_health Battery health (design capacity vs actual capacity). gpu Graphics card model(s). cpu CPU model, core/thread count, current frequency. memory RAM total, used, available. disk Disk usage for mounted filesystems. os Distro name, version, kernel, package manager. network Active network interfaces and their IP addresses. all A concise summary covering every topic above.

Data sources (in priority order)

  • /proc/* and /sys/* — direct kernel interfaces, fastest and most accurate. No subprocess, no extra packages.
  • Command-line tools (upower, lspci, lscpu, ip) — used when the proc/sys files don't give enough detail.

All subprocess and heavy-stdlib imports are deferred inside run() so importing this module is free.

SystemInfoTool

Answer questions about the user's system hardware and software.

Reads from /proc and /sys wherever possible (no subprocess overhead) and falls back to CLI tools for richer detail.

app

Application tool — open, search, and install apps.

Actions

open Launch an installed application by name. Tries (in order): gtk-launch → desktop file Exec= field → plain binary exec.

search Search for packages matching a query using the distro's package manager (apt, dnf, pacman, …). Also searches local .desktop files for installed applications.

install Open the user's terminal pre-filled with the correct install command for this distro (e.g. sudo apt install <pkg>). The user must press Enter to confirm — the assistant never runs installs itself.

All subprocess imports are deferred to the first run() call (lazy loading).

AppTool

Open, search, and install applications.

installed_apps

Installed applications detector.

Scans .desktop files, Flatpak, Snap, system packages, and PATH binaries.

InstalledAppsTool

Bases: Tool

List or search installed applications and CLI tools.

process_info

Process and power information tool.

Answers: what is slowing my computer, what is draining the battery, which process uses the most CPU/RAM.

ProcessInfoTool

Bases: Tool

Show running processes and resource/power usage.