npm · @three-ws/avatar-cli

Avatars belong in your
build pipeline.

Scaffold a hash-anchored avatar manifest from a wallet and a GLB, validate it against the published schema, and print an embed snippet that actually renders. Offline, deterministic, and safe to put in a release script.

npm install -g @three-ws/avatar-cli

or run it once with npx @three-ws/avatar-cli

Runtime deps
1
Network calls
0
Commands
4
Node
18+
Four commands

The whole surface area

No config file, no project scaffold, no state directory. Each command does one thing to one file and tells you what happened.

init

writes

Scaffolds a schema-valid manifest from a wallet, a name, and a mesh file. Reads the mesh once to compute its SHA-256 and size, so the document cannot disagree with the bytes it was built from.

three-ws-avatar init \
  --owner eip155:1:0x742d35Cc… \
  --name "Nicholas" \
  --mesh ./michelle.glb \
  --skeleton mixamo \
  --out manifest.json

validate

exits 0/1

Checks a manifest against @three-ws/avatar-schema and prints every error at the instance path where it happened. Non-zero exit makes it a build gate rather than a report.

$ three-ws-avatar validate broken.json
✖ broken.json is invalid (7 errors)
  • / must have required property 'mesh'
  • /id must match a schema in anyOf

hash

pipe-safe

Lowercase hex SHA-256 of a file, alone on stdout so it composes. The same digest init writes into mesh.sha256, which is what makes drift detection a one-line comparison.

$ sha=$(three-ws-avatar hash ./michelle.glb)
$ echo "$sha"
28d788538f7b22b8e00c1d715fffc380…

preview

emits HTML

Validates, then prints a resolver URL, an <agent-3d> snippet paired with the loader that registers it, and a zero-install iframe. Warns when mesh.uri is still a local path.

$ three-ws-avatar preview manifest.json
› web component
<script type="module" src="…/agent-3d.js"></script>
<agent-3d src="…/michelle.glb"></agent-3d>
Proof, not a promise

The snippet on the left is rendering on the right

This is not a screenshot. The markup below is exactly what preview prints, pasted into this page unmodified. If it ever stopped working, this section would break in public.

preview output
<script type="module" src="/agent-3d/latest/agent-3d.js"></script>
<agent-3d
  src="https://three.ws/avatars/michelle.glb"
  style="width:100%;height:420px"></agent-3d>

Both lines matter. <agent-3d> is an unknown element until the loader registers it, so pasting only the tag gives you an empty box with no warning. That is why preview always prints them together.

Viewer unavailable The component loader did not register. The snippet is still correct; see the web component reference.

michelle.glb · Mixamo rig · served from three.ws

The artifact

What a manifest actually is

Seven required fields. uri says where the mesh is, sha256 says what it must be. Those are different promises, and only the second one survives someone re-uploading a different model to the same path.

{
  "schemaVersion": 1,
  "id": "eip155:1:0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "name": "Nicholas",
  "mesh": {
    "uri": "https://three.ws/avatars/michelle.glb",
    "sha256": "28d788538f7b22b8e00c1d715fffc380bb06a304613d522c20523d3d6ff79bc2",
    "format": "glb",
    "kBytes": 830
  },
  "skeleton": "mixamo",
  "owner": {
    "chain": "eip155:1",
    "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
  },
  "createdAt": "2026-07-30T19:23:34.595Z"
}
  • id A CAIP-10 account, or a name service handle ending in .eth, .ws, or .sol.
  • mesh.sha256 Content address for the model. The field that catches a silent re-export.
  • skeleton One of avaturn, mixamo, rpm, vrm-humanoid, custom. Decides which animation library retargets onto the rig.
  • owner Chain-scoped account, so ownership is unambiguous off-platform.

Five optional fields the schema also accepts: animations, accessories, traits, creator, and signature. The last one is the interesting one: everything else is a claim you are making, while a signature is a claim someone else can verify without trusting your server.

In your release

Fail the build, not the page

A manifest that is only correct on the day you wrote it is a document, not a guarantee. Two checks turn it into one. Neither needs network access.

01 Did the mesh change?

expected=$(node -p "require('./manifest.json').mesh.sha256")
actual=$(three-ws-avatar hash ./michelle.glb)
[ "$expected" = "$actual" ] || {
  echo "mesh hash mismatch"; exit 1;
}

Catches the re-export nobody mentioned, the lossy optimization pass, the teammate who swapped the model and kept the filename, and the CDN that started serving an error page with a 200.

02 Is every manifest still valid?

find . -name '*.avatar.json' \
  -not -path './node_modules/*' -print0 \
  | xargs -0 -n1 three-ws-avatar validate

xargs exits non-zero if any single invocation failed, so one bad manifest fails the whole step. Wire it into whatever already runs on release.