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
| Layer | What it adds | Where it lives | Trust |
|---|---|---|---|
| Plugin tools | one new function the model can call | ~/.visvoai/tools/*.py | global only — never project |
| Custom agents | a subagent with its own prompt + tool tier | .visvoai/agents/ or ~/.visvoai/agents/ | project needs one-time approval |
| Skills | reusable step-by-step instructions | .visvoai/skills/ or ~/.visvoai/skills/ | project needs one-time approval |
| MCP servers | an out-of-process tool server | config.toml [mcp_servers.*] | project needs one-time approval |
| Config & theme | keys, defaults, skill libraries, palette | config.toml, prefs | n/a |
| Forking the source | your own branded CLI product | visvoai-cli → your repo | n/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_authorsabove)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 therun_agentrostertools—read-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 listmodel— 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.tomldeclares exactly one console script:visvoai = "visvoai.cli.main:cli". Renaming your product's CLI is apyproject.tomledit plus amain.pyClick 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-clidepends onvisvoai-core>=0.1.0andvisvoai-ai[all]>=0.2.2from PyPI — both MIT, both part of the same public surface. You can forkvisvoai-clialone and keep consumingvisvoai-core/visvoai-aias normal dependencies, or vendor all three if you want to diverge at the runtime level too. - Brand assets are data, not code.
palette_tokens.jsonandassets/*are shipped via[tool.setuptools.package-data]specifically so they're easy to swap wholesale — replace the JSON, replace the logo asset, rebuild;theme.pyreads 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
AgentSpecentries (see Custom agents), a fork can replaceBUILTIN_AGENTSinagents.pywith 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.