npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  1. Local p mcp now serves ONLY 3 tools (pitcher_dev_sync, pitcher_dev_sync_revert, pitcher_publish_app), down from 124. Everything else is the hosted server https://pitcher-mcp.fly.dev/mcp. p mcp no longer reads NG_API_KEY/NG_ORG/NG_URL/NG_ENV; run p login once (--domain or PITCHER_DOMAIN when logged in to several orgs).
  2. Every other command uses your p login session. --api-key (automation) still wins when passed; NG_API_KEY is used only when you have NO p login session, and is otherwise ignored with a notice. --org no longer defaults to dev on the session path: the session's org is the target. --domain cannot be combined with --api-key or --org.
  3. The Claude Desktop .mcpb extension is retired. Claude Desktop adds the remote URL natively.
  4. p analytics kpi requires --type (one of the 12 KPI types listed in p analytics kpi --help). The old default all always returned a 500.
  5. Node ≥ 22.12 (engines).

Help

p --help
# or for specific command help:
p command --help

Sign 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 --all
  • p logout is 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 new p login over 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, else PITCHER_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 (mode 0600, directories 0700). The access token refreshes itself; an expired session asks you to p login again.

  • The sign-in listener uses http://127.0.0.1:53682/callback only. Over SSH, forward port 53682 and use --no-browser.

  • Precedence: --api-key → your p login session → NG_API_KEY. With a session, --org acme (or --url https://acme.my.pitcher.com) selects the acme session; a --url/--publish-url on another host is refused rather than sent your token.

  • CI is unaffected: it passes --api-key and 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 logout the tools answer AUTH until you p login and restart the server.
  • pitcher_publish_app is always marked destructiveHint. pitcher_dev_sync/_revert get it unless the session's org is a test org (an unknown environment counts as production).
  • dev-sync is a one-shot sync as a tool; the continuous watch mode 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 Production

Options

  • --api-key <apiKey>: API Key for authentication (can also use NG_API_KEY env 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 pitcher

Options

  • --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 use NG_API_KEY env 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 the dist/ folder
  • The watch command 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 myapikey

To 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 myapikey

To 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 myapikey

To 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 myapikey

To 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 myapikey

To 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 myapikey

To 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 myapikey

To 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 myapikey

Run 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 myapikey

The 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 myapikey

create 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 myapikey

list 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 myapikey

To 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 myapikey

MCP: 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 myapikey

List 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 myapikey

To 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 myapikey

add 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 myapikey

add-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 myapikey

list 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 myapikey

upsert 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.json

The --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.json

Exactly 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).