Canvas CLI
A command-line interface for managing Canvas workspaces, contexts, dotfiles and documents with integrated AI assistance.
Install
One-liner (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/canvas-ui/canvas-cli/main/scripts/install.sh | bash
This resolves the newest GitHub Release, downloads the single-file
binary for your platform, verifies it against the release's SHA256SUMS,
installs it to ~/.local/bin/canvas together with the shortcut wrappers
(ws, ctx, context, dot, agent, ag, hi) and offers to wire the shell
prompt integration into your rc file.
No runtime is required — the binaries are bun-compiled and self-contained.
To initialize a local agent, select the AgentD repository or a downloaded archive:
canvas init agent . --runtime-package [email protected]:canvas-ui/canvas-agentd.git
canvas init agent . --runtime-package ~/Downloads/canvas-agentd-main.zip
canvas init agent . --runtime-package ./canvas-agentd-0.3.2.tgz
Local runtimes support Linux x64/arm64. Git sources use your Git credentials;
Git repositories are cloned without submodules and packed for installation. ZIP
archives require unzip. Both sources are cached under ~/.canvas/runtime/agent.
Subsequent starts reuse the installed package. Without an explicit source, managed
agent installation uses https://github.com/canvas-ui/canvas-agentd.git.
Flags can be passed through the pipe:
# Non-interactive, prompt integration wired into ~/.bashrc (or ~/.zshrc)
curl -fsSL .../install.sh | bash -s -- --prompt
# Pin a version, install elsewhere, skip the extras
curl -fsSL .../install.sh | bash -s -- --version 2.1.13 --dir ~/bin --no-prompt --no-shortcuts
| Flag | Effect |
|---|---|
--version <ver> | Install a specific release (2.1.11 or v2.1.11) |
--dir <path> | Install directory (default ~/.local/bin, or $CANVAS_INSTALL_DIR) |
--prompt / --no-prompt | Wire / skip the prompt integration without being asked |
--no-shortcuts | Install only canvas |
--local [dir] | Dev install: wrappers around a source checkout (see below) |
Run scripts/install.sh again at any time to upgrade in place.
One-liner (Windows)
irm https://raw.githubusercontent.com/canvas-ui/canvas-cli/main/scripts/install.ps1 | iex
Installs canvas.exe into %LOCALAPPDATA%\Programs\canvas, writes .cmd
shims for the shortcuts and adds the directory to your user PATH.
Manual binary download
Every release attaches single-file binaries, downloaded as-is, not archived:
| Platform | Architecture | Asset |
|---|---|---|
| Linux | x64 | canvas-linux |
| Linux | ARM64 | canvas-linux-arm |
| macOS | x64 | canvas-macos |
| macOS | ARM64 (Apple Silicon) | canvas-macos-arm |
| Windows | x64 | canvas-windows.exe |
These stable URLs always resolve to the latest published release. Download SHA256SUMS to verify the binary.
sha256sum -c SHA256SUMS --ignore-missing # every release ships SHA256SUMS
chmod +x canvas-linux
mv canvas-linux ~/.local/bin/canvas
macOS and Windows binaries are not code-signed yet, so Gatekeeper and SmartScreen will warn on first run.
npm
npx @augmentd-labs/canvas-cli # one-off
npm install -g @augmentd-labs/canvas-cli # installs the `canvas` command
The unscoped canvas-cli name on npmjs is an unrelated package — use the
scoped name. The npm package installs only the canvas command; the shortcuts
(ctx, context, dot, ws, agent, ag, hi) ship with the standalone
binaries and the source install. Node.js v20 LTS or newer.
From source (development)
git clone https://github.com/canvas-ui/canvas-cli ~/Code/canvas/canvas-cli
cd ~/Code/canvas/canvas-cli
pnpm install # corepack enable, if pnpm is missing
# Wrappers in ~/.local/bin pointing at this checkout (bun if present, else node)
scripts/install.sh --local
# …or run it directly
node bin/canvas.js --help
--local writes small exec-wrappers, so edits to the checkout take effect
immediately without reinstalling.
Shell prompt integration
scripts/update-prompt.sh prefixes your prompt with the Canvas connection
state and the bound context URL:
[● (work) admin@dev://universe://work/reports] user@host:~$
The dot is green when the bound remote last answered, red when it is
unreachable or nothing is bound, and yellow when a remote is bound but
nothing has verified it yet. That state is written by canvas remote bind,
canvas remote ping and canvas context bind; a server that answers with an
error still counts as reachable.
The context URL is read from ~/.canvas/config/cli-session.json and refreshed
from the server at most every 30 seconds (CANVAS_CONTEXT_UPDATE_TIMEOUT),
with a 0.5s timeout so a dead remote never stalls your prompt.
The installer sets this up for you (--prompt). Manually:
mkdir -p ~/.canvas/scripts
curl -fsSL https://raw.githubusercontent.com/canvas-ui/canvas-cli/main/scripts/update-prompt.sh \
-o ~/.canvas/scripts/update-prompt.sh
# ~/.bashrc or ~/.zshrc
[ -f "$HOME/.canvas/scripts/update-prompt.sh" ] && . "$HOME/.canvas/scripts/update-prompt.sh"
It hooks PROMPT_COMMAND on bash and precmd_functions on zsh, keeping your
existing PS1 intact behind the status block. Colour codes are marked
non-printing, so line editing and wrapping stay correct.
Requires jq and curl; without jq the script leaves your prompt untouched.
There is no PowerShell equivalent yet.
Usage
canvas --help
The grammar
One shape, at every level:
canvas <module> [<resource>] [<noun>] <verb> [<term>…] [--flags]
ctx note add "buy milk" --title Groceries # noun, then verb
ctx note list
ctx notes # plural noun == "<noun> list"
ctx note rm <docId> # unlink from this context
ws universe file list # same grammar, workspace scope
ws universe note add ./notes.md --title X
ag lucy sessions # and for agents
Four rules cover the rest:
-
A plural noun lists it.
ctx notesisctx note list, at any depth. -
rmunlinks,deletedestroys.ctx note rm <id>drops the document from this context and leaves it in the workspace;ctx note delete <id>destroys it and demands--force. -
An address beats a noun, a noun beats a resource.
ctx note listis the note noun even if you own a context callednote— address that one asctx user@remote:note. -
-reads the argument from stdin, for every noun the CLI can create:journalctl --user --since today | grep -i err | ctx note add - --title "errors" echo https://example.com | ctx tab add -Only
-pulls the pipe, so nothing hangs waiting on a stdin you never meant to send.file addis the exception: its argument is a path on disk.
--help works at every level and prints the usage line for what you asked
about: canvas ws --help, canvas ctx note --help, canvas ctx note add --help.
Modules
| Module | Aliases | What it does |
|---|---|---|
workspace | ws | Manage workspaces |
context | ctx | Manage contexts |
agent | ag, hi | Manage agents and prompt them |
remote | remotes | Manage remote Canvas servers |
dot | Workspace-backed dotfile manager (per-device link map) | |
role | ag-role | Container/role orchestration (placeholder) |
auth | Authentication & API tokens | |
alias | Resource aliases | |
config | cfg, settings | CLI configuration |
server | Manage a local Canvas server (PM2) |
The ws, ctx, context, dot, agent, ag and hi binaries are standalone
shortcuts for their module. ag and hi are the same thing — two spellings of
canvas agent, kept until usage settles on one.
# Workspaces and contexts
canvas ws list # or: canvas workspaces
canvas ws universe # a named resource shows itself
canvas ws universe trees # the trees in it; `tree` shows one
canvas ws universe tree rm /old/path -r --purge
canvas ctx bind <context>
canvas ctx # the bound context
canvas ctx tree paths # one path per line
canvas contexts
# A context is a path in one tree of one workspace — `create` takes all three
canvas ctx create MBAG universe://project-foo/tasks/baz
canvas ctx create MBAG --workspace universe --url /project-foo/tasks/baz \
--tree directory --base-url /project-foo
# Remotes, tokens and this device
canvas remote admin@dev # a known remote id is a resource too
canvas remote admin@dev ping
canvas remote admin@dev device # how this machine is registered there
canvas auth tokens # token list | create | rm | set
canvas dot device # this machine
canvas dot devices # every entry → device → local path
# Config (list | show | get | set | delete | edit | validate)
canvas config get server.url
canvas config set server.url http://127.0.0.1:8001
Nouns
A noun is a document schema, so the vocabulary is the same one the web and the
server use. Both ctx and ws carry the full set:
| Noun | Plural | Schema | Verbs |
|---|---|---|---|
note | notes | data/schema/note | list, add, get, rm, delete |
tab | tabs | data/schema/tab | list, add, get, rm, delete |
todo (task) | todos | data/schema/task | list, add, get, rm, delete |
link | links | data/schema/link | list, add, get, rm, delete |
file | files | data/schema/file | list, add, upload, index, get, rm, delete |
email | emails | data/schema/message/email | list, get, rm, delete |
identity | identities | data/schema/identity | list, get, rm, delete |
dotfile | dotfiles | data/schema/dotfile | list, get (writes belong to canvas dot) |
doc | docs | any | list, get, rm, delete |
tree | trees | — | show, paths, rm (workspace) |
ctx todo add "ship the refactor" --due 2026-09-01T09:00:00Z --task-priority 2
ctx tab add https://example.com --title "Example"
ctx link add git+ssh://[email protected]/canvas-ui/canvas-cli --title canvas-cli
ctx file add ./report.pdf # uploads the bytes (embeddable)
ctx file index ~/Photos # records a device pointer, bytes stay put
ws work note add "meeting recap" --path notes:/standup
ctx doc list --feature data/schema/event # any schema, by feature
Lists are rendered per schema — a note shows its preview and tags, a file its
size, type and whether the bytes are stored or on a device:<id>, a todo its
status, due date and priority. -f json always carries the whole document,
-f csv the same columns unclipped.
A context can carry a saved view — the feature filters the web toolbox
persists onto it. Listing through a context folds that view in, exactly as the
web does, so a context filtered to notes lists no tabs even when it holds
hundreds. ctx show prints it as filter, and --all lists past it:
ctx show # filter any: text, text/plain, note
ctx tabs # (empty) — this context filters to …
ctx tabs --all # every tab in the context
ctx update --clear-filter # drop the saved view for good
--clear-filter clears the binding and the toolbox chips the web renders from
it, so the filter does not come back on the next toolbox save. Sort order and
the other view settings are left alone.
AI assistance
# Prompt an agent (the default verb, so it can be elided)
hi lucy "whats the weather today"
canvas agent lucy "any new PRs to review?"
# Pipe stdin into the prompt
tail -n500 /var/log/syslog | hi linus "any idea what those ACPI errors are?"
# Conversations
ag lucy sessions # * marks the one it is talking in
ag lucy session new "refactor spike"
ag lucy session use <sessionId>
hi lucy --session <sessionId> "back to this one"
# What can it actually do?
ag lucy tools # tools the runtime exposes to it
ag lucy skills
ag lucy skill add ./my-skill # a path or URL, or --name for inline
# Workspace-bound queries
canvas ws work agent prompt lucy "any new emails for this customer?"
hi lucy --workspace work --context foo "draft a reply please"
Remotes
Remotes are identified as user@remote-name. The first remote added becomes
the default; bind switches between them.
canvas remote add idnc_sk@canvas https://canvas.idnc.sk # prompts for login if no --token
canvas remote add admin@dev http://127.0.0.1:8001 --token <api-token>
canvas remotes # list remotes
canvas remote bind admin@dev
Packages (lazy modules)
The core CLI is auth, remotes (including workspace mirrors — canvas remote mirror …, built in since 2.11.0), workspaces, contexts, agents and config.
A local server or the desktop app lives in a separate package that is fetched
on first use into ~/.canvas/packages/<name> (no global npm, no sudo) and
loaded from there:
| package | commands | brings |
|---|---|---|
server | canvas server … | a local canvas-server checkout under pm2 |
desktop | canvas desktop … | the desktop app release for this platform |
Using one of those commands before its package exists shows what it is and
offers the download; --yes / non-interactive runs get the install command
instead. canvas package list | install <name> | update | remove manages
them. Each package is one self-contained file published to npm as
@augmentd-labs/canvas-cli-<name> (CANVAS_PACKAGE_SPEC_<NAME> overrides the
latest dist-tag with a version, tarball or git spec); from a source checkout
the workspace copies in packages/cli-* are used directly.
Updating (canvas update)
One command keeps everything Canvas put on this device current:
canvas update # check, show the plan, apply (asks first on a terminal)
canvas update check # report only; `canvas update --check` does the same
canvas update cli --to 2.7.1 # pin the CLI to one release
canvas update edge # just the canvas-edge daemon (or: packages | services | server)
canvas update --yes --skip-restart
| component | how it is checked | how it is updated |
|---|---|---|
cli | newest GitHub Release (binary) or the npm registry | a release binary downloads the asset for this platform next to itself, verifies SHA256SUMS, runs it once and swaps it in (the same steps as install.sh); an npm install runs npm install -g; a source checkout is left to git pull |
package:<name> | latest on npm vs the installed version (a retired package — mirror — is removed) | npm update in ~/.canvas/packages/<name> |
service:edge | latest of @augmentd-labs/canvas-edge on npm vs ~/.canvas/edge (built-in mirror module) | npm update there, then the daemon is re-created under pm2 (or respawned) when daemon mirrors exist |
service:server | remote branch HEAD vs ~/.canvas/server (server package) | git pull --ff-only + npm install, then pm2 restart when it runs |
Packages go first (they carry the service updaters), then services, the CLI
last — the new canvas is used by your next command. Without a terminal
nothing is applied unless --yes is given. Set GITHUB_TOKEN if the
unauthenticated GitHub API rate limit gets in the way. A package can expose
what it runs to canvas update by exporting services from its module
({ id, label, installed(), latest(), update({ io, onStep }), restart?({ io }) }).
Mirrors (roaming profile)
canvas remote mirror (alias canvas mirror) keeps workspaces from a hub in sync as real folders on this
machine (~/Workspaces/<workspace> by default) through canvas-fuse --mirror:
pinned folders stay available offline, everything else is fetched on demand,
edits made offline are pushed when the hub is reachable, and a file that
changed on both sides is never overwritten — the hub's version keeps the name
and yours is saved next to it as name (conflict from <device> <date>).ext
(--conflicts rename, the default). Use --conflicts prompt for the hub's
conflict inbox instead. Existing mirrors with conflicts: "prompt" keep that
setting; change it to "rename" in mirrors.json and restart the mirror to
use conflict copies.
canvas remote mirror init is the first-run wizard (arrow-key prompts on a
terminal, plain readline when piped): log in to a server (or pick a
configured remote), choose the mirror root, then either mirror workspaces
from the hub as local folders (fuse or daemon first, then a multi-select of
workspaces, then — only if you want it — the folders to keep offline / limit
the sync to) or publish local folders as new hub workspaces that stay in
sync in place — or both. pm2 is checked before anything is written: when it
is missing the wizard offers to install it, or continues unsupervised
(--no-service skips the question). The same goes for canvas-edge, the
folder-sync daemon (@augmentd-labs/canvas-edge, from canvas-common): when it cannot be found
the wizard offers to fetch it (canvas remote mirror edge install: the npm package
into ~/.canvas/edge, no global npm, no sudo; CANVAS_EDGE_PACKAGE overrides
the spec; canvas remote mirror edge update pulls the latest and restarts) — or a local checkout, whose path is remembered in
mirrors.json (edgeBin; CANVAS_EDGE_BIN overrides it). Run the wizard
again later to add more or to restart what is configured. Folder names keep the case of the workspace
(~/Workspaces/Universe); the lowercase name is the hub's identity.
canvas remote mirror init # wizard: hub login, root, mirror and/or publish, start
canvas remote mirror init --hub-url https://canvas.example.org --email [email protected] --password … \
--root ~/Workspaces --workspace Universe,devel:UI/,Docs/ --publish ~/Code/UI:ui --conflicts rename --service --yes
canvas remote mirror add work --pin Contracts/ --conflicts rename
canvas remote mirror publish ~/Code/UI --name ui # this folder becomes workspace 'ui' on the hub, synced in place (daemon)
canvas remote mirror publish ~/Notes --attach # sync into an existing hub workspace of the same name
canvas remote mirror status # daemon state, cursor/head, pending, conflicts, lag
canvas remote mirror sync # reconcile now
canvas remote mirror pin add devel Photos/2026/
canvas remote mirror conflicts # what waits in the hub inbox
canvas remote mirror conflicts devel --resolve 100042 --keep both
canvas remote mirror restart all # stop + start (after editing mirrors.json, or a dead mount)
canvas remote mirror service install # pm2 processes, restart on crash, start at login
canvas remote mirror stop all
State: config/mirrors.json (this device's mirror list), the mount's own
cache/ledger under ~/.canvas/<remote>/fuse/workspaces/<workspace>/, and the
device's status report on the hub (Workspace › Settings › Sync). Requires the
canvas-fuse binary (CANVAS_FUSE_BIN overrides the lookup) and, for
service, pm2.
Output formats
Lists accept -f, --format with table (default), json or csv; -r, --raw
dumps the raw API JSON. Tables show the columns that matter for what you asked
about and clip long values; json is always the complete record, and csv
carries the table's columns unclipped with ISO timestamps.
canvas ws list -f json
canvas ctx notes -f csv
canvas ctx note get <docId> # one record, down the page
Configuration
The CLI keeps its state in ~/.canvas/ (%USERPROFILE%\Canvas\ on Windows,
overridable with CANVAS_USER_HOME):
| File | Contents |
|---|---|
config/cli.json | General settings (canvas config list | get | set | edit) |
config/remotes.json | Registered remotes with their URLs and auth tokens — managed via canvas remote add | bind | rename, not by hand |
config/cli-session.json | The current session: bound remote and its status, bound context and URL |
config/cli-aliases.json | User-defined command aliases (canvas alias) |
config/mirrors.json | Workspaces mirrored on this device (canvas remote mirror init | add | remove) |
scripts/update-prompt.sh | Prompt integration, if installed |
AI prompts (hi, canvas agent) run against agents hosted on the bound Canvas
server — model/provider configuration lives server-side, not in the CLI.
Releasing
See RELEASE.md.
Licence
Copyright (C) 2024-2026 Jozef Melich.
Canvas CLI is licensed under the AGPL-3.0-or-later and under no other terms. No commercial exemption is offered for this component, to anyone. The Canvas clients stay free software in all cases.
Contributing needs no CLA here, only a DCO sign-off (git commit -s). See
CONTRIBUTING.md. The dual-licensed Canvas components are
listed in NOTICE.
This project is funded by Augmentd Labs
Remotes behind nginx client certificate authentication
Configure the identity before Canvas login:
canvas remote add user@private https://canvas.example.org --tls-cert /absolute/path/client-chain.crt --tls-key /absolute/path/client.key
canvas remote tls set user@private --tls-cert /absolute/path/client-chain.crt --tls-key /absolute/path/client.key
canvas remote tls show user@private
canvas remote tls clear user@private
The certificate file contains the leaf first, then issuing intermediates. Use a
protected unencrypted RSA or EC PEM private key (chmod 600 on Unix; a user-only
ACL on Windows). Certificate/key paths are saved under tls.certFile and
tls.keyFile in the shared remotes store. Canvas password/API-token login remains
required. Server certificate verification stays enabled.
Mirror onboarding accepts the same --tls-cert/--tls-key options when using
--hub-url; an existing selected hub uses its saved TLS settings. Daemon mirrors
require canvas-edge 0.5.0 or newer; FUSE mirrors require canvas-fuse 0.10.0 or newer.
Generated Docker configurations bind the certificate/key read-only and set
CANVAS_TLS_CERT/CANVAS_TLS_KEY to container paths. The source paths must exist
on the Docker host; move certificate files securely when deploying elsewhere.
After renewal, replace the files at the same paths and restart daemon mirrors, mounts, and containers. Short CLI commands load files on each invocation. TLS configuration survives login/logout and remote renames. Clearing configuration does not delete certificate/key files or Windows native imports.
For coordinated development before canvas-api-client 0.4.0 is published, pack it from the sibling canvas-common checkout and install that tarball locally. Publish the shared package before releasing this CLI; do not substitute production certificate files into tests.