268 lines
15 KiB
Python
268 lines
15 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-feature": {
|
|
"description": "Add a joinery feature to a board: tenon (male tongue), mortise (pocket), hole, slot, dado, or rabbet. Use for 'add a tenon', 'cut a mortise', 'drill a hole', 'cut a slot'.",
|
|
"arguments": [
|
|
{"flag": "--kind", "variable": "kind", "description": "tenon | mortise | hole | slot | dado | rabbet"},
|
|
{"flag": "--part", "variable": "part", "default": "", "description": "Board id/name (default: most recent)"},
|
|
{"flag": "--face", "variable": "face", "default": "end_b", "description": "Which face: end_a, end_b, top, bottom, left, right"},
|
|
{"flag": "--along", "variable": "along", "default": "", "description": "Position along the board (e.g. '3 in'), or 1st offset on an end"},
|
|
{"flag": "--across", "variable": "across", "default": "", "description": "Offset across the face from centre"},
|
|
{"flag": "--width", "variable": "width", "default": "", "description": "Feature width, e.g. '1.5 in'"},
|
|
{"flag": "--height", "variable": "height", "default": "", "description": "Feature height/thickness"},
|
|
{"flag": "--depth", "variable": "depth", "default": "", "description": "Cut depth, or tenon protrusion length"},
|
|
{"flag": "--diameter", "variable": "diameter", "default": "", "description": "Hole diameter, e.g. '0.5 in'"},
|
|
{"flag": "--rotation", "variable": "rotation", "default": "", "description": "Rotate the feature about its face normal, degrees"},
|
|
],
|
|
"code": code(
|
|
'cmd = [ws, "feature", kind]\n'
|
|
'if part: cmd += ["--part", part]\n'
|
|
'if face: cmd += ["--face", face]\n'
|
|
'for flag, val in [("--along", along), ("--across", across), ("--width", width),\n'
|
|
' ("--height", height), ("--depth", depth), ("--diameter", diameter),\n'
|
|
' ("--rotation", rotation)]:\n'
|
|
' if val != "": cmd += [flag, str(val)]'
|
|
),
|
|
},
|
|
"wood-connect": {
|
|
"description": "Move/orient one board so its tenon/mortise seats into another's matching feature. Use for 'connect', 'assemble', 'join the pieces together', 'fit them together'.",
|
|
"arguments": [
|
|
{"flag": "--anchor", "variable": "anchor", "description": "Feature id that stays put, e.g. f1"},
|
|
{"flag": "--moving", "variable": "moving", "description": "Feature id whose board moves to mate, e.g. f2"},
|
|
],
|
|
"code": code('cmd = [ws, "connect", anchor, moving]'),
|
|
},
|
|
"wood-explode": {
|
|
"description": "Back connected boards off along their joint axes for an exploded view. Use for 'explode', 'back off the connections', 'show it pre-assembled'.",
|
|
"arguments": [
|
|
{"flag": "--distance", "variable": "distance", "description": "How far to separate, e.g. '3 in'"},
|
|
],
|
|
"code": code('cmd = [ws, "explode", distance]'),
|
|
},
|
|
"wood-assemble": {
|
|
"description": "Re-seat all connections (reverse an explode / re-fit the joints). Use for 'assemble', 'put it back together', 're-fit', 'close it up'.",
|
|
"arguments": [],
|
|
"code": code('cmd = [ws, "assemble"]'),
|
|
},
|
|
"wood-disconnect": {
|
|
"description": "Break a connection so the pieces become independent (they stay where they are). Use for 'disconnect', 'break the connection', 'separate them'.",
|
|
"arguments": [
|
|
{"flag": "--connection", "variable": "connection", "description": "Connection id, e.g. c1"},
|
|
],
|
|
"code": code('cmd = [ws, "disconnect", connection]'),
|
|
},
|
|
"wood-feature-delete": {
|
|
"description": "Remove a joinery feature by its id. Use for 'delete the mortise', 'remove that hole'.",
|
|
"arguments": [
|
|
{"flag": "--fid", "variable": "fid", "description": "Feature id, e.g. f1"},
|
|
],
|
|
"code": code('cmd = [ws, "feature-delete", fid]'),
|
|
},
|
|
"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}")
|