@displaydev/cli
CLI for display.dev — publish HTML and Markdown with public, company, private, or invited-person access.
Spaces files and folders
Create, read, update, and organize files and folders. Run dsp --help or a
command group's --help for its options.
dsp space list
dsp space list-contents <spaceId>
dsp file create --parent-id <folderId> --preview
dsp file create <file> --parent-id <folderId> --content-type text/markdown --idempotency-key <requestKey>
dsp file get <fileId>
dsp file read <fileId>
dsp file search <fileId> <query>
dsp file edit <fileId> --old "old text" --new "new text" --base-revision <revisionId> --idempotency-key <requestKey>
dsp file export <fileId> --output <outputPath>
dsp file history <fileId>
Space and folder contents accept --sort name|modified|format|owner and --dir asc|desc. format sorts the displayed Type label, including labels for arbitrary file formats; --type remains a separate filter. Only Name keeps folders first, in either direction. Equal Type or Owner values follow Name in the selected direction, and missing owners stay last.
Space and folder child lists also support --sort createdBy for the displayed creator name. Use --dir asc or --dir desc. Equal creator names follow item Name in that direction. Unknown creators stay last. This option is not available in Home, Shared with me, the Space directory, or Trash.
Home (dsp file list --view home) and Shared with me (--view shared-with-me) also accept format and owner. dsp file list returns files only. Use dsp folder find --view home for Home folders, or --view shared-with-me for shared folders. Home shows folders before files under every sort. Shared with me mixes folders and files. The Space directory, Trash and fixed-order name search retain their existing sort choices. Pass each cursor back with the same query; if it is rejected after an item changes or becomes inaccessible, restart without a cursor.
Add --created-by <userId> to Space/folder contents or Home/Shared with me lists. Use --include-creators to return accessible creator choices across all matching pages before creator filtering, then select a returned userId. Created by is the original creating person, independently of Owner, Shared by, or later edits. Restart without a cursor when changing it. Space directory, Trash and fixed-order search do not accept these options.
dsp folder list <folderId> --include-creators
dsp folder list <folderId> --created-by <userId>
dsp file list --view home --created-by <userId>
Preview the destination and audience before creation. Omitted --parent-id
selects My Space only for the commands that explicitly support it. Use the
source's actual media type. A request key uses 16–128 letters, digits, periods,
underscores, tildes, or hyphens. Keep the exact request key and source after an
uncertain response. File updates retain the explicit base revision ID.
Reconcile a stale revision rather than silently replacing the base.
Display retains Spaces request results for 24 hours. After that window, even the original request key can execute as a new operation. Read current state before retrying an older uncertain result. Reconcile that result before retrying. Do not rely on the request key alone to prevent a duplicate.
Spaces commands use file, folder, Space, and revision IDs. Copy each ID exactly. Do not pass those IDs to numbered compatibility selectors. Command groups include:
| Group | Tasks |
|---|---|
dsp space | Browse Spaces, inspect current settings, create/manage direct membership, rename, delete/restore, and inspect or permanently delete items in Trash |
dsp folder | Browse, create, rename, copy, move, find, inspect/change access, request access, and Trash/restore |
dsp file | Create/read/search/edit/replace/download, rename, copy, move, inspect history, Keep/delete eligible revisions, inspect/change access, and Trash/restore |
dsp group | Create groups, manage membership, and assign group administrators |
dsp member | Preview Manager coverage and deactivate a member with explicit confirmation |
dsp access-request | Approve or dismiss one stored request with current sharing authority |
dsp comment / dsp thread | Native discussions, replies, paging, editing, deletion, resolution, and reopening |
dsp watch / dsp unwatch | Personal discussion subscriptions with --file <id> |
dsp search | Cross-file indexed search in Spaces. Legacy organizations retain name and exact-source modes. |
Permanently delete items in Trash
Use dsp space list-trash <spaceId> to find the item IDs. Preview one explicit
Space with dsp space delete-trash <spaceId> <itemId>… --preview. Use
dsp space delete-trash <spaceId> --all --preview to review all eligible Trash
in that Space. Review the selection, exclusions, and storage to free before
confirming.
Repeat the same Space and selection with --confirm, --if-match <etag>, and
--idempotency-key <requestKey>. Quote the argument so that the ETag's own
double quotes reach the CLI. If the response is uncertain, keep the same Space,
selection, ETag, and key. If the preview changed or a recovery hold blocked the
request, get a fresh preview before trying again. My Space owners and Shared
Space root Managers can use this operation with a person credential and the
required scope.
JSON results for Spaces commands go to stdout. Diagnostics and continuation instructions go
to stderr. file export streams exact bytes unless an output path is chosen.
Overwriting a local output requires --overwrite. A failed command returns a nonzero exit code.
The numbered-artifact exit-code table does not define every Spaces error. Keep cursors with their original query and filters.
A default personal key follows its owner's current Space access. Valid existing scoped personal keys also work in Spaces. They retain their scope limits and the Member publish cap, together with the owner's current Space access. Migration preserves the key and secret. OAuth keeps its approved scope limits.
Organization service keys need their own grants of access to eligible Shared Spaces. They do not inherit their creator's access. Organization roles alone grant no private content read. Do not switch identities, use compatibility commands, or widen an audience after denial.
Local MCP accepts local file paths. Hosted MCP accepts inline source or staged
upload IDs. Hosted export_file returns a download descriptor, while local MCP
can write an explicit output path. Upload and download credentials are secrets.
Keep them out of shared logs and final replies. Public-link access alone does
not authorize creating or editing files.
See Spaces and files and the CLI reference for task guidance.
Install
npm install -g @displaydev/cli
Authenticate
# Interactive login via email OTP
dsp login
# Or set an API key (from the dashboard)
export DISPLAYDEV_API_KEY=sk_live_...
Run dsp whoami to check which account and organization your credential uses.
For scripts, dsp whoami --json prints user, organization, membership,
and keyType. Organization keys return null for user and membership.
Agent-native login
An agent must not ask for, read, or relay a sign-in password or OTP. The user runs
dsp login in their own terminal and completes the prompt or browser flow there.
The agent then reuses the CLI's saved sign-in. It must not inspect the credential
store, request tokens in chat, or start sign-in merely to bypass a permission denial.
The invocation examples below document the CLI interface for a person or an
explicitly controlled integration. They do not instruct an agent to collect OTPs.
The command adapts to its environment:
| Invocation | Behavior |
|---|---|
dsp login --email [email protected] (TTY) | Sends OTP, prompts for the code in the terminal. |
dsp login --email [email protected] (non-TTY) | Sends OTP and exits 0. The human completes verification in their own terminal. |
dsp login --email [email protected] --code 123456 | Verifies the code, persists the token, exits. |
For structured output that doesn't rely on prose parsing:
dsp login --email [email protected] --json
# → {"status":"otp_sent","email":"[email protected]",
# "next":{"command":"dsp login","args":["--email","[email protected]","--code","<code>"]}}
dsp login --email [email protected] --code 123456 --json
# → {"status":"authenticated","method":"otp","email":"[email protected]",
# "suggestedActions":[{"type":"install_skill","command":"npx",
# "args":["skills","add","display-dev/skill","--skill","display-dev"],
# "requiresApproval":true}]}
dsp login --email [email protected] --json
# → {"status":"authenticated","method":"sso",
# "suggestedActions":[{"type":"install_skill","command":"npx",
# "args":["skills","add","display-dev/skill","--skill","display-dev"],
# "requiresApproval":true}]}
SSO is selected automatically when the organization for the submitted email requires it.
Login failures use {"status":"error","error":"<code>"}. Stable error
codes are email_required, api_key_required, input_failed,
invalid_api_key, sso_required, disposable_email, send_failed,
invalid_or_expired_code, access_denied, device_code_expired,
authentication_failed, and configuration_failed.
The OTP is single-use and expires after ten minutes. A human-controlled
integration can use the compatible --code form, which places the OTP briefly
in process arguments. Agents must not receive or relay it. The resulting
session token is persisted and used only by the CLI.
After OTP, SSO, or API-key authentication, the CLI suggests the optional display.dev skill installation:
npx skills add display-dev/skill --skill display-dev
Installing the skill changes agent-owned configuration. An agent must get the user's approval before running the command. Login never runs the installer.
Usage
# Publish a file. User-scoped credentials create a private artifact;
# org-scoped service keys create a company artifact. Publishes without a
# credential create a public artifact you can claim.
# The display name defaults to the filename without the extension.
dsp publish ./report.html
# Share a report with the organization
dsp publish ./report.html --company
# Override the display name
dsp publish ./report.html --name "Q1 Report"
# Publish as public (no login required to view)
dsp publish ./report.html --name "Q1 Report" --public
# Share with external recipients (repeatable)
dsp publish ./proposal.html --name "Q1 Proposal" \
--share [email protected] --share [email protected]
# Update an existing artifact from the version you edited
dsp publish ./report.html --id abc123 --base-version 5
# Change an existing artifact back to company auth
dsp publish ./report.html --id abc123 --base-version 5 --company
# Remove all guest shares from an existing artifact
dsp publish ./report.html --id abc123 --base-version 5 --clear-shares
# Browse and search artifacts
dsp list --author [email protected]
dsp search "quarterly"
dsp search "invoice total" --in abc123@5
# Read a bounded source range, pinned to a version
dsp read abc123@5 --offset 0 --limit 65536
# Replace one exact source span
dsp edit abc123 --base-version 5 --old "Q1" --new "Q2"
# Get artifact metadata, versions, outline, and open threads
dsp get-metadata abc123
# Make an independent copy from the current version
dsp make-copy abc123
# Within your organization, copy a retained version and invite a reviewer
dsp make-copy abc123@3 --name "Q1 plan" --visibility private --share [email protected]
# Delete an artifact
dsp delete abc123 --confirm
# Start MCP server (for Claude Desktop / Claude Code)
dsp mcp
Copy defaults and destination
dsp make-copy <shortId> needs no metadata lookup. The server uses the current
version, Copy of <source name>, source visibility and no inherited recipients.
Use --visibility private for a private copy. The public browser copy page
initially selects Private. JSON includes sourceVersion, the version copied.
An eligible public artifact from another organization can be copied on every plan, using only its current version. The source organization must be active and allow public artifacts. A 404 means missing or unavailable source/version; it does not identify the cause. Confirm the active destination organization first. Bounded retries keep the same operation key and request. A key cannot be reused with different inputs or another destination. After an unresolved interruption, check that organization's artifacts before starting a new copy.
Publish flags
| Flag | Effect |
|---|---|
--name <name> | Display name override. Optional — defaults to the filename without the extension on create. On update, renames the artifact (re-derives the slug). |
--id <shortId> | Update an existing artifact (publishes a new version). |
--base-version <n> | Required with --id; the current version you edited from. |
--public | Set visibility to public (no auth required to view). |
--company | Set visibility to company auth. This is the default for org-scoped service keys. |
--visibility <level> | Set visibility to public, company, or private. |
--share <email> | Grant guest access to an external email (repeatable). |
--share-with <emails> | Grant guest access to a comma-separated list of emails. |
--clear-shares | Remove all guest shares. Only valid with --id. |
--theme <theme> | Markdown rendering theme. |
--reload | Refresh open viewer tabs when the new version is ready. |
Visibility and share flags default to "keep current" on update: omitting --public, --company, and --visibility leaves visibility unchanged; omitting --share, --share-with, and --clear-shares leaves the guest list unchanged. Use only one of --public, --company, or --visibility. Do not combine --clear-shares with either --share or --share-with.
On create, omitting visibility uses the credential context: user sessions and user-owned keys create Private artifacts, while organization-scoped service keys create Company artifacts. CI jobs using user-scoped credentials should pass --company when the result is meant for the organization.
For update mode, fetch the current version first with dsp get-metadata <shortId>. Use dsp search <text> --in <shortId> and dsp read <shortId> for bounded context, then dsp edit for one exact replacement. Use dsp export when you intentionally need the complete local file, and dsp publish --id for whole-source replacement. If the artifact changed on the server, reconcile against the current version and retry with that baseline.
Updates keep the format established for the artifact. Use a .md file for a Markdown
artifact and a .html file for an HTML artifact. Publish a new artifact when
converting between Markdown and HTML.
Testing
The CLI suite uses three distinct fetch-faking strategies, each pinned to a layer of the stack. New contributors pick the one that matches the layer they're testing — adding a fourth is almost always wrong.
- Unit-level fetch fake (
stubFetchJson(...)) — for exercisingApiClientand anything that composes it purely in-process. Low ceremony, full control over responses, no cross-process boundary. Use thestubFetchJson(body, init?)helper fromcli/src/test-helpers.ts— it stubsglobal.fetchwith a fresh JSONResponseper call and returns thevi.fn()so tests can still inspectfetchMock.mock.calls[...]. Fall back to rawvi.stubGlobal('fetch', vi.fn().mockRejectedValue(...))only for network- failure cases. Representative example:cli/src/api-client.spec.tsand the integration wire-up assertion incli/src/mcp-mode-selection.spec.ts. - Hand-rolled
createMockApiClient()— for MCP tool specs that verify argument-routing from an MCP tool handler to the rightApiClientmethod. No HTTP in the loop at all; the fake is a plain object ofvi.fn()s. Representative example:cli/src/mcp-server.spec.tsandcli/src/mcp-publish-file-read.spec.ts. - Real localhost HTTP fixture (
createHttpFixture+spawnCli) — forprogram.actionE2E specs that need to exercise commander argv parsing, the stdout/stderr split, exit codes, and the full HTTP round-trip. Drives the CLI as a real child process. Representative examples:cli/src/cli-mode-selection.spec.ts,cli/src/cli-subcommands.spec.ts,cli/src/login-otp.spec.ts,cli/src/login-otp-send.spec.ts.
MCP
The CLI doubles as an MCP server over stdio. Add to your MCP client config:
{
"mcpServers": {
"display": {
"command": "dsp",
"args": ["mcp"],
"env": {
"DISPLAYDEV_API_KEY": "sk_live_..."
}
}
}
}
Inspecting an existing artifact
Use dsp, not an anonymous web fetch, when inspecting an artifact URL. Gated
artifacts return 403 without display.dev credentials.
The short ID is the path segment before the first hyphen. For
https://display.dsp.so/YQyXggrM-product-video-script, it is YQyXggrM.
| Need | Command |
|---|---|
| Uploaded bytes | dsp export <shortId> |
| HTML as Markdown | dsp export <shortId> --format markdown |
| Specific version | dsp export <shortId>@<n> |
| Bounded source context | dsp read <shortId>[@<n>] [--offset <bytes>] [--limit <bytes>] |
| Metadata, versions, outline, and open threads | dsp get-metadata <shortId> |
| Browse artifacts | dsp list |
| Search artifact names | dsp search <query> |
| Search within one source | dsp search <query> --in <shortId>[@<n>] |
| Exact partial edit | dsp edit <shortId> --base-version <n> --old <text> --new <text>; use --old-file / --new-file for multiline UTF-8 inputs |
| Deprecated compatibility | dsp find [query] (writes a migration notice to stderr) |
Read-only commands are safe for inspection. Commands such as publish, make-copy,
edit, share, rename, delete, logo mutations, comment mutations, thread
mutations, and watch mutations change published state; run them only when the
user requests the change. delete requires --confirm.
The CLI defaults to https://api.display.dev. Set
DISPLAYDEV_API_URL=http://api.display.test:3331 explicitly for local work.
Publishing this package
Production Delivery evaluates every master push and calls
publish-cli.yml as a reusable child only when its planner admits the CLI. The
child publishes only when cli/package.json contains a version newer than npm.
Whenever user-visible CLI behavior changes, update the version in the same change. While the package is pre-1.0:
- Increase the minor version for a new feature or breaking change.
- Increase the patch version for a backward-compatible fix.
- Do not use
1.0.0merely to signal a breaking change; that version is the separate stability commitment.
Publishing uses npm OIDC Trusted Publishing. Do not add an NPM_TOKEN or
--provenance; sigstore provenance is unavailable for this private repository.
The npm trusted-publisher entry must bind display-dev/display-dev to the
calling workflow filename production-delivery.yml, not the reusable child
filename. Update that package setting before changing the production caller.