Skip to main content

Annealage Mesh

Annealage Mesh is a little web tool for building 3D-printable parts with an agent, by pointing at them. You give it a folder, it serves up a 3D viewer in the browser with a Claude Code chat pane beside it, and the two of you get to work in there.

Ask for a part and the agent writes the CAD script in that folder, runs it, and the STL turns up in the viewer. Click the face that's wrong, say what's wrong with it, and off it goes to fix the script.

Annealage Mesh: the model with a human's orange pin and the agent's cyan callouts, the review panel, and the chat pane mid-answer

Why

I built this while iterating on a 3D-printed part with Claude Code. The CAD was generated from a script, I'd look at a render, and then spend ages typing things like "no, the inside corner on the far wall near the fan, not that one" trying to describe which face I meant. It was a pain, and half the time the agent picked the wrong spot anyway.

Pointing at the thing is just so much easier. So we built a viewer where I click the face and type the comment right there, and the agent gets it back with the actual coordinates, no guessing.

It's bidirectional too, which turned out to be the good bit. The agent can write its own callouts (a location plus a note) and they show up as pins in the viewer for me to see and reply to. So it ends up being a shared surface, I mark up what I want changed, the agent pins its questions on the geometry, and we go back and forth pointing at the same model instead of describing it in words.

Install

Python 3.10+. Four runtime dependencies, and only one of them is large: microdot for the server, platformdirs to find where your settings file belongs on your OS, tomli to read that file on Python 3.10 (3.11 and up have tomllib built in), and the Claude Agent SDK, which is how the chat pane talks to Claude Code. The SDK bundles the Claude Code CLI, so installing this pulls about 90 MB; the other three are pure Python and tiny. three.js 0.160.0 is vendored inside the package and served locally, so the viewer itself needs no network access at all.

On Linux you'll also want bubblewrap and socat:

apt install bubblewrap socat

That's what keeps the agent's shell contained, and agent mode won't start without them rather than quietly running you an uncontained one. macOS has its own sandbox built into the OS so there's nothing to install there. If you'd rather not bother, annealage-mesh view gives you the viewer on its own and needs neither.

Run it straight from GitHub, nothing to install, via uv:

uvx --from git+https://github.com/Annealage/mesh annealage-mesh ./path/to/part

Or install it as a tool:

uv tool install git+https://github.com/Annealage/mesh
# or
pipx install git+https://github.com/Annealage/mesh

Once it's up on PyPI that shortens to uvx annealage-mesh ./part.

Usage

Point it at a folder:

annealage-mesh ./build

It sets the folder up if it isn't already (a models/ and images/ directory, a CLAUDE.md stub, a .gitignore, and git init with one commit if git is installed), starts a local server, prints the URL with a per-run token in it, and opens your browser. Every .stl in there shows up in the viewer, toggle them on/off in the side panel.

There are three subcommands for when you want less than all of that:

annealage-mesh view ./build     # the viewer alone: no agent, no scaffold, no git
annealage-mesh init ./build     # set the folder up and stop
annealage-mesh doctor ./build   # what's installed, what's configured, then stop

If a folder of yours is actually called view, init or doctor, spell it ./view and it's read as the directory. Running inside Claude Code already? The bare form flips to viewer-only and says so, so you don't get an agent inside an agent.

  • Drag to orbit, scroll / pinch to zoom, right-drag or two-finger to pan.
  • Flip to "Add pin" mode, click the model to drop a pin, then type a comment against it in the panel.
  • Hit Submit. Your pins get written to mesh-comments.json in the served folder, which is what the agent reads.
  • Type in the chat pane to put the agent to work in that folder. Interrupt stops a turn mid-flight, and each turn shows what it cost.
  • The agent works the viewer too, not just the folder. It can move the camera, hide and show parts, screenshot what's on screen and pin its own callouts, so "show me the underside of that boss" is something it does rather than tells you to do.
  • Hit Pause in the topbar and everything that changes the view gets refused until you hit it again, so you can line up a shot or type a comment without it moving underneath you. It can still look while paused.
  • Need a distance between two features? Pick any two placed pins (yours or the agent's) in the "Measure" panel for ΔX/ΔY/ΔZ and the direct distance, drawn as a line in the view.
  • Attach a picture to a message with the paperclip, a paste, or a drag and drop: a photo of the printed part, a slicer screenshot, a reference drawing.
  • Hit "Sketch" to draw straight on the 3D view, circle the wall that's wrong, and send that as the picture. Quicker than a pin when the shape of the problem is the point.
  • "Export" in the chat header writes the conversation into review/ as markdown you can commit. The agent can do it too, with your approval, when you ask it for a record of what you decided.
  • The gear opens Settings: port, host, model, effort, and a couple of viewer preferences, each shown with where its value came from (a flag, this project's config, your own settings, or the built-in default) so you know which file to edit. Anything that needs a restart says so rather than pretending to apply. There's a Diagnostics block in there too, the same facts doctor prints, which is what you want when you're looking at this on a phone with no terminal.

It works on a phone too, the three panes become tabs and navigation is all touch (one finger orbits, two fingers pan / zoom). --host tailscale binds your tailnet address instead of loopback, which is what I use to look at a part on my phone while the agent iterates on the desktop.

Sessions are kept, so -c picks up the most recent conversation for that folder and -r lists what's there. Reloading the browser mid-turn doesn't lose anything, the conversation belongs to the session rather than the socket.

There's a fuller walkthrough in docs/user-guide.md covering the loop, the flags, remote access and what to do when something's off.

A few files turn up in the served folder:

  • mesh-comments.json - your pins and comments, written on submit (also appended to mesh-comments.log).
  • mesh-callouts.json - callouts to show in the viewer. Write pins here and they appear live (cyan, read-only). This is how an agent points back at the model.
  • images/ - pictures you attached, sketches you drew, and screenshots the agent saved. Meant to be committed.
  • .mesh/ - session event logs, this project's own config, any allow-always decisions you made, and a lock file so two servers can't fight over one folder. All of it gitignored except config.toml, which is shareable and holds no secret.
  • review/ - transcripts you exported. Created the first time you export one, not before.
  • CLAUDE.md - a stub describing the folder's contract, generated once if you don't already have one. Never overwritten.

What it'll ask you about

The agent's shell runs sandboxed, so a command that stays inside the project folder just runs without asking. That's deliberate, regenerating a part twenty times would be miserable otherwise. Anything that writes through its edit tools, wants out of the folder, or reaches the network gets you a card in the chat pane with the full command or file contents on it, and you allow it, allow it for the rest of the session, or deny it with a reason. The reason goes to the agent verbatim, so "not that file, do the enclosure instead" is more use to it than a bare no.

Its viewer tools split by what a mistake would cost. Reading anything, and driving the view itself, never asks: it can move the camera and hide parts freely, because you're looking at the screen while it happens and a card per camera move would just get clicked without reading. Pause is the control for that, not a prompt. What does ask is the three that leave something behind after you close the page: writing a callout, deleting one, and saving a screenshot into the folder.

An approval card for a Write, showing the whole file path and contents, with Allow, Always allow and Deny

Two things worth knowing about the containment. It stops writes and network, not reads, so a sandboxed shell can still read anything your user can. And the model can't drop the sandbox for a command by asking, which it does try if you let it.

If the folder you point it at has its own .claude/settings.json or .mcp.json in it, mesh won't start the agent until you've said you trust that folder. Those files can declare hooks, hooks are shell commands, and one kind runs before you've typed anything at all, so an unpacked model archive off the internet isn't something to hand a shell to sight unseen. Read them, then --trust-project-config accepts them, recorded against the exact contents you read so any later edit asks again.

It binds to 127.0.0.1 by default, and the startup banner tells you what it's reachable on every run. On anything that isn't loopback the token in the URL stops being defence in depth and becomes the only thing between the network and an agent with a shell, so keep that URL to yourself.

For AI agents

If you're an agent (or setting one up) working outside the chat pane, the contract is still just two JSON files in the served folder, unchanged.

Read the human's feedback from mesh-comments.json:

{
  "submitted_at": "...",
  "count": 1,
  "annotations": [
    { "id": 1, "part": "bracket", "label": "+Z", "point": [12.5, -3.2, 44.0],
      "normal": [0, 0, 1], "faceIndex": 1234, "comment": "this fillet's too sharp" }
  ]
}

point is the click location in model space (same units as the STL), so you can map a comment straight to a spot in the CAD script that generated it.

Write your own callouts to mesh-callouts.json and they show up as cyan pins in the viewer, live:

{
  "annotations": [
    { "id": 1, "author": "agent", "part": "bracket", "label": "+Y", "point": [0, 20, 10],
      "comment": "moved this wall out 2mm, that clear enough?" }
  ]
}

point and comment are the only fields that really matter, the rest are display niceties.

There's also a Claude Code skill in skill/ that wires this up as a workflow, so you can just tell Claude to use Annealage Mesh when it's working on printable models.

Licence

PolyForm Noncommercial 1.0.0, free to use for any noncommercial purpose. Commercial use needs a separate licence; see COMMERCIAL.md.

The Claude Code skill in skill/annealage-mesh/ is MIT, so it can be copied into any agent configuration without restriction.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

annealage_mesh-2.0.0.tar.gz (684.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

annealage_mesh-2.0.0-py3-none-any.whl (550.9 kB view details)

Uploaded Python 3

File details

Details for the file annealage_mesh-2.0.0.tar.gz.

File metadata

  • Download URL: annealage_mesh-2.0.0.tar.gz
  • Upload date:
  • Size: 684.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for annealage_mesh-2.0.0.tar.gz
Algorithm Hash digest
SHA256 2e503d9805889709e2aff2aca0900985bb3e74e1736ec5b469d2145bf3adaf15
MD5 c3e45acb034a82da33d604c5210be48f
BLAKE2b-256 52baf29ee68ca13e12e229bb9c5133b69bbcb0ad86bee3679138f381303ae5fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for annealage_mesh-2.0.0.tar.gz:

Publisher: publish.yml on Annealage/mesh

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file annealage_mesh-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: annealage_mesh-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 550.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for annealage_mesh-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e502653087f5b8ace2ad466ef25840f091a80d2367eceb6042c477e473c0f8a7
MD5 b50cf2794dc2b724fe29101aa2a8d8c4
BLAKE2b-256 4bbbda4df53bd65ec59967bd995b76803e9eb757d206e2079db0031c02761173

See more details on using hashes here.

Provenance

The following attestation bundles were made for annealage_mesh-2.0.0-py3-none-any.whl:

Publisher: publish.yml on Annealage/mesh

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page