@pitcher/cli-scripts
v3.0.1
Published
The Pitcher CLI for various tasks in its ecosystem.
Downloads
1,402
Readme
@pitcher/cli-scripts
Pitcher CLI Tools provide a suite of command-line utilities to streamline your workflow. For the best experience, we recommend installing the tools globally using
npm install -g @pitcher/cli-scripts.
Releases are published to npm automatically: a merge to main that bumps version in package.json publishes that version, and a merge without a bump publishes nothing (.github/workflows/cli-scripts-publish.yml; release flow in CLAUDE.md).
⚠️ Breaking changes in 3.0.0
- Local
p mcpnow serves ONLY 3 tools (pitcher_dev_sync,pitcher_dev_sync_revert,pitcher_publish_app), down from 124. Everything else is the hosted serverhttps://pitcher-mcp.fly.dev/mcp.p mcpno longer readsNG_API_KEY/NG_ORG/NG_URL/NG_ENV; runp loginonce (--domainorPITCHER_DOMAINwhen logged in to several orgs).- Every other command uses your
p loginsession.--api-key(automation) still wins when passed;NG_API_KEYis used only when you have NOp loginsession, and is otherwise ignored with a notice.--orgno longer defaults todevon the session path: the session's org is the target.--domaincannot be combined with--api-keyor--org.- The Claude Desktop
.mcpbextension is retired. Claude Desktop adds the remote URL natively.p analytics kpirequires--type(one of the 12 KPI types listed inp analytics kpi --help). The old defaultallalways returned a 500.- Node ≥ 22.12 (
engines).
Help
p --help
# or for specific command help:
p command --helpSign in (p login)
People sign in with their own Pitcher account; API keys are for automation (CI, deploy scripts).
p login --domain acme.my.pitcher.com # opens the browser; --no-browser prints the URL instead
p whoami # every session: domain, environment, email, token expiry, status
p logout acme.my.pitcher.com # or: p logout --allp logoutis local only: it deletes the session file and makes no Auth0 call. The refresh token is not revoked; it expires on its own. Your hosted MCP connectors (ChatGPT, Claude) are unaffected. A newp loginover an existing session likewise just replaces the file. (Revoking is avoided on purpose: the CLI and the hosted server share one Auth0 client, and a revoke can delete the whole grant, signing every connector out.)One session per org domain. Commands pick it with
--domain, elsePITCHER_DOMAIN, else the only session. Logged in to several orgs and nothing explicit → an error naming them (never a silent pick).Sessions live in
${XDG_CONFIG_HOME:-~/.config}/pitcher/sessions/<domain>.json(mode0600, directories0700). The access token refreshes itself; an expired session asks you top loginagain.The sign-in listener uses
http://127.0.0.1:53682/callbackonly. Over SSH, forward port 53682 and use--no-browser.Precedence:
--api-key→ yourp loginsession →NG_API_KEY. With a session,--org acme(or--url https://acme.my.pitcher.com) selects theacmesession; a--url/--publish-urlon another host is refused rather than sent your token.CI is unaffected: it passes
--api-keyand has no session.
MCP server
Pitcher's MCP tools come from two places:
| Server | Tools | Add it with |
|---|---|---|
| Hosted (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (121 tools, prompts, resources) | claude mcp add --transport http pitcher https://pitcher-mcp.fly.dev/mcp — Claude Desktop: Settings → Connectors → Add custom connector, same URL |
| Local p mcp (stdio) | only pitcher_dev_sync, pitcher_dev_sync_revert, pitcher_publish_app (they zip a folder on YOUR machine) | claude mcp add pitcher-local -- p mcp |
Claude Code users add both. The local server runs on your p login session (no API key, no env block); the domain is fixed when it starts. Signed in to several orgs? Add one entry per org:
"pitcher-local-acme": { "command": "p", "args": ["mcp", "--domain", "acme.my.pitcher.com"] }- Auth and org are pinned when the server starts and are never tool inputs. The session token is refreshed before each tool call; after
p logoutthe tools answer AUTH until youp loginand restart the server. pitcher_publish_appis always markeddestructiveHint.pitcher_dev_sync/_revertget it unless the session's org is a test org (an unknown environment counts as production).dev-syncis a one-shot sync as a tool; the continuouswatchmode is CLI-only.
This section is a reference only. For app-development conventions (modules, settings, mount points, SFDC), see the pitcher-apps skill, not this README.
To run the publish-app command:
# Signed in with p login: publishes to your session's org
p publish-app <folder>
# Automation (CI): an API key; --org picks the org
p publish-app <folder> --org pitcher --api-key myapikey
# With an API key and no --org, the org is dev
p publish-app --api-key myapikey
# Publish to a specific organization subdomain (e.g., pitcher.my.pitcher.com)
p publish-app --api-key myapikey --org pitcher
# Dry run with version increment
p publish-app --api-key myapikey --org pitcher --dry-run --increment-version
# Publish and install to specific instances
p publish-app --api-key myapikey --org pitcher --increment-version --install-to 01JB8KQRM3XYZ2P4N5W7T9VHFA,01JB8KR0P6QS4T8X1M9Y3C7ZBE
# Publish and install to all instances with a specific environment name (case-insensitive)
p publish-app --api-key myapikey --org pitcher --increment-version --install-to-env-name Demo
# Combine both: install to specific instances AND all instances in an environment
p publish-app --api-key myapikey --org pitcher --increment-version --install-to 01JB8KS5F2WH9V6D8R3N7JXMQK --install-to-env-name Demo
# Alternative: Set API key via environment variable
export NG_API_KEY=myapikey
p publish-app --org pitcher --increment-version --install-to-env-name ProductionOptions
--api-key <apiKey>: API Key for authentication (can also useNG_API_KEYenv variable)--org <organization>: Organization subdomain (e.g., "pitcher" for pitcher.my.pitcher.com, default: "dev")--increment-version: Increment version in app.json with timestamp--dry-run: Perform a dry run without publishing--publish-url <url>: Explicit publish URL--install-to <instanceIds>: Comma-separated list of instance IDs to install the app to after publishing--install-to-env-name <envName>: Environment name to filter instances and install the app to after publishing (case-insensitive)
To run the dev-sync command:
Sync a marketplace app directly to S3 for live development testing without publishing. This is useful for testing app changes in a pre-production environment.
# One-time sync from dist folder (on your p login session)
p dev-sync sync --file-id <pitcher_file_id>
# Signed in to several orgs: pick one
p dev-sync sync --file-id <pitcher_file_id> --domain pitcher.my.pitcher.com
# Watch mode - auto-sync on file changes (reverts on Ctrl+C)
p dev-sync watch --file-id <pitcher_file_id> --api-key myapikey
# Watch with custom debounce (default: 2500ms)
p dev-sync watch --file-id <pitcher_file_id> --debounce 5000
# Manually revert dev override to restore original version
p dev-sync revert --file-id <pitcher_file_id> --api-key myapikey
# Alternative: Set API key via environment variable
export NG_API_KEY=myapikey
p dev-sync watch --file-id <pitcher_file_id> --org pitcherOptions
--file-id <id>: (Required) File ID of the installed app (pitcher_file.id) - copy this from the installed apps view--api-key <apiKey>: API Key for authentication (can also useNG_API_KEYenv variable)--org <organization>: Organization subdomain (default: "dev")--debounce <ms>: Debounce time in milliseconds for watch mode (default: 2500)
Notes
- Run from your app's project root (with
dist/folder) or directly from thedist/folder - The
watchcommand automatically reverts changes on graceful shutdown (Ctrl+C) - Apps with dev override show a badge in the installed apps view
To run the files command:
Manage Pitcher content files — list within an instance, fetch by id, create (upload or link), or update.
# List files in an instance (paginated)
p files list --instance-id 01... --api-key myapikey
# Filter by tags or specific IDs
p files list --instance-id 01... --tags brochure,demo --api-key myapikey
# Get a single file by id
p files get 01FILEID... --api-key myapikey
# Create a file from a local upload
p files create --instance-id 01... --name "Q3 Deck" --file ./deck.pdf --api-key myapikey
# Create a link/WEB file from a source URL
p files create --instance-id 01... --name "Product Page" --source-url https://example.com --api-key myapikey
# Update a file's tags or metadata (metadata/app-metadata are deep-merged, not replaced)
p files update 01FILEID... --tags updated,reviewed --api-key myapikeyTo run the folders command:
Manage Pitcher content folders — list, fetch (or the instance root), create, and update smart-folder rules.
# List folders in an instance (paginated)
p folders list --instance-id 01... --api-key myapikey
# Get a folder by id
p folders get 01FOLDERID... --api-key myapikey
# Get the instance root folder
p folders get --root --instance-id 01... --api-key myapikey
# Create a folder under a parent
p folders create --name "Marketing" --parent-folder-id 01PARENTID... --api-key myapikey
# Update a folder's name or access type
p folders update 01FOLDERID... --name "Sales Enablement" --access-type restricted --api-key myapikeyTo run the search command:
# Ranked cross-type search (files, canvases, users, apps) within an instance
p search --instance-id 01... --query "quarterly deck" --api-key myapikeyTo run the instances command:
Discover Pitcher instances — resolve names to instance IDs before running other commands.
# List instances (optionally filter by region/storage-region/environment)
p instances list --api-key myapikey
p instances list --environment Demo --api-key myapikey
# Get a single instance by id
p instances info 01INSTANCEID... --api-key myapikeyTo run the canvases command:
Read Pitcher canvases — presentations, sections, and blocks.
# List canvases in an instance (paginated)
p canvases list --instance-id 01... --api-key myapikey
# Only templates or only sections
p canvases list --instance-id 01... --is-template --api-key myapikey
p canvases list --instance-id 01... --is-section --api-key myapikey
# Only blocks (blocks are hidden unless --is-block is passed)
p canvases list --instance-id 01... --is-block --api-key myapikey
# Get a single canvas by id
p canvases get 01CANVASID... --api-key myapikeyTo run the shared-links command:
Create and read share links for canvases (returns the short_url).
# Create a share link for a canvas
p shared-links create --canvas-id 01CANVASID... --api-key myapikey
# Fetch the existing share link for a canvas
p shared-links get --canvas-id 01CANVASID... --api-key myapikeyTo run the tags command:
List tags in an instance (the server requires a search term of at least 3 characters).
# List tags matching a search term
p tags list --instance-id 01... --search "demo" --api-key myapikey
# Filter by type
p tags list --instance-id 01... --search "demo" --type canvases --api-key myapikeyTo run the email-template command:
Create/list/get HTML email templates for the emails app. create, list, and get require --instance-id (scaffold does not).
# Create a template from an HTML file (inline CSS)
p email-template create --instance-id 01... --name "Welcome" --body-file ./welcome.html --api-key myapikey
# ...or pass the HTML inline instead of a file (use one, not both)
p email-template create --instance-id 01... --name "Welcome" --body-html "<p>Hi {{recipient.firstName}}</p>" --api-key myapikey
# List templates (optionally filter by name/status/category/target)
p email-template list --instance-id 01... --api-key myapikey
# Get a template by id
p email-template get --instance-id 01... --id 01TEMPLATEID... --api-key myapikey
# Generate a starter HTML file with tokens in context
p email-template scaffold --out ./welcome.html --api-key myapikeyRun p email-template create --help for the full token/marker reference.
scaffold returns the starter HTML in the envelope data (never printed raw to stdout); pass --out <path> to write it to a file instead.
To run the dsr-landing command:
Create/list DSR (Digital Sales Room) landing-page templates, and set one as the instance-scope default. create, list, and set-default require --instance-id (scaffold does not).
# Create a landing template from an HTML file (must contain the email-gate marker)
p dsr-landing create --instance-id 01... --name "Default" --html-file ./landing.html --api-key myapikey
# ...or pass the HTML (and optionally CSS) inline instead of a file (use one, not both)
p dsr-landing create --instance-id 01... --name "Default" --html "<div data-pitcher-email-gate></div>" --api-key myapikey
# Create and set as default in one call
p dsr-landing create --instance-id 01... --name "Default" --html-file ./landing.html --default --api-key myapikey
# Set an existing template as the default
p dsr-landing set-default --instance-id 01... --id 01TEMPLATEID... --api-key myapikey
# List templates in an instance
p dsr-landing list --instance-id 01... --api-key myapikey
# Generate a starter HTML file with the required marker and tokens in context
p dsr-landing scaffold --out ./landing.html --api-key myapikeyThe landing HTML (whether via --html-file or --html) must contain the data-pitcher-email-gate marker verbatim (the platform injects the email-gate form there) — create fails client-side before any request if it's missing. Provide exactly one of --html / --html-file (and at most one of --css / --css-file).
Run p dsr-landing create --help for the full token/marker reference (editable-region attributes, recipient/customer/org/rep tokens).
scaffold returns the starter HTML in the envelope data; pass --out <path> to write it to a file instead.
To run the announcements command:
Create/list announcement records for the wa-announcements app. create and list require --instance-id (scaffold does not).
# Create from a JSON data file
p announcements create --instance-id 01... --from-file ./announcement.json --api-key myapikey
# Create from convenience flags only
p announcements create --instance-id 01... --title "New feature" --body "<p>Check it out</p>" --api-key myapikey
# List announcements
p announcements list --instance-id 01... --api-key myapikey
# Generate a fully populated starter data.json
p announcements scaffold --out ./announcement.json --api-key myapikeycreate requires either --from-file or both --title and --body. --title/--body/--priority override matching keys from --from-file when both are given. Run p announcements create --help for the full data field catalog (media, links, scheduling, target groups, popup overrides) — note start_at/expires_at are naive local dates (YYYY-MM-DD[THH:mm]), not ISO/UTC.
scaffold returns the starter JSON in the envelope data; pass --out <path> to write it to a file instead.
To run the themes command:
Read canvas/branding themes (colors via settings, fonts via typography, plus assets/logo) to reference when authoring email/DSR/canvas content. Read-only.
# List all themes in the org (returns full theme objects)
p themes list --api-key myapikey
# Narrow to one instance; add per-theme canvas usage and canvas ids
p themes list --instance-id 01... --with-usage --with-canvas-ids --api-key myapikey
# Get one theme by id
p themes get --id 01THEMEID... --api-key myapikeylist is server-capped (newest-first) with no --limit/--page; when the cap is hit the envelope carries a truncation warning. --instance-id narrows to themes bound to that instance and excludes org-level (default) themes, so omit it to see everything usable in the org.
To run the org command:
Read the organization the API key belongs to.
p org get --api-key myapikeyTo run the analytics command:
Read rollup analytics totals (org/instance), KPI metrics, and content/user engagement analytics. org/instance are Django rollups; kpi and the 7 verbs below hit the clickhouse-api service (clickhouse.pitcher.com) via the same env resolution as everything else — --org/--url pick the environment (routed via x-original-domain). Access follows the signed-in user's own role (or the key user's, with an API key): popularity, users, files, and canvases need an org-admin key; engagement, viewers, and sessions work with a member key. kpi and sessions return the raw payload; the other row-heavy verbs return a shaped envelope (summary, rows, total_count, returned, has_more, and sometimes truncated/warnings) capped by --limit (default 20, max 100). Date-range verbs default to a 30-day window when --start-date/--end-date are omitted. viewers and sessions require --canvas-id; sessions also requires --user.
# Organization-wide rollups (org-admin key required)
p analytics org --api-key myapikey
# Instance rollups (instance-admin or org-admin key required)
p analytics instance --instance-id 01... --api-key myapikey
# KPI metrics from the clickhouse analytics service (PIT-7647).
# Access follows the signed-in user's own role on the instance; --org/--url pick the
# env exactly as for other commands (they route via x-original-domain).
# Activity metrics need a date window; inventory counts (total-members, ...) do not.
p analytics kpi --instance-id 01... --type total-members --api-key myapikey
p analytics kpi --instance-id 01... --type total-active-users \
--start-date 2026-01-01 --end-date 2026-01-31 --api-key myapikey
# DSR/content engagement rows (member key)
p analytics engagement --instance-id 01... --engagement-level high --limit 50 --api-key myapikey
# Most-viewed canvases / view-analysis (org-admin key)
p analytics popularity --instance-id 01... --sort-by total_views --sort-order DESC --api-key myapikey
# User roster (org-admin key)
p analytics users --instance-id 01... --api-key myapikey
# Active-user statistics instead of the roster (org-admin key)
p analytics users --instance-id 01... --stats --start-date 2026-01-01 --end-date 2026-01-31 --api-key myapikey
# File usage analytics (org-admin key)
p analytics files --instance-id 01... --file-type pdf --sort-by views --sort-order desc --api-key myapikey
# Per-section canvas engagement list (org-admin key)
p analytics canvases --instance-id 01... --group-ids 01GROUPID... --api-key myapikey
# DSR canvas viewers — who viewed a given canvas (member key)
p analytics viewers --instance-id 01... --canvas-id 01CANVASID... --viewer-type external --api-key myapikey
# Per-viewer session event timeline for a DSR canvas (member key)
p analytics sessions --instance-id 01... --canvas-id 01CANVASID... --user [email protected] --api-key myapikeyMCP: all ten verbs are exposed as read-only pitcher_analytics_* tools, alongside a pitcher://analytics/catalog resource (which verb answers which question — returns, time model, key tier) and four query prompts — top_content, content_engagement_summary, who_viewed_canvas, adoption_snapshot.
To run the users command:
Read users in the org (read-only). Org scope comes from the API key via the proxy x-org-id header — there is no --org-id flag.
# List users in the org (paginated)
p users list --api-key myapikey
# Filter by instance membership, roles, active status, or content-distribution group
p users list --instance-id 01... --api-key myapikey
p users list --instance-id 01... --instance-role editor --api-key myapikey
p users list --role admin --api-key myapikey
p users list --is-active --api-key myapikey
p users list --cdg 01GROUPID... --api-key myapikey
# Get a single user by id (user ids are integers, not ULIDs)
p users get --id 2446 --api-key myapikeyList filters: --instance-id, --instance-role (server-side requires --instance-id), --role, --is-active, --cdg, plus --page/--page-size (max 500). ID shapes: user ids are integers; instance and group ids are ULIDs.
To run the invitations command:
Read pending user invitations (read-only).
# List pending invitations (paginated)
p invitations list --api-key myapikey
# Get a single invitation by id (invitation ids are 'uinv_...' strings, not ULIDs)
p invitations get --id uinv_0yKQ41LLqOI2kn4E --api-key myapikeyTo run the memberships command:
Read instance memberships, add members, and change their role.
# List memberships (filter by instance or user; user ids are integers)
p memberships list --instance-id 01... --api-key myapikey
p memberships list --user-id 2446 --api-key myapikey
# Add a user to an instance (always created as 'member')
p memberships add --instance-id 01... --user-id 2446 --api-key myapikey
# Change a membership's role (org-admin key required; membership ids are integers)
p memberships set-role --membership-id 3537 --role editor --api-key myapikeyadd always creates the membership as member; use set-role to change it. set-role needs an org-admin key (an instance-admin key can add but not set-role). --membership-id is the integer membership row id from memberships list (not a user id). --role is the short role name (e.g. editor) discovered via p roles list — the prefixed key (instance:editor) is rejected. --scope defaults to instance; --scope organization targets the organization membership row instead — note its row id is NOT discoverable via this CLI today (memberships list returns instance memberships only), so use it only when you already have the org-membership id from another source.
To run the groups command:
Read/create content-distribution groups and manage their members.
# List groups (filter by instance)
p groups list --instance-id 01... --api-key myapikey
# Get a group by id
p groups get --id 01GROUPID... --api-key myapikey
# Create a group (name min 3 chars)
p groups create --instance-id 01... --name "West Region" --api-key myapikey
# Add a user to a group (user ids are integers)
p groups add-user --group-id 01GROUPID... --user-id 2446 --api-key myapikey
# List a group's members
p groups list-users --group-id 01GROUPID... --api-key myapikeyadd-user is rejected on auto-login-managed groups (their membership is derived, not manually editable).
To run the roles command:
Read the org's role set (6 system roles + any custom roles) — the discovery aid for p memberships set-role.
# List the org's roles
p roles list --api-key myapikey
# Filter by scope
p roles list --scope instance --api-key myapikeylist returns a { items: [...] } object; each item's role_key is prefixed (org:admin, instance:editor). Pass the short name (e.g. editor) to set-role, not the prefixed key. Like set-role, roles list currently needs an org-admin key: while the granular-permissions rollout is off (the production default), the endpoint 403s non-org-admin callers.
To run the metadata-template command:
Read and additively build an instance's metadata-field schema. list and upsert require --instance-id (get only needs --id; scaffold makes no request).
# Read the schema for an instance (0 or 1 template — one per instance)
p metadata-template list --instance-id 01... --api-key myapikey
# Get one template by id (ids are ULIDs)
p metadata-template get --id 01TEMPLATEID... --api-key myapikey
# Print a starter schema to fill in (optionally to a file)
p metadata-template scaffold --out ./schema.json
# Additively upsert the schema (merges by section label)
p metadata-template upsert --instance-id 01... --sections-file ./schema.json --api-key myapikeyupsert merges the incoming sections by section label — a section whose label matches an existing one is replaced, other sections are preserved. It cannot remove a section or field (that's a UI operation). options for select/multi-select fields are { label, value } objects (bare strings are rejected). upsert requires an instance- or org-admin key; a non-admin member key is rejected (400 → VALIDATION).
To run the dsr command:
Create and share a Digital Sales Room (a canvas share link) and get a link back in one command. Six subcommands: create (build + share, with optional landing-page binding), get (read an existing share back), update (change or clear the access settings of an existing share), delete (revoke a share), bind-landing (bind/unbind a landing template on an existing share), and scaffold (print a starter {source,security,landing} spec for --config-file).
The create endpoint is a get-or-create: sharing an already-shared canvas returns the existing link and updates only the fields you pass. That is deliberate (it mirrors Admin's "Create link once / Copy link after"), but it means create with individual flags can never remove a value — omitting a flag leaves the stored value alone. Use update for that, and --clear <field> to remove one.
A --config-file spec is different: its security block is sent as written, so the dsr scaffold starter's auth_strategy: any_email, valid_until: null and empty lists would all be written onto an existing share. That is why --config-file defaults to --if-exists error (individual flags default to return); pass --if-exists return to apply a spec to an already-shared canvas on purpose.
# simplest — email-gated link (server default auth_strategy=any_email)
p dsr create --instance-id 01... --canvas-id 01... --api-key myapikey
# open link
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy none
# ShareBox + security + branded landing
p dsr create --instance-id 01... --sharebox-file-ids 01A,01B --account-id 01... \
--auth-strategy specified_domains --allowed-domains acme.com --valid-until 2026-12-01T00:00:00Z \
--landing-template-id 01... --landing-context '{"rep":"Sam"}'
# bind/unbind a landing template on an existing share
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01...
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id none
# merge fields from a FILE instead of inline JSON (the only spelling that works in PowerShell)
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
p dsr create --instance-id 01... --canvas-id 01... --landing-template-id 01... --landing-context-file ./ctx.json
# read an existing share back
p dsr get --id 01...
# refuse to reuse an existing share instead of silently returning it
p dsr create --canvas-id 01... --if-exists error
# send the download preference explicitly — either direction
p dsr create --canvas-id 01... --allow-download
p dsr create --canvas-id 01... --no-allow-download
# change an existing share — only the flags you pass are sent
p dsr update --id 01... --valid-until 2026-12-01T00:00:00Z
p dsr update --id 01... --auth-strategy specified_domains --allowed-domains acme.com
# REMOVE a value (this is the only way — omitting the flag is a no-op)
p dsr update --id 01... --clear valid-until
p dsr update --id 01... --clear allowed-emails --clear allowed-domains
# revoke the link entirely (the canvas is kept)
p dsr delete --id 01...
# scaffold a spec for --config-file
p dsr scaffold --out share.jsonThe --config-file spec
p dsr scaffold prints the starter. The spec has three blocks — source, security, landing — and the CLI sends source and security spread verbatim into the create POST body, so their keys are the wire (snake_case) names, not flag names. landing is not part of that body: it becomes a follow-up PUT to /{id}/landing-page after the share exists.
The source block picks what is shared. The scaffold emits canvas_id (an example value) and an empty sharebox_items: [], so both source keys are visible in the printed JSON; an empty array asks for nothing, so the untouched scaffold shares canvas_id. To share files instead, fill sharebox_items, delete the canvas_id line, and add instance_id (or pass --instance-id). p dsr create --help lists the same field names.
"source": { "instance_id": "01...", "sharebox_items": [{ "file_id": "01..." }, { "file_id": "01...", "page_index": 2 }] }| Key | Type | Meaning |
|-----|------|---------|
| canvas_id | ULID string | Share an existing canvas. Exactly one of canvas_id / sharebox_items. |
| sharebox_items | [{ file_id, page_index? }] | Build a new ShareBox canvas from these files and share that. file_id is a ULID; an integer page_index shares that single page of the file instead of the whole file (--sharebox-file-ids can only send whole files, so a per-page share needs the spec). Exactly one of canvas_id / sharebox_items. |
| canvas_name | string | Name of the ShareBox-built canvas. Server default is ShareBox <timestamp>. |
| account_id | ULID string | Account attached to the ShareBox-built canvas. Required when the instance is configured to require an account on canvas creation. |
| template_id | ULID string | Canvas template the ShareBox-built canvas is built from. Omitted, the server looks for the instance's share-tagged template. |
| instance_id | ULID string | The instance to create in. --instance-id overrides it when both are given, and it is also what the landing-page PUT is scoped to. |
canvas_name, account_id, template_id and security.tags name or shape the canvas the server mints, so they only mean anything for a sharebox_items source — with canvas_id the canvas already exists and there is nothing to name. tags is the one the CLI enforces: a non-empty tags alongside canvas_id is rejected rather than sent to be dropped. The scaffold's empty tags: [] is accepted, since it asks for nothing.
security holds the access settings (auth_strategy, allowed_emails, allowed_domains, valid_until, allow_forward, allow_download, tags) and every key in it is spread into the POST body exactly as written. The CLI checks three of them and no more: auth_strategy against the allowed values, the allowed_emails/allowed_domains that strategy requires, and tags (above). The rest — allow_download included — it forwards untouched and makes no claim about, so read the created share back if you need to confirm a value took. The scaffold deliberately omits allow_download: it is stored on the canvas (every link on that canvas reads it), and the server writes it only when the key is present — omitted, an existing canvas keeps its current value and a new ShareBox canvas carries none, which the viewer treats as downloads allowed. Add "allow_download": false to the spec to turn downloads off. Sending the block as written also matters against an already-shared canvas — see the --if-exists note above. landing holds landing_page_template_id (discover ids with p dsr-landing list) and landing_page_context; left at the scaffold's null / {} no landing page is bound at all. Binding one — from the spec or from --landing-template-id — needs an instance: --instance-id or source.instance_id. Without it the CLI skips the bind and returns the share with a warning naming the flag.
update PATCHes /core/api/protected/shared-links/{id}, which distinguishes an absent key from an explicit null — so omitting --valid-until leaves the stored expiry exactly as it is, and --clear valid-until is what resets it. --clear is repeatable and accepts valid-until, allowed-emails, allowed-domains (the CLI sends null for the first and [] for the lists, matching what each field's server schema accepts). An empty --allowed-emails/--allowed-domains is rejected on both create and update, because "" from an unset shell variable would otherwise be sent as [] and wipe the stored list. --clear is a separate flag rather than an in-value sentinel such as --valid-until "" because an empty string is something a script can produce by accident from an unset variable, and none is already a real --auth-strategy value. tags and --allow-download are create-time only — the PATCH surface does not accept them, and --clear says so rather than dropping them silently.
--allow-download has a --no-allow-download twin, so both directions are sendable (PIT-7521). Without it commander could only ever produce true or nothing: passing the flag said “allow”, and there was no way to say “do not allow” — the OFF state could not be expressed at all, only left out. Declaration order is load-bearing: the positive flag of each pair is declared first, because commander takes a boolean's default from whichever it sees first. ⚠️ The value records the sharer's intent and drives the viewer's UI; it is not a server-side download block — the file URL is already in the payload the browser receives.
Clearing an allow-list is refused while a strategy that needs it is in force, because an empty allow-list denies every recipient rather than admitting them — the clear would brick the link, not open it. specified_emails/verified_emails need allowed-emails; specified_domains/verified_domains need allowed-domains. The check covers a strategy passed in the same call and the one already stored on the share (read from the public GET /core/api/shared-links/{id}/config, since the PATCH surface has no GET). If that read fails the update still goes through, with a warning saying the check did not run. Two ways out:
# relax the gate in the same call (or in an earlier one)
p dsr update --id 01... --auth-strategy any_email --clear allowed-emails
# or just replace the list instead of clearing it
p dsr update --id 01... --allowed-emails [email protected]--landing-context from a file
--landing-context takes a JSON object as a single shell argument, which PowerShell cannot express: it strips the double quotes a JSON object cannot do without, so on Windows the flag has no usable spelling at all. --landing-context-file <path> is the same field read off disk, and it is available on both dsr create and dsr bind-landing:
echo '{"rep":"Sam","account":"Acme"}' > ctx.json
p dsr bind-landing --id 01... --instance-id 01... --landing-template-id 01... --landing-context-file ./ctx.jsonExactly one of the two may be given: passing both fails VALIDATION (exit 3) before any request, naming the pair. The file must hold a JSON object — valid JSON that is an array or a scalar is rejected with the same code, as is malformed JSON. --landing-context-file is a landing flag like the rest, so it is mutually exclusive with --config-file (whose landing.landing_page_context block is that spec's own way of saying the same thing).
p dsr get — read a share back
p dsr get --id <id> returns the share's current settings. The protected shared-links surface has no GET, so this wraps the public pre-auth config the sharing page itself loads (GET /core/api/shared-links/{id}/config) and returns that body unchanged. Nothing is reshaped, so the field names are the wire names.
That body is the share's access config, and nothing else: auth_strategy, allowed_domains, allowed_emails, valid_until, allow_forward (plus allow_forward_raw, the rep's own toggle before the admin allow_dsr_forwarding setting is applied), the landing-page binding as landing_page_template_id / landing_page_template_snapshot / landing_page_context, and created_at / modified_at. There is no canvas or file field — the endpoint selects neither, so hold on to the canvas that create returned if you need it.
p dsr get --id 01...
# confirm what a create actually stored
p dsr create --instance-id 01... --canvas-id 01... --auth-strategy specified_domains --allowed-domains acme.com
p dsr get --id 01...Use it to confirm what create or update stored (create is a get-or-create, so what you passed is not always what the share ends up with) and to check a link before changing it. There is one id failure mode: any id that matches no share answers 404 → NOT_FOUND (exit 5). The endpoint does not validate the id's format — it matches pitcher_sharedlink.id, a plain varchar — so a malformed id is a miss like any other and returns NOT_FOUND too, not VALIDATION.
create returns the share as { id, type, file, canvas, instance, organization, status, url, short_url } — url is the full link; there is no full_url field.
get reads a public, pre-auth endpoint, so it needs no special permission beyond a valid key. Creating, updating and deleting need a share-capable key (canvas.share on the link's own instance when enable_granular_permissions is on); binding a landing page needs --instance-id, and an admin or editor key to bind any share in the org — a member key can bind only the shares that member created (PIT-7525). On create, a denied bind returns the link with a warning. Sending --allow-download / --no-allow-download (or allow_download in a spec) with an existing --canvas-id needs you to own that canvas or hold canvas.update on it — the setting is stored on the canvas — otherwise the create is refused as AUTH and nothing is written; a ShareBox source is always yours, so it never hits this. Discover landing template IDs via p dsr-landing list.
To run the ld command:
Author L&D (MicroLearn) content — courses, modules, certificates, and assessments — against the standalone L&D backend. The general form is p ld <resource> <verb>, plus the top-level p ld create-canvas recipe (create → set-canvas → publish in one step). --instance-id is required on every subcommand.
Unlike the other commands, L&D targets a separate backend (not <org>.my.pitcher.com). The base URL is resolved automatically from the instance's ld-app config; override with --ld-url or the NG_LD_API_URL env var (e.g. for a region whose ld-app config is unavailable). Auth is the same org x-api-key. The surface is authoring-only — enrollment, attempts, progress, groups, and analytics are not exposed (the org key cannot reach them). set-canvas likewise has no --require-recompletion: the backend rejects that flag for API-key callers (the bulk rep-completion reset is human-admin-only), so it could only ever 403.
# headline: author a designed canvas course in one step (create → set-canvas → publish)
p ld create-canvas --instance-id 01... --title "Onboarding 101" --canvas '{"content":{"data":[]}}' --publish
# courses
p ld course list --instance-id 01... --status draft
p ld course create --instance-id 01... --title "Intro" --description "A short intro course." --level Beginner --duration 10 --sections-count 1
p ld course set-canvas --instance-id 01... --id 01... --canvas-file ./canvas.json
p ld course publish --instance-id 01... --id 01...
p ld course add-to-modules --instance-id 01... --id 01... --module-ids m1,m2
# modules / certificates / assessments
p ld module create --instance-id 01... --title "Basics" --subtitle "Getting started" --description "Foundational module."
p ld module add-courses --instance-id 01... --id m1 --course-ids c1,c2
p ld cert create --instance-id 01... --title "Certified Rep"
p ld assessment create --instance-id 01... --title "Quiz 1"
p ld assessment add-question --instance-id 01... --id a1 --question '{"type":"true_false","questionText":"...","config":{},"points":1}'
# override the L&D base URL (else resolved from ld-app config)
NG_LD_API_URL=https://<lambda-host>/dev/api p ld course list --instance-id 01...MCP: the same surface is exposed as pitcher_ld_* tools, plus a create_ld_canvas_course recipe prompt and a pitcher://design/ld-canvas-course resource. NG_LD_API_URL is the only way to override the L&D base URL under MCP (no per-call override, by design).
