Skip to content

Latest commit

 

History

History
255 lines (191 loc) · 9.67 KB

File metadata and controls

255 lines (191 loc) · 9.67 KB
title CLI
description A tiny, Git-free pull/push CLI for editing Corpus documents from your terminal or CI — authenticated by an API key and scoped to one corpus.
sidebar
order
7

The Corpus CLI is a small terminal tool for listing, pulling, and pushing the documents in one corpus, using an API key. It is the way automation edits canonical documents without a browser: a CI job that regenerates a runbook, a script that syncs docs from another system, or just you, editing in your own $EDITOR instead of the web UI.

Unlike MCP — whose only write is a human-reviewed proposal — the CLI reads and writes canonical documents. That is why it deliberately uses the corpus-scoped REST API and an API key instead of the MCP/OAuth transport. It pushes new versions through the same optimistic-concurrency contract the web editor enforces, so nothing is ever silently overwritten.

The CLI is scoped to the one corpus its API key is bound to. It can only see and edit that corpus's documents — never the rest of the project.

Install and setup

Install the standalone package, then run the guided setup once:

npm install --global @nicia-ai/corpus-cli
corpus setup

The packaged CLI requires Node.js 22 or newer.

setup defaults to https://corpus.nicia.ai and asks only for the API key (masked), verifies the connection, then writes a private 0600 config file under $XDG_CONFIG_HOME/corpus/config.json (normally ~/.config/corpus/config.json). On Windows it uses %LOCALAPPDATA%\\Corpus\\config.json and relies on Windows ACLs rather than POSIX mode bits. Self-hosters pass --url; use CORPUS_CONFIG to choose another config path.

Confirm the whole local path is healthy at any time:

corpus doctor

Doctor reports the installed CLI version and checks configuration permissions, key format, working-directory writability, authentication, server reachability, and how many documents the credential can see.

For a non-interactive setup (for example, a development container), inject the key through the environment so it does not land in shell history:

CORPUS_API_KEY="cck_…" corpus setup --url "https://corpus.example.com"

The CLI also accepts environment variables. They override saved configuration, which is convenient in CI:

Variable Value
CORPUS_URL Your Corpus base URL, e.g. https://corpus.example.com.
CORPUS_API_KEY A cck_… key minted by a project owner (see API keys).
export CORPUS_URL="https://corpus.example.com"
export CORPUS_API_KEY="cck_…"

From a Corpus source checkout, pnpm corpus remains an equivalent development entry point:

pnpm corpus <setup|doctor|mcp|list|pull|push> …

It is a thin wrapper over the REST endpoints documented below — if you'd rather call those directly from CI or curl, you can.

Commands

mcp add

Write an OAuth MCP server entry for Cursor, Claude Code, or VS Code — the fast path after Connect this corpus in the UI. No API key required; the agent completes browser sign-in on first use.

corpus mcp add --client cursor --name corpus-default
corpus mcp add --client claude-code --name corpus-marketing
corpus mcp add --client vscode --url https://corpus.example.com --name corpus-ops
Flag Default Notes
--client cursor cursor writes ~/.cursor/mcp.json; vscode writes .vscode/mcp.json in the cwd; claude-code prints the claude mcp add command.
--url $CORPUS_URL/mcp or https://corpus.nicia.ai/mcp Hosted or self-hosted MCP endpoint.
--name corpus-default Local MCP server name — use corpus-<slug> to match the UI.

list

List the documents in the bound corpus, one per line as slug, version, and title:

$ corpus list
api-style-guide   v4   API Style Guide
deploy-runbook    v2   Deploy Runbook

Only the corpus's members appear — this is the same scope an agent sees over MCP.

pull <slug> [path]

Download a document's current markdown to a local file (default <slug>.md):

$ corpus pull api-style-guide
pulled api-style-guide (v4) → api-style-guide.md

Alongside the markdown, pull writes a small version sidecar — api-style-guide.md.corpus.json — recording the version you fetched. Keep it next to the file; push reads it to detect conflicts. Pulling a slug that isn't in your corpus fails with not found.

push <slug> [path]

Upload your local edits as a new version (default file <slug>.md):

$ corpus push api-style-guide
pushed api-style-guide → v5

push sends the version recorded in the sidecar as the version you started from. If the document has moved on since you pulled, the push is rejected rather than clobbering the newer version:

$ corpus push api-style-guide
conflict: the document is at v6. Pull, reapply your change, and push again.

Pull the current version, reapply your change, and push again. This is the same conflict contract as the web editor — see Versioning & history.

On success, push updates the sidecar to the new version, so you can keep editing and pushing without a fresh pull.

The version sidecar

Every pulled file gets a companion <file>.corpus.json:

{ "slug": "api-style-guide", "docVersion": 4 }

It exists so push can do optimistic concurrency — "I'm editing version 4; reject me if the server has moved past it." Treat it as a lockfile-style artifact: leave it beside the markdown, and don't hand-edit it. A file with no sidecar is treated as version 0 — a brand-new document (see below).

Creating documents

Pushing a slug that doesn't exist yet creates it — and adds it to the corpus your key is bound to, so it shows up in list and is readable immediately:

$ printf '# Onboarding\n\nWelcome…\n' > onboarding.md
$ corpus push onboarding ./onboarding.md
pushed onboarding → v1

Because there's no sidecar, the push starts from version 0 and the server creates version 1.

The CLI syncs the markdown body. A new document's title comes from its content — a title: in YAML frontmatter, otherwise the first # heading, otherwise the slug. (The same is true on every push: to control the title from the CLI, set it in frontmatter or a top-level heading; otherwise manage it in the web editor.)

Scope & permissions

An API key is a Connection credential, bound to a single corpus. The CLI inherits that scope exactly:

  • list shows only that corpus's documents.
  • pull of a document outside the corpus returns not found.
  • push to a slug that already exists outside the corpus is refused — the key can grow its own corpus, never reach into another.

Any project member's key can read and write through it; minting keys is owner-only. Revoke a key the moment it might be exposed — see API keys.

A typical loop

export CORPUS_URL="https://corpus.example.com"
export CORPUS_API_KEY="cck_…"

corpus pull deploy-runbook           # → deploy-runbook.md (+ sidecar)
$EDITOR deploy-runbook.md            # make your changes
corpus push deploy-runbook           # → new version, sidecar updated

REST API

The CLI is a thin client over three endpoints under /api/v1/docs. Each takes the API key as a bearer token and is scoped to the key's corpus:

Method & path Does
GET /api/v1/docs List the corpus's documents (slug, title, docVersion).
GET /api/v1/docs/:slug Fetch one document's markdown + metadata. 404 if not a member.
PUT /api/v1/docs/:slug Write a new version. Body: { markdown, clientVersion, title? }.
curl -H "Authorization: Bearer $CORPUS_API_KEY" \
  "$CORPUS_URL/api/v1/docs/api-style-guide"

A PUT whose clientVersion is behind the server returns 409 with the currentVersion; a PUT to a slug that exists outside the bound corpus returns 403. These are exactly the conflict and scope rules the CLI surfaces above.

Portability

The list/pull/push logic lives in a runtime-agnostic core (cli/core.ts) with zero node: imports — it speaks only the web fetch standard and a tiny injected filesystem port:

type Files = {
  readText: (path: string) => Promise<string | undefined>; // undefined = absent
  writeText: (path: string, data: string) => Promise<void>;
};

The published corpus binary is the Node shell that wires node:fs + node:process to that core. To run the CLI elsewhere — Deno, Bun, a Cloudflare Worker, or a WASM host — supply your own fetch and Files adapter and call the same exported list/pull/push. (The test suite does exactly this: the worker's fetch plus an in-memory file map.)