CLI for Margins — review layer for Markdown in Git.
Margins is a review platform where humans and AI agents are equal participants. It renders Markdown files from a Git repository in a clean UI where reviewers can open discussions, propose changes, and approve content. margins-cli exposes every Margins action as a shell command, making it usable in scripts, CI pipelines, and by AI agents.
Install globally (recommended):
npm install -g margins-cliRun without installing:
npx margins-cli <command>Or clone and build locally:
git clone https://github.com/alvistar/margins-cli.git
cd margins-cli
npm install && npm run build
npm link # makes 'margins' available globally# Log in via browser (one-time)
margins auth login
# See your workspaces
margins workspace list
# Push local markdown to a brand-new workspace (no GitHub repo needed)
margins workspace push --project my-docs --dir ./docs
# List open discussions in the current repo
margins discuss list
# Create a discussion on a file
margins discuss create --path docs/intro.md --body "This section needs a concrete example."Two methods are supported. The active credential is resolved in priority order:
| Priority | Source | Set by |
|---|---|---|
| 1 | --api-key <key> flag |
any command |
| 2 | MARGINS_API_KEY env var |
shell / CI environment |
| 3 | Stored static API key | margins config set-key |
| 4 | Stored Keycloak access token | margins auth login |
margins auth loginOpens your browser to complete OAuth 2 PKCE against Keycloak. On success, the access token and refresh token are stored locally. The token is refreshed automatically before it expires — no re-login needed.
Mint a key from the Margins web UI or the API (POST /api/keys), then store it:
margins config set-key mrgn_...
# or per-invocation:
margins --api-key mrgn_... workspace list
# or via environment:
MARGINS_API_KEY=mrgn_... margins workspace listStatic keys support two scopes: comment (read + create discussions) and edit (full write access).
Available on every command.
| Flag | Description |
|---|---|
-v, --version |
Print version and exit |
--json |
Output as JSON — for scripting and agents |
--verbose |
Enable debug logging |
--no-color |
Disable ANSI colors |
--server-url <url> |
Override the server URL (default: https://margins.app) |
--api-key <key> |
Override the API key for this invocation |
Manage local CLI configuration.
Display the active configuration.
margins config show
margins config show --jsonShows the active server URL, the masked API key or token, and whether auth came from auth login or config set-key.
Store a static Margins API key.
margins config set-key mrgn_abc123...Saves the key to the global config file. Clears any previously stored Keycloak session.
Override the server URL (useful for self-hosted Margins instances).
margins config set-url https://margins.example.comAuthentication commands.
Log in via browser using Keycloak OAuth 2 + PKCE.
margins auth loginOpens a browser window to complete the OAuth flow. On completion, stores the Keycloak access token and refresh token locally. Subsequent commands use the token automatically, refreshing it transparently when it expires.
Note: The Keycloak client must have
http://localhost:*registered as a valid redirect URI. See TODOS.md for the Keycloak admin configuration step.
Show the currently authenticated identity.
margins auth whoami
margins auth whoami --jsonCalls GET /api/auth/whoami and displays your user ID, email, and role.
Revoke the stored session and clear local credentials.
margins auth logoutRevokes the Keycloak refresh token and clears the stored access/refresh tokens from the config file. The server URL is preserved.
Manage Margins workspaces. A workspace is the unit of review in Margins. There are two kinds:
- GitHub workspaces — connect a GitHub repository. Margins clones the repo and syncs markdown files on demand. Created with
workspace create <repo-url>. - Local workspaces — no repository. You push markdown files directly via
workspace push --project <name>. Useful for solo work, drafts, or content that does not live in a Git repo yet.
You can also push local markdown into a GitHub workspace via workspace push --workspace <id> — the content lands on a virtual @local branch alongside the real git branches, so you can review uncommitted edits before pushing them upstream.
List all workspaces you have access to.
margins workspace list
margins workspace list --jsonDisplays workspace slug, name, sync status, and last synced time.
Create a new workspace from a GitHub repository URL.
margins workspace create https://github.com/org/repoIf a workspace for that repository already exists and you are not a member, you will be auto-joined to it.
Open a workspace in the browser.
margins workspace open # uses slug from .margins.json
margins workspace open my-repoIf no slug is provided, reads workspace_slug from .margins.json in the current directory (or any parent).
Trigger a git sync to pull the latest content from the repository.
margins workspace sync # uses .margins.json
margins workspace sync my-repo
margins workspace sync my-repo --branch main| Flag | Description |
|---|---|
--branch <branch> |
Branch to sync (defaults to the workspace's default branch) |
Local workspaces cannot be synced this way — they receive content via
workspace push. Callingsyncon a local workspace returnsLOCAL_SYNC_NOT_SUPPORTED(HTTP 422).
Push local markdown files to a workspace for review. This is the only way to
get content into a local workspace, and the way to overlay uncommitted
edits onto a GitHub workspace via the virtual @local branch.
# Create a brand-new local workspace and push files in one step
margins workspace push --project my-docs --dir ./docs
# Push more files to the same workspace later (re-use the workspace ID)
margins workspace push --workspace 0cfbdc14-c023-4c84-bc4a-e027e13cefab --dir ./docs
# Overlay local edits onto an existing GitHub workspace (lands on @local branch)
margins workspace push --workspace <github-workspace-id> --dir ./docs| Flag | Required | Description |
|---|---|---|
--project <name> |
one of | Create a new local workspace with this name. Slug becomes local/<your-username>/<name>. The name must be alphanumeric (hyphens, dots, underscores allowed). |
--workspace <id> |
one of | Push to an existing workspace by UUID. Use this for re-pushes and for pushing into GitHub workspaces. |
--dir <path> |
no | Directory to recursively scan for .md files. Defaults to the current directory. Hidden files, node_modules/, and symlinks are skipped. |
Behavior:
- Recursively scans
--dirfor.mdfiles (max 50 per push, max 1 MB per file, max 10 MB total) - For each file, computes a SHA-256 hash of the content. If the hash matches an existing artifact, the file is skipped. Otherwise it is added or changed.
- Output (with
--json):{ "added": 2, "changed": 0, "skipped": 0 } - For local workspaces, content lands on the
mainbranch. - For GitHub workspaces, content lands on the virtual
@localbranch — visible in the branch switcher alongside real git branches, but never pushed upstream.
Example: review your local edits before committing them
cd ~/my-project # has docs/spec.md, README.md
margins workspace push --workspace <gh-ws-id> # uploads to @local
margins workspace open # opens browser, switch to @local branch
# ...review, comment, refine...
git commit -am "Refine spec" # then commit for real
margins workspace sync # pull the committed version into mainManage discussions on Markdown artifacts.
List discussions in a workspace.
margins discuss list # uses .margins.json, shows open discussions
margins discuss list my-repo
margins discuss list my-repo --status resolved
margins discuss list my-repo --path docs/intro.md
margins discuss list --json| Flag | Description | Default |
|---|---|---|
--path <path> |
Filter by artifact path | — |
--status <status> |
Filter by status: open or resolved |
open |
Create a new discussion on an artifact.
margins discuss create \
--path docs/intro.md \
--body "This section needs a concrete example."
margins discuss create my-repo \
--path docs/api.md \
--body "Consider adding a rate limit note here." \
--anchor-heading "Authentication"
margins discuss create my-repo \
--path docs/api.md \
--body "Typo: 'recieve' should be 'receive'." \
--anchor-text "recieve the response"| Flag | Required | Description |
|---|---|---|
--path <path> |
yes | Artifact path within the workspace |
--body <body> |
yes | Discussion body text |
--anchor-heading <heading> |
no | Anchor the discussion to a heading |
--anchor-text <text> |
no | Anchor the discussion to a text selection |
Post a reply to an existing discussion.
margins discuss reply d_abc123 --body "Fixed in the latest commit."
margins discuss reply d_abc123 --body "Agreed." --workspace my-repo| Flag | Required | Description |
|---|---|---|
--body <body> |
yes | Reply body text |
--workspace <slug> |
no | Workspace slug (alternative to .margins.json) |
Mark a discussion as resolved.
margins discuss resolve d_abc123
margins discuss resolve d_abc123 --summary "Updated the docs to include this example."
margins discuss resolve d_abc123 --workspace my-repo| Flag | Required | Description |
|---|---|---|
--summary <summary> |
no | Short description of how the issue was resolved |
--workspace <slug> |
no | Workspace slug (alternative to .margins.json) |
Generate shell completion scripts.
margins completions -s zsh # zsh
margins completions -s bash # bash
margins completions -s fish # fish| Flag | Required | Description |
|---|---|---|
-s, --shell <shell> |
yes | Target shell: bash, zsh, or fish |
Zsh — add to ~/.zshrc:
eval "$(margins completions -s zsh)"Bash — add to ~/.bashrc or ~/.bash_profile:
eval "$(margins completions -s bash)"Fish — add to ~/.config/fish/config.fish:
margins completions -s fish | sourceAfter reloading your shell, press Tab after margins workspace sync to get live workspace slug completion from the API.
All commands support --json for structured output:
margins workspace list --json
# → [{ "slug": "my-repo", "name": "My Repo", "syncStatus": "synced", ... }]
margins discuss list my-repo --json
# → [{ "id": "d_...", "path": "docs/intro.md", "body": "...", "status": "open", ... }]For non-interactive use (CI, agents), use environment variables instead of stored credentials:
MARGINS_API_KEY=mrgn_... MARGINS_SERVER_URL=https://margins.example.com margins workspace list --jsonExit codes: 0 on success, 1 on any error (auth failure, network error, not found, etc.). Error details are written to stderr.
When a slug argument is omitted, the CLI walks up from the current directory looking for a .margins.json file. This allows running commands from anywhere inside a repository without repeating the workspace slug.
Example .margins.json:
{
"workspace_slug": "local/avigano/my-docs",
"workspace_id": "0cfbdc14-c023-4c84-bc4a-e027e13cefab",
"default_branch": "main",
"mode": "local",
"server_url": "https://margins.example.com"
}| Field | Description |
|---|---|
workspace_slug |
Default workspace slug for discuss and workspace commands |
workspace_id |
Default workspace UUID. Used by workspace push --workspace for re-pushes — more reliable than slug because it doesn't depend on slug resolution. |
default_branch |
Default branch for workspace sync. For local workspaces this is main; for GitHub-overlay mode (local edits pushed to a GitHub workspace's @local branch) this is @local. |
mode |
"local" for local workspaces, "github-overlay" for local edits overlaid on a GitHub workspace via the @local virtual branch. Used by skills to decide push behavior on subsequent runs. Optional; absence implies "local". |
server_url |
Server URL override (lower priority than --server-url and MARGINS_SERVER_URL) |
Project-scoped credentials:
.margins.jsonis intended to be committed to the repository so teammates share the same workspace identity. It does NOT contain credentials. For project-scoped API keys + server URL, use a.margins/directory at the project root containingconfig.jsonand setMARGINS_CONFIG_DIRto point at it. Add.margins/to.gitignoresince it contains credentials.
The CLI resolves the config directory in this order:
| Priority | Condition | Path used |
|---|---|---|
| 1 | MARGINS_CONFIG_DIR env var is set |
$MARGINS_CONFIG_DIR/config.json |
| 2 | ~/.config/margins/config.json already exists |
~/.config/margins/config.json |
| 3 | Platform default (fallback) | macOS: ~/Library/Preferences/margins/config.json · Linux/XDG: ~/.config/margins/config.json · Windows: %APPDATA%/margins/Config/config.json |
Preferred location on all platforms: ~/.config/margins/config.json
If that file exists (e.g. you created it manually, or you're on Linux), it takes precedence over the macOS ~/Library/Preferences/ default. To migrate on macOS:
mkdir -p ~/.config/margins
cp ~/Library/Preferences/margins/config.json ~/.config/margins/config.jsonThe MARGINS_CONFIG_DIR override is intended for tests and CI — it fully isolates the config from your user profile.
| Field | Set by | Description |
|---|---|---|
apiKey |
config set-key |
Static Margins API key (mrgn_...) |
serverUrl |
config set-url |
Server URL override |
accessToken |
auth login |
Keycloak JWT access token |
refreshToken |
auth login |
Keycloak refresh token (auto-refresh) |
accessTokenExpiresAt |
auth login |
Access token expiry (epoch ms) |
keycloakIssuer |
auth login |
Keycloak realm URL |
keycloakClientId |
auth login |
Keycloak client ID |
Running
margins auth loginclears any previously storedapiKey. Runningmargins config set-keyclears any stored Keycloak session.
git clone https://github.com/alvistar/margins-cli.git
cd margins-cli
npm install
# Build
npm run build # compiles to dist/index.mjs via tsdown
# Run from source (no build required)
npm run dev -- workspace list
# Tests
npm test # vitest run (114 tests)
npm run test:watch # watch modeThe CLI is built as ESM. The bin/margins.js shebang entry imports ../dist/index.mjs.
How
npx github:alvistar/margins-cliworks: npm clones the repo, runsnpm install, then runs thepreparescript (npm run build) automatically. This compilessrc/todist/before the binary is executed — no pre-built files need to be committed.