diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfbe7b5..9b47c98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,13 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 # version comes from package.json#packageManager - - uses: actions/setup-node@v4 with: node-version: 22 - cache: pnpm + + # The enterprise Actions policy only permits GitHub-owned Marketplace + # actions. Corepack reads the pinned pnpm version from package.json. + - run: corepack enable # --frozen-lockfile also fails on lockfile drift, which is a review gate # in its own right. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 786ccb6..0e8b36c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,14 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 # version comes from package.json#packageManager - - uses: actions/setup-node@v4 with: node-version: 22 registry-url: https://registry.npmjs.org - cache: pnpm + + # Keep release compatible with the enterprise Actions allow-list while + # still using the pnpm version pinned in package.json. + - run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run typecheck diff --git a/README.md b/README.md index 5b6f63f..3971697 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # meshy-cli -A small, well-structured command-line interface for the [Meshy AI API](https://docs.meshy.ai/) — text-to-3D, image-to-3D (standard and smart-topology), remesh, convert, resize, rigging, animation, retexture, 2D image generation, multi-color print output, and the `balance` endpoint. Built for humans and AI agents. +A small, well-structured command-line interface for the [Meshy AI API](https://docs.meshy.ai/) — text-to-3D, image-to-3D (standard and smart-topology), text-to-motion, remesh, convert, resize, rigging, animation, retexture, 2D image generation, multi-color print output, and the `balance` endpoint. Built for humans and AI agents. Two layers. `meshy make` chains the documented flows so that one command produces one model. Underneath, a per-endpoint command for every resource @@ -126,6 +126,10 @@ meshy-cli text-to-3d create --mode refine --preview-task-id # image → 3D meshy-cli image-to-3d create --image-url https://example.com/cat.png +# text → standalone motion clip (Prime/FBX by default; Swift produces BVH) +meshy-cli text-to-motion create --prompt "a character waving" --duration 3 +meshy-cli text-to-motion create --prompt "a quick dodge to the left" --duration 2.5 --mode swift + # smart topology: component-aware low-poly with a native polycount meshy-cli image-to-3d create --image-url cat.png --model-type smart-topology --target-polycount 10000 @@ -164,6 +168,7 @@ each one in full. | `resize` | `/resize` | [docs](https://docs.meshy.ai/en/api/resize) | | `rigging` | `/rigging` | [docs](https://docs.meshy.ai/en/api/rigging) | | `animate` | `/animations` | [docs](https://docs.meshy.ai/en/api/animations) | +| `text-to-motion` | `/text-to-motion` | [docs](https://docs.meshy.ai/en/api/text-to-motion) | | `retexture` | `/retexture` | [docs](https://docs.meshy.ai/en/api/retexture) | | `text-to-image` | `/text-to-image` | [docs](https://docs.meshy.ai/en/api/text-to-image) | | `image-to-image` | `/image-to-image` | [docs](https://docs.meshy.ai/en/api/image-to-image) | diff --git a/package.json b/package.json index d5ec5e1..4023a01 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "meshy-cli", "version": "0.1.2", - "description": "Official command-line interface for the Meshy AI API — text-to-3D, image-to-3D, remesh, rigging, animate, retexture, 2D images, multi-color print, balance.", + "description": "Official command-line interface for the Meshy AI API — text-to-3D, image-to-3D, text-to-motion, remesh, rigging, animate, retexture, 2D images, multi-color print, balance.", "license": "MIT", "type": "module", "packageManager": "pnpm@11.18.0", diff --git a/skills/meshy-cli/SKILL.md b/skills/meshy-cli/SKILL.md index 79c1c96..41b601e 100644 --- a/skills/meshy-cli/SKILL.md +++ b/skills/meshy-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: meshy-cli -description: "Generate 3D models and 2D images with the Meshy API through the meshy-cli command — text-to-3D, image-to-3D, remesh, rigging, animation, retexture, printability. Use for any Meshy asset request." +description: "Generate 3D models, motion clips, and 2D images with the Meshy API through the meshy-cli command — text-to-3D, image-to-3D, text-to-motion, remesh, rigging, animation, retexture, printability. Use for any Meshy asset request." license: MIT compatibility: Requires meshy-cli on PATH and a stored credential or MESHY_API_KEY; network access to api.meshy.ai metadata: @@ -32,7 +32,7 @@ than starting over, or the finished step is paid for twice. ## Everything else -`meshy resources` indexes the 16 endpoint commands; each carries the same verbs: +`meshy resources` indexes the 17 endpoint commands; each carries the same verbs: ```bash meshy create [flags] [--data ''] [--async] [--timeout ] @@ -60,6 +60,9 @@ These are API rules, not preferences — ignoring them produces failed tasks: its result before calling `animate`. - **`animate` takes a rigging task id**, not a model task id, plus an integer `--action-id`. +- **`text-to-motion` produces a standalone skeletal clip**, not an animated + character. Pass a 2–10 second duration in 0.5-second increments. Prime is the + default (FBX, 10 credits); Swift returns BVH (3 credits). - **`repair-printability` drops textures and invalidates UVs.** Run it before texturing, or re-`retexture` afterwards. - **`multi-image-to-3d` is beta**; use `image-to-3d` unless multi-view input was diff --git a/src/client/endpoints/text-to-motion.ts b/src/client/endpoints/text-to-motion.ts new file mode 100644 index 0000000..69c3ecd --- /dev/null +++ b/src/client/endpoints/text-to-motion.ts @@ -0,0 +1,8 @@ +import { TaskEndpoint, type HttpFetch } from "./base.js"; + +/** POST/GET/LIST/DELETE /openapi/v1/text-to-motion. */ +export class TextToMotionEndpoint extends TaskEndpoint { + constructor(http: HttpFetch) { + super(http, "/text-to-motion"); + } +} diff --git a/src/client/index.ts b/src/client/index.ts index 15b138a..a5bf2d4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -18,6 +18,7 @@ import { RiggingEndpoint } from "./endpoints/rigging.js"; import { TaskEndpoint, type HttpFetch } from "./endpoints/base.js"; import { TextTo3DEndpoint } from "./endpoints/text-to-3d.js"; import { TextToImageEndpoint } from "./endpoints/text-to-image.js"; +import { TextToMotionEndpoint } from "./endpoints/text-to-motion.js"; import { mapHttpError, MeshyApiError } from "./errors.js"; import type { MeshyConfig } from "../internal/config.js"; import { logger } from "../internal/logger.js"; @@ -87,6 +88,7 @@ export class MeshyClient { readonly animate: AnimateEndpoint; readonly retexture: RetextureEndpoint; readonly textToImage: TextToImageEndpoint; + readonly textToMotion: TextToMotionEndpoint; readonly imageToImage: ImageToImageEndpoint; readonly multiColorPrint: MultiColorPrintEndpoint; readonly analyzePrintability: AnalyzePrintabilityEndpoint; @@ -111,6 +113,7 @@ export class MeshyClient { this.animate = new AnimateEndpoint(this.v1Fetch); this.retexture = new RetextureEndpoint(this.v1Fetch); this.textToImage = new TextToImageEndpoint(this.v1Fetch); + this.textToMotion = new TextToMotionEndpoint(this.v1Fetch); this.imageToImage = new ImageToImageEndpoint(this.v1Fetch); this.multiColorPrint = new MultiColorPrintEndpoint(this.v1Fetch); this.analyzePrintability = new AnalyzePrintabilityEndpoint(this.v1Fetch); @@ -129,6 +132,7 @@ export class MeshyClient { case "animate": return this.animate; case "retexture": return this.retexture; case "text-to-image": return this.textToImage; + case "text-to-motion": return this.textToMotion; case "image-to-image": return this.imageToImage; case "multi-color-print": return this.multiColorPrint; case "analyze-printability": return this.analyzePrintability; @@ -159,6 +163,7 @@ export const RESOURCE_NAMES = [ "animate", "retexture", "text-to-image", + "text-to-motion", "image-to-image", "multi-color-print", "analyze-printability", diff --git a/src/cmd/resources.ts b/src/cmd/resources.ts index 8742bb7..fa0274e 100644 --- a/src/cmd/resources.ts +++ b/src/cmd/resources.ts @@ -28,6 +28,7 @@ export const RESOURCES: ResourceEntry[] = [ { name: "animate", summary: "apply an animation clip to a rigged mesh" }, { name: "retexture", summary: "regenerate textures" }, { name: "text-to-image", summary: "2D image generation" }, + { name: "text-to-motion", summary: "generate a standalone skeletal motion clip from text" }, { name: "image-to-image", summary: "2D image editing" }, { name: "multi-color-print", summary: "color-separated 3D print output" }, { name: "analyze-printability", summary: "inspect a model for 3D-printing issues (free)" }, diff --git a/src/cmd/text-to-motion.ts b/src/cmd/text-to-motion.ts new file mode 100644 index 0000000..8f82b5f --- /dev/null +++ b/src/cmd/text-to-motion.ts @@ -0,0 +1,61 @@ +/** + * text-to-motion — generate a standalone skeletal motion clip from a prompt. + */ + +import { Option } from "commander"; +import { UsageError } from "../internal/errors.js"; +import { parseNumber } from "../internal/flags.js"; +import { buildResourceCommand, type ResourceCommandSpec } from "../internal/task-command.js"; + +const MIN_DURATION_SECONDS = 2; +const MAX_DURATION_SECONDS = 10; +const DURATION_STEP_SECONDS = 0.5; + +const spec: ResourceCommandSpec = { + name: "text-to-motion", + description: + "Generate a standalone skeletal motion clip from a text prompt. Prime returns FBX; " + + "Swift returns BVH", + create: { + description: "Create a text-to-motion task", + configure(cmd) { + return cmd + .option("--prompt ", "required — describe the character motion; max 400 chars") + .addOption( + new Option("--mode ", "generation model (default: prime)") + .choices(["prime", "swift"]), + ) + .option( + "--duration ", + "required — clip length from 2 to 10 seconds in 0.5-second increments", + parseNumber, + ); + }, + toPayload(opts) { + if (!opts.prompt && !opts.data) { + throw new UsageError("--prompt is required"); + } + if (opts.duration == null && !opts.data) { + throw new UsageError("--duration is required"); + } + if (opts.duration != null) { + const duration = opts.duration as number; + const onStep = Number.isInteger(duration / DURATION_STEP_SECONDS); + if (duration < MIN_DURATION_SECONDS || duration > MAX_DURATION_SECONDS || !onStep) { + throw new UsageError("--duration must be from 2 to 10 seconds in 0.5-second increments"); + } + } + return { + prompt: opts.prompt, + mode: opts.mode, + duration: opts.duration, + }; + }, + toDefaults() { + return { mode: "prime" }; + }, + }, + endpointOf: (c) => c.textToMotion, +}; + +export const textToMotionCommand = buildResourceCommand(spec); diff --git a/src/internal/download.ts b/src/internal/download.ts index ab95b58..d9f264d 100644 --- a/src/internal/download.ts +++ b/src/internal/download.ts @@ -71,10 +71,14 @@ export function enumerateArtifacts(task: Task): Artifact[] { // Rigging / animate-style endpoints nest outputs under `result`. if (task.result && typeof task.result === "object") { + const motionFormat = task.result["motion_format"]; for (const [key, value] of Object.entries(task.result)) { if (typeof value !== "string" || !/^https?:/.test(value)) continue; const extMatch = key.match(/_(fbx|glb|usdz|obj|png|jpg|jpeg|webp)_url$/i); - const preferredExt = extMatch ? extMatch[1]!.toLowerCase() : ""; + let preferredExt = extMatch ? extMatch[1]!.toLowerCase() : ""; + if (key === "motion_url" && (motionFormat === "fbx" || motionFormat === "bvh")) { + preferredExt = motionFormat; + } out.push({ key, url: value, preferredExt }); } } diff --git a/src/root.ts b/src/root.ts index 78b1da9..5a6935f 100644 --- a/src/root.ts +++ b/src/root.ts @@ -36,6 +36,7 @@ import { retextureCommand } from "./cmd/retexture.js"; import { riggingCommand } from "./cmd/rigging.js"; import { textTo3dCommand } from "./cmd/text-to-3d.js"; import { textToImageCommand } from "./cmd/text-to-image.js"; +import { textToMotionCommand } from "./cmd/text-to-motion.js"; const ROOT_LONG = `meshy-cli — command-line interface for the Meshy AI API. @@ -52,7 +53,7 @@ EXAMPLES: meshy make "a red sports car" --max-credits 25 # refuse if the estimate is over # every endpoint, one at a time - meshy resources # index of the 16 commands + meshy resources # index of the 17 commands meshy text-to-3d create --mode preview --prompt "a red sports car" meshy image-to-3d get meshy delete # unified: works for any task @@ -75,7 +76,7 @@ AUTHENTICATION: separate credentials.dev.json, so staging cannot clobber a production login. RESOURCE COMMANDS: - All 16 endpoint commands stay available and stay supported — they are + All 17 endpoint commands stay available and stay supported — they are indexed by \`meshy resources\` instead of listed here, so this help does not grow with the API. \`meshy resources --help\` also carries the shared create/get/list/wait/delete verb contract. @@ -138,6 +139,7 @@ export function buildRootCommand(): Command { animateCommand, retextureCommand, textToImageCommand, + textToMotionCommand, imageToImageCommand, multiColorPrintCommand, analyzePrintabilityCommand, diff --git a/tests/client.test.ts b/tests/client.test.ts index b18fdab..5f3d667 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -100,6 +100,27 @@ test("MeshyClient.imageTo3d.create — POSTs to v1 with JSON body", async () => } }); +test("MeshyClient.textToMotion — uses the v1 text-to-motion family", async () => { + const { calls, restore } = installFetch(() => jsonResponse({ result: "motion-task-1" })); + try { + const client = new MeshyClient(buildConfig()); + const id = await client.textToMotion.create({ + prompt: "a character waving", + mode: "prime", + duration: 3, + }); + assert.equal(id, "motion-task-1"); + assert.equal(calls[0]!.url, "https://api.example.com/v1/text-to-motion"); + assert.deepEqual(JSON.parse(calls[0]!.body!), { + prompt: "a character waving", + mode: "prime", + duration: 3, + }); + } finally { + restore(); + } +}); + test("MeshyClient — retrieve percent-encodes task ids with unusual characters", async () => { const { calls, restore } = installFetch(() => jsonResponse({ id: "abc/slash", status: "SUCCEEDED", type: "image-to-3d", progress: 100 }), diff --git a/tests/download.test.ts b/tests/download.test.ts index 80be207..1b49237 100644 --- a/tests/download.test.ts +++ b/tests/download.test.ts @@ -111,6 +111,24 @@ test("enumerateArtifacts — animate-style result.*_url", () => { assert.deepEqual(keys, ["animation_fbx_url", "animation_glb_url"]); }); +test("enumerateArtifacts — text-to-motion uses motion_format for the extension", () => { + const arts = enumerateArtifacts( + task({ + result: { + motion_url: "https://example.com/motion-clip", + motion_format: "bvh", + duration_ms: 3000, + mode: "swift", + }, + }), + ); + assert.deepEqual(arts, [{ + key: "motion_url", + url: "https://example.com/motion-clip", + preferredExt: "bvh", + }]); +}); + test("downloadArtifacts — single 2D file, content-type matches user ext, no conversion", async () => { const url = "https://cdn.example.com/img_0"; const png = await tinyPngBytes(); diff --git a/tests/surface.test.ts b/tests/surface.test.ts index 9191e41..798c719 100644 --- a/tests/surface.test.ts +++ b/tests/surface.test.ts @@ -151,6 +151,21 @@ test("rigging carries no texture flag and documents its bundled animations", () assert.match(sub(root, "rigging").description(), /walking and running/i); }); +test("text-to-motion exposes the v1 motion contract", () => { + const cmd = sub(root, "text-to-motion"); + for (const verb of ["create", "get", "wait", "delete", "list"]) { + sub(cmd, verb); + } + const create = sub(cmd, "create"); + const flags = createFlags("text-to-motion"); + for (const flag of ["--prompt", "--mode", "--duration"]) { + assert.ok(flags.has(flag), `text-to-motion is missing ${flag}`); + } + const mode = create.options.find((o) => o.long === "--mode"); + assert.deepEqual(mode?.argChoices, ["prime", "swift"]); + assert.match(mode?.description ?? "", /default: prime/); +}); + test("descriptions and flag help stay lean: no credit prices in the command surface", () => { // Pricing lives in the skills (linked to https://docs.meshy.ai/en/api/pricing), // not in --help output that rides along in every agent context. "credit