"""Generate the wood-* CmdForge tools: the documented woodworking command vocabulary. Each is a thin wrapper over the `woodshop` CLI so the logic lives in one place; pa-load-tools turns these into Claude function schemas. The arg descriptions ARE the LLM's documentation — keep them clear and example-rich. Run this after changing the woodshop CLI to refresh the tools: python scripts/gen_wood_tools.py """ import os import stat from pathlib import Path import yaml CMDFORGE_PY = "/home/rob/.local/share/pipx/venvs/cmdforge/bin/python" CMDFORGE_DIR = Path.home() / ".cmdforge" BIN_DIR = Path.home() / ".local" / "bin" WS = 'ws = os.path.expanduser("~/PycharmProjects/woodshop/.venv/bin/woodshop")' def code(body: str) -> str: """Wrap a command-building body that sets `cmd`, then runs it.""" return (f"import subprocess, os\n{WS}\n{body}\n" "r = subprocess.run(cmd, capture_output=True, text=True)\n" "out = (r.stdout + r.stderr).strip()\n") TOOLS = { "wood-place": { "description": "Place a new board of dimensional lumber. Use for 'place', 'add', 'put', 'grab', 'cut me a' board.", "arguments": [ {"flag": "--stock", "variable": "stock", "description": "Nominal lumber size, e.g. 2x4, 2x6, 1x4, 4x4"}, {"flag": "--length", "variable": "length", "description": "Length with units, e.g. '6 ft', '72 in', '3 ft 6 in'"}, ], "code": code('cmd = [ws, "place", stock, length]'), }, "wood-join": { "description": "Attach one board to another at an angle, optionally offset along the target. Use for 'attach', 'join', 'connect', 'fasten'.", "arguments": [ {"flag": "--part-b", "variable": "part_b", "description": "Id or name of the board being attached, e.g. p2"}, {"flag": "--to", "variable": "to", "default": "", "description": "Board to attach to, e.g. p1 or 'front rail'. Omit for the most recent board."}, {"flag": "--angle", "variable": "angle", "default": "90", "description": "Angle in degrees between the boards (default 90)"}, {"flag": "--offset", "variable": "offset", "default": "", "description": "Distance from the anchor end, e.g. '10 in'. Omit to attach at the end."}, {"flag": "--anchor", "variable": "anchor", "default": "end_b", "description": "Measure offset from 'end' (far end) or 'start'"}, ], "code": code( 'cmd = [ws, "join", part_b]\n' 'if to: cmd += ["--to", to]\n' 'if angle: cmd += ["--angle", str(angle)]\n' 'if offset: cmd += ["--offset", offset]\n' 'if anchor: cmd += ["--anchor", anchor]' ), }, "wood-stand": { "description": "Stand a board up vertically (e.g. a table or chair leg). Use for 'stand up', 'make it vertical', 'upright'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name. Omit for the most recent board."}, {"flag": "--tilt", "variable": "tilt", "default": "90", "description": "Tilt degrees, 90 = straight up (default 90)"}, ], "code": code('cmd = [ws, "stand"] + ([part] if part else []) + ["--tilt", str(tilt)]'), }, "wood-lay": { "description": "Lay a board flat / horizontal. Use for 'lay it down', 'make it flat', 'horizontal'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name. Omit for the most recent board."}, ], "code": code('cmd = [ws, "lay"] + ([part] if part else [])'), }, "wood-rotate": { "description": "Rotate / re-orient a board. Use for 'rotate', 'turn', 'angle it'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name (default: most recent)"}, {"flag": "--yaw", "variable": "yaw", "default": "", "description": "Heading in the horizontal plane, degrees"}, {"flag": "--tilt", "variable": "tilt", "default": "", "description": "Elevation toward vertical, degrees"}, {"flag": "--roll", "variable": "roll", "default": "", "description": "Rotation about the board's own length, degrees"}, ], "code": code( 'cmd = [ws, "rotate"] + ([part] if part else [])\n' 'if yaw != "": cmd += ["--yaw", str(yaw)]\n' 'if tilt != "": cmd += ["--tilt", str(tilt)]\n' 'if roll != "": cmd += ["--roll", str(roll)]' ), }, "wood-move": { "description": "Move/slide a board by an offset (or to an absolute position). Use for 'move', 'slide', 'shift', 'nudge'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name (default: most recent)"}, {"flag": "--dx", "variable": "dx", "default": "", "description": "X offset, e.g. '5 in', '-2 ft' (X = along the first board)"}, {"flag": "--dy", "variable": "dy", "default": "", "description": "Y offset"}, {"flag": "--dz", "variable": "dz", "default": "", "description": "Z offset (up/down)"}, ], "code": code( 'cmd = [ws, "move"] + ([part] if part else [])\n' 'if dx != "": cmd += ["--dx", dx]\n' 'if dy != "": cmd += ["--dy", dy]\n' 'if dz != "": cmd += ["--dz", dz]' ), }, "wood-trim": { "description": "Cut a board down to a new length. Use for 'cut it to', 'trim to', 'shorten to', 'make it N feet'.", "arguments": [ {"flag": "--length", "variable": "length", "description": "New length with units, e.g. '4 ft'"}, {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name (default: most recent)"}, ], "code": code('cmd = [ws, "trim", length] + (["--part", part] if part else [])'), }, "wood-copy": { "description": "Duplicate a board, offset by dx/dy/dz. Use for 'copy', 'duplicate', 'another one like that'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board to copy (default: most recent)"}, {"flag": "--dx", "variable": "dx", "default": "", "description": "X offset for the copy, e.g. '46 in'"}, {"flag": "--dy", "variable": "dy", "default": "", "description": "Y offset"}, {"flag": "--dz", "variable": "dz", "default": "", "description": "Z offset"}, ], "code": code( 'cmd = [ws, "copy"] + ([part] if part else [])\n' 'if dx != "": cmd += ["--dx", dx]\n' 'if dy != "": cmd += ["--dy", dy]\n' 'if dz != "": cmd += ["--dz", dz]' ), }, "wood-rename": { "description": "Give a board a human-friendly name so it can be referred to by name later. Use for 'call it', 'name it', 'this is the'.", "arguments": [ {"flag": "--name", "variable": "name", "description": "The name, e.g. 'front-left leg'"}, {"flag": "--part", "variable": "part", "default": "", "description": "Board id (default: most recent)"}, ], "code": code('cmd = [ws, "rename", name] + (["--part", part] if part else [])'), }, "wood-sand": { "description": "Sand a board smooth. Use for 'sand', 'smooth', 'finish'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name. Omit to sand the most recent board ('it')."}, ], "code": code('cmd = [ws, "sand"] + ([part] if part else [])'), }, "wood-delete": { "description": "Remove a board. Use for 'delete', 'remove', 'get rid of', 'scrap'.", "arguments": [ {"flag": "--part", "variable": "part", "default": "", "description": "Board id or name (default: most recent)"}, ], "code": code('cmd = [ws, "delete"] + ([part] if part else [])'), }, "wood-undo": { "description": "Undo the last operation. Use for 'undo', 'never mind', 'take that back', 'go back'.", "arguments": [], "code": code('cmd = [ws, "undo"]'), }, "wood-clear": { "description": "Clear the whole scene and start over. Use for 'clear', 'start over', 'reset', 'new project'.", "arguments": [], "code": code('cmd = [ws, "clear"]'), }, "wood-cutlist": { "description": "Report the cut list / bill of materials: every board, board-feet, and how much lumber to buy. Use for 'cut list', 'what do I need to buy', 'bill of materials', 'how much wood'.", "arguments": [], "code": code('cmd = [ws, "cutlist"]'), }, "wood-save": { "description": "Save the current design as a named project. Use for 'save this as', 'save the project', 'remember this design'.", "arguments": [ {"flag": "--name", "variable": "name", "description": "Project name, e.g. 'coffee table'"}, ], "code": code('cmd = [ws, "save", name]'), }, "wood-open": { "description": "Open a previously saved project (replaces the current scene). Use for 'open', 'load the', 'go back to my'.", "arguments": [ {"flag": "--name", "variable": "name", "description": "Name of the project to open"}, ], "code": code('cmd = [ws, "open", name]'), }, "wood-projects": { "description": "List saved projects. Use for 'what projects do I have', 'list my designs'.", "arguments": [], "code": code('cmd = [ws, "projects"]'), }, } WRAPPER = ('#!/bin/bash\n# CmdForge wrapper for \'{name}\'\n# Auto-generated - do not edit\n' 'exec "{py}" -m cmdforge.runner "{name}" "$@"\n') for name, spec in TOOLS.items(): tool_dir = CMDFORGE_DIR / name tool_dir.mkdir(parents=True, exist_ok=True) config = { "name": name, "description": spec["description"], "category": "Other", "version": "0.2.0", "arguments": spec["arguments"], "steps": [{"type": "code", "code": spec["code"], "output_var": "out"}], "output": "{out}", } (tool_dir / "config.yaml").write_text(yaml.safe_dump(config, sort_keys=False)) wrapper = BIN_DIR / name wrapper.write_text(WRAPPER.format(name=name, py=CMDFORGE_PY)) wrapper.chmod(wrapper.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) print(f"created {name}") print(f"\n{len(TOOLS)} wood-* tools written to {CMDFORGE_DIR} and {BIN_DIR}")