A Claude Code / Cursor skill describing how to use the ContextDock CLI (@contextdock/cli) without making the common mistakes that even careful source-reading produces.
/cli-skill.md with frontmatter intact. Drop it into your skills directory verbatim.
SKILL.md — include this URL in agent prompts so they can fetch it directly
@contextdock/cli is a thin, scriptable wrapper around the ContextDock agent REST API at https://contextdock.web.app/api/agent. The headline command is contextdock context --budget <n> — it packs markdown from the user's library under a token budget, ready to paste or pipe into a coding agent.
Even reading the source carefully, agents make these mistakes:
| Wrong instinct | Reality |
|---|---|
Recommend npm install -g @contextdock/cli |
Returns 404 by design — the package is intentionally not published to npm. It's distributed via this page (https://contextdock.web.app/cli-skill) and installed locally. Use node "<repo>/cli/dist/index.js" or npm link from cli/. |
Tell the user to "keep contextdock mcp running in a terminal" |
mcp is a stdio subprocess — Claude Desktop spawns it, pipes JSON-RPC, kills it on exit. Manually running it in a terminal does nothing useful. |
Omit the env: { CONTEXTDOCK_API_KEY } block in Claude Desktop's MCP config |
The block is the recommended path for host-spawned MCP. Relying on ~/.contextdock/config.json is a fallback only. |
| Ask the user to paste their API key into chat | Use contextdock login (no flag) — it prompts on stdin and writes the file directly, so the key never enters chat history or process arguments. |
Treat KEY_INVALID and KEY_REVOKED as the same error |
KEY_REVOKED means the owner deliberately revoked a previously-valid key — re-login does not help. They must mint a new one. |
Assume /api/agent/* accepts Firebase ID tokens |
It only accepts cdk_live_... API keys. The two surfaces are deliberately isolated. |
Pass --lists l_xyz to context to assemble a list |
context / assemble_context takes --docs / --bundles / --tags / --query only — not list IDs. Pull tagIds via lists get <id> --json first, then pass to context --tags. |
Resolve a list's tagIds against docs list --tag <t> just to add up tokens |
lists get <id> --json (and bundles get <id> --json) return docCount, per-level tokens totals, and a docs[] table. Read the envelope; no extra fetches needed. |
@contextdock/cli is deliberately NOT published to npm. Distribution is via this very page (https://contextdock.web.app/cli-skill) plus a local install from the repo — there is no plan to ship it to the npm registry. npm install -g @contextdock/cli returns 404 by design and always will. Use one of these instead:
| Method | Command |
|---|---|
| Direct (works anywhere, no setup) | node "C:\Software Projects\Context Manager\cli\dist\index.js" <args> |
| Global symlink (one-time) | cd "C:\Software Projects\Context Manager\cli" && npm link → then contextdock <args> works globally |
The repo lives at C:\Software Projects\Context Manager\cli\. The committed esbuild bundle is cli/dist/index.js. If you change anything under cli/src/, rebuild before invoking:
cd "C:\Software Projects\Context Manager\cli" && npm run build
The dist bundle is shebanged (#!/usr/bin/env node) and runs on Node 18+.
API keys look like cdk_live_<32 base62 chars> — 41 chars, never expire, scoped (read always; write/delete optional). Per-key rate limit: 120 requests/min.
Mint a key:
read-only unless the task requires writes).Credential resolution order (highest priority first):
$CONTEXTDOCK_API_KEY env var — preferred for CI and Claude Desktop config blocks.~/.contextdock/config.json (Windows: %USERPROFILE%\.contextdock\config.json) — written by contextdock login. Shape: { "apiKey": "cdk_live_...", "baseUrl": "https://contextdock.web.app" }.Not logged in. Run `contextdock login` first.$CONTEXTDOCK_BASE_URL overrides the API host (staging / self-hosted). $CONTEXTDOCK_CONFIG_HOME redirects the config dir — test-only, do not recommend it to users.
Run contextdock --help or contextdock <command> --help for live flag details. Summary:
contextdock login [--key <key>] [--base-url <url>] # save key to config file (validates via /api/agent/me)
contextdock whoami [--json] # print identity, key id/name, scopes
contextdock logout # delete saved key
contextdock bundles list [--json]
contextdock bundles get <bundleId> [-o <file>] [--json]
contextdock bundles create <name> --docs <id1,id2,...> [--description <text>] [--shared]
contextdock bundles update <bundleId> [--name <n>] [--description <t>]
[--shared|--no-shared] [--pinned|--no-pinned] # PATCH; at least one field required
contextdock bundles delete <bundleId> --yes # hard delete; --yes is mandatory
contextdock bundles add-docs <bundleId> --docs <id1,id2,...> # idempotent append
contextdock bundles remove-docs <bundleId> --docs <id1,id2,...> # idempotent remove
contextdock lists list [--json]
contextdock lists get <listId> [--variant original|keyPoints|summary] [-o <file>] [--json]
contextdock lists create <name> [--description <s>] [--tags <t1,t2,...>] [--tag-match any|all]
[--docs <id1,id2,...>] [--shared]
contextdock lists update <id> [--name <s>] [--description <s>] [--tags <t1,t2>] [--tag-match any|all]
[--docs <id1,id2>] [--shared|--no-shared] # PATCH; --tags / --docs REPLACE
contextdock lists delete <id> --yes # hard delete; --yes mandatory
contextdock lists duplicate <id> # readers can clone; copy is private
contextdock lists add-docs <id> --docs <id1,id2,...> # idempotent (extras only)
contextdock lists remove-docs <id> --docs <id1,id2,...> # idempotent (extras only)
contextdock lists add-tags <id> --tags <t1,t2,...> # idempotent
contextdock lists remove-tags <id> --tags <t1,t2,...> # idempotent
contextdock docs list [--tag <tag>] [--search <q>] [--limit <n>] [--json] # limit default 50, max 200
contextdock docs get <docId> [-o <file>] [--json]
contextdock docs import <googleDocsUrl> [--tags <t1,t2>] [--personal] # validates host == docs.google.com / drive.google.com
contextdock docs create <name> --content <markdown>|--file <path>
[--tags <t1,t2>] [--personal] # manual markdown doc (no Google source)
contextdock docs update <docId> [--name <n>] [--content <markdown>|--file <path>]
[--tags <t1,t2>] [--personal|--no-personal] # PATCH; --tags REPLACES (not appends)
contextdock docs delete <docId> --yes # hard delete; --yes is mandatory
contextdock tags list [--json] # list every tag in the workspace
contextdock search <query...> [--json] # variadic: `contextdock search refund policy` works without quotes
contextdock context --budget <n> [--docs <ids>] [--bundles <ids>] [--tags <tags>]
[--query <text>] [--prefer original|keyPoints|summary]
[--metadata] [-o <file>] [--json]
contextdock mcp # stdio MCP server, no args
Mutating commands (v0.2.0+): --yes is mandatory on both bundles delete and docs delete; the CLI refuses without it (exit 1). On bundles update and docs update, at least one field flag is required — empty PATCH bodies are rejected client-side. docs update --tags replaces the tag list (does not append) — pass an empty value to clear. add-docs / remove-docs are idempotent: re-adding an existing id is a no-op, removing a missing id is a no-op.
| Command flavor | stdout | stderr |
|---|---|---|
bundles get / lists get / docs get / context (default) | Raw markdown | Stats line (e.g. Assembled 7 docs, 48201 / 50000 tokens) |
Any of those with --json | Full JSON envelope {data: ...} | (silent) |
Any of those with -o <file> | (silent) | Wrote N bytes to <file> + stats |
This split makes piping safe:
contextdock context --budget 50000 --tags refunds > ctx.md # only markdown lands in ctx.md
context requires at least one filter--docs, --bundles, --tags, or --query. Without any, the CLI exits 1 with At least one filter is required: --docs, --bundles, --tags, or --query. --budget is always required (range 1000–125000 tokens; outside that range the API returns BUDGET_TOO_SMALL / BUDGET_EXCEEDED).
A "list" is a separate primitive: a named collection of tagIds (dynamic — every doc carrying any/all of the listed tags is included on read) plus a small docIds extras array for ad-hoc additions. Bundles are static doc-id arrays. Use bundles when membership is hand-curated; use lists when membership should follow a tag rule and stay current.
lists update --tags a,b REPLACES the entire tagIds array. Same for --docs. Use lists add-tags / lists add-docs to append safely.lists remove-docs only touches the extras array. Tag-matched docs cannot be removed individually — drop the tag from tagIds instead.tagIds ≤ 20, extras docIds ≤ 100. Oversize bodies return 400.LIST_NOT_FOUND (exit 3) covers both missing lists and lists private to another user — never 403, by design.bundles get <id> --json and lists get <id> --json both return an envelope enriched with docCount, a per-level tokens totals map, and a docs[] table — so you don't have to re-resolve membership against docs list --tag <t> just to add up tokens. Bundle entries are {id, title, tokens}; list entries also carry aiSummary and summary (each null when the level hasn't been generated). The envelope also carries a truncated boolean — true when the resolved member set exceeds the detail cap (default 200 docs), in which case docs[] is capped but docCount and tokens still reflect the full set.
# Pick the cheapest level that fits a budget — no content fetches, no membership resolution
contextdock lists get l_xyz --json | jq '.data.tokens'
# → { "original": 92000, "keyPoints": 41000, "summary": 12000, "aiSummary": 600 }
# Same shape on bundles
contextdock bundles get b_xyz --json | jq '.data.tokens.keyPoints'
To assemble a list under budget, read the list's tagIds, then hand them to context:
contextdock lists get l_xyz --json | jq -r '.data.tagIds | join(",")'
# → finance,ops
contextdock context --tags finance,ops --budget 100000 --prefer keyPoints
Watch the tag-match semantics gap: context --tags a,b always uses any semantics (union — docs carrying tag a OR tag b). Lists with tagMatch: "all" cannot be replicated through context --tags directly. The detail-envelope tokens totals already honor the list's tagMatch, so for budget planning the envelope is the right read. For actual assembly under tagMatch: "all", fetch the list's pre-assembled markdown via contextdock lists get l_xyz --variant <v> (the server applies the correct intersection).
mcp is not a daemoncontextdock mcp hands stdin/stdout to the MCP SDK for JSON-RPC framing. The command writes nothing to stdout itself — anything printed there corrupts the protocol and the host (Claude Desktop, Cursor, Claude Code) silently disconnects. Status messages, if any, must go to stderr. Do not add console.log, print, or process.stdout.write to the mcp command path — and warn the user if they're patching it.
Do NOT tell the user to "keep contextdock mcp running in a terminal". It is a stdio subprocess that the host (Claude Desktop / Cursor / etc.) spawns, pipes JSON-RPC through, and kills on exit. Running it manually in a terminal does nothing useful — there is no listener for it to talk to, and the user will sit watching a silent process. The only correct way to start it is to register it in the host's MCP config and let the host spawn it.
Edit:
%APPDATA%\Claude\claude_desktop_config.json~/Library/Application Support/Claude/claude_desktop_config.jsonBecause the CLI is not on npm (and won't be), point at node + the absolute path to dist/index.js:
{
"mcpServers": {
"contextdock": {
"command": "node",
"args": [
"C:\\Software Projects\\Context Manager\\cli\\dist\\index.js",
"mcp"
],
"env": {
"CONTEXTDOCK_API_KEY": "cdk_live_..."
}
}
}
}
After npm link from the repo's cli/ directory, the simpler form works (the contextdock shim resolves to the local checkout):
{
"mcpServers": {
"contextdock": {
"command": "contextdock",
"args": ["mcp"],
"env": { "CONTEXTDOCK_API_KEY": "cdk_live_..." }
}
}
}
The env block is strongly recommended even if ~/.contextdock/config.json exists. It pins the key to this MCP server entry (so revoking the file doesn't silently break Claude Desktop), survives moving the config between machines, and makes the wiring auditable in one place. The config file is the fallback, not the primary path for host-spawned subprocesses.
Restart Claude Desktop. Verify by asking it to "list my ContextDock bundles" — that exercises the list_bundles MCP tool end-to-end.
Every CLI failure goes through CliError with one of five exit codes that mirror the API error taxonomy.
| Exit | Meaning | Typical API code field |
|---|---|---|
| 1 | Generic / validation / network | Validation failed, NO_FILTER_SPECIFIED, Network error: ... |
| 2 | Auth | KEY_INVALID, KEY_REVOKED, INSUFFICIENT_SCOPE, "Not logged in" |
| 3 | Not found | DOC_NOT_FOUND, BUNDLE_NOT_FOUND |
| 4 | Token budget | BUDGET_TOO_SMALL, BUDGET_EXCEEDED |
| 5 | Rate limit | RATE_LIMIT_EXCEEDED |
Important distinctions:
KEY_REVOKED ≠ KEY_INVALID: the key was valid once but the owner revoked it. Re-login won't help. Mint a new key in Settings → API Keys.INSUFFICIENT_SCOPE: read-only key calling a write route. Mint a key with write scope (or delete) — there is no silent downgrade.*_NOT_FOUND (404), never 403 — this avoids leaking the existence of private resources owned by other users.RATE_LIMIT_EXCEEDED: read X-RateLimit-Reset (Unix epoch seconds) from the response and sleep until then. Limiter is per-key, shared across REST + MCP.Ask the user to provide a key only when all of the following are true:
$CONTEXTDOCK_API_KEY is unset (echo $CONTEXTDOCK_API_KEY empty).~/.contextdock/config.json does not exist.Even then, do not ask them to paste the key into chat. Tell them:
Mint a key at https://contextdock.web.app → Settings → API Keys → Create Key, copy it, then run
contextdock loginand paste at the prompt. The CLI writes it to~/.contextdock/config.jsonfor you.
contextdock login (no flags) reads the key off stdin so it never touches process arguments or shell history.
mcp command path — JSON-RPC owns it./api/agent/* — that surface only accepts cdk_live_... keys, by design.npm install -g @contextdock/cli — the package is deliberately not published to npm and there is no plan to publish it. Point users at this page (https://contextdock.web.app/cli-skill), at npm link from the repo's cli/ directory, or at the absolute node dist/index.js path.npm publish from the cli/ workspace, even when "the version looks ready" — distribution is via the website + local install only.cdk_live_... key into project files. If the user shares one in chat, remind them the transcript still has the original and they should rotate it in Settings → API Keys./api/agent/* (API key) and /api/* (Firebase ID token) auth. They are isolated by design — a key cannot mint more keys, create users, or change roles.Source of truth: skills/contextdock-cli/SKILL.md in the ContextDock repo. The raw markdown is also served at /cli-skill.md.