Commands
# From root directorybun run cli <command>
# Direct executionbun run apps/cli/index.ts <command>
# Once installed globally (see /cli/install/ — the package is viite-cli, not viite)viite <command> # or the shorter alias: vt <command>Global flag: --verbose / -v — enable verbose logging (default false).
Nine top-level commands: branch, read, ingest, worktree, scrape, tasks, agent, local,
record. The eight covered here run and exit; agent runs a supervised loop and has
its own page.
Branch Commands
Section titled “Branch Commands”# Turn free text (e.g. a task title) into a valid git branch name and create itviite branch create "fix: This is a bug fix"
# Preview the sanitized name without creating the branchviite branch create "feature: new api" --dry-runbranch create <text> only creates the local branch object — it does not check it out or create a
worktree. The sanitizer lowercases and trims the input, replaces punctuation
(: \s , ( ) ~ ^ ? * [ ] \ @ { }) with -, collapses repeated -/., strips leading/trailing
-/.//, and truncates to 60 characters.
Flags:
--dry-run,-d- print the sanitized branch name instead of creating it (default:false)
Read & Ingest Commands (Gmail)
Section titled “Read & Ingest Commands (Gmail)”Two ways to pull from a source:
| Command | Pattern | Use Case |
|---|---|---|
read source gmail | Direct (sync) | Quick reads, returns raw content immediately |
ingest source gmail | Task-based (async) | Full workflow with analysis/enrichment steps |
# Direct Gmail read (synchronous, no workflow/job created)viite read source gmail --filter=user@example.com --limit-days=7 --with-url
# Gmail read with defaults (7 days, no URL filter, falls back to your session's email)viite read source gmail
# Trigger the Gmail ingestion workflow (async)viite ingest source gmail --filter=user@example.com --limit-days=7 --save-to=databaseread source gmail flags:
--filter,-f- filter by sender email address (falls back toGMAIL_USER_EMAIL, then the authenticated session’s own email, if omitted)--limit-days,-l- limit to emails from the last N days (default:7)--with-url,-u- only include emails containing URLs (default:false)
ingest source gmail flags: same as above, plus:
--save-to,-s- one ofconsole,database,csv,notion(default:console)
Worktree Commands
Section titled “Worktree Commands”Manage git worktrees for parallel development with automatic port offset configuration.
# Create a worktree, auto-assigning the next free port offsetviite worktree create feature-auth
# Create a worktree at an explicit offset (must be a multiple of 100)viite worktree create bugfix-login 200
# Create without opening terminals / without running bun installviite worktree create feature-x --no-openviite worktree create feature-x --no-install
# List all worktreesviite worktree list
# Remove a worktree (skip prompts with --yes)viite worktree remove feature-auth --yes
# Remove worktree and delete the branchviite worktree remove feature-auth --delete-branchThe port-offset argument to worktree create is optional — omit it and the CLI scans existing
worktrees’ .env files and auto-assigns the next unused multiple of 100 (printing
Auto-assigned port offset: <n>). New branches are created from a freshly-fetched origin/main,
not whatever is checked out in the main repo. worktree create also copies gitignored .env files
and .claude/settings.local.json / .claude/agents/ into the new worktree.
Port offset scheme (base ports, offset is added to each):
| Offset | API | Dashboard | Dev website |
|---|---|---|---|
| 0 | 3000 | 5000 | 5003 |
| 100 | 3100 | 5100 | 5103 |
| 200 | 3200 | 5200 | 5203 |
worktree create flags:
--open,-o- open terminal sessions running the dev servers after creation (default:true)--install,-i- rungit pull --rebase origin main && bun installafter creation (default:true)
worktree remove flags:
--delete-branch,-d- also delete the git branch (default:false)--force,-f- force removal even with uncommitted changes (default:true)--yes,-y- skip confirmation prompts, for scripted/non-TTY callers (default:false)
Terminal integration: unless --no-open is passed, creating a worktree opens two sessions in
your default terminal — one running the API, one running the dashboard, both already cd-ed into the
worktree and bound to its port offset. The terminal is resolved from PLATFORM_TERMINAL/TERMINAL,
then TERM_PROGRAM, then the OS default (Terminal.app on macOS, x-terminal-emulator on Linux).
iTerm2 gets tabs in one window; Terminal.app gets one window per server, because scripting a tab
there needs Accessibility permission.
Worktree PR Commands
Section titled “Worktree PR Commands”Manage GitHub PRs for worktrees. Requires GitHub CLI (gh) installed and
authenticated.
# List PRs with associated worktreesviite worktree pr listviite worktree pr list --all # All open PRs (for finding PRs to checkout)viite worktree pr list --all --author @me # Your open PRs
# Create a PR from the current worktree branchviite worktree pr createviite worktree pr create --title "Add feature" --body "Description" --draft
# Checkout an existing PR into a new worktree (port offset is optional/auto-assigned, as with `worktree create`)viite worktree pr checkout 123viite worktree pr checkout 123 100
# Show PR status for the current branchviite worktree pr statusviite worktree pr status feature-branch --checks
# Sync worktree with its base branchviite worktree pr sync # rebase by defaultviite worktree pr sync --mergeviite worktree pr sync --pushPR List flags:
--all- show all open PRs, not just ones with a local worktree (default:false)--author,-a- filter by author (@mefor your own PRs)--limit,-l- maximum PRs to show (default:50)
PR Create flags:
--title,-t- PR title (prompts if omitted)--body,-b- PR description (prompts if omitted, optional)--draft,-d- create as a draft PR (default:false)--base- base branch (default:main)
PR Checkout flags:
--open,-o- open terminal sessions running the dev servers (default:true)--install,-i- runbun install(default:true)
PR Status flags:
--checks,-c- show detailed CI check results instead of a one-line summary (default:false)
PR Sync flags:
--rebase,-r- use rebase strategy (default strategy either way, unless--mergeis set)--merge,-m- use merge strategy instead--push,-p- push after a successful sync (rebase + push prompts to confirm a--force-with-leasepush)--base,-b- base branch to sync with (auto-detected from the PR, falls back tomain)
Scrape Command
Section titled “Scrape Command”viite scrape leboncoin --url "https://www.leboncoin.fr/recherche?category=71"viite scrape leboncoin -u "https://..." --pages 5 --delay 500viite scrape leboncoin -u "https://..." --saveScrapes protected sites via a Chrome CDP connection. <source> must be one of leboncoin,
autoscout24, malt.
Flags:
--url,-u- page URL to scrape (prompted interactively if omitted)--cdp-endpoint- Chrome CDP endpoint (default:http://localhost:9222)--pages,-p- number of pages to scrape (default:1)--delay,-d- milliseconds to wait between pages (default:500)--save- save results to the database (default:false)--json- machine-readable output mode (default:false, not yet implemented)
Tasks Command
Section titled “Tasks Command”viite tasks # local dev APIviite tasks --target viite-ai # read the API URL from that section of coolify.envviite tasks --local --jsonLists the workflows on a deployed target, reading the target’s API URL out of
deploy/coolify/coolify.env by default.
Flags:
--target,-t-coolify.envsection name to read the API URL from (e.g.viite-ai)--api-url- explicit API URL override, skips thecoolify.envlookup entirely--local- use the local dev API instead (default:false)--json- machine-readable output; also disables interactive login (fails immediately if there’s no valid session, instead of prompting)
URL resolution priority: --api-url > --local > coolify.env scan (override the file path with
COOLIFY_ENV_FILE).
Agent Command
Section titled “Agent Command”viite agent watches a board for cards tagged for the agent and runs a coding session for each one.
It is the only command with a security boundary of its own, so it is documented separately —
see Agent.
Local Command
Section titled “Local Command”viite local doctor # what can this machine run, and what is missing?viite local doctor --config <path> # inspect a config other than the defaultLists the recipes declared in ~/.platform-cli/agent.json, whether each one’s tool is actually
installed, and what to type to install the ones that aren’t. Offline by design — it reads the
local config and probes PATH; no API, no auth, no board. Exits non-zero if anything needs
attention, so it works as a setup check in a script.
✓ mov-to-mp4 — ffmpeg found (from:path to:path)✗ transcribe — "whisper" is not installed install with: brew install whisper-cppRecipes are what a local card is allowed to run here. A card names a recipe and supplies
parameters; it never carries a command, a binary, or a directory:
{ "recipes": [ { "name": "mov-to-mp4", "command": "ffmpeg", "args": ["-i", "{from}", "-c:v", "libx264", "-c:a", "aac", "{to}"], "install": "brew install ffmpeg", "params": { "from": { "type": "path", "roots": ["~/Movies"] }, "to": { "type": "path", "roots": ["~/Movies"] } }, "timeoutMs": 3600000 } ]}Same rule as repos: a card can ask, only this file can grant. Four properties follow from it,
and each is enforced rather than documented:
- No shell. The command is spawned with an argv array, so
;,|and$( )in a value are inert data. - A placeholder is a whole argument.
"{from}"is one argv element;"--input={from}"is rejected when the config loads, because partial interpolation is how a value smuggles in a second argument. Filter strings (-vf fps=12,scale=800:-1) therefore cannot be parameterised — pass a fixed value or split the argument. - Undeclared parameters are refused, not ignored.
- Path parameters are jailed in their
roots, compared after resolving symlinks — so neither../../.ssh/id_rsanor a symlink planted inside an allowed directory escapes. An emptyrootsmeans nothing is allowed, never everything.
A recipe whose tool is missing does not fail the card as an error: the run ends as missing_tool
and the card shows the install line, because the machine that knows is not the machine showing the
board.
A recipe this machine does not declare at all is caught earlier still: agent watch tells the
board the recipe names it has, so the card is refused at the click and names what is on offer
instead. That check only applies to a runner that is actually watching — nothing is precomputed, and
nothing here changes the rule that only this file grants what runs.
Record Command
Section titled “Record Command”viite record # open the recorder on DASHBOARD_URLviite record --local # http://localhost:5000viite record --url https://… # explicit dashboard URLviite record --no-autostart # open the page without pressing StartOpens Business Studio’s screen recorder (/content/record-screen?autostart=1) in your default
browser and presses Start for you. Record, stop, and the video uploads straight to a connected
YouTube channel.
This command does not record anything itself, and that is the design. The capture runs in the
browser (getDisplayMedia + MediaRecorder), so the bytes live in the tab and go directly to
Google — nothing is written to your disk, and there is no temp file to clean up. A local ffmpeg
recipe would have needed one. What the command does is spare you finding the tab.
Two things follow from that and cannot be worked around:
- The browser still asks which screen, window or tab to share.
getDisplayMediarequires a user gesture and an explicit choice of surface; no URL can consent on your behalf. autostartonly clicks the button. If you are not signed in, you land on the sign-in page instead and nothing starts.
The recording is held in memory, so it stops itself after 30 minutes or 500 MB, whichever comes first, and says which. On macOS the machine’s own audio cannot be captured — tick Chrome’s own box in the share dialog for a tab’s or window’s audio, and use the microphone option for narration.
Needs DASHBOARD_URL (or --url / --local): the dashboard is a different origin and port from the
API, so PLATFORM_API_URL is not it. Without one, the command says so rather than guessing an origin
and opening a page that isn’t there.
