# agentmodel

`agentmodel` is AgentModel in your terminal. Describe a part and it comes
back as parametric OpenSCAD with a customizer, versioned in your account at
[model.brightmotion.io](https://model.brightmotion.io) exactly as if you had
made it on the site. From there you turn the knobs, render and measure the
mesh on your own machine, push OpenSCAD you wrote yourself, and share the
result — and so can your AI agent, without ever opening a browser.

## Install

```
curl -fsSL https://model.brightmotion.io/install.sh | sh
```

It needs **Node 20 or newer** and nothing else: one JavaScript file lands in
`~/.agentmodel/bin/` and a two-line shim at `~/.local/bin/agentmodel`. Make
sure that directory is on your `PATH` — the installer tells you if it is not.
Re-run the same line any time to upgrade.

Two optional extras, only for rendering on your machine:

- [OpenSCAD](https://openscad.org) — needed by `pull stl`, `pull png`,
  `pull views`, `measure` and `check`.
- `ffmpeg` — needed by `pull gif` and `pull mp4`.

Everything else works without either. The server stores OpenSCAD source, not
meshes, so rendering always happens where you are.

## Sign in

```
agentmodel login
```

That prints a URL and a short code, then opens the URL when it can:

```
Open  https://model.brightmotion.io/cli/auth?code=ABCD-EFGH
Code  ABCD-EFGH
Waiting for approval… (expires in 10 min)
```

Check that the code on the page matches the one in your terminal, press
**Approve**, and the terminal finishes on its own. No password is ever typed
into the terminal, which is the point: an agent can run this for you, show you
those two lines, and never see a credential.

```
agentmodel whoami
agentmodel logout
```

The token lands in `~/.config/agentmodel/credentials.json`, mode 600.

**On a machine with no browser** — ssh, a container, a CI job — the URL is
printed anyway, so open it wherever you are. `agentmodel login --no-browser`
never attempts to open one; neither does the CLI when its output is a pipe or
when `AGENTMODEL_NO_BROWSER=1` is set. A denied code, an expired code and the
ten-minute timeout all exit **3**.

**For scripts and CI**, skip `login` entirely and set a token in the
environment:

```
export AGENTMODEL_TOKEN=…
```

Or let it sign in with a password each run:

```
export AGENTMODEL_EMAIL=you@example.com
export AGENTMODEL_PASSWORD=…
```

`agentmodel login <email>` also works interactively and prompts for the
password.

## Your first model

Ask for something. This is the one step that calls a model and costs a few
cents; everything after it is free.

```
$ agentmodel new "a 40mm cable clip for a 6mm cable"
✓ conversation 8f3a2c1d-7b0e-4a51-9c2f-1d6e0b4a77c3
→ generating with google/gemini-3.7-flash…
  wrote ./a-40mm-cable-clip-for-a-6mm-cable/cable-clip.scad
→ rendering STL…
  wrote ./a-40mm-cable-clip-for-a-6mm-cable/cable-clip.stl
```

The first eight characters of that id are all you ever have to type. Look at
what you got — four angles plus a contact sheet, rendered locally:

```
$ agentmodel pull 8f3a views
v1 (8f3a2c1d)
→ rendering views (iso/front/right/top + sheet)…
  wrote cable-clip-views.png (+ 4 angles)
```

Now the interesting part. The model came with a customizer, and those knobs
are yours to turn without asking a model anything:

```
$ agentmodel params 8f3a
name       value  type    range        note
cable_d        6  number  [4:1:12]     cable diameter
length        40  number  [20:1:80]    clip length
wall         2.4  number  [1.6:0.2:4]  wall thickness
3 parameter(s) at v1 (8f3a2c1d)
```

```
$ agentmodel set 8f3a wall=3 --check
✓ v2 (b1c47e02) — wall=3
40 × 12 × 9 mm
  volume     2.7481 cm³  (2748.10 mm³)
  surface    41.2205 cm²
  triangles  1284
  manifold   yes
```

`set` rewrote the assignment **in the source** and checkpointed a new version,
so the code stays the single source of truth. Nothing was billed. `--check`
rendered the result and measured it. Check it properly before you print it:

```
$ agentmodel check 8f3a --strict
```

Then send it to someone:

```
$ agentmodel share link 8f3a
https://model.brightmotion.io/share/8f3a2c1d
```

And write down how it went, so the note lives next to the geometry that
produced it:

```
$ agentmodel note 8f3a "printed at 0.2mm — fits perfectly"
```

## How it thinks

Three ideas carry the whole surface, and they mirror the web app exactly.

**A conversation is a tree of messages.** Retries, edits and `--at` all branch
it. The **leaf** is the active tip — what the site's preview shows and where a
new message lands.

**A generation is an assistant message carrying OpenSCAD.** Generations are
what you preview, parameterise, pull and diff. `history` labels them `v1…vN`
along the active branch, oldest first.

**A `<ref>` names one of them:**

| Ref        | Means                                                      |
| ---------- | ---------------------------------------------------------- |
| `latest`   | the newest generation on the active branch (the default)   |
| `v3`       | the third generation on the active branch                  |
| `~1`       | one generation back from latest (`~2`, …)                  |
| `9b75b55a` | a message-id prefix — reaches any generation on any branch |

The `v` numbers follow the active branch, so moving the leaf back with
`focus` renumbers it: the generations you left ahead of the leaf lose their
`vN` and are addressed by their id prefix instead. Nothing is deleted, and
the CLI says so when it happens:

```
$ agentmodel focus 8f3a v1
✓ leaf → v1 (v2 b1c47e02 now off-branch)

$ agentmodel history 8f3a
…
1 generation(s) on other branches — history --all-branches
```

The rules that follow from it:

- **Reads never move anything.** `pull --at v2`, `params --at v2`,
  `diff v2 v4` inspect old states in place.
- **Writes at an old ref branch from it.** `set --at v2 width=44`,
  `say --at v2 "…"`, `push --at v2` create a new message parented on v2 and
  move the leaf there — the same thing as clicking an old version in the web
  preview and tweaking it.
- **`focus <conv> <ref>`** moves the leaf without creating anything.
- **`history <conv>`** prints the tree so you can see what `v2` or `~1` will
  resolve to before acting. `*` marks the active branch, `←leaf` the tip, and
  `--all-branches` adds what is off it, marked `(off-branch)`.

Every id — conversation, share, folder — accepts a unique prefix, and anywhere
a conversation is expected you can paste a `/share/…` or `/editor/…` URL.

## Catalog

```
agentmodel list
  [--folder <id>] [--since 30d] [--limit 20]
  [--privacy private|unlisted|public]
agentmodel search <query> [--by <handle>]
agentmodel show <conv> [--messages]
agentmodel history <conv> [--all-branches]
agentmodel profile [<handle>]
agentmodel watch [<conv>]
```

`show` gives title, folder, privacy, model, generation and message counts, the
latest ref, links (web / share / profile) and cost to date. `--messages` adds
the transcript — one row per message, with the version ref where there is
one, the files that were attached and the text itself:

```
$ agentmodel show 8f3a --messages
ref  role       when              attachments  text
v1   assistant  2026-09-06 09:14               A 40mm cable clip…
     user       2026-09-06 09:20  clip.jpg     make the lip deeper
v2   assistant  2026-09-06 09:20               Deeper lip, same…
```

The table truncates the text to the column; `--json` carries every message
whole, with the attachment records (id, filename, media type, size, URL) —
which is how you confirm the model was actually given your photo.

`profile` is the CLI view of `/u/<handle>` and works signed out; with no
handle it guesses yours from your email's local part. `watch` tails your
account's live event stream as JSON lines until you stop it.

In `list`, the model column names the model that made the newest generation,
`push` when the versions are OpenSCAD you pushed yourself, and `—` when
nothing has been generated yet.

`list` is also `ls`, and `history` is also `versions`.

## Fetch and render

```
agentmodel pull <conv> [format]
  [--at <ref>] [-o <path>]
  [--frames 36] [--size 700] [--render-timeout 300]
```

`pull` fetches the **source** for a ref and renders everything else on your
machine. The formats:

- `scad` — the source (the default). `-o -` writes it to stdout.
- `stl` — binary STL.
- `png` — 900 px isometric (elev 62°, az 25°).
- `views` — iso/front/right/top at 700 px plus a 2×2 contact sheet.
- `gif` / `mp4` — a turntable orbit; when both are asked for, the frames are
  rendered once.
- `bundle` — scad + stl + iso png + views sheet + `manifest.json`, zipped.

`-o` is a file for single-file formats and a directory for `views` and
`bundle`; without it everything lands in `./<slug-of-title>/`.
`--render-timeout` bounds each format, and a format that fails is reported
while the rest still run. The table of what was written shows paths relative
to where you are; `--json` gives the absolute ones, for a script that may run
from anywhere.

## Create — this is what spends money

```
agentmodel new "<prompt>"
  [-a photo.jpg]... [--model gemini-3.8-flash]
  [--folder <id>] [--title "..."] [--no-generate]
  [--formats scad,stl,png,views] [--out <dir>]
agentmodel say <conv> "<message>" [-a img.png]... [--at <ref>]
agentmodel generate <conv> [--attempts 3] [--at <ref>]
agentmodel models
```

`new` creates the conversation and generates; `--no-generate` just files the
prompt and attachments for later. `say` appends a turn and generates from it.
`generate` (also `gen`) re-runs from the leaf without adding a message — the
retry button. All three take `--formats` (default `stl,scad`) and `--out`.

`-a` / `--attach` is repeatable: at most 4 per message, images ≤ 10 MB, video
≤ 30 MB. The media type comes from the extension — `.jpg .jpeg .png .webp
.gif` for images, `.mp4 .mov .webm` for video. **Video is only understood by
Google models**; attach one to an Anthropic or OpenAI model and the server
replaces it with a note saying a video was attached that the model cannot see.

`--folder` takes a folder id or a unique prefix of it (`folders list` shows
them). It is checked **before** anything is created, so a typo costs nothing
and exits 2. In the rare case where the conversation is created and filing it
then fails, the id is printed and put in the JSON as
`{ "conversationId": …, "filed": false, "error": … }` with exit 1 — the model
exists and is paid for, so never run `new` again on that message.

With `--json`, the object these three print names things exactly:

- `prompt` — the text sent to the model, or `null` for a bare `generate`,
  which sends none.
- `conversationTitle` — the thread's title.
- `title` — what the model named this generation.
- `attachments` — the records the server stored: `id`, `filename`,
  `mediaType`, `bytes`. An empty array means the message really had none, so
  this is how you confirm the model was given your photo.
- `artifacts` — absolute paths to every file written; `usdCost` and `usage`
  are what this generation cost.

`models` prints the picker with prices per 1M tokens. Aliases you can pass to
`--model`:

```
gemini-3.7-flash   gemini-3.8-flash   gemini-3.1-pro
claude-fable-5     claude-sonnet-5    claude-opus-4.8
gpt-6-astra        gpt-5.6-sol        glm-5.3
```

Or any full id from the `models` table.

## Parameters — cheap, deterministic, no model

```
agentmodel params <conv> [--at <ref>]
agentmodel set <conv> width=42 hole_d=6.5
  [--at <ref>] [--check] [--message "..."]
agentmodel diff <conv> <ref1> <ref2> [--bbox]
agentmodel focus <conv> <ref>
```

`params` prints the parsed OpenSCAD customizer block — name, value, type,
range or options, group, comment. It lists exactly what OpenSCAD's own
customizer would: names starting with `$`, everything under a
`/* [Hidden] */` group, and values computed from other variables are not
parameters and are left out. `set` on one of those is refused:

```
$ agentmodel set 8f3a eps=0.01
✗ eps is not a customizer parameter (hidden or derived)
```

`set` rewrites those assignments in the source and checkpoints a new
generation; no model is called and nothing is billed. `--check` renders the
result and reports the mesh.

`diff` shows the parameter changes and a unified source diff; `--bbox` renders
both sides and adds the bounding-box delta.

If the change is a dimension, it is `set`, not another generation.

## Push your own OpenSCAD

```
agentmodel push <conv> model.scad [--at <ref>] [-m "..."]
agentmodel push --new "<title>" model.scad [--folder <id>]
```

Push OpenSCAD you wrote — or that your own model wrote — as a real generation
on the thread. The site cannot tell it apart from a generated one: same params
panel, same versioning, same share links, and nothing is billed. `--new`
catalogs a local file as a fresh conversation. Source is capped at 1 MB.

## Share

```
agentmodel share link <conv>
agentmodel share email <conv> <address> [--message "..."]
agentmodel share list <conv>
agentmodel share revoke <conv> <shareId>
agentmodel privacy <conv> private|unlisted|public
```

`share link` makes the conversation unlisted and prints its `/share/<id>` URL
— anyone with the link can view it; saving or forking needs an account. Undo
it with `privacy <conv> private`. `share email` mints a revocable token per
recipient and auto-invites them on sign-up; those are what `share list` and
`share revoke` operate on. `public` also lists the creation on your
`/u/<handle>` profile.

## Organize

```
agentmodel rename <conv> "New title"
agentmodel move <conv> --folder <id>
agentmodel folders list
agentmodel folders new "name"
agentmodel fork <conv|share-url> [--at <ref>]
agentmodel note <conv> "fits perfectly at 0.2mm" [--at <ref>]
agentmodel delete <conv> [--yes]
```

`fork` copies any conversation you can read — yours, a share link, someone's
public creation — into your own account. `note` is the lab-notebook move: a
timestamped annotation with no model call. `move` is also `mv`.

Anywhere a folder is expected — `move`, `new --folder`, `list --folder` — a
unique prefix of its id works, the same way conversation ids do.

`delete` removes a whole conversation: every version on it, permanently. It
names what it is about to delete and asks first; `--yes` (also `-y`) skips
the question, which is what a script or an agent needs. It is also `rm`.

## Measure and check

```
agentmodel measure <conv|file.stl> [--at <ref>]
agentmodel check <conv|file.stl> [--at <ref>] [--strict]
```

Both render the ref into a temporary directory, analyse the STL and throw the
directory away. You get the bounding box, volume (in mm³ and cm³), surface
area, triangle count, and whether the mesh is **manifold** — every edge shared
by exactly two triangles. `check` adds the verdict, and with `--strict` exits
1 when the mesh is not manifold, so a script can gate on it. Either command
also takes an `.stl` file directly, with no account involved.

```
$ agentmodel measure 8b30bfa5 --at v2
110 × 110 × 11 mm
  volume     54.9352 cm³  (54935.24 mm³)
  surface    244.7216 cm²
  triangles  4096
  manifold   yes
  degenerate  16 zero-area facets (slicers drop these; not a
              manifold problem)
```

That last line is information, not a warning: zero-area facets are a normal
by-product of CSG, every slicer drops them, and they are not holes. The
verdict to act on is `manifold`.

## Output and exit codes

Human tables go to **stderr**. `--json` puts exactly one JSON object on
**stdout** and suppresses the tables, so `agentmodel list --json | jq`
composes while a person still sees what happened. Reads go quiet under
`--json`; writes still print their one-line confirmation on stderr. `--quiet`
silences stderr entirely. (`watch` is the exception: it streams JSON lines.)

| Code | Meaning               |
| ---- | --------------------- |
| 0    | ok                    |
| 1    | error                 |
| 2    | not found / bad usage |
| 3    | auth or approval      |
| 4    | quota / spend cap     |

`agentmodel --help` lists every command; `agentmodel <command> --help` shows
one with its flags.

## Environment

| Variable                | Default                             |
| ----------------------- | ----------------------------------- |
| `AGENTMODEL_API_URL`    | `https://model.brightmotion.io`     |
| `AGENTMODEL_TOKEN`      | the token `login` stored            |
| `AGENTMODEL_EMAIL`      | an alternative to `login`           |
| `AGENTMODEL_PASSWORD`   | with `AGENTMODEL_EMAIL`             |
| `AGENTMODEL_MODEL`      | `google/gemini-3.7-flash`           |
| `AGENTMODEL_NO_BROWSER` | `1` = never open a browser on login |
| `OPENSCAD_BIN`          | auto-detected                       |

## For agents

Every command takes `--json`, so an agent never has to parse a table. Give
your agent the skill file and it knows the whole surface:

```
mkdir -p .claude/skills/agentmodel
curl -fsSL https://model.brightmotion.io/skill.md \
  -o .claude/skills/agentmodel/SKILL.md
```

Use the equivalent skills folder for whatever tool you run. There is also a
short index for agents at
[model.brightmotion.io/llms.txt](https://model.brightmotion.io/llms.txt),
which carries the setup instructions inline, and this manual as raw markdown
at [model.brightmotion.io/docs.md](https://model.brightmotion.io/docs.md).

**The one rule to give an agent about money.** `new`, `say` and `generate`
call a model and cost you money; the agent should say what it is about to run
and let you stop it. `params`, `set`, `diff`, `push`, `pull`, `measure`,
`check`, `share` and `list` are free and deterministic.
