agents/openai.yaml
interface: display_name: "STEP Parts" short_description: "Find and download STEP files from step.parts" default_prompt: "Use $step-parts to find and download a STEP file from step.parts."
earthtojake/text-to-cad · GitHub
Find, evaluate, and download common purchasable CAD parts from step.parts, including named off-the-shelf actuators, servos, motors, electronics boards, connectors, screws, bolts, nuts, washers, bearings, standoffs, and other catalog components. Use when Codex needs to search the hosted step.parts catalog before creating simplified placeholder geometry, resolve fuzzy part names, standards, aliases, or dimensions, choose a matching part, fetch a canonical .step file, verify checksums, or use the step.parts API/OpenAPI/catalog endpoints for standard part discovery.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add earthtojake/text-to-cad --skill step-parts설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
agents/openai.yamlinterface: display_name: "STEP Parts" short_description: "Find and download STEP files from step.parts" default_prompt: "Use $step-parts to find and download a STEP file from step.parts."
LICENSEMIT License Copyright (c) 2026 Thompson Labs LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
references/step-parts-api.md# step.parts API Reference
## Origins
Default to the API origin `https://api.step.parts`. Use a different origin only when the user supplies another hosted step.parts-compatible API domain. Static HTML pages live on `https://www.step.parts`; GLB and PNG preview URLs in API records point directly at Vercel Blob. STEP URLs are environment-aware and production records use commit-pinned GitHub LFS media. If the API domain does not resolve, treat the hosted service as unavailable and ask for a deployed origin when a live download is required.
## Machine Endpoints
| Endpoint | Use |
| --- | --- |
| `https://www.step.parts/llms.txt` | Human-readable agent guide with endpoint summary and examples. |
| `/v1/parts` | Search, filter, paginate, and retrieve absolute asset URLs. |
| `/v1/parts/{id}` | Fetch one enriched part record by stable id. |
| `/v1/catalog/schema` | JSON Schema, field semantics, result ordering, and family attribute meanings. |
| `/v1/catalog/parts.index.json` | Compact id/name/facet discovery index for cheap lookups before fetching details. |
| `/v1/openapi.json` | OpenAPI 3.1 contract for generating clients/tools. |
## `/v1/parts` Query Parameters
- `q`: tokenized metadata search across id, name, description, category, family, stepSource, productPage, tags, aliases, standard fields, attribute keys, and attribute values. Every token must match.
- `tag`, `category`, `family`, `standard`: repeatable filters. Values may also be comma-separated. Values within one facet are ORed, and selected facet fields are ANDed together.
- `page`: 1-based page number.
- `pageSize`: default `100`, max `500`.
Unfiltered results start with a fixed 100-part showcase, then continue in stable source catalog order. Filtered results are ordered by stable source catalog order.
Examples:
```text
https://api.step.parts/v1/parts?q=M3&tag=screw&page=2
https://api.step.parts/v1/parts?pageSize=100
https://api.step.parts/v1/parts?category=fastener&family=socket-head-cap-screw&standard=ISO%204762
https://api.step.parts/v1/parts?q=lengthMm%2012
```
## Response Fields
`/v1/parts` returns:
- `catalog`: part count, last modified timestamp, catalog checksum, and URLs for schema/OpenAPI.
- `items`: enriched part records with absolute `pageUrl`, `apiUrl`, `stepUrl`, `glbUrl`, and `pngUrl`.
- `page`, `pageSize`, `total`, `totalPages`, `hasNextPage`, `hasPreviousPage`.
- `facets`: available `tags`, `categories`, `families`, and `standards` with counts.
- `filters`: parsed active filters.
Each part record contains:
- `id`: stable snake_case identifier and asset filename base.
- `name`, `description`, `category`, `family`, `tags`, `aliases`.
- `standard`: optional `{ body, number, designation }`.
- `attributes`: family-specific scalar facts.
- `stepUrl`, `glbUrl`, `pngUrl`.
- `byteSize`, `sha256`.
## Asset URL Patterns
Use returned URLs when possible. Patterns are:
```text
https://www.step.parts/step/{id}.step
Use the absolute `glbUrl` and `pngUrl` returned by the API record. Preview assets are served from Vercel Blob.
https://www.step.parts/parts/{id}
```
The `/step/{id}.step` route serves local checked-out STEP bytes in local/dev mode and redirects to commit-pinned GitHub LFS media in production.
## Download And Verification
When downloading a STEP file:
1. Fetch a part record from `/v1/parts` or `/v1/parts/{id}`.
2. Download `stepUrl`.
3. Compare the file SHA-256 to the part record's `sha256` when it is not null.
4. Keep the original `.step` extension and preserve the source id in the filename unless the user asks otherwise.
scripts/download_step_part.py#!/usr/bin/env python3
"""Search step.parts for common standard parts and download STEP files."""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
DEFAULT_ORIGIN = "https://api.step.parts"
DEFAULT_OUT_DIR = tempfile.gettempdir()
USER_AGENT = "step-parts-skill/1.0"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Search the step.parts hosted catalog for low-level common standard parts "
"(screws, bolts, bearings, electronics parts, motors, connectors, etc.) "
"and optionally download canonical STEP files."
),
)
parser.add_argument("query", nargs="?", help="Fuzzy search query, for example 'M3 socket head 12'.")
parser.add_argument("--id", dest="part_id", help="Fetch a specific part id instead of searching.")
parser.add_argument("--origin", default=DEFAULT_ORIGIN, help=f"API origin. Default: {DEFAULT_ORIGIN}")
parser.add_argument("--download", action="store_true", help="Download the selected STEP file.")
parser.add_argument("--all", action="store_true", help="With --download, download every result on the returned page.")
parser.add_argument("--out-dir", default=DEFAULT_OUT_DIR, help="Directory for downloaded STEP files. Default: active temp directory.")
parser.add_argument("--filename", help="Filename to use when downloading one selected part.")
parser.add_argument("--overwrite", action="store_true", help="Overwrite an existing downloaded file.")
parser.add_argument("--limit", type=int, default=10, help="Search page size. The API caps this at 500.")
parser.add_argument("--page", type=int, default=1, help="1-based search page.")
parser.add_argument("--tag", action="append", default=[], help="Repeatable tag filter.")
parser.add_argument("--category", action="append", default=[], help="Repeatable category filter.")
parser.add_argument("--family", action="append", default=[], help="Repeatable family filter.")
parser.add_argument("--standard", action="append", default=[], help="Repeatable standard filter, for example 'ISO 4762'.")
parser.add_argument("--timeout", type=float, default=30.0, help="HTTP timeout in seconds.")
return parser.parse_args()
def origin_url(origin: str) -> str:
parsed = urllib.parse.urlparse(origin)
if not parsed.scheme or not parsed.netloc:
raise SystemExit(f"Invalid origin: {origin!r}")
return origin.rstrip("/")
def build_url(origin: str, path: str, params: list[tuple[str, str]] | None = None) -> str:
url = f"{origin_url(origin)}{path}"
if params:
return f"{url}?{urllib.parse.urlencode(params)}"
return url
def request(url: str, timeout: float) -> bytes:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read()
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise SystemExit(f"HTTP {exc.code} for {url}: {detail}") from exc
except urllib.error.URLError as exc:
raise SystemExit(f"Failed to fetch {url}: {exc.reason}") from exc
def fetch_json(url: str, timeout: float) -> Any:
data = request(url, timeout)
try:
return json.loads(data)
except json.JSONDecodeError as exc:
raise SystemExit(f"Expected JSON from {url}: {exc}") from exc
def search_parts(args: argparse.Namespace) -> dict[str, Any]:
params: list[tuple[str, str]] = [
("page", str(max(1, args.page))),
("pageSize", str(max(1, args.limit))),
]
if args.query:
params.append(("q", args.query))
for key in ("tag", "category", "family", "standard"):
for value in getattr(args, key):
params.append((key, value))
return fetch_json(build_url(args.origin, "/v1/parts", params), args.timeout)
def get_part(args: argparse.Namespace, part_id: str) -> dict[str, Any]:
safe_id = urllib.parse.quote(part_id, safe="")
return fetch_json(build_url(args.origin, f"/v1/parts/{safe_id}"), args.timeout)
def selected_parts(args: argparse.Namespace) -> list[dict[str, Any]]:
if args.part_id:
return [get_part(args, args.part_id)]
result = search_parts(args)
items = result.get("items", [])
if not items:
raise SystemExit("No parts matched the query.")
if args.download and args.all:
return items
return [items[0]]
def filename_for(part: dict[str, Any], requested_filename: str | None, allow_requested: bool) -> str:
if requested_filename and allow_requested:
return requested_filename
step_url = str(part.get("stepUrl") or "")
name = Path(urllib.parse.urlparse(step_url).path).name
if name:
return name
return f"{part['id']}.step"
def step_download_url(part: dict[str, Any], origin: str) -> str:
step_url = part.get("stepUrl")
if not step_url:
raise SystemExit(f"Part {part.get('id', '<unknown>')} does not include stepUrl.")
return urllib.parse.urljoin(f"{origin_url(origin)}/", str(step_url))
def write_download(part: dict[str, Any], args: argparse.Namespace, allow_requested_filename: bool) -> dict[str, Any]:
step_url = step_download_url(part, args.origin)
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / filename_for(part, args.filename, allow_requested_filename)
if path.exists() and not args.overwrite:
raise SystemExit(f"Refusing to overwrite existing file: {path}")
data = request(step_url, args.timeout)
path.write_bytes(data)
actual_sha256 = hashlib.sha256(data).hexdigest()
expected_sha256 = part.get("sha256")
checksum_ok = expected_sha256 is None or expected_sha256 == actual_sha256
if not checksum_ok:
raise SystemExit(
f"Checksum mismatch for {path}: expected {expected_sha256}, got {actual_sha256}",
)
return {
"id": part.get("id"),
"name": part.get("name"),
"path": str(path),
"stepUrl": step_url,
"pageUrl": part.get("pageUrl"),
"apiUrl": part.get("apiUrl"),
"byteSize": len(data),
"sha256": actual_sha256,
"checksumVerified": expected_sha256 is not None,
}
def compact_part(part: dict[str, Any]) -> dict[str, Any]:
return {
"id": part.get("id"),
"name": part.get("name"),
"category": part.get("category"),
"family": part.get("family"),
"standard": part.get("standard"),
"attributes": part.get("attributes"),
"stepUrl": part.get("stepUrl"),
"pageUrl": part.get("pageUrl"),
"apiUrl": part.get("apiUrl"),
"sha256": part.get("sha256"),
}
def main() -> int:
args = parse_args()
if args.filename and (not args.download or args.all):
raise SystemExit("--filename can only be used with --download for one selected part.")
if not args.part_id and not args.query and not any([args.tag, args.category, args.family, args.standard]):
raise SystemExit("Provide a query, --id, or at least one facet filter.")
if not args.download:
if args.part_id:
output: Any = compact_part(get_part(args, args.part_id))
else:
result = search_parts(args)
result["items"] = [compact_part(part) for part in result.get("items", [])]
output = result
json.dump(output, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
parts = selected_parts(args)
downloads = [
write_download(part, args, allow_requested_filename=len(parts) == 1)
for part in parts
]
json.dump({"downloads": downloads}, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md---
name: step-parts
description: Find, evaluate, and download common purchasable CAD parts from step.parts, including named off-the-shelf actuators, servos, motors, electronics boards, connectors, screws, bolts, nuts, washers, bearings, standoffs, and other catalog components. Use when Codex needs to search the hosted step.parts catalog before creating simplified placeholder geometry, resolve fuzzy part names, standards, aliases, or dimensions, choose a matching part, fetch a canonical .step file, verify checksums, or use the step.parts API/OpenAPI/catalog endpoints for standard part discovery.
---
# CAD Parts
Provenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad).
Use the installed local skill files as the runtime source of truth; the
repository link is only for provenance and release review.
## Overview
Use the hosted step.parts machine endpoints instead of scraping HTML or relying on local repository files. Treat `https://api.step.parts` as the canonical API origin and `https://www.step.parts` as the site/static-asset origin unless the user provides a different hosted mirror. Network/DNS failures are inconclusive: if `api.step.parts` cannot be reached from the sandbox, retry once with network permission before reporting a miss or using placeholder geometry. Do not describe a part as unavailable unless the API was reachable and returned no relevant candidates.
When a CAD assembly includes named off-the-shelf actuators, servos, motors, electronics boards, connectors, or other purchasable components, search step.parts before creating simplified placeholder geometry. For named servos, motors, and actuators, search both exact model strings and common aliases/vendor spellings before giving up. For example, `STS3215` may also appear as `ST3215`, `3215`, `Waveshare Feetech ST3215`, or under `family=feetech`. If the API was reachable and no exact or near-exact match is available, record the search miss and then use a documented envelope or simplified stand-in.
## Quick Workflow
1. Interpret the requested part into search terms and optional facets:
- `q` for fuzzy tokens, standards, aliases, dimensions, source/product URLs, and attribute names/values.
- `category`, `family`, `standard`, or `tag` when the user gives an exact facet.
2. Search `/v1/parts` and inspect `items`, `total`, and `facets`. For actuator model numbers, retry likely aliases, dropped letters, vendor names, and relevant family facets before treating an empty result as a miss.
3. If results are ambiguous, present the best few options with `id`, `name`, `standard`, and key attributes before choosing. If one result clearly matches, return the selected record details without downloading unless the user asked for a local STEP file.
4. When an exact or near-exact off-the-shelf actuator model is found, prefer downloading and using its STEP file unless there is a clear assembly-time reason to use a simplified envelope. Record that choice explicitly.
5. When the user asks to download or save a STEP file, download its `stepUrl`, then verify the file with the record's `sha256` when present.
6. Return the local path when downloaded, plus the selected part id and page/API URLs so the user can trace provenance.
## CAD Viewer Handoff
After completing step.parts work that creates or updates a local `.step` or `.stp` file, you must ALWAYS hand the explicit file path to `$cad-viewer` when that skill is installed. `$cad-viewer` must start CAD Viewer if it is not already running and return link(s) to the relevant created or updated file(s); if `$cad-viewer` is unavailable or startup fails, report that instead of silently omitting the handoff.
## Bundled Downloader
Use `scripts/download_step_part.py` for deterministic search, download, and checksum verification:
```bash
python scripts/download_step_part.py "M3 socket head 12" --download
python scripts/download_step_part.py --id iso4762_socket_head_cap_screw_m3x12 --download
python scripts/download_step_part.py "bearing 608zz" --limit 5
```
Useful options:
- `--origin`: override `https://api.step.parts` only when the user provides another hosted API origin.
- `--tag`, `--category`, `--family`, `--standard`: repeatable facet filters.
- `--out-dir`: override the download directory when the user asks for a specific destination.
- `--all`: with `--download`, download every result on the returned page as individual STEP downloads.
- `--overwrite`: replace an existing output file.
The script prints JSON to stdout. For searches, it prints matched records. For downloads, it prints saved file paths, checksums, and source URLs.
## API Reference
Read `references/step-parts-api.md` when you need endpoint details, field meanings, or query semantics. Prefer:
- `/v1/parts` for filtered search with absolute asset URLs.
- `/v1/parts/{id}` for one enriched record.
- Returned `stepUrl` for STEP downloads.
- `/v1/catalog/parts.index.json` for a compact discovery index.
- `/v1/catalog/schema` for field and family attribute meanings.
- `/v1/openapi.json` when generating a client or tool.
## Search Guidance
- Query tokens are ANDed by the API, so start specific but not overconstrained. For example, use `M3 SHCS 12` before adding exact family and standard filters.
- Values within one facet are ORed together, and selected `tag`, `category`, `family`, and `standard` fields are ANDed together. Use exact facets to narrow within known categories, then rank manually by name and attributes.
- Standards can be queried as `ISO 4762`, `ISO4762`, or the exact `standard.designation`.
- The `attributes` object contains family-specific facts such as `thread`, `lengthMm`, `bore1Mm`, `material`, `profileSeries`, `slotSizeMm`, and dimensions in millimeters.
- Part, GLB, and PNG URL patterns are predictable on `https://www.step.parts`; STEP URLs are environment-aware and may resolve to GitHub LFS media in production. Use catalog/API `stepUrl` for downloads.