Skip to content

Add benchup, a CLI and MCP server for editing .bup projects - #41

Merged
dannycoulombe merged 13 commits into
BenchUp3D:mainfrom
furilo:benchup-cli
Nov 13, 2026
Merged

dannycoulombe merged 13 commits into
BenchUp3D:mainfrom
furilo:benchup-cli

Conversation

@furilo

@furilo furilo commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Second PR from #13. It adds benchup, a command-line tool that reads and edits .bup projects from a shell or an AI coding agent, and serves the same tools over MCP. It builds on benchup-tools from #40, whose commits it also contains until that PR is merged.

The CLI

benchup guide                          # conventions, workflow and woodworking recipes for agents
benchup new shelf.bup --name "Shelf"
benchup scene shelf.bup                # objects, joints, dimensions, materials
benchup call shelf.bup create_board '{"name":"Side","origin_mm":[0,0,0],"size_mm":[18,300,900]}'
benchup call shelf.bup --batch ops.jsonl --dry-run   # many calls, one undo step, all-or-nothing
benchup call shelf.bup check_model     # problems to fix before building
benchup undo shelf.bup
benchup mcp shelf.bup                  # the same tools over MCP (stdio)
benchup open shelf.bup                 # open it in the desktop app
  • Every change is one undo step, labelled Agent: ….
  • Saves are atomic, and the CLI refuses to overwrite a file that changed on disk.
  • Shared projects are refused unless --allow-shared is passed.
  • In a batch, a call can use earlier results with references such as "$1.created[0]".

User documentation is in docs/cli.md. docs/agent-guide.md is compiled into the binary and printed by benchup guide. A Claude Code skill is included in crates/benchup-cli/skill/.

How it was shaped

I ran 12 furniture tasks four times with coding agents that knew only benchup --help. The tasks included editing the example project, drawers with dovetails, a back in a rabbet, a curved bedside table, a leaning shelf, dimensions and cut lists. After each round, I fixed the tools and the guide. Across the 12 tasks:

Round Commands Errors
1 ~701 72
2 285 21
3 218 3
4 ~245 ~7

The remaining errors come from limits in the core, listed below. Agents now check their work with a dry run ending in check_model.

Per task: commands (errors) in each round
Task Round 1 Round 2 Round 3 Round 4
Lengthen the desk in Example.bup 43 (3) 27 (0) 19 (1) ~61 (0), filled the notch in front of the post and searched for walls
Bookcase: 5 to 4 shelves per bay, keeping the joints 21 (3), orphaned joints 22 (0) 16 (0) 14 (0)
Dining table with mortise-and-tenon aprons 22 (0), joints reversed 18 (0) 15 (0) 15 (0)
Chest of 3 drawers, dovetailed fronts, one component 46 (3) 32 (9) 19 (0) 14 (0)
Back panel in a rabbet and a set-back plinth 14 (0) 27 (5) 17 (0) 18 (0)
Remake the bookcase in 19 mm solid oak 24 (3), no material 25 (0) 17 (0) 17 (1)
Bedside table: curved side edges, chamfered top ~261 (35) 26 (1) 24 (0) 36 (4), curve segment count
Leaning ladder shelf at 15° ~91 (16) 45 (1) 32 (0) 22 (0)
Dimensions and a label for the workshop 21 (1) 13 (0) 12 (0) 11 (0)
Modules, one shelf component, tags ~142 (8), joints broken 31 (5) 27 (2) 19 (~2), stopped short on purpose
Cut list and sheets to buy 8 (0) 12 (0) 8 (0) 8 (0)
A 1200 mm shelf in a 473 mm bay 8 (0) 7 (0) 12 (0) 10 (0), stopped and asked

The shelf-component task cannot be done fully: a board inside a component instance cannot be joined to the sides outside it. The agents now explain this and offer an alternative.

The resulting changes to benchup-tools:

  • Joints:
    • roles are documented (a carries the tenon or tongue, b receives it);
    • add_joint picks the woodworking orientation, validates the ranges the core uses, and refuses joints that cannot be cut, naming the joints a new one collides with;
    • get_object on a joint shows its settings and status;
    • new remove_joint and flip_joint tools;
    • erasing a board removes its joints, which works around Erasing a board leaves its wood joints pointing at a missing id #36.
  • check_model: reports boards that cut another in two, overlaps without a joint (checked against the real solids), joints that cannot be cut or look reversed, thickness that does not match the material, open solids and loose geometry.
  • New tools: get_cut_list, create_stock_material, move_to and get_guide. resize_board takes an anchor, and add_dimension can measure along an axis.
  • Faces: outward normals use a ray-parity test instead of "away from the part's centre", which flipped faces of concave parts (push_pull can create a side face with an inverted normal #37). push_pull and draw_polyline return the ids they create.
  • Groups: editing tools reach parts inside groups without enter_object.
  • Scene: it lists the axes (the front faces -Y), dimensions, texts, loose geometry, component definitions and inherited materials.

Changes outside the new crates

benchup-app now uses benchup_tools::files for the .bup format. The format had been implemented twice, and the copies had drifted: the tools could not read the PNG previews the app now saves. The CLI keeps a file's container when saving. cargo check -p benchup-app is clean, and the app's project and desktop-file tests pass.

.github/workflows/pr.yml runs the tests of both crates. site/llms.txt now mentions the command, noting that it is built from source for now, and links to its documentation.

Still to do

Testing

cargo test --locked -p benchup-tools -p benchup-cli passes: 49 tests in the tools and 17 in the CLI.

🤖 Generated with Claude Code

furilo and others added 11 commits November 13, 2026 00:29
Built on benchup-tools: list tools and schemas, read the scene, run any
tool or an all-or-nothing batch as one undo step, undo/redo, and serve
the same tools over MCP on stdio (protocol code from BenchUp3D#3). Saves are
atomic and refuse to overwrite a file changed on disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- benchup open hands a .bup to the installed desktop app (bundle id on
  macOS, file association elsewhere), or to --app / BENCHUP_APP.
- erase removes the joints of the boards it deletes (the core keeps them,
  BenchUp3D#36), refuses joint ids with a pointer to the new
  remove_joint tool, and the scene flags joints whose board is gone.
- Scope errors name every object to open, from the outside in; the help,
  MCP instructions and enter_object say how long a group stays open.
- find_objects: query is optional and its description matches what it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From 12 scenarios run by agents with only the CLI help to go on:

- Joints: add_joint picks the woodworking orientation (depth is where a
  goes into b; tongues and tenons across a's thickness, dovetails and laps
  across its broad side), validates ranges as the core does, refuses joints
  that cannot be cut, and get_object shows roles, settings, supported
  orientations and status.
- Faces: outward normals use a ray-parity test instead of "away from the
  part's centre", which flipped faces of concave parts and made push_pull
  go the wrong way. Concave faces get a center too.
- New tools: create_stock_material, move_to, check_model (unjoined
  overlaps, uncuttable or orphaned joints, open solids, loose geometry),
  get_cut_list and get_guide. resize_board takes an anchor.
- push_pull and draw_polyline return what they create (face_id), group
  brings a face's edges along, and the scene lists axes (front is -y),
  loose geometry ids, dimensions, texts and component definitions.
- CLI: "$N.field" references between batch calls, --dry-run, --brief,
  the project name in scene, benchup guide, and a Claude Code skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app saves projects as a PNG preview with the document in a private
buPd chunk, but benchup-tools only read gzip and plain JSON, so the CLI
could not open files saved by the desktop app. The format now lives in
benchup_tools::files and project_files.rs delegates to it. The CLI keeps
a file's container when it saves: a PNG keeps its (now stale) preview with
the document chunk replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/agent-guide.md is compiled into the tools (benchup guide, get_guide),
so the guide in the binary always matches it, and maintainers review it
with the other docs. docs/cli.md is the user documentation for the CLI;
the crate README points to both.

The guide now reflects the second round of agent tests: joints on one
board must keep 1 to 2 mm apart (backs in a groove when shelves are
housed, drawer bottoms in the sides only, tenon depth below P - s - t),
components cannot be joined to outside boards, dry runs ending with
check_model, result fields and nested references, leaning pieces built
upright and rotated by -θ, and recipes for cut lists and dimensions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- add_joint names the joints already on the boards when a new one cannot
  be cut, refuses boards at different group levels and assemblies with a
  clear reason, and returns a and b.
- check_model confirms box overlaps with the real solids (no alarm for a
  notched board around a post), warns when a joint's roles look reversed,
  follows materials set on parent groups, and reports the size of the
  boards rather than of the annotations around them.
- get_cut_list explains how to get minimum_stock_pieces; joint kinds use
  add_joint's names; push_pull also returns face_id; group explains a
  group of loose geometry; --brief keeps a scene's objects; descriptions
  say what tools return and what happens to joints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the third round of agent tests:

- Editing tools open the group around their targets for the call when all
  ids share one, as push_pull already did; enter_object is only needed to
  create parts inside a group.
- flip_joint swaps a joint's a and b, keeping its settings and re-picking
  the orientation; check_model points to it for reversed joints.
- check_model reports boards that cut another in two (passes_through) and
  boards whose thickness does not match their material, and counts the
  joints that cut.
- add_dimension can measure along an axis and returns length_mm and
  whether each end attached; its description no longer promises snapping.
- The scene shows materials inherited from groups and only the joint
  settings each kind uses.
- CLI: --brief keeps bounds and reading tools' answers; a failed batch
  shows the earlier results; group explains a part made from loose faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Editing inside groups without enter_object, flip_joint, settings by kind,
what "joints must not meet" means, rabbets with housed shelves, plinth and
top joints, drawer proportions, lengthening notched boards with push_pull,
tapered ladder uprights, dimensions (straight or along an axis, no
snapping), outlines in vertical planes, arc radius, housed parts requested
as components, and fixing problems the user did not mention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- check_model returns passed (no errors or warnings); ok only says it ran.
- Tools taking ids also accept a single "id".
- When a joint cannot be cut, only joints whose region touches it are
  blamed; with none, the hint points at the board's own geometry (curves
  with many segments, vertices near the joint planes).
- draw_polyline lists each new face with its center, so a split face's
  pieces can be told apart; get_object's description names center_mm.
- Guide: odd thickness changes and shelf centres, drawer bottoms stopping
  short of the front, components keeping existing joints, ladder uprights
  with level cuts, splitting faces to lengthen notched boards, walls in the
  example room, backs behind dividers, chamfered tops, curve segment
  counts, and what to do when there is no one to ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lack

The guide said there were no chamfer tools; BenchUp has a Router and a
Cutting Plan, which the tools do not expose yet. llms.txt now points
agents to the benchup command and the agent guide, noting that it is
built from source for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resolve "$N" references only in batches, and only text that is exactly
  a reference; "$12.50 hardware" stays as written.
- Splice only referenced lists of ids into lists; a point stays one item.
- MCP reloads the project before each call when it changed on disk, so a
  save in BenchUp no longer blocks the session.
- check_model and get_cut_list use the assigned stock material, not the
  first one with the same name; the cut list shows the stock thickness.
- Ignore bytes after IEND when reading or rewriting PNG projects.
- New projects get the usual permissions under the umask.
- Agent history entries use the translatable "Agent edit" label.
- Windows: open projects through explorer.exe with a plain path, so no
  shell parses "&" or "%".
- Read-only calls no longer copy the document; check_model evaluates
  joints once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@furilo

furilo commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

I reviewed this PR and fixed what came up in d6edc76:

Bugs

  • $N references: these are now resolved only in batches, and only when the text is exactly a reference. "$12.50 hardware", "$5 shelf" and "$3.99" are kept as written, and a single call never resolves references.
  • Lists in references: a referenced list is spliced into a list only when it holds ids. A point such as "$1.faces[0].center_mm" in points_mm stays one item.
  • MCP after a save in BenchUp: the server now reads the file again before each call if it changed on disk. Before, every later change failed with CHANGED_ON_DISK until the server was restarted. A group opened with enter_object stays open if it still exists.
  • Materials with the same name: check_model and get_cut_list now use the assigned stock material instead of the first one with that name. Two "Plywood" materials of different sizes no longer cause false thickness_mismatch warnings or share one cut list group. stock_mm now includes the thickness.
  • PNG projects: bytes after IEND are ignored when reading and rewriting. The old app decoder accepted them, but the shared decoder failed with truncated_chunk.
  • benchup open on Windows: the \\?\ prefix is removed from the path, and the file opens through explorer.exe instead of cmd /C start, so & and % in paths are not interpreted. Explorer exits with 1 even when it opens the file, so on Windows the exit code is ignored. This is not tested on Windows yet.

Smaller fixes

  • New projects get the usual permissions under the user's umask instead of a forced 0644.
  • Agent history entries use the translatable label "Agent edit" (cli.agent_edit, added to all catalogs and registered as a history label in benchup-core/build.rs). They no longer include the tool name, because history labels can't take parameters.
  • Read-only calls no longer copy the document, and calls that change it copy only the model, not the history. check_model evaluates joints once instead of twice.

New tests cover references, MCP after an outside save, same-named materials, trailing PNG bytes and permissions under the umask. docs/cli.md and docs/agent-guide.md are updated. Locally, cargo test -p benchup-tools -p benchup-cli passes; benchup-app was not built.

🤖 Generated with Claude Code

@dannycoulombe
dannycoulombe merged commit 2856cb8 into BenchUp3D:main Nov 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants