VisvoAI CLI Docs
Customizing

Extending VisvoAI CLI

Every real way to plug into, customize, or fork the CLI — from a five-line tool to your own branded product

VisvoAI CLI is built as a layered system, lightest extension first. Every layer below is a real, working mechanism in the source — not aspirational. They compose: a plugin tool, a custom agent, and a skill can all reference each other in the same session.

The menu, lightest to heaviest

LayerWhat it addsWhere it livesTrust
Plugin toolsone new function the model can call~/.visvoai/tools/*.pyglobal only — never project
Custom agentsa subagent with its own prompt + tool tier.visvoai/agents/ or ~/.visvoai/agents/project needs one-time approval
Skillsreusable step-by-step instructions.visvoai/skills/ or ~/.visvoai/skills/project needs one-time approval
MCP serversan out-of-process tool serverconfig.toml [mcp_servers.*]project needs one-time approval
Config & themekeys, defaults, skill libraries, paletteconfig.toml, prefsn/a
Forking the sourceyour own branded CLI productvisvoai-cli → your repon/a

The first four are additive — they extend a stock install without touching its code. Forking is the only layer that changes the application itself.

Each section below is the short version. The full treatment lives on its own page: Plugin tools, Custom agents, Skills, MCP servers, and Config & theme.

Plugin tools

The lightest extension: a plain Python function becomes a model-callable tool. visvoai/cli/toolkit.py is the entire contract:

# ~/.visvoai/tools/mytools.py
import subprocess
from visvoai.cli.toolkit import make_cli_tool

def git_authors(days: int = 30) -> str:
    """Who committed to this repo recently, with commit counts."""
    out = subprocess.run(
        ["git", "shortlog", "-sn", f"--since={days} days ago", "HEAD"],
        capture_output=True, text=True, timeout=10)
    return out.stdout or out.stderr

TOOLS = [make_cli_tool(git_authors, gate=None)]

make_cli_tool reads the schema from your type hints and the model-facing description from your docstring, caps output so one call can't flood the context window, and turns any exception into an "ERROR: …" string instead of crashing the turn.

Gate is declared per tool, not inferred:

  • gate=None — free, runs silently (pure reads: git_authors above)
  • gate="approve" — the user confirms each call before it runs (mutations: tagging a release, hitting a paid API)
  • gate="self" — the function does its own internal confirmation and is passed through ungated

Sync and async functions both work — an async def is awaited directly on the UI event loop, no thread bridge needed.

The boundary: plugin tools are global-only, by design. Only ~/.visvoai/tools/*.py is imported; there is no project-level equivalent. Importing a .py file is code execution inside the CLI process — a cloned repo must never be able to inject Python into your session just by being checked out. If you need project-scoped custom behavior, use a project skill (instructions, not code) or a project MCP server (a separate process, still subject to one-time approval).

Custom agents

An agent is a named subagent the main model can delegate a self-contained task to via run_agent. Each dispatch builds a fresh graph — its own system prompt, its own tool set, empty history. Only the subagent's final message returns to the caller; it never sees the rest of your conversation.

---
description: Reviews a diff for bugs and risky changes
tools: read-only
---
You are a meticulous code reviewer. Examine the requested diff or files, read
enough surrounding context to judge them, and report concrete findings with
file:line references. Order findings by severity.

Save that as .visvoai/agents/reviewer.md (project, shareable, may be checked in) or ~/.visvoai/agents/reviewer.md (global, every project). The frontmatter:

  • description — the one-liner shown in the run_agent roster
  • toolsread-only (search/analysis, no approval prompts, the shell itself refuses write-classified commands and runs sandboxed at the OS level) · full (the standard tool set, mutations still hit the same approval gate as the top level) · or an explicit comma-separated tool list
  • model — optional deployment id override; omit to inherit the session's model

The non-obvious part: the two built-in agents, explore and general, are defined as plain AgentSpec entries in the exact same shape as a user's .md file — there is no separate "built-in" code path. The roster is three layers merged with later winning on name: builtin → global → project. That means a file literally named explore.md or general.md in ~/.visvoai/agents/ overrides the built-in the next time the roster loads. The CLI's own agents create helper refuses to let you create one under those names (to stop an accidental shadow), but hand-writing the file still works — the override mechanism is real, just not wrapped in a command. Use it deliberately if your team wants different default recon/delegation behavior than stock VisvoAI ships with.

The boundary: tool tier is decided at graph build time by the CLI, never by the model reading the prompt — an agent's prompt can ask for a tool it wasn't given, but it cannot grant itself one. run_agent itself is never included in a subagent's tool set (depth is capped at 1 — no subagent can dispatch its own subagents). A project agent's .md is repo-controlled text that steers tool use on your machine, so it needs one-time approval in /agents, hashed on the full definition — any edit re-prompts. Global agents are implicitly trusted (you wrote them).

Skills

A skill is reusable step-by-step instructions the model loads on demand via read_skill(skill, args, resource) — knowledge, not capability. Loading a skill never grants a new tool; the model still acts through its existing tool set, still behind the same approval gates.

---
description: Draft release notes from git history
args:
  from_tag: "the tag to diff from"
---
Run `git log $from_tag..HEAD --oneline` and summarize the commits into
release notes grouped by feature/fix/chore. See format.md for the template.

Save as ~/.visvoai/skills/release-notes/SKILL.md (a directory skill, so it can carry supporting files like format.md alongside it) or a flat <name>.md for a one-file skill. $from_tag and any other declared $placeholder get substituted when the body is read; $ARGUMENTS expands to every given arg as name: value lines.

Progressive disclosure is structural, not a convention you have to remember: supporting files next to SKILL.md are only fetched when the body's own text names them via resource=, never loaded speculatively. For a skill with several branches (e.g. "quick checklist" vs. "deep checklist"), the body classifies first, then tells the model which single reference file to load — see examples/skills/pr-review/ in the repo for the real three-file version of this pattern.

External skill libraries: [skills] extra_dirs = ["~/.claude/skills"] in config.toml merges in a folder you already have from another tool. Trust follows which config declared the directory, not where the directory physically sits — a dir listed in your global config is implicitly trusted even if it happens to live inside a repo; a dir listed in a project's .visvoai/config.toml needs the same one-time approval as any other project-defined skill.

The boundary: a skill cannot execute anything by itself — "no skill runner" is the literal design. If your workflow needs its own tools, it needs a plugin tool or an MCP server, not a skill pretending to be one.

MCP servers

Two TOML tables, ~/.visvoai/config.toml (global) or <project>/.visvoai/config.toml (project, project wins on name conflict):

[mcp_servers.chrome]              # stdio: CLI spawns the subprocess
command = "npx"
args = ["-y", "chrome-devtools-mcp@latest"]

[mcp_servers.linear]              # remote: streamable HTTP
url = "https://mcp.linear.app/mcp"
headers = { Authorization = "Bearer ${LINEAR_API_KEY}" }

or the equivalent one-liner: visvoai mcp add chrome -- npx -y chrome-devtools-mcp@latest.

${VAR} values expand from the environment at connect time — never paste a literal secret into the file. Discovered tools are exposed to the model as server__tool and are selectable by name in a custom agent's explicit tool list, and included wholesale in tools: full agents. They are never available to a read-only agent — a remote server's side effects can't be classified as safe-by-construction the way a sandboxed local shell can.

Sessions are held open for the app's lifetime, not reconnected per call — a stateful server (a browser via chrome-devtools-mcp, a live DB connection) keeps its state (open tabs, a transaction) across turns.

The boundary: a project-defined server is repo-controlled config that can spawn a subprocess or call out to a URL on your machine the moment you open that repo — it gets the same one-time-approval treatment as a project agent or skill, recorded outside the repo so a checkout alone never silently grants trust.

Config and theme

Two config files layer the same way everywhere in the CLI — ~/.visvoai/config.toml (global, yours) under <project>/.visvoai/config.toml (project, shareable) — project wins on conflicting keys. API keys layer three ways: exported env var → <project>/.visvoai/secrets.toml → global config.toml, highest precedence first, so a .env or shell export always overrides a stored default.

Theme: the CLI ships six built-in palettes (default, cosmic, sunset, emerald, warm, technical), each rendered in light and dark, switchable at runtime (Ctrl+T flips light/dark; the palette picker cycles the six) and persisted to your prefs. There is no config-file key to define a brand-new seventh palette — the token table (palette_tokens.json) is a vendored, generated asset shipped inside the package, not a user-editable input. If you need your own brand colors (not just a different built-in), that is a fork, not a config change — edit palette_tokens.json and rebuild.

Forking the CLI

Everything above extends a stock install. If you want to ship your own product on top of VisvoAI CLI — a branded internal tool, a vertical variant for one workflow, a distribution with a different default agent roster baked in — the path is a source fork, and the package structure makes that concrete rather than hand-wavy:

  • Single entry point. pyproject.toml declares exactly one console script: visvoai = "visvoai.cli.main:cli". Renaming your product's CLI is a pyproject.toml edit plus a main.py Click group rename — there's no hidden second entry point to track down.
  • The dependency is a public package, not a git submodule of private code. visvoai-cli depends on visvoai-core>=0.1.0 and visvoai-ai[all]>=0.2.2 from PyPI — both MIT, both part of the same public surface. You can fork visvoai-cli alone and keep consuming visvoai-core/visvoai-ai as normal dependencies, or vendor all three if you want to diverge at the runtime level too.
  • Brand assets are data, not code. palette_tokens.json and assets/* are shipped via [tool.setuptools.package-data] specifically so they're easy to swap wholesale — replace the JSON, replace the logo asset, rebuild; theme.py reads both by path at import time and doesn't otherwise know or care whose brand it is.
  • A default agent/skill roster ships as data, not logic. Since built-in agents are just AgentSpec entries (see Custom agents), a fork can replace BUILTIN_AGENTS in agents.py with your own roster without touching the dispatch, trust, or tool-tier machinery at all — that machinery is generic over "whatever's in the dict."
  • MIT, no copyleft trap. The whole package is MIT-licensed; a fork that never upstreams anything is a fully legitimate outcome, not an edge case the license discourages.

Where forking is the wrong tool: if what you actually want is "my team's default tools/agents/skills," you don't need a fork — ship a config.toml + .visvoai/agents/ + .visvoai/tools/ in your team's dotfiles repo and have everyone point VISVOAI_HOME at it. Fork only when you need to change the application — its name, its entry point, its brand, or its underlying tool-tier/trust logic — not when you're configuring the one that already ships.

On this page