# @hypersoniclabs/helix-cli

`helix` — the **HELIX creator CLI**: scaffold, validate, and publish Instant Worlds to the HELIX
backend. It shares bundle validation with the server via `@hypersoniclabs/helix-manifest`, so "validate passed but
publish failed" can't happen for contract reasons.

## Architecture boundary

All creator feature and operational logic, validation, file mutation, network behavior, and tests
live here. MCP packages may expose transport schemas, resources, instructions, and thin delegation
to these commands, but must not reimplement this logic.

## Install

```bash
npm install -g @hypersoniclabs/helix-cli
```

## Authentication

`helix login` opens your browser to the website's `/cli-login` page; you sign in there and the CLI
captures a **client token** via a one-shot loopback redirect. For CI/headless, pass a token directly:
`helix login --token <token>` (mint one in the **Creator Hub** → *Settings → Client Tokens*).

The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
`Authorization: Bearer <token>` on publish calls. `helix login` validates it against
`GET /api/v1/users/me` before saving and reads the token's `exp` so the CLI knows when it lapses.

> **The backend & website default to the build's environment:** the published `@hypersoniclabs/*`
> CLI targets production (`api.helixgame.com` / `helixgame.com`); the staging `@helixdev/*` CLI targets
> the staging backend (`helix-backend-staging.up.railway.app`) + staging website (`next.helixgame.com`).
> Override per run with `--api-url`/`HELIX_API_URL` and `--web-url`/`HELIX_WEB_URL` (e.g. a local/dev backend).

## Commands

| Command | What it does |
|---|---|
| `helix login [--api-url <url>] [--web-url <url>] [--token <t>] [--no-browser]` | Sign in via the browser (`/cli-login`), or `--token` for CI/headless. Stores the client token. |
| `helix logout` | Remove the stored token. |
| `helix whoami` | Show current login status. |
| `helix init <dir>` | Scaffold a minimal Three.js world (`helix.json` + `index.html` + `main.js`). |
| `helix install [--update]` | Resolve a world's `systems`/`abilities` pins (a v0.2 **or v0.3** manifest) → materialize the modules, the `three` import map, and `helix.runtime.ts`. |
| `helix validate [dir]` | Validate a bundle locally — the exact rules the server enforces. |
| `helix publish [dir] [--thumbnail <file>] [--upload-source] [--source-dir <path>]` | Validate → resolve/create the world → upload files → finalize → set the cover image → print the play URL. `--upload-source` additionally sends the project's source through a separate private channel so the world can be edited on the website later — **off by default**. |
| `helix list` | List your worlds. |
| `helix item list-slots [--json]` | Print the Character-Creator vocabulary a wearable declares: the cosmetic slot tags, and the genders. |
| `helix item publish <mesh.glb> --title <t> [--collectible-supply <n>] [initial distribution flags] [--dry-run\|--quote]` | Publish a Standard item by default, or one fixed-supply Collectible. The item definition is separate from acquisition routes. |
| `helix item distribution create\|list\|update\|disable …` | Manage Marketplace/World Claim or Buy routes without republishing the item or paying another publish fee. |
| `helix device create <dir> --id <slug> --title <t> --model <mesh.glb> --template standing-desk\|clock` | Scaffold a GLB-backed `helix.device/1` definition. There is intentionally no website Device editor. |
| `helix device validate [dir] [--json]` | Validate the definition, GLB bytes and named-node bindings, typed state/controls, capabilities, and Home+World compatibility. |
| `helix device test [dir] [--mode single\|multiplayer\|both] [--json]` | Exercise every interaction, persistence remount, and deterministic one-/two-client authority convergence. |
| `helix preview-video <slug> <youtube-url>` | Set or clear a published world's YouTube preview. |
| `helix thumbnail set <slug> <file>` | Upload a thumbnail or preview image through the CLI-owned media path. |
| `helix assets search\|get\|versions\|track\|install\|update` | Search the typed Vault with explainable ranking; inspect/manage annotations and immutable versions; install by durable ID with verified SHA-256 receipts. Material installs default to lean runtime KTX2 maps; use `--material-renditions source` for source PNGs or `all` for both families. |
| `helix assets start\|status\|generate\|resume` | Start or poll generation without duplicates. Props/characters use the main Dreamer path; standalone assets use the shared broker and report the auto-published `vaultAssetId`. `generate` and `resume` block waiting for the job (see `--timeout`/`--poll-interval` below). |
| `helix assets voices [--search <text>] [--category premade\|professional] [--page-size <n>] [--page-token <token>] [--json]` | Discover safe text-to-speech voices through the authenticated HELIX catalog, including language, labels, preview, default/recommended selection, and pagination. |
| `helix assets generate-image\|generate-material\|generate-audio\|generate-environment` | Run the supported standalone generation adapters. Material is a validated PBR bundle (1K default, explicit 2K); splats declare object/environment scope. Animation remains an explicit unavailable boundary. |
| `helix assets start-reference\|generate-reference` | Start or fully drive a character job from a local four-view PNG/JPEG/WebP. The backend validates and reuses the exact sheet; it skips image generation while preserving mesh, texture, rig, LOD, thumbnail, billing, and Vault publication. `generate-reference` blocks waiting for the job (see `--timeout`/`--poll-interval` below). |
| `helix assets generate\|generate-reference\|resume [--timeout <ms>] [--poll-interval <ms>]` | These three drive a Dreamer prop/character job to completion (concept → mesh → texture → rig → publish) and block until it finishes. `--timeout` bounds how long the CLI process waits before giving up (default **1200000ms / 20min**, unchanged if omitted); `--poll-interval` sets how often it checks job status while waiting (default **2000ms**). A timeout is a CLIENT-SIDE giveup, not a job failure — the Dreamer job keeps running server-side. The error prints the exact `helix assets resume <job-id>` command to check on or continue it; on a genuinely slow stage (e.g. mesh generation), rerun with a larger `--timeout`, or just poll separately with `helix assets status <job-id> --pipeline dreamer`. |
| `helix assets materials [--resolution <res>] [--human]` | Discover the small built-in material palette. `--resolution` keeps only materials carrying that texture resolution. |
| `helix assets material <id> [--resolution <res>] [--human]` | Resolve one material to its immutable map URLs at the resolution you pick (default: the pack's own default). |
| `helix assets credits\|check-loaders` | Render asset credits from provenance and reject models requiring unsupported loaders. |
| `helix character import <body.glb> [--map <json>] [--fit source\|canonical] [--provenance <json>]` | Convert one self-contained third-party humanoid through the public `helix-humanoid@1` pipeline. The command rejects external/remote resources and resource bombs before parsing, generates bounded LODs, and emits a source/license/transformation/checksum receipt beside the output. `--fit canonical` aligns the rest skeleton/proportions; it does not prove that Base Male-targeted garments fit a different body surface. |
| `helix character retarget-animation <clip.fbx\|clip.glb>` | Retarget a raw Mixamo/Meshy clip directly to `helix-humanoid@1`. FBX conversion is deterministic and runs inside the CLI; no hidden Blender/DCC pre-export is required. |
| `helix world audit\|source-audit\|perf-gate\|prove-live` | Run the blocking world QA, performance, and deployed-build identity gates. |
| `helix world scene-source inventory\|compile <helsinki\|bistro> <archive>` | Verify and safely inventory a known CC BY source archive, then compile deterministic local proxy/detail GLBs and an existing-schema Scene v2 package. |
| `helix world scene-source publish <world-id> <package-dir> [--publish]` | Validate the Scene v2 package and print its multipart build receipt. Execution remains dry-run-only until backend private-pin entitlement lands; `--publish` currently fails closed, and access control never changes CC BY rights. |
| `helix unreal init\|validate\|doctor\|package [project]` | Create and gate a shared-runtime or native-game publish manifest, check optional editor MCP, and assemble a local upload-shaped package. |
| `helix unreal publish <candidate> --world <slug>` | Upload a **cooked** Unreal world to HELIX: verify the candidate, presign, upload every artifact, finalize, and promote it as the world's active build. |
| `helix doctor [--project <dir>]` | Print the environment + whether the @helix toolchain (CLI/MCP/SDK/manifest) is current. |

## Unreal runtime projects

`helix unreal init <project> --mode shared-runtime|native-game` creates `helix.unreal.json`.
Shared-runtime projects ship cooked content/Blueprints against a pinned HELIX runtime; native-game
projects ship separate signed client/server builds and may use custom C++ and plugins. Both require
Unreal 5.8, the HelixSDK plugin, and HelixPlatform identity/session integration.

Networking is explicit: `unreal-native`, `helixnet`, `custom`, or `none`. Only `helixnet` requires
contract v21; blank Unreal projects using arbitrary GameMode/Pawn classes and native networking do
not need Lyra Experiences, Colyseus, or the HelixNet contract.

`helix unreal validate` is the publish gate and does not require MCP. `helix unreal doctor` adds the
loopback/editor-only Unreal MCP authoring checks. `helix unreal package` copies validated cooked or
native artifacts into a local checksummed staging directory — it is a local assembly step, not a
publish.

### Publishing a cooked world

`helix unreal publish <candidate> --world <slug-or-id>` takes the directory the Unreal
`HelixWorldPublish` commandlet writes — `world-build-manifest.json`, its `.sha256` receipt,
`cook-plan.json`, and `artifacts/<role>/…` — and gets it live in one command:

```
helix unreal publish Saved/HelixPublish --world my-world --max-players 32 --visibility unlisted
```

It verifies the candidate against the bytes on disk (every digest recomputed — a manifest is a
claim, not evidence), registers the Build and presigns each artifact, streams the uploads,
finalizes so the server re-verifies every byte by size and SHA-256 and countersigns the manifest,
then promotes the Build as the world's active release. `--no-promote` stops after verification;
`--verify-delivery` additionally checks what a client would actually download.

The idempotency key defaults to the cooked manifest's own digest, so re-running after a dropped
connection resumes the same Build rather than minting a second one.

Only **shared-runtime** worlds publish this way. A World Build may not contain creator
executables, so `native-game` builds are a separate distribution path.

## Working in a git worktree? Run this first

```
npm run check:dev-links
```

`package.json` links its sibling HELIX packages by relative path
(`file:../helix-manifest`, `file:../helix-web-sdk`). That is right in the canonical
layout, where the siblings sit beside this clone — but in a **git worktree** `../` is
the worktree parent, so the links resolve to whatever happens to be there: a checkout
parked on an older branch, or nothing at all.

The symptom is not "broken dev link". It is **a branch that looks broken**: `tsc`
reporting `has no exported member …` in files your change never touched, and whole
suites failing at import (`could not find the dev shell in @hypersoniclabs/helix-sdk`).
People have concluded a PR was broken on the strength of exactly this.

`check:dev-links` names which link is wrong, which imported symbols the linked sibling
does not export, and how to fix it without repointing a shared directory other
worktrees resolve through.

## Publish flow

### Platform system updates

`helix install` records platform-owned systems in `public/helix.runtime.json`.
Compatible ranges such as `^0.2` are externalized and resolved to an active,
immutable catalog module when a world launches, so a compatible platform fix can
reach an already-published world without creator edits or republishing. A new
runtime version reaches no world until the platform deliberately publishes it to
the catalog and it goes active there — building or merging one changes nothing on
its own, and there is no rolling "latest". Exact
versions such as `0.2.43` map directly to the immutable catalog URL and remain
pinned. A custom Vite config is never rewritten: install reports
`legacy-bundled` until the creator explicitly externalizes `@helix/*`.
`helix validate`, `helix publish`, and the publish API refuse that legacy mode,
so every newly accepted system-bearing web World is either safely compatible or
explicitly pinned. `latest` and `*` are not accepted publish ranges because they
can cross a compatibility boundary.

The platform runtime is on three **0.185.1**. The version is a property of the
pinned system — `humanoid-character@^0.3` resolves to an envelope declaring it —
and `helix install` bakes it into the import map immutably, so an already-published
world keeps running whatever it baked. A world behind that line still publishes:
`helix validate` and `helix publish` only warn, naming the upgrade path (re-pin
`^0.3`, `helix install --update`, rebuild, republish), and `helix doctor` reports it
(and a local devDep `three` that has drifted off the platform runtime) before you build.

That update contract is deliberately narrower than "the SDK updates itself":

| World dependency | What the built bytes contain | What can advance without republishing |
| --- | --- | --- |
| Platform systems (`@helix/humanoid-character`, `@helix/engine-core` and declared subpaths) with a compatible range | Bare imports plus a generated import-map entry | The compatible catalog target on the next fresh launch |
| Platform systems with an exact version | Bare imports plus the exact immutable catalog URL | Nothing; the pin stays byte-identical |
| Creator SDK (`@helix3/helix-sdk`) | The SDK module graph bundled into the world's own JavaScript | Nothing; SDK changes require rebuilding and republishing the world |
| Installed abilities (`public/helix_modules/*`) | Verified ability code/assets copied into the world build and indexed by `installed.json` | Nothing; update/install and republish are explicit creator actions |
| Creator game code and assets | The world's own immutable build files | Nothing; the platform never rewrites a published creator bundle |

The generated import map, `helix.runtime.json`, and
`helix_modules/installed.json` are the authoritative way to inspect which side
of this boundary a particular build uses. Browser or proxy caching may serve a
previous compatible resolver response during a transient outage; the descriptor's
`lastKnownGood` field is provenance metadata, not an executable in-world fallback.
Without a cached resolver response, a cold resolver failure fails closed.

Native features remain individually overridable without pinning the whole
character runtime. The character system's **native avatar features** — carried
emotes and the radial wheel (including its Customize door), x-ray, pointing,
sitting, the avatar camera, the two camera legs, and rendering each player's
universal avatar — are constructed by the character chassis itself, so every
character World has them whether it was built on `Character` or on
`CharacterMultiplayer`, and a World published today receives ones added later on
its next fresh launch.

A World refuses what it does not want by NAME, in the character config it passes
at create time — not in `helix.json`, and nothing here is a publish-time concern:

```ts
config: { character: { native: { disabled: ['xray', 'pointing'] } } }
```

Ids: `emotes`, `emote-wheel`, `xray`, `pointing`, `sitting`, `avatar-camera`,
`camera-first-person`, `camera-third-person`, `universal-avatar`. It is a
**blocklist, never an allow-list**, for the same reason this whole delivery
contract exists: an allow-list would freeze every already-published World out of
every feature added afterwards. A refusal also removes the feature's key,
controller button, touch pad and prompt; `universal-avatar` is the top switch and
takes emotes, the wheel, x-ray, pointing, sitting and the avatar camera with it
(the camera legs survive — a World still needs a camera); refusing BOTH camera
legs throws at create; and an unrecognised id is kept with a warning rather than
rejected, because it may belong to a newer runtime. The older switches still
work and route into the same resolver: `CharacterMultiplayer.create`'s
`emotes: false`, `emoteWheel: false` and `universalAvatars: false`, and the
config keys `character.emote.enabled`, `character.emote.wheel`,
`character.xray.enabled`, `character.universalAvatar.enabled`. (`localControl:
false` is not one of these — it declares a spectator body the World drives its
own camera and input for.) Every other compatible native fix continues to update
normally.

This surface needs `@helix/humanoid-character` **0.2.54** or newer; an older
runtime ignores `character.native.disabled` silently, so confirm against the
resolved system's config schema rather than assuming. The full creator reference
is the character world recipe in the HELIX MCP docs
(`read_doc({ name: "character-world" })`, section 8g); the authoritative id list
and defaults are that config schema, readable from the installed system
manifest.

The runtime descriptor is mandatory for bundles that declare systems. It makes
the delivery boundary inspectable by the CLI, MCP, backend, and fleet census;
it does not execute code or silently rewrite an old minified bundle.

`helix publish` maps directly onto the backend's Instant World API:

1. `checkBundle` validates `helix.json` + files via `@hypersoniclabs/helix-manifest`.
2. `GET /api/v1/instant-worlds/me` → reuse the world with the manifest's slug, else
   `POST /api/v1/instant-worlds`.
3. `POST /api/v1/instant-worlds/:id/builds` → presigned upload tickets.
4. Files PUT directly to storage (6 concurrent), content-type/length bound to each ticket.
5. `POST /api/v1/instant-worlds/:id/builds/:buildId/finalize` → byte-exact verify, activate, publish.
6. `GET /api/v1/instant-worlds/:slug` → the play URL.

### Opt-in source upload

A published world ships only its **built bundle**, so there is nothing on the server to edit later.
`--upload-source` adds a second, separate upload that fixes that:

7. `POST /api/v1/instant-worlds/:id/source` → a presigned PUT into a **private** bucket (this call
   also adopts the world into a project, which is the key its future workspace hangs from).
8. `helix-source.tar.gz` PUT with `cache-control: no-store`.
9. `POST /api/v1/instant-worlds/:id/source/finalize` → the server verifies the stored byte count
   equals the declared one, then records the version.

- **Off by default.** Source is never sent unless you pass the flag. Not a prompt — an interactive
  confirm would break CI and agent-driven publishes.
- **What goes in**: the project directory, honouring `.gitignore`, always excluding `node_modules`,
  `.git`, `dist` and `.vite` — the same exclude set the website's own workspace checkpoints use.
- **Which directory**: `[dir]` is the BUILT bundle, so the source defaults to its parent. Override
  with `--source-dir <path>`.
- **Never part of the build.** The bundle's content-type allowlist rejects archives, raw `.ts` is
  excluded from a bundle by design, the bundle size budget is the playable budget, and every build
  file lands on a public CDN path with a year-long immutable cache. Source uses its own private
  channel for exactly that reason.
- **Limit**: 128 MiB compressed, checked locally before anything is uploaded.

Before upload, module-based multiplayer worlds are checked for server realizability: every bank must
be published with the declared checksum and the shared manifest validator must accept all module
budgets under the 100,000 tick-node ceiling. Fix the named module/bank when this gate refuses a
publish. For exceptional recovery only, `--skip-realizability-check` bypasses this check; flat-v0
worlds are unaffected.

The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported for other CLI
modules. Agent integrations should instruct or delegate to the `helix` commands above rather than
reimplementing operational behavior in a transport package.

## Vault lifecycle

Use the durable Vault UUID as the handle. Search results include independent lexical, hard-filter,
measured-performance, reuse, and availability explanations; they do not collapse those signals into
a made-up score. `helix assets install` resolves an immutable version, follows the `/download`
indirection, verifies its server checksum and byte count, installs related material maps, and writes
`public/helix.assets.json`. Material installs keep the descriptor and default to the runtime KTX2
map family only; choose source PNGs with `--material-renditions source`, or both families with
`--material-renditions all`. Reinstalling with a different selection removes the stale opposite
family. Non-material assets keep their existing related-artifact behavior. For a reproducible agent build, pass `--asset-version`,
`--checksum-sha256`, and `--size-bytes` together; a partial pin or any metadata/download mismatch
fails before a provenance receipt is written.

Generation is source-agnostic at the backend:

- `prop` and `character` start through the main Dreamer route because they use its verified
  universal-item flow.
- `image`, `material`, `audio`, and `gaussian_splat` start through the shared asset route.
  Gaussian splats must declare `object` or `environment` scope.
- `animation` fails locally with `CAPABILITY_UNAVAILABLE`; it never starts a chargeable job.

Run `helix assets status <job-id> --pipeline dreamer|asset` to poll an existing job. A successful
default-on generation reports `vaultAssetId` and `vaultAutoPublish: "published"`; an explicit
creator opt-out reports `"disabled"`.

For `.helix-scene.json`, the open Vault catalog is not the scene-local budget. Reuse a small
material palette and instance repeated assets. The canonical ceilings are:

| Profile | Unique materials | Draw calls | Decoded texture memory | Triangles | Particles |
| --- | ---: | ---: | ---: | ---: | ---: |
| mobile | 48 | 300 | 256 MiB | 750,000 | 5,000 |
| desktop | 128 | 1,200 | 1 GiB | 4,000,000 | 50,000 |
| cinematic | 256 | 4,000 | 4 GiB | 15,000,000 | 250,000 |

These are publish-contract ceilings, not frame-rate promises; world runtime QA still applies.

The lifecycle acceptance harness intentionally has no CLI-local demo. Point it
at a built world in `helix-web-demo-worlds` that consumes the published SDK and
humanoid runtime:

```bash
npm run qa:vault-lifecycle -- \
  --world-template /path/to/helix-web-demo-worlds/worlds/sdk-showcase
```

Publishing is an explicit release-only action and additionally requires a new
approved slug:

```bash
npm run qa:vault-lifecycle -- \
  --world-template /path/to/helix-web-demo-worlds/worlds/sdk-showcase \
  --publish --world-slug ch1146-vault-lifecycle-qa-<unique-suffix>
```

The publish receipt requires one usage increment, one world association, and
an idempotent second publish with no additional increment.

### Audio generation

All three audio modes use the same authenticated Dreamer route and automatically publish the
completed MP3 to Vault. The command waits up to 30 minutes so a ten-minute music request has enough
time to render, ingest, and publish. It prints the durable `vaultAssetId`, measured Spark charge,
provider cost, and server-stamped `billingEvidence`. It does not download another local copy unless
`--output` is supplied.

```bash
# Sound effect
helix assets generate-audio "heavy metal door slamming shut" \
  --mode sound_effect --duration-seconds 3 --prompt-influence 0.4 --output door.mp3

# Music
helix assets generate-audio "hopeful orchestral exploration theme" \
  --mode music --duration-seconds 180 --force-instrumental

# Text to speech
helix assets voices --search narrator --page-size 10
helix assets generate-audio --mode text_to_speech \
  --text "Welcome to HELIX." --voice-id <id-from-catalog> \
  --language-code en --stability 0.5 --speaker-boost
```

Use the opaque `--page-token` printed by a page to continue. Voice cloning and
voice administration are deliberately not exposed.

Use a deterministic four-view sheet when you already have approved FRONT, BACK,
LEFT, and RIGHT character art and do not want Dreamer to generate another image:

```bash
helix assets generate-reference ./character-four-views.png \
  "compact friendly service robot, game-ready humanoid proportions" \
  --title "Service Robot" \
  --target-polycount 12000
```

The reference endpoint is character-only. The file is image-guarded, normalized,
checksum-bound to the job, semantically validated, and mirrored before approval.
Retries reuse those same bytes and never invoke or charge the image provider.

## Material texture resolution

A platform material can ship more than one texture resolution. `--resolution` picks which one you
get; without it you get the pack's declared default, byte-for-byte what every earlier CLI returned.

```bash
helix assets material brick-block                      # the pack's default resolution
helix assets material brick-block --resolution 2k      # the 2K variant's map URLs
helix assets material brick-block --resolution 2048    # same thing — numeric spelling
helix assets materials --resolution 2k --human         # only materials that carry 2K
```

Accepted values are the keys the catalog itself declares (`1k`, `2k`, …), case-insensitively, plus
the numeric aliases `1024`/`2048`, which map onto the variant authored at that pixel edge.

Both commands print JSON by default (agents and the MCP delegation parse it); `--human` prints a
readable summary instead. Either way the output states **which resolution was resolved and which
ones the material offers**, under `resolution`:

```jsonc
"resolution": {
  "requested": "2k", "resolved": "2k", "pixels": 2048, "default": "1k",
  "available": [{ "key": "1k", "pixels": 1024 }, { "key": "2k", "pixels": 2048 }],
  "applicable": true
}
```

Three rules make this safe to rely on:

- **No silent fallback, ever.** Asking for a resolution the material does not carry is an error that
  names the ones it does. You never receive a different resolution than the one you asked for.
- **Old packs keep working.** A catalog with no `resolutions` block (`schemaVersion: 1`) is treated
  as having exactly one resolution, keyed `default`, derived from its existing `maps`. Such a pack
  never claims to be "1k" — it does not say how big its textures are, so neither do we.
- **Procedural materials are not an error.** `glass` and `procedural_water` carry no texture maps;
  a resolution request against them is ignored and the output says so in `resolution.note`.

`helix assets install` has no resolution flag: Vault related-artifact roles are semantic
(`source.albedo`, `runtime.ktx2.normal`, `sky.backdrop`), never resolution-tagged, so a filter there
would filter nothing.

## Item publish flow

`helix item publish` creates an item definition. It defaults to **Standard**: unlimited issuance,
no public serial, and no resale. `--collectible-supply N` is the only edition selector: it fixes a
positive supply, assigns public serials, makes instances Marketplace-resellable, and reserves
serial `#1` for the creator. There are no separate tradable, serial, giftable, personal, or
discoverability switches.

1. Local validation, before anything leaves the machine: the `--kind`, the Character-Creator
   `--slot` and `--gender` (**both required** for a wearable, exact-match), the title/slug/tag
   limits, and the GLB container magic. `--dry-run` stops here and prints exactly what would be
   sent.

`--kind` accepts `wearable`, `avatar`, `add_on`, `vehicle`, `prop`, and `home` — `prop` and `home` are the current
product names (see helixgame.com) for what the API still calls `home_item` and `home_shell`; both
spellings work identically and `helix item publish --kind prop …` sends the exact same
`"kind":"home_item"` on the wire as `--kind home_item …` does. This is a CLI-vocabulary alias only —
it is unrelated to the backend's separate `prop` universal-item kind (art/collectible items).

An `add_on` declares `--fits` and optional `--ops`; each flag accepts inline JSON or a JSON file.
A base that hosts add-ons declares `--host-kind` and a measured `--host-surface` the same way.
The JSON keys are sent unchanged as the backend's `fits`, `ops`, `hostKind`, and `hostSurface`
payload fields. `--dry-run --json` is the offline contract check before uploading anything.
The published host packs are `vehicle` and `character`; a character host must be an avatar and
its measured surface must declare `"skeleton":"helix-humanoid@1"`.

```bash
helix item publish ./tunic.glb --kind add_on --title "Fantasy Tunic" \
  --fits ./tunic-fits.json --ops ./tunic-ops.json --dry-run --json

helix item publish ./base.web.glb --kind avatar --title "Universal Base" \
  --thumbnail ./base.webp --host-kind character \
  --host-surface ./character-surface.json --dry-run --json
```
2. `--quote` asks the server for the definition publish fee, the creator tier's included free-
   Collectible units, excess issuance authorization cost, creator serial `#1`, price floor and
   resale policy. It uploads nothing and charges nothing.
3. `POST /api/v1/universal-items/upload` — `multipart/form-data` with a `mesh` file part, an
   optional `thumbnail` file part, and the metadata as ONE JSON string in `payload`.
4. The server verifies the mesh **inline** (triangles, texture edges, materials, and — for a
   wearable — whether it is a skinned garment or a rigid socketed accessory) and returns the
   created item together with the verification verdict and warnings.

Price, world, schedule and limits live on a separate distribution. Price `0` means a shell-
confirmed **Claim**; a positive price means **Buy** and must meet the server-owned floor. A world
route also has a stable key, which world code passes to
`Helix.marketplace.purchaseDistributionKey(key)`. `--max-per-player unlimited` explicitly clears a
cap; free Collectibles default to one per player when omitted. Distribution changes never republish
the definition.

```bash
# Standard, free Marketplace Claim
helix item publish ./postcard.glb --title "Harbor Postcard"
helix item distribution create <item-id> --channel marketplace --price-lix 0

# Standard, paid World Buy
helix item distribution create <item-id> --channel world --world fishing-cove \
  --key fishing_rod --price-lix 250 --max-per-player unlimited

# Collectible, free World Claim (creator receives #1 from the 500 fixed units)
helix item publish ./trophy.glb --title "Season One Trophy" --collectible-supply 500 \
  --distribution-channel world --distribution-price-lix 0 --world tournament \
  --distribution-key season_one --max-per-player 1 --quote
# Review the quote, then run the same command without --quote to publish.

# Collectible, paid Marketplace Buy
helix item publish ./bluefin.glb --title "Legendary Bluefin" --collectible-supply 10000
helix item distribution create <item-id> --channel marketplace --price-lix 500 --max-claims 9000

helix item distribution update <distribution-id> --price-lix 600 --ends-at 2026-12-01T00:00:00Z
helix item distribution disable <distribution-id>
```

Collectible resale uses Marketplace escrow. The creator royalty is fixed at 5%; the launch seller
platform fee is 10% for Free and 5% for Plus (future Gold 2.5%, Diamond 0%). The buyer pays no
surcharge, creator self-sale omits a redundant royalty leg, and self-purchase is prohibited.
Every acquisition has one 24-hour relisting cooldown. Basic requires email; Verified means a unique
verified mobile plus good standing and may resell immediately subject to that cooldown. Identity Verified
is reserved for later high-risk capability, and tooling accepts future Business Verified. Payout holds are
separate: 3 days base, risk-extendable up to 15 days.

The slot table in `src/item.ts` MIRRORS `helix-backend-api → src/universal-items/cc-wearable-slot.ts`,
and the gender list mirrors `src/universal-items/cc-gender.ts` the same way. They are a local fast
path so a typo costs no upload; the server remains the authority and its 400 lists every valid
value. Adding a slot or a gender means editing both, in the same PR wave.

`--gender` takes `male`, `female`, or `male,female`, and is **required** for `--kind wearable`.
There is no `both`/`unisex`/`all`: Unreal's cosmetics enum has exactly Male and Female, so a garment
that fits every body names both — one HELIX row with `{male, female}` is the web equivalent of
Unreal's two entries sharing one mesh. The field goes on the wire as `genders`; `ccGenders` is the
database column and the backend rejects it by name rather than dropping it in silence.

## Develop

```bash
npm install
npm test       # jest
npm run build  # tsc → dist/ (bin: dist/index.js)
npm run lint
```
