woodshop/scripts/gen_wood_tools.py

127 lines
4.7 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."""
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")'
PLACE = f'''import subprocess, os
{WS}
r = subprocess.run([ws, "place", stock, length], capture_output=True, text=True)
out = (r.stdout + r.stderr).strip()
'''
JOIN = f'''import subprocess, os
{WS}
cmd = [ws, "join", part_b]
if to: cmd += ["--to", to]
if angle: cmd += ["--angle", str(angle)]
if offset: cmd += ["--offset", offset]
if anchor: cmd += ["--anchor", anchor]
r = subprocess.run(cmd, capture_output=True, text=True)
out = (r.stdout + r.stderr).strip()
'''
SAND = f'''import subprocess, os
{WS}
cmd = [ws, "sand"] + ([part] if part else [])
r = subprocess.run(cmd, capture_output=True, text=True)
out = (r.stdout + r.stderr).strip()
'''
DELETE = f'''import subprocess, os
{WS}
cmd = [ws, "delete"] + ([part] if part else [])
r = subprocess.run(cmd, capture_output=True, text=True)
out = (r.stdout + r.stderr).strip()
'''
UNDO = f'''import subprocess, os
{WS}
r = subprocess.run([ws, "undo"], capture_output=True, text=True)
out = (r.stdout + r.stderr).strip()
'''
TOOLS = {
"wood-place": {
"description": "Place a new board of dimensional lumber into the scene. Use for any 'place', 'add', 'put', 'grab', 'cut me a' board command.",
"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": PLACE,
},
"wood-join": {
"description": "Attach/join one board to another at an angle, optionally offset along the target board. Use for 'attach', 'join', 'connect', 'fasten', 'screw to'.",
"arguments": [
{"flag": "--part-b", "variable": "part_b",
"description": "Id of the board being attached, e.g. p2"},
{"flag": "--to", "variable": "to", "default": "",
"description": "Id of the board to attach to, e.g. p1. Omit to use the most recently touched board."},
{"flag": "--angle", "variable": "angle", "default": "90",
"description": "Angle in degrees between the two boards (default 90)"},
{"flag": "--offset", "variable": "offset", "default": "",
"description": "Distance from the anchor end, e.g. '10 in'. Omit to attach at the very end."},
{"flag": "--anchor", "variable": "anchor", "default": "end_b",
"description": "Measure offset from 'end_a' (start) or 'end_b' (far end)"},
],
"code": JOIN,
},
"wood-sand": {
"description": "Sand a board smooth. Use for 'sand', 'smooth', 'finish'.",
"arguments": [
{"flag": "--part", "variable": "part", "default": "",
"description": "Id of the board to sand, e.g. p1. Omit to sand the most recently touched board ('it')."},
],
"code": SAND,
},
"wood-delete": {
"description": "Remove a board from the scene. Use for 'delete', 'remove', 'get rid of', 'scrap'.",
"arguments": [
{"flag": "--part", "variable": "part", "default": "",
"description": "Id of the board to delete, e.g. p2. Omit for the most recently touched board."},
],
"code": DELETE,
},
"wood-undo": {
"description": "Undo the last operation. Use for 'undo', 'never mind', 'take that back', 'go back'.",
"arguments": [],
"code": UNDO,
},
}
WRAPPER = '''#!/bin/bash
# CmdForge wrapper for '{name}'
# Auto-generated - do not edit
exec "{py}" -m cmdforge.runner "{name}" "$@"
'''
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.1.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}: {tool_dir/'config.yaml'} + {wrapper}")