upim is a command-line client for the UnoPim 3.1 REST API. It is meant for people and for coding agents. The runtime dependency is Effect 4. Node.js 24 or newer runs the TypeScript entrypoint directly.
npm install -g @kylelittle/upimNode.js 24 or newer is required. The binary is upim. The published package runs compiled JavaScript. A checkout runs the TypeScript entrypoint directly.
From a checkout:
npm install
npm linkFrom GitHub:
npm install -g github:klittle32/upim
pnpm add -g github:klittle32/upim
bun add -g github:klittle32/upimInstall the agent skill into the project you are working in. Pi and other Agent Skills clients discover .agents/skills/upim.
upim skill install
upim skill install --user
upim skill install --path .letta/skills/upim
upim skill install --path ~/.claude/skills/upimWith no --path, the skill is written to .agents/skills/upim in the current project. --user writes ~/.agents/skills/upim. --path is any other harness directory: a relative path starts at the current directory, and ~ is your home directory.
UnoPim integrations use four values: base URL, OAuth client id, client secret, and the robot user's username and password. Create them under Configuration → Integrations, then generate the secret key. The robot password is shown once.
Interactive setup:
upim config initNon-interactive setup:
upim config init --non-interactive \
--base-url https://pim.example.com \
--client-id "$UPIM_CLIENT_ID" \
--client-secret "$UPIM_CLIENT_SECRET" \
--username "$UPIM_USERNAME" \
--password "$UPIM_PASSWORD" \
--save-passwordOr write the file yourself. The default location follows the XDG base directory spec:
- config:
$XDG_CONFIG_HOME/upim/config.jsonor~/.config/upim/config.json - tokens:
$XDG_STATE_HOME/upim/tokens.jsonor~/.local/state/upim/tokens.json
Both files are written with mode 0600. See examples/config.json.
upim config path
upim config example
upim config show--config path/to/config.json stores tokens beside that file unless UPIM_TOKEN_FILE is set. Profiles live in one document. upim config use staging selects one, and --profile staging overrides it for a single command.
Environment variables override the selected profile and are not written back:
| Variable | Purpose |
|---|---|
UPIM_BASE_URL |
Base URL |
UPIM_CLIENT_ID |
OAuth client id |
UPIM_CLIENT_SECRET |
OAuth client secret |
UPIM_USERNAME |
Robot username |
UPIM_PASSWORD |
Robot password |
UPIM_PROFILE |
Profile name |
UPIM_CONFIG_FILE |
Config file, same as --config |
UPIM_CONFIG_HOME |
Directory that contains config.json |
UPIM_STATE_HOME |
Directory that contains tokens.json |
UPIM_TOKEN_FILE |
Exact token file path |
The API uses the OAuth 2.0 password grant. POST /oauth/token is called with HTTP Basic auth of clientId:clientSecret and a JSON body:
{ "username": "robot", "password": "secret", "grant_type": "password" }Refresh uses the same endpoint:
{ "refresh_token": "...", "grant_type": "refresh_token" }upim stores the access token, refresh token, and expiry. It refreshes about 60 seconds before expires_in, and again once if a call returns 401. A failed refresh falls back to the password grant when a password is available. Token requests are not sent before every API call: the defaults are 10 token requests per minute and 120 API requests per minute. A 429 waits for Retry-After (or an exponential backoff) and retries up to 5 times.
upim auth login
upim auth status
upim auth refresh
upim auth logoutAPI commands log in on their own when a saved token is missing and credentials are available.
Start with upim endpoints --json. Each action includes the describe command that prints its fields. Failures are one JSON object on stderr, with error, message, and path.
upim --wizard walks through a command in the terminal. It needs a TTY.
The base URL is the site root, for example https://pim.example.com, not a path ending in /api/v1/rest.
Command help stays a flag list. The field reference is separate, and the same record is what agents should read:
upim describe products create
upim describe products create --json
upim schema --jsondescribe prints the method, path, parameters, a static JSON Schema, and a copy-paste command. --json is the stable machine-readable form; its example.command uses the CLI flag names. schema prints one OpenAPI 3.1 document generated from those Effect schemas.
upim describe products create --live --family default reads the attribute dictionary from the configured server and fills the value buckets. It does not change what is rejected locally.
The schemas check the static envelope only. A product body must include sku, family, and type, and values must use the documented buckets, but attribute codes inside those buckets are not known until you ask the server:
upim attributes list --json
upim families get default --jsonUnknown keys are sent through. A 422 from UnoPim is the real validation result. Measurement routes document the path and not the JSON body, so those payloads are not checked locally. --help for an action ends with upim describe <resource> <action>.
upim products list --limit 100 --pagination-type search_after --all
upim products list --filter 'updated_at:>=:2026-08-01 00:00:00' --pagination-type search_after
upim products get shirt-1 --with-completeness
upim products create --file product.json
upim attributes patch color --data '{"type":"select"}'
upim media product upload --file ./front.webp --sku shirt-1 --attribute image
upim passports publish shirt-1 --data '{"channel_id":1,"locale_ids":[1]}'
upim api GET /api/v1/rest/products --query limit=10Filters accept either a raw JSON --filters object, as documented by UnoPim, or repeatable --filter key:operator:value clauses such as sku:IN:a,b and status:=:true. --all follows links.next until it is null. It exits non-zero, and does not print a partial catalog, if it reaches --max-pages (default 1000) or sees a repeated links.next. Resume from the page or search_after in the URL from that error, or raise --max-pages. 304 Not Modified is returned as { "notModified": true } when you pass --if-none-match. --meta adds the status line and response headers.
Covered resources: attributes and options, attribute groups, families and variant structures, categories, category fields and options, products, configurable products, channels, locales, currencies, association types and fields, product/category/swatch media, digital product passports, and measurement families, units, and attribute bindings. upim api sends any other path with the same authentication.
npm test
npm run check
npm run journal -- "note about the current milestone"npm run journal records development notes with @earendil-works/pi-durable (and Chord, pi-ai, and the built-in coding tools) in .durable/. Those packages are dev-only.
Pushing a vX.Y.Z tag publishes that version to npm and opens the GitHub release. The tag name, without the leading v, must match version in package.json. A message longer than the version number becomes the release notes. Otherwise GitHub drafts them from the commits.
npm version patch -m "Release %s
- What changed."
git push origin main --follow-tagsnpm version minor and npm version major work the same way. The working tree has to be clean. The workflow uses npm trusted publishing, so a release does not ask for an npm token or a second login.
The package has to exist before npm will store that link. After the first publish, allow this repository's release.yml workflow to run npm publish directly:
npm trust github @kylelittle/upim --file release.yml --repo klittle32/upim --allow-publishLeave the environment name blank. New trusted publishers otherwise default to staged publishing, which would wait for a manual approval on every release.