woodshop/scripts/gen_wood_tools.py

210 lines
11 KiB
Python

"""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-select": {
"description": "Set the current selection — the board future commands like 'rotate that' or 'delete it' act on. Use for 'select', 'pick', 'grab the', 'use the'.",
"arguments": [
{"flag": "--part", "variable": "part", "description": "Board id or name to select, e.g. p3 or 'front-left leg'"},
],
"code": code('cmd = [ws, "select", part]'),
},
"wood-undo": {
"description": "Undo the last operation. Use for 'undo', 'never mind', 'take that back', 'go back'.",
"arguments": [],
"code": code('cmd = [ws, "undo"]'),
},
"wood-redo": {
"description": "Redo the last undone operation. Use for 'redo', 'put it back', 'never mind that undo'.",
"arguments": [],
"code": code('cmd = [ws, "redo"]'),
},
"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}")