@doufunao123/asset-gateway 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +30 -3
  2. package/dist/index.js +1390 -457
  3. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command9 } from "commander";
4
+ import { Command as Command12 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { existsSync as existsSync2, unlinkSync } from "fs";
@@ -11,68 +11,13 @@ import { Command } from "commander";
11
11
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
12
  import { homedir } from "os";
13
13
  import { dirname, join } from "path";
14
-
15
- // src/errors.ts
16
- var GatewayError = class extends Error {
17
- code;
18
- exitCode;
19
- suggestion;
20
- constructor(message, options) {
21
- super(message);
22
- this.name = "GatewayError";
23
- this.code = options.code;
24
- this.exitCode = options.exitCode ?? 1;
25
- this.suggestion = options.suggestion;
26
- }
27
- };
28
- function configError(message, suggestion) {
29
- return new GatewayError(message, {
30
- code: "CONFIG_ERROR",
31
- exitCode: 1,
32
- suggestion: suggestion ?? "Run asset-gateway auth set <token> to configure credentials"
33
- });
34
- }
35
- function notFoundError(message) {
36
- return new GatewayError(message, {
37
- code: "NOT_FOUND",
38
- exitCode: 1
39
- });
40
- }
41
- function apiError(message, suggestion) {
42
- return new GatewayError(message, {
43
- code: "GATEWAY_API_ERROR",
44
- exitCode: 3,
45
- suggestion: suggestion ?? "Check if the gateway is running: asset-gateway provider health"
46
- });
47
- }
48
- function httpClientError(message, suggestion) {
49
- return new GatewayError(message, {
50
- code: "HTTP_CLIENT_ERROR",
51
- exitCode: 3,
52
- suggestion: suggestion ?? "Check network connectivity to the gateway"
53
- });
54
- }
55
- function internalError(message) {
56
- return new GatewayError(message, {
57
- code: "INTERNAL_ERROR",
58
- exitCode: 2
59
- });
60
- }
61
- function normalizeError(error2) {
62
- if (error2 instanceof GatewayError) {
63
- return error2;
64
- }
65
- if (error2 instanceof Error) {
66
- return internalError(error2.message);
67
- }
68
- return internalError(String(error2));
69
- }
14
+ import { AssetForgeError } from "@doufunao123/assetforge-sdk";
70
15
 
71
16
  // src/meta.ts
72
17
  var CLI_NAME = "asset-gateway";
73
- var CLI_VERSION = "0.6.0";
18
+ var CLI_VERSION = "0.19.0";
74
19
  var CLI_DESCRIPTION = "Universal asset generation gateway CLI";
75
- var DEFAULT_GATEWAY_URL = "https://assets.xiaomao.chat";
20
+ var DEFAULT_GATEWAY_URL = "https://asset.origingame.dev";
76
21
 
77
22
  // src/config.ts
78
23
  function configDir() {
@@ -94,8 +39,9 @@ function loadAuthConfig() {
94
39
  const parsed = JSON.parse(content);
95
40
  return { token: parsed.token, gateway_url: parsed.gateway_url };
96
41
  } catch (error2) {
97
- throw configError(
98
- `Failed to parse auth config at ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
42
+ throw new AssetForgeError(
43
+ `Failed to parse auth config at ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`,
44
+ { code: "CONFIG_ERROR" }
99
45
  );
100
46
  }
101
47
  }
@@ -166,107 +112,8 @@ ${formatHuman(val, indent + 2)}`;
166
112
  }).join("\n");
167
113
  }
168
114
 
169
- // src/client.ts
170
- import { readFile } from "fs/promises";
171
- import { basename } from "path";
172
- var GatewayClient = class {
173
- constructor(baseUrl, token) {
174
- this.baseUrl = baseUrl;
175
- this.token = token;
176
- }
177
- async get(path) {
178
- return this.request("GET", path);
179
- }
180
- async post(path, body) {
181
- return this.request("POST", path, { body });
182
- }
183
- async put(path, body) {
184
- return this.request("PUT", path, { body });
185
- }
186
- async delete(path) {
187
- return this.request("DELETE", path);
188
- }
189
- async uploadFile(filePath) {
190
- let content;
191
- try {
192
- content = await readFile(filePath);
193
- } catch (error2) {
194
- throw configError(
195
- `Failed to read file ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`
196
- );
197
- }
198
- const form = new FormData();
199
- const arrayBuffer = content.buffer.slice(
200
- content.byteOffset,
201
- content.byteOffset + content.byteLength
202
- );
203
- form.append("file", new Blob([arrayBuffer]), basename(filePath));
204
- return this.request("POST", "/api/assets/upload", { form });
205
- }
206
- async request(method, path, options = {}) {
207
- const url = new URL(path, ensureTrailingSlash(this.baseUrl));
208
- const headers = new Headers(options.headers);
209
- if (this.token) {
210
- headers.set("authorization", `Bearer ${this.token}`);
211
- }
212
- let body;
213
- if (options.form) {
214
- body = options.form;
215
- } else if (options.body !== void 0) {
216
- headers.set("content-type", "application/json");
217
- body = JSON.stringify(options.body);
218
- }
219
- let response;
220
- try {
221
- response = await fetch(url, { method, headers, body });
222
- } catch (error2) {
223
- throw httpClientError(
224
- error2 instanceof Error ? error2.message : String(error2)
225
- );
226
- }
227
- const text = await response.text();
228
- const payload = parseResponse(text);
229
- if (!response.ok) {
230
- const preview = typeof payload === "string" ? payload : JSON.stringify(payload);
231
- if (response.status === 404) {
232
- throw notFoundError(`HTTP 404 - ${truncate(preview, 512)}`);
233
- }
234
- throw apiError(`HTTP ${response.status} - ${truncate(preview, 512)}`);
235
- }
236
- if (isEnvelope(payload)) {
237
- if (!payload.ok) {
238
- const errMsg = typeof payload.error === "object" && payload.error !== null ? JSON.stringify(payload.error) : String(payload.error);
239
- throw apiError(errMsg);
240
- }
241
- return payload.data;
242
- }
243
- return payload;
244
- }
245
- };
246
- function isEnvelope(value) {
247
- return typeof value === "object" && value !== null && "ok" in value;
248
- }
249
- function ensureTrailingSlash(url) {
250
- return url.endsWith("/") ? url : `${url}/`;
251
- }
252
- function parseResponse(text) {
253
- if (!text) {
254
- return null;
255
- }
256
- try {
257
- return JSON.parse(text);
258
- } catch {
259
- return text;
260
- }
261
- }
262
- function truncate(value, maxLength) {
263
- if (value.length <= maxLength) {
264
- return value;
265
- }
266
- return `${value.slice(0, maxLength)}...[truncated]`;
267
- }
268
-
269
115
  // src/commands/common.ts
116
+ import { AssetForge, AssetForgeError as AssetForgeError2, toAssetForgeError } from "@doufunao123/assetforge-sdk";
270
117
  function getGlobalOptions(command) {
271
118
  return command.optsWithGlobals();
272
119
  }
@@ -275,13 +122,13 @@ function createContext(command, requireAuth = true) {
275
122
  const url = resolveGatewayUrl({ gatewayUrl: globals.gatewayUrl });
276
123
  const token = resolveToken({ token: globals.token });
277
124
  if (requireAuth && !token) {
278
- throw configError(
125
+ throw new AssetForgeError2(
279
126
  "Authentication required. Use --token, set ASSET_GATEWAY_TOKEN, or run asset-gateway auth set <token>.",
280
- "asset-gateway auth set <token>"
127
+ { code: "CONFIG_ERROR" }
281
128
  );
282
129
  }
283
130
  return {
284
- client: new GatewayClient(url, token),
131
+ client: new AssetForge({ apiKey: token ?? "anonymous", baseUrl: url }),
285
132
  human: Boolean(globals.human),
286
133
  fields: globals.fields
287
134
  };
@@ -291,16 +138,40 @@ function printSuccess(commandName, data, ctx) {
291
138
  output(success(commandName, filtered), ctx.human);
292
139
  }
293
140
  function printError(commandName, err, human = false) {
294
- const normalized = normalizeError(err);
141
+ const normalized = toAssetForgeError(err);
295
142
  output(
296
- error(commandName, normalized.code, normalized.message, normalized.suggestion),
143
+ error(commandName, normalized.code, normalized.message, getSuggestion(normalized)),
297
144
  human
298
145
  );
299
- process.exit(normalized.exitCode);
146
+ process.exit(getExitCode(normalized));
300
147
  }
301
148
  function isRecord(value) {
302
149
  return typeof value === "object" && value !== null && !Array.isArray(value);
303
150
  }
151
+ function getSuggestion(error2) {
152
+ if (isRecord(error2.details) && typeof error2.details.suggestion === "string") {
153
+ return error2.details.suggestion;
154
+ }
155
+ if (error2.code === "CONFIG_ERROR") {
156
+ return "asset-gateway auth set <token>";
157
+ }
158
+ if (error2.code === "NETWORK_ERROR" || error2.code === "FETCH_UNAVAILABLE") {
159
+ return "Check network connectivity to the gateway";
160
+ }
161
+ if (error2.code === "API_ERROR") {
162
+ return "Check if the gateway is running: asset-gateway provider health";
163
+ }
164
+ return void 0;
165
+ }
166
+ function getExitCode(error2) {
167
+ if (error2.code === "INTERNAL_ERROR") {
168
+ return 2;
169
+ }
170
+ if (error2.status || error2.code === "API_ERROR" || error2.code === "NETWORK_ERROR") {
171
+ return 3;
172
+ }
173
+ return 1;
174
+ }
304
175
 
305
176
  // src/commands/auth.ts
306
177
  function createAuthCommand() {
@@ -356,13 +227,17 @@ function mask(token) {
356
227
 
357
228
  // src/commands/describe.ts
358
229
  import { Command as Command2 } from "commander";
230
+
231
+ // src/describe-schemas.ts
359
232
  var SCHEMAS = {
360
233
  auth: {
361
234
  description: "Credential management",
362
235
  subcommands: {
363
236
  set: {
364
237
  description: "Save token and gateway URL locally",
365
- params: { token: { type: "string", required: true, description: "API token (agk_ prefix for admin, or any API key)" } }
238
+ params: {
239
+ token: { type: "string", required: true, description: "API token or admin token" }
240
+ }
366
241
  },
367
242
  status: { description: "Show current authentication status" },
368
243
  clear: { description: "Remove saved credentials" }
@@ -372,91 +247,327 @@ var SCHEMAS = {
372
247
  description: "Generate assets via the gateway",
373
248
  subcommands: {
374
249
  image: {
375
- description: "Generate an image from a text prompt",
250
+ description: "Generate or edit an image",
376
251
  params: {
377
- "--prompt": { type: "string", required: true, description: "Image description prompt" },
378
- "--provider": { type: "string", required: false, description: "Provider to use" },
379
- "--transparent": { type: "bool", default: false, description: "Request transparent background" },
380
- "--model": { type: "string", required: false, description: "Model to use" },
381
- "--size": { type: "string", required: false, description: "Image size (e.g. 1024x1024)" },
382
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
252
+ "--prompt": { type: "string", required: true, description: "Image prompt" },
253
+ "--provider": { type: "string", required: false },
254
+ "--transparent": { type: "bool", description: "Transparent background" },
255
+ "--model": { type: "string" },
256
+ "--size": { type: "string", description: 'e.g. "1024x1024"' },
257
+ "--input": { type: "string", description: "Image URL for editing" },
258
+ "--ref": { type: "string[]", description: "Reference image URLs (repeatable)" },
259
+ "--edit-mode": { type: "string", description: "edit | inpaint | restyle | expand" },
260
+ "--session": { type: "string", description: "Multi-turn session id" },
261
+ "--output-dir": { type: "string", default: "." }
383
262
  }
384
263
  },
385
264
  video: {
386
- description: "Generate a video from a text prompt",
265
+ description: "Generate a video from text or an input image",
387
266
  params: {
388
- "--prompt": { type: "string", required: true, description: "Video description prompt" },
389
- "--provider": { type: "string", required: false, description: "Provider to use" },
390
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
267
+ "--prompt": { type: "string", required: true },
268
+ "--provider": { type: "string" },
269
+ "--input": { type: "string", description: "Image URL for I2V" },
270
+ "--output-dir": { type: "string", default: "." }
391
271
  }
392
272
  },
393
- audio: {
394
- description: "Generate audio from a text prompt",
273
+ batch: {
274
+ description: "Batch-generate assets with shared parameters and optional compose step",
395
275
  params: {
396
- "--prompt": { type: "string", required: true, description: "Audio description prompt" },
397
- "--type": { type: "string", required: false, description: "Audio type: bgm or sfx" },
398
- "--duration": { type: "number", required: false, description: "Duration in seconds" },
399
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
276
+ "--prompt": { type: "string[]", required: true, description: "One prompt per frame" },
277
+ "--asset-type": { type: "string", default: "image" },
278
+ "--transparent": { type: "bool" },
279
+ "--size": { type: "string" },
280
+ "--ref": { type: "string[]" },
281
+ "--compose": { type: "string", description: "horizontal | vertical | grid" },
282
+ "--columns": { type: "number" },
283
+ "--frame-size": { type: "string", description: "e.g. 64x64" },
284
+ "--output-dir": { type: "string", default: "." }
285
+ }
286
+ },
287
+ sfx: {
288
+ description: "Generate sound effects (impacts, footsteps, UI sounds, ambience)",
289
+ params: {
290
+ "--prompt": { type: "string", required: true },
291
+ "--duration": { type: "number", required: true, description: "Duration in seconds (1-5s for short SFX, 5-15s for ambience, max 30s)" },
292
+ "--output-dir": { type: "string", default: "." }
293
+ }
294
+ },
295
+ music: {
296
+ description: "Generate music using the gateway music provider",
297
+ params: {
298
+ "--prompt": { type: "string", required: true },
299
+ "--duration": { type: "number", description: "Seconds" },
300
+ "--force-instrumental": { type: "bool", description: "Request instrumental output when supported" },
301
+ "--output-format": { type: "string", description: "Provider-specific output format override when supported" },
302
+ "--output-dir": { type: "string", default: "." }
303
+ }
304
+ },
305
+ tts: {
306
+ description: "Text-to-speech via MiMo v2.5 TTS",
307
+ params: {
308
+ "--prompt": { type: "string", required: true },
309
+ "--voice": { type: "string", description: "MiMo prebuilt voice name (default: server config, usually Mia)" },
310
+ "--context": { type: "string", description: "Natural-language style or director instruction" },
311
+ "--output-dir": { type: "string", default: "." }
400
312
  }
401
313
  },
402
314
  model: {
403
- description: "Generate a 3D model",
315
+ description: "3D model generation (Meshy AI)",
316
+ params: {
317
+ "--image": { type: "string", description: "Reference image path or URL" },
318
+ "--format": { type: "string", default: "glb" },
319
+ "--prompt": { type: "string" },
320
+ "--ai-model": { type: "string", description: "meshy-5 | meshy-6 | latest" },
321
+ "--polycount": { type: "number" },
322
+ "--pbr": { type: "bool" },
323
+ "--hd-texture": { type: "bool" },
324
+ "--pose-mode": { type: "string", description: "a-pose | t-pose" },
325
+ "--auto-size": { type: "bool" },
326
+ "--negative-prompt": { type: "string" },
327
+ "--multiview": { type: "string", description: "Comma-separated multiview image paths or URLs" },
328
+ "--output-dir": { type: "string", default: "." }
329
+ }
330
+ },
331
+ character: {
332
+ description: "Generate a fully processed 3D character (Meshy AI)",
404
333
  params: {
405
- "--image": { type: "string", required: false, description: "Reference image URL" },
406
- "--prompt": { type: "string", required: false, description: "Model description prompt" },
407
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
334
+ "--prompt": { type: "string", description: "Character description prompt" },
335
+ "--image": { type: "string", description: "Reference image path or URL" },
336
+ "--images": { type: "string", description: "Comma-separated reference image paths or URLs" },
337
+ "--format": { type: "string", default: "glb" },
338
+ "--polycount": { type: "number" },
339
+ "--pbr": { type: "bool" },
340
+ "--hd-texture": { type: "bool" },
341
+ "--pose-mode": { type: "string", description: "a-pose | t-pose" },
342
+ "--ai-model": { type: "string", description: "meshy-5 | meshy-6 | latest" },
343
+ "--auto-size": { type: "bool" },
344
+ "--output-dir": { type: "string", default: "." }
345
+ }
346
+ },
347
+ prop: {
348
+ description: "Generate a fully processed 3D prop (Meshy AI)",
349
+ params: {
350
+ "--prompt": { type: "string", description: "Prop description prompt" },
351
+ "--image": { type: "string", description: "Reference image path or URL" },
352
+ "--images": { type: "string", description: "Comma-separated reference image paths or URLs" },
353
+ "--format": { type: "string", default: "glb" },
354
+ "--polycount": { type: "number" },
355
+ "--pbr": { type: "bool" },
356
+ "--hd-texture": { type: "bool" },
357
+ "--ai-model": { type: "string", description: "meshy-5 | meshy-6 | latest" },
358
+ "--auto-size": { type: "bool" },
359
+ "--texture-prompt": { type: "string" },
360
+ "--output-dir": { type: "string", default: "." }
408
361
  }
409
362
  },
410
363
  text: {
411
- description: "Generate text via LLM",
364
+ description: "LLM text via proxy",
365
+ params: {
366
+ "--prompt": { type: "string", required: true },
367
+ "--model": { type: "string" },
368
+ "--max-tokens": { type: "number" },
369
+ "--output-dir": { type: "string", default: "." }
370
+ }
371
+ },
372
+ sprite: {
373
+ description: "Generate character animation spritesheet (AutoSprite)",
374
+ params: {
375
+ "--prompt": { type: "string", required: true, description: "Character description" },
376
+ "--input": { type: "string", description: "Reference image path or URL" },
377
+ "--animation-type": { type: "string", default: "walk", description: "walk, run, idle, jump, attack, death, cast, dance, wave, interact, or custom text" },
378
+ "--style": { type: "string", description: "Art style: 16-bit, hd-pixel, isometric, retro-8bit, anime, chibi, painterly, vector" },
379
+ "--frame-count": { type: "number", default: 8, description: "Number of animation frames" },
380
+ "--frame-size": { type: "number", default: 256, description: "Frame size in pixels (square)" },
381
+ "--is-humanoid": { type: "boolean", default: true },
382
+ "--output-dir": { type: "string", default: "." }
383
+ }
384
+ },
385
+ world: {
386
+ description: "Generate a 3D world or environment",
412
387
  params: {
413
- "--prompt": { type: "string", required: true, description: "Text prompt" },
414
- "--model": { type: "string", required: false, description: "Model to use" },
415
- "--max-tokens": { type: "number", required: false, description: "Maximum tokens" },
416
- "--output-dir": { type: "string", default: ".", description: "Directory to save output" }
388
+ "--prompt": { type: "string", required: true },
389
+ "--input": { type: "string", description: "Input image path or URL" },
390
+ "--model": { type: "string", default: "marble-1.1" },
391
+ "--display-name": { type: "string" },
392
+ "--output-dir": { type: "string", default: "." }
417
393
  }
418
394
  }
419
395
  }
420
396
  },
421
- provider: {
422
- description: "Provider management",
397
+ process: {
398
+ description: "Image/video post-process on gateway (ImageMagick/ffmpeg/rembg)",
399
+ subcommands: {
400
+ crop: {
401
+ description: "Smart crop (trim / power_of2)",
402
+ params: {
403
+ "--input": { type: "string", required: true, description: "File path or URL" },
404
+ "--mode": { type: "string", default: "tightest" },
405
+ "--output-dir": { type: "string", default: "." }
406
+ }
407
+ },
408
+ resize: {
409
+ description: "Resize to exact width/height",
410
+ params: {
411
+ "--input": { type: "string", required: true },
412
+ "--width": { type: "number", required: true },
413
+ "--height": { type: "number", required: true },
414
+ "--output-dir": { type: "string", default: "." }
415
+ }
416
+ },
417
+ compose: {
418
+ description: "Sprite sheet from multiple images",
419
+ params: {
420
+ "--input": { type: "string[]", required: true },
421
+ "--direction": { type: "string", default: "horizontal" },
422
+ "--columns": { type: "number" },
423
+ "--padding": { type: "string" },
424
+ "--frame-width": { type: "number" },
425
+ "--frame-height": { type: "number" },
426
+ "--output-dir": { type: "string", default: "." }
427
+ }
428
+ },
429
+ "remove-bg": {
430
+ description: "Remove background (rembg / fallback)",
431
+ params: {
432
+ "--input": { type: "string[]", required: true },
433
+ "--bg-color": { type: "string" },
434
+ "--output-dir": { type: "string", default: "." }
435
+ }
436
+ }
437
+ }
438
+ },
439
+ process3d: {
440
+ description: "3D model post-processing (Meshy AI)",
423
441
  subcommands: {
424
- list: { description: "List available providers" },
425
- health: {
426
- description: "Check provider health",
427
- params: { name: { type: "string", required: false, description: "Specific provider name" } }
442
+ remesh: {
443
+ description: "Remesh a model and optionally change format or polygon count",
444
+ params: {
445
+ "--task-id": { type: "string", required: false, description: "Meshy task ID" },
446
+ "--model-url": { type: "string", required: false, description: "Model URL when remeshing without a task" },
447
+ "--format": { type: "string", required: true },
448
+ "--polycount": { type: "number" },
449
+ "--topology": { type: "string", description: "triangle | quad" },
450
+ "--auto-size": { type: "bool" },
451
+ "--output-dir": { type: "string", default: "." }
452
+ }
453
+ },
454
+ retexture: {
455
+ description: "Regenerate textures for an existing Meshy model",
456
+ params: {
457
+ "--task-id": { type: "string", required: true },
458
+ "--prompt": { type: "string", description: "Text style prompt for the material pass" },
459
+ "--style-image": { type: "string", description: "Style image path or URL" },
460
+ "--pbr": { type: "bool" },
461
+ "--hd-texture": { type: "bool" },
462
+ "--ai-model": { type: "string", description: "meshy-5 | meshy-6 | latest" },
463
+ "--output-dir": { type: "string", default: "." }
464
+ }
465
+ },
466
+ rig: {
467
+ description: "Rig a Meshy model for character animation",
468
+ params: {
469
+ "--task-id": { type: "string", required: false, description: "Meshy task ID" },
470
+ "--height": { type: "number", description: "Character height in meters" },
471
+ "--model-url": { type: "string", description: "Model URL when rigging without a task" },
472
+ "--output-dir": { type: "string", default: "." }
473
+ }
474
+ },
475
+ animate: {
476
+ description: "Apply a preset animation to a rigged character. Common presets: 0=Idle, 1=Walk, 4=Attack, 8=Dead, 14=Run, 16=RunFast, 466=Jump. 500+ presets available.",
477
+ params: {
478
+ "--task-id": { type: "string", required: true, description: "Meshy rigging task ID" },
479
+ "--action-id": {
480
+ type: "number",
481
+ required: true,
482
+ description: "Animation preset ID (see docs for full list)"
483
+ },
484
+ "--fps": { type: "number", description: "Target frame rate: 24, 25, 30, or 60" },
485
+ "--output-dir": { type: "string", default: "." }
486
+ }
487
+ },
488
+ refine: {
489
+ description: "Refine a Meshy preview model into a higher quality result",
490
+ params: {
491
+ "--task-id": { type: "string", required: true, description: "Meshy preview task ID" },
492
+ "--pbr": { type: "bool" },
493
+ "--hd-texture": { type: "bool" },
494
+ "--texture-prompt": { type: "string" },
495
+ "--ai-model": { type: "string", description: "meshy-5 | meshy-6 | latest" },
496
+ "--output-dir": { type: "string", default: "." }
497
+ }
428
498
  }
429
499
  }
430
500
  },
501
+ upload: {
502
+ description: "Upload and list gateway assets",
503
+ subcommands: {
504
+ file: { description: "Upload file \u2192 URL", params: { "<path>": { required: true } } },
505
+ list: { description: "List uploads" },
506
+ delete: { description: "Delete by filename (admin)", params: { "<filename>": { required: true } } }
507
+ }
508
+ },
509
+ provider: {
510
+ description: "Provider discovery and health",
511
+ subcommands: {
512
+ list: { description: "List providers" },
513
+ health: { description: "Health check", params: { "[name]": { type: "string", required: false } } }
514
+ }
515
+ },
516
+ voice: {
517
+ description: "Design, clone, list, and delete MiMo voices",
518
+ subcommands: {
519
+ design: {
520
+ description: "Generate a MiMo voice from a text description",
521
+ params: {
522
+ "--voice-prompt": { type: "string", required: true },
523
+ "--preview-text": { type: "string", required: true },
524
+ "--style": { type: "string" },
525
+ "--name": { type: "string" },
526
+ "--save-as": { type: "string" },
527
+ "--output": { type: "string", description: "Write preview WAV to this path" }
528
+ }
529
+ },
530
+ clone: {
531
+ description: "Clone a MiMo voice from an mp3/wav sample",
532
+ params: {
533
+ "--audio": { type: "string", required: true, description: "Voice sample mp3/wav file or data URL" },
534
+ "--preview-text": { type: "string", required: true },
535
+ "--audio-mime": { type: "string" },
536
+ "--style": { type: "string" },
537
+ "--name": { type: "string" },
538
+ "--save-as": { type: "string" },
539
+ "--output": { type: "string", description: "Write preview WAV to this path" }
540
+ }
541
+ },
542
+ list: { description: "List saved voices", params: { "--type": { type: "string", description: "vc | vd" } } },
543
+ delete: { description: "Delete a saved voice", params: { "<voice-id>": { type: "string", required: true }, "--type": { type: "string", description: "vc | vd" } } }
544
+ }
545
+ },
431
546
  job: {
432
- description: "Job management",
547
+ description: "Async job history",
433
548
  subcommands: {
434
549
  list: {
435
550
  description: "List jobs",
436
551
  params: {
437
- "--status": { type: "string", required: false, description: "Filter by status" },
438
- "--limit": { type: "number", required: false, description: "Maximum number of jobs to return" }
552
+ "--status": { type: "string" },
553
+ "--limit": { type: "number" }
439
554
  }
440
555
  },
441
- status: {
442
- description: "Get job status",
443
- params: { id: { type: "string", required: true, description: "Job ID" } }
444
- },
445
- cancel: {
446
- description: "Cancel a job",
447
- params: { id: { type: "string", required: true, description: "Job ID" } }
448
- }
556
+ status: { description: "Job detail", params: { "<id>": { type: "string", required: true } } },
557
+ cancel: { description: "Cancel pending/running", params: { "<id>": { type: "string", required: true } } }
449
558
  }
450
559
  },
451
560
  describe: {
452
- description: "Self-describe available commands (JSON Schema)",
561
+ description: "Command introspection (this output)",
453
562
  params: {
454
- command: { type: "string", required: false, description: "Specific command to describe" }
563
+ "[command]": { type: "string", required: false, description: "Top-level group: generate, process, \u2026" }
455
564
  }
456
565
  }
457
566
  };
567
+
568
+ // src/commands/describe.ts
458
569
  function createDescribeCommand() {
459
- return new Command2("describe").description("Self-describe available commands (JSON Schema)").argument("[command]", "Specific command to describe").action(function(commandArg) {
570
+ return new Command2("describe").description("Self-describe available commands (JSON Schema)").argument("[command]", "Specific command group to describe (e.g. generate, process)").action(function(commandArg) {
460
571
  const globals = this.optsWithGlobals();
461
572
  if (!commandArg) {
462
573
  output(
@@ -486,19 +597,109 @@ function createDescribeCommand() {
486
597
  }
487
598
 
488
599
  // src/commands/generate.ts
489
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
490
- import { join as join2 } from "path";
600
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
601
+ import { dirname as dirname2, extname, join as join2 } from "path";
602
+ import { AssetForgeError as AssetForgeError3 } from "@doufunao123/assetforge-sdk";
491
603
  import { Command as Command3 } from "commander";
492
604
  function inferExtension(assetType) {
493
- const map = { image: "png", audio: "mp3", tts: "mp3", video: "mp4", model3d: "glb", text: "txt" };
605
+ const map = {
606
+ image: "png",
607
+ audio: "mp3",
608
+ sfx: "mp3",
609
+ music: "mp3",
610
+ tts: "wav",
611
+ video: "mp4",
612
+ model3d: "glb",
613
+ character: "glb",
614
+ prop: "glb",
615
+ text: "txt",
616
+ sprite: "png",
617
+ world: "spz",
618
+ animation: "glb",
619
+ material: "zip",
620
+ hdri: "hdr",
621
+ vfx: "zip",
622
+ lut: "cube",
623
+ shader: "glsl",
624
+ font_typeface: "json",
625
+ skybox: "png",
626
+ decal: "png",
627
+ heightmap: "png"
628
+ };
494
629
  return map[assetType] ?? "bin";
495
630
  }
631
+ function inferExtFromResult(result) {
632
+ const meta = result.metadata;
633
+ if (!meta) return null;
634
+ const ct = meta.content_type;
635
+ if (!ct) return null;
636
+ const map = {
637
+ "image/gif": "gif",
638
+ "image/webp": "webp",
639
+ "image/png": "png",
640
+ "image/jpeg": "jpg",
641
+ "video/mp4": "mp4",
642
+ "audio/mpeg": "mp3",
643
+ "audio/wav": "wav",
644
+ "audio/x-wav": "wav",
645
+ "model/gltf-binary": "glb",
646
+ "model/gltf+json": "gltf",
647
+ "model/stl": "stl",
648
+ "model/vnd.usdz+zip": "usdz"
649
+ };
650
+ return map[ct] ?? null;
651
+ }
652
+ function infer3dExtension(format) {
653
+ const map = {
654
+ glb: "glb",
655
+ fbx: "fbx",
656
+ obj: "obj",
657
+ usdz: "usdz",
658
+ stl: "stl",
659
+ "3mf": "3mf",
660
+ gltf: "gltf"
661
+ };
662
+ return format ? map[String(format).toLowerCase()] ?? null : null;
663
+ }
496
664
  function stripDataUri(data) {
497
665
  const idx = data.indexOf(";base64,");
498
666
  return idx >= 0 ? data.slice(idx + 8) : data;
499
667
  }
500
- async function saveOutput(result, assetType, outputDir) {
501
- const ext = inferExtension(assetType);
668
+ function inferMimeType(filePath) {
669
+ const ext = extname(filePath).toLowerCase();
670
+ const map = {
671
+ ".jpg": "image/jpeg",
672
+ ".jpeg": "image/jpeg",
673
+ ".png": "image/png",
674
+ ".webp": "image/webp",
675
+ ".gif": "image/gif",
676
+ ".mp4": "video/mp4",
677
+ ".mov": "video/quicktime"
678
+ };
679
+ return map[ext] ?? "application/octet-stream";
680
+ }
681
+ function toInputFile(value) {
682
+ if (!value) {
683
+ return void 0;
684
+ }
685
+ if (!existsSync3(value)) {
686
+ return value;
687
+ }
688
+ const b64 = readFileSync2(value).toString("base64");
689
+ return `data:${inferMimeType(value)};base64,${b64}`;
690
+ }
691
+ function parseCommaSeparatedValues(raw) {
692
+ if (!raw) {
693
+ return [];
694
+ }
695
+ return String(raw).split(",").map((value) => value.trim()).filter(Boolean);
696
+ }
697
+ function collectValue(value, previous = []) {
698
+ previous.push(value);
699
+ return previous;
700
+ }
701
+ async function saveOutput(result, assetType, outputDir, preferredFormat) {
702
+ const ext = inferExtFromResult(result) ?? infer3dExtension(preferredFormat) ?? inferExtension(assetType);
502
703
  const timestamp = Date.now();
503
704
  const filename = `${assetType}_${timestamp}.${ext}`;
504
705
  mkdirSync2(outputDir, { recursive: true });
@@ -524,22 +725,109 @@ async function saveOutput(result, assetType, outputDir) {
524
725
  }
525
726
  return null;
526
727
  }
728
+ async function saveNamedOutput(result, assetType, filePath) {
729
+ mkdirSync2(dirname2(filePath), { recursive: true });
730
+ if (result.output_data) {
731
+ const raw = String(result.output_data);
732
+ if (assetType === "text") {
733
+ writeFileSync2(filePath, raw, "utf8");
734
+ } else {
735
+ writeFileSync2(filePath, Buffer.from(stripDataUri(raw), "base64"));
736
+ }
737
+ return filePath;
738
+ }
739
+ if (result.output_url) {
740
+ const response = await fetch(String(result.output_url));
741
+ if (!response.ok) {
742
+ return null;
743
+ }
744
+ const buffer = Buffer.from(await response.arrayBuffer());
745
+ writeFileSync2(filePath, buffer);
746
+ return filePath;
747
+ }
748
+ return null;
749
+ }
750
+ function parseFrameSize(raw) {
751
+ const [width, height] = raw.split("x");
752
+ const frameWidth = Number(width);
753
+ const frameHeight = Number(height);
754
+ if (!Number.isFinite(frameWidth) || !Number.isFinite(frameHeight)) {
755
+ throw new Error("frame size must be formatted as WIDTHxHEIGHT");
756
+ }
757
+ return { frame_width: frameWidth, frame_height: frameHeight };
758
+ }
759
+ function assertHas3dInput(prompt, image, images) {
760
+ if (!prompt && !image && parseCommaSeparatedValues(images).length === 0) {
761
+ throw new Error("Provide at least one of --prompt, --image, or --images");
762
+ }
763
+ }
764
+ function toJsonObject(value) {
765
+ return value;
766
+ }
767
+ function isPackOnlyResult(value) {
768
+ return value.ok === false && value.pack_only === true;
769
+ }
770
+ async function runGeneratedAssetCommand(command, sdkMethod, commandName, assetType, options) {
771
+ const ctx = createContext(command);
772
+ const generate = ctx.client[sdkMethod];
773
+ const data = await generate.call(ctx.client, String(options.prompt), {
774
+ provider: options.provider,
775
+ model: options.model,
776
+ size: options.size,
777
+ quality: options.quality,
778
+ background: options.background,
779
+ output_format: options.outputFormat,
780
+ n: options.n ? Number(options.n) : void 0,
781
+ preset: options.preset,
782
+ transparent: options.transparent ? true : void 0,
783
+ input: toInputFile(options.input)
784
+ });
785
+ if (isPackOnlyResult(data)) {
786
+ throw new AssetForgeError3("This asset type requires a library pack. Run: asset-gateway pack list", {
787
+ code: "PACK_ONLY",
788
+ details: data
789
+ });
790
+ }
791
+ const localPath = await saveOutput(data, assetType, String(options.outputDir ?? "."));
792
+ if (localPath) data.local_path = localPath;
793
+ printSuccess(commandName, data, ctx);
794
+ }
795
+ function createGeneratedAssetCommand(name, sdkMethod, assetType, description) {
796
+ const command = new Command3(name).description(description).requiredOption("--prompt <text>", "Asset description prompt").option("--provider <id>", "Provider to use").option("--model <model>", "Model to use").option("--size <size>", "Output size when supported").option("--input <path>", "Input file path or URL when supported").option("--quality <quality>", "Image quality: low, medium, high").option("--background <background>", "Image background: auto, opaque, transparent").option("--output-format <fmt>", "Image output format: png, webp, jpeg").option("--n <num>", "Number of image variants (1-10)").option("--preset <preset>", "Image preset id").option("--transparent", "Request transparent background when supported").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
797
+ const commandName = `generate.${assetType}`;
798
+ try {
799
+ await runGeneratedAssetCommand(this, sdkMethod, commandName, assetType, options);
800
+ } catch (error2) {
801
+ printError(commandName, error2);
802
+ }
803
+ });
804
+ if (name === "font-typeface") {
805
+ command.alias("font_typeface");
806
+ }
807
+ return command;
808
+ }
527
809
  function createGenerateCommand() {
528
810
  const command = new Command3("generate").description("Generate assets via the gateway");
529
811
  command.addCommand(
530
- new Command3("image").description("Generate an image from a text prompt").requiredOption("--prompt <text>", "Image description prompt").option("--provider <id>", "Provider to use").option("--transparent", "Request transparent background").option("--model <model>", "Model to use").option("--size <size>", "Image size (e.g. 1024x1024)").option("--input <url>", "Input image URL for editing (Gemini/Grok)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
812
+ new Command3("image").description("Generate an image from a text prompt").requiredOption("--prompt <text>", "Image description prompt").option("--provider <id>", "Provider to use").option("--transparent", "Request transparent background").option("--model <model>", "Model to use").option("--size <size>", "Image size (e.g. 1024x1024)").option("--quality <quality>", "Image quality: low, medium, high").option("--background <background>", "Image background: auto, opaque, transparent").option("--output-format <fmt>", "Image output format: png, webp, jpeg").option("--n <num>", "Number of image variants (1-10)").option("--preset <preset>", "Image preset id").option("--mask <path>", "PNG alpha mask path or URL for inpainting").option("--input <url>", "Input image URL for editing (Gemini/Grok)").option("--ref <urls...>", "Reference image URLs for multi-image editing (repeatable)").option("--edit-mode <mode>", "Edit mode: edit, inpaint, restyle, expand").option("--session <id>", "Session ID for multi-turn editing").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
531
813
  try {
532
814
  const ctx = createContext(this);
533
- const body = {
534
- asset_type: "image",
535
- prompt: options.prompt
536
- };
537
- if (options.provider) body.provider = options.provider;
538
- if (options.transparent) body.transparent = true;
539
- if (options.model) body.model = options.model;
540
- if (options.size) body.size = options.size;
541
- if (options.input) body.input_file = options.input;
542
- const data = await ctx.client.post("/api/generate", body);
815
+ const data = await ctx.client.image(options.prompt, {
816
+ provider: options.provider,
817
+ model: options.model,
818
+ size: options.size,
819
+ quality: options.quality,
820
+ background: options.background,
821
+ output_format: options.outputFormat,
822
+ n: options.n ? Number(options.n) : void 0,
823
+ preset: options.preset,
824
+ mask: toInputFile(options.mask),
825
+ transparent: options.transparent ? true : void 0,
826
+ input: toInputFile(options.input),
827
+ reference_images: options.ref,
828
+ edit_mode: options.editMode,
829
+ session_id: options.session
830
+ });
543
831
  const localPath = await saveOutput(data, "image", options.outputDir);
544
832
  if (localPath) data.local_path = localPath;
545
833
  printSuccess("generate.image", data, ctx);
@@ -549,15 +837,16 @@ function createGenerateCommand() {
549
837
  })
550
838
  );
551
839
  command.addCommand(
552
- new Command3("video").description("Generate a video from a text prompt").requiredOption("--prompt <text>", "Video description prompt").option("--provider <id>", "Provider to use").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
840
+ new Command3("video").description("Generate a video from a text prompt (or image-to-video with --input)").requiredOption("--prompt <text>", "Video description prompt").option("--provider <id>", "Provider to use").option("--input <url>", "Reference image URL for image-to-video (Grok)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
553
841
  try {
554
842
  const ctx = createContext(this);
555
- const body = {
556
- asset_type: "video",
557
- prompt: options.prompt
843
+ const requestOptions = {
844
+ provider: options.provider,
845
+ input: options.input
558
846
  };
559
- if (options.provider) body.provider = options.provider;
560
- const data = await ctx.client.post("/api/generate", body);
847
+ const data = await ctx.client.video(options.prompt, {
848
+ ...requestOptions
849
+ });
561
850
  const localPath = await saveOutput(data, "video", options.outputDir);
562
851
  if (localPath) data.local_path = localPath;
563
852
  printSuccess("generate.video", data, ctx);
@@ -567,41 +856,100 @@ function createGenerateCommand() {
567
856
  })
568
857
  );
569
858
  command.addCommand(
570
- new Command3("audio").description("Generate audio from a text prompt").requiredOption("--prompt <text>", "Audio description prompt").option("--type <type>", "Audio type: bgm or sfx").option("--duration <seconds>", "Duration in seconds").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
859
+ new Command3("batch").description("Batch generate multiple assets with shared parameters").requiredOption("--prompt <texts...>", "Multiple prompts (one per frame)").option("--asset-type <type>", "Asset type", "image").option("--transparent", "Request transparent background").option("--size <size>", "Image size").option("--ref <urls...>", "Reference image URLs").option("--compose <direction>", "Auto-compose: horizontal, vertical, grid").option("--columns <n>", "Grid columns for compose").option("--frame-size <size>", "Frame size for compose (e.g. 64x64)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
571
860
  try {
572
861
  const ctx = createContext(this);
862
+ const shared = {};
863
+ if (options.transparent) shared.transparent = true;
864
+ if (options.size) shared.size = options.size;
865
+ if (options.ref && options.ref.length > 0) shared.reference_images = options.ref;
573
866
  const body = {
574
- asset_type: "audio",
575
- prompt: options.prompt
867
+ asset_type: options.assetType,
868
+ prompts: options.prompt,
869
+ shared
576
870
  };
577
- if (options.type) body.audio_type = options.type;
578
- if (options.duration) body.duration = Number(options.duration);
579
- const data = await ctx.client.post("/api/generate", body);
871
+ if (options.compose) {
872
+ const compose = { direction: options.compose };
873
+ if (options.columns) compose.columns = Number(options.columns);
874
+ if (options.frameSize) Object.assign(compose, parseFrameSize(options.frameSize));
875
+ body.compose = compose;
876
+ }
877
+ const data = await ctx.client.batch(body);
878
+ const assetType = String(options.assetType);
879
+ mkdirSync2(options.outputDir, { recursive: true });
880
+ if (Array.isArray(data.frames)) {
881
+ for (const frame of data.frames) {
882
+ if (typeof frame !== "object" || frame === null) continue;
883
+ const record = frame;
884
+ const index = typeof record.index === "number" ? record.index : 0;
885
+ const localPath = await saveNamedOutput(
886
+ record,
887
+ assetType,
888
+ join2(options.outputDir, `frame_${String(index).padStart(3, "0")}.${inferExtension(assetType)}`)
889
+ );
890
+ if (localPath) record.local_path = localPath;
891
+ }
892
+ }
893
+ if (typeof data.spritesheet === "object" && data.spritesheet !== null) {
894
+ const record = data.spritesheet;
895
+ const localPath = await saveNamedOutput(
896
+ record,
897
+ "image",
898
+ join2(options.outputDir, "spritesheet.png")
899
+ );
900
+ if (localPath) record.local_path = localPath;
901
+ }
902
+ printSuccess("generate.batch", data, ctx);
903
+ } catch (error2) {
904
+ printError("generate.batch", error2);
905
+ }
906
+ })
907
+ );
908
+ command.addCommand(
909
+ new Command3("sfx").description("Generate sound effects (short audio clips: impacts, footsteps, UI sounds, ambience)").requiredOption("--prompt <text>", "Sound effect description").requiredOption("--duration <seconds>", "Duration in seconds (1-5s for short SFX, 5-15s for ambience, max 30s)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
910
+ try {
911
+ const ctx = createContext(this);
912
+ const data = await ctx.client.audio(options.prompt, {
913
+ duration: Number(options.duration)
914
+ });
580
915
  const localPath = await saveOutput(data, "audio", options.outputDir);
581
916
  if (localPath) data.local_path = localPath;
582
- printSuccess("generate.audio", data, ctx);
917
+ printSuccess("generate.sfx", data, ctx);
583
918
  } catch (error2) {
584
- printError("generate.audio", error2);
919
+ printError("generate.sfx", error2);
585
920
  }
586
921
  })
587
922
  );
588
923
  command.addCommand(
589
- new Command3("tts").description("Text-to-speech synthesis via Qwen3-TTS").requiredOption("--prompt <text>", "Text to synthesize").option("--voice <name>", "Voice name or custom voice ID", "Cherry").option("--language <lang>", "Language hint: Auto, Chinese, English, Japanese, etc.", "Auto").option("--model <model>", "Qwen3-TTS model", "qwen3-tts-flash").option("--instructions <text>", "Natural language speaking instructions (for instruct models)").option("--provider <id>", "Provider to use").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
924
+ new Command3("music").description("Generate music using the gateway music provider").requiredOption("--prompt <text>", "Music description prompt").option("--duration <seconds>", "Duration in seconds").option("--force-instrumental", "Request instrumental output when supported").option(
925
+ "--output-format <fmt>",
926
+ "Provider-specific output format override when supported"
927
+ ).option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
590
928
  try {
591
929
  const ctx = createContext(this);
592
- const params = {
930
+ const data = await ctx.client.music(options.prompt, {
931
+ duration: options.duration ? Number(options.duration) : void 0,
932
+ force_instrumental: options.forceInstrumental ? true : void 0,
933
+ output_format: options.outputFormat
934
+ });
935
+ const localPath = await saveOutput(data, "music", options.outputDir);
936
+ if (localPath) data.local_path = localPath;
937
+ printSuccess("generate.music", data, ctx);
938
+ } catch (error2) {
939
+ printError("generate.music", error2);
940
+ }
941
+ })
942
+ );
943
+ command.addCommand(
944
+ new Command3("tts").description("Text-to-speech via MiMo v2.5 TTS").requiredOption("--prompt <text>", "Text to synthesize").option("--voice <name>", "MiMo prebuilt voice name (default: server config, usually Mia)").option("--context <text>", "Natural-language style or director instruction").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
945
+ try {
946
+ const ctx = createContext(this);
947
+ const params = {};
948
+ if (options.context) params.context = options.context;
949
+ const data = await ctx.client.tts(options.prompt, {
593
950
  voice: options.voice,
594
- language_type: options.language
595
- };
596
- if (options.instructions) params.instructions = options.instructions;
597
- const body = {
598
- asset_type: "tts",
599
- prompt: options.prompt,
600
- model: options.model,
601
- params
602
- };
603
- if (options.provider) body.provider = options.provider;
604
- const data = await ctx.client.post("/api/generate", body);
951
+ params: Object.keys(params).length > 0 ? toJsonObject(params) : void 0
952
+ });
605
953
  const localPath = await saveOutput(data, "tts", options.outputDir);
606
954
  if (localPath) data.local_path = localPath;
607
955
  printSuccess("generate.tts", data, ctx);
@@ -611,27 +959,27 @@ function createGenerateCommand() {
611
959
  })
612
960
  );
613
961
  command.addCommand(
614
- new Command3("model").description("Generate a 3D model").option("--image <url>", "Reference image URL").option("--prompt <text>", "Model description prompt").option("--model-version <v>", "Tripo model version (e.g. P1-20260311)").option("--face-limit <n>", "Max face count (48-20000)").option("--pbr", "Enable PBR textures").option("--texture-quality <q>", "Texture quality: standard or detailed").option("--auto-size", "Auto-scale to real-world dimensions").option("--negative-prompt <text>", "Negative prompt").option("--multiview <urls>", "4 image URLs comma-separated: front,left,back,right").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
962
+ new Command3("model").description("Generate a 3D model with Meshy AI").option("--image <input>", "Reference image path or URL").option("--format <fmt>", "Output format: glb, fbx, obj, usdz, stl, 3mf", "glb").option("--prompt <text>", "Model description prompt").option("--ai-model <name>", "Meshy model: meshy-5, meshy-6, latest").option("--polycount <n>", "Target polygon count").option("--pbr", "Enable PBR textures").option("--hd-texture", "Request 4K texture output").option("--pose-mode <mode>", "Character pose mode: a-pose or t-pose").option("--auto-size", "Estimate real-world size automatically").option("--negative-prompt <text>", "Negative prompt").option("--multiview <inputs>", "Comma-separated multiview image paths or URLs").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
615
963
  try {
964
+ assertHas3dInput(options.prompt, options.image, options.multiview);
616
965
  const ctx = createContext(this);
617
- const body = {
618
- asset_type: "model3d"
619
- };
620
966
  const params = {};
621
- if (options.image) body.input_file = options.image;
622
- if (options.prompt) body.prompt = options.prompt;
623
- if (options.modelVersion) params.model_version = options.modelVersion;
624
- if (options.faceLimit) params.face_limit = Number(options.faceLimit);
625
- if (options.pbr) params.pbr = true;
626
- if (options.textureQuality) params.texture_quality = options.textureQuality;
627
- if (options.autoSize) params.auto_size = true;
628
967
  if (options.negativePrompt) params.negative_prompt = options.negativePrompt;
629
968
  if (options.multiview) {
630
- params.multiview = String(options.multiview).split(",").map((value) => value.trim()).filter(Boolean);
969
+ params.multiview = parseCommaSeparatedValues(options.multiview).map((value) => toInputFile(value)).filter((value) => Boolean(value));
631
970
  }
632
- if (Object.keys(params).length > 0) body.params = params;
633
- const data = await ctx.client.post("/api/generate", body);
634
- const localPath = await saveOutput(data, "model3d", options.outputDir);
971
+ const data = await ctx.client.model3d(options.prompt ?? "", {
972
+ input: toInputFile(options.image),
973
+ format: options.format,
974
+ ai_model: options.aiModel,
975
+ polycount: options.polycount ? Number(options.polycount) : void 0,
976
+ pbr: options.pbr ? true : void 0,
977
+ hd_texture: options.hdTexture ? true : void 0,
978
+ pose_mode: options.poseMode,
979
+ auto_size: options.autoSize ? true : void 0,
980
+ params: Object.keys(params).length > 0 ? toJsonObject(params) : void 0
981
+ });
982
+ const localPath = await saveOutput(data, "model3d", options.outputDir, options.format);
635
983
  if (localPath) data.local_path = localPath;
636
984
  printSuccess("generate.model", data, ctx);
637
985
  } catch (error2) {
@@ -640,54 +988,178 @@ function createGenerateCommand() {
640
988
  })
641
989
  );
642
990
  command.addCommand(
643
- new Command3("text").description("Generate text via LLM").requiredOption("--prompt <text>", "Text prompt").option("--model <model>", "Model to use").option("--max-tokens <n>", "Maximum tokens").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
991
+ new Command3("character").description("Generate a fully processed 3D character with Meshy AI").option("--prompt <text>", "Character description prompt").option("--image <input>", "Reference image path or URL").option("--images <inputs>", "Comma-separated reference image paths or URLs").option("--format <fmt>", "Output format: glb, fbx, obj, usdz, stl, 3mf", "glb").option("--polycount <n>", "Target polygon count").option("--pbr", "Enable PBR textures").option("--hd-texture", "Request 4K texture output").option("--pose-mode <mode>", "Character pose mode: a-pose or t-pose").option("--ai-model <name>", "Meshy model: meshy-5, meshy-6, latest").option("--auto-size", "Estimate real-world size automatically").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
644
992
  try {
993
+ assertHas3dInput(options.prompt, options.image, options.images);
645
994
  const ctx = createContext(this);
646
- const body = {
647
- asset_type: "text",
648
- prompt: options.prompt
649
- };
650
- if (options.model) body.model = options.model;
651
- if (options.maxTokens) body.max_tokens = Number(options.maxTokens);
652
- const data = await ctx.client.post("/api/generate", body);
653
- const localPath = await saveOutput(data, "text", options.outputDir);
995
+ const images = parseCommaSeparatedValues(options.images).map((value) => toInputFile(value)).filter((value) => Boolean(value));
996
+ const data = await ctx.client.character(options.prompt ?? "", {
997
+ input: toInputFile(options.image),
998
+ images: images.length > 0 ? images : void 0,
999
+ format: options.format,
1000
+ polycount: options.polycount ? Number(options.polycount) : void 0,
1001
+ pbr: options.pbr ? true : void 0,
1002
+ hd_texture: options.hdTexture ? true : void 0,
1003
+ pose_mode: options.poseMode,
1004
+ ai_model: options.aiModel,
1005
+ auto_size: options.autoSize ? true : void 0
1006
+ });
1007
+ const localPath = await saveOutput(data, "character", options.outputDir, options.format);
654
1008
  if (localPath) data.local_path = localPath;
655
- printSuccess("generate.text", data, ctx);
1009
+ printSuccess("generate.character", data, ctx);
656
1010
  } catch (error2) {
657
- printError("generate.text", error2);
1011
+ printError("generate.character", error2);
658
1012
  }
659
1013
  })
660
1014
  );
661
- return command;
662
- }
663
-
664
- // src/commands/job.ts
665
- import { Command as Command4 } from "commander";
666
- function createJobCommand() {
667
- const command = new Command4("job").description("Job management");
668
1015
  command.addCommand(
669
- new Command4("list").description("List jobs").option("--status <status>", "Filter by status").option("--limit <n>", "Maximum number of jobs to return").action(async function(options) {
1016
+ new Command3("prop").description("Generate a fully processed 3D prop with Meshy AI").option("--prompt <text>", "Prop description prompt").option("--image <input>", "Reference image path or URL").option("--images <inputs>", "Comma-separated reference image paths or URLs").option("--format <fmt>", "Output format: glb, fbx, obj, usdz, stl, 3mf", "glb").option("--polycount <n>", "Target polygon count").option("--pbr", "Enable PBR textures").option("--hd-texture", "Request 4K texture output").option("--ai-model <name>", "Meshy model: meshy-5, meshy-6, latest").option("--auto-size", "Estimate real-world size automatically").option("--texture-prompt <text>", "Optional texture prompt for the final material pass").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
670
1017
  try {
1018
+ assertHas3dInput(options.prompt, options.image, options.images);
671
1019
  const ctx = createContext(this);
672
- const params = new URLSearchParams();
673
- if (options.status) params.set("status", options.status);
674
- if (options.limit) params.set("limit", options.limit);
675
- const query = params.toString();
676
- const path = query ? `/api/jobs?${query}` : "/api/jobs";
677
- const data = await ctx.client.get(path);
678
- printSuccess("job.list", data, ctx);
1020
+ const images = parseCommaSeparatedValues(options.images).map((value) => toInputFile(value)).filter((value) => Boolean(value));
1021
+ const data = await ctx.client.prop(options.prompt ?? "", {
1022
+ input: toInputFile(options.image),
1023
+ images: images.length > 0 ? images : void 0,
1024
+ format: options.format,
1025
+ polycount: options.polycount ? Number(options.polycount) : void 0,
1026
+ pbr: options.pbr ? true : void 0,
1027
+ hd_texture: options.hdTexture ? true : void 0,
1028
+ ai_model: options.aiModel,
1029
+ auto_size: options.autoSize ? true : void 0,
1030
+ texture_prompt: options.texturePrompt
1031
+ });
1032
+ const localPath = await saveOutput(data, "prop", options.outputDir, options.format);
1033
+ if (localPath) data.local_path = localPath;
1034
+ printSuccess("generate.prop", data, ctx);
679
1035
  } catch (error2) {
680
- printError("job.list", error2);
1036
+ printError("generate.prop", error2);
681
1037
  }
682
1038
  })
683
1039
  );
684
1040
  command.addCommand(
685
- new Command4("status").description("Get job status").argument("<id>", "Job ID").action(async function(id) {
1041
+ new Command3("text").description("Generate text via LLM").requiredOption("--prompt <text>", "Text prompt").option("--model <model>", "Model to use").option("--max-tokens <n>", "Maximum tokens").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
686
1042
  try {
687
1043
  const ctx = createContext(this);
688
- const data = await ctx.client.get(`/api/jobs/${encodeURIComponent(id)}`);
689
- printSuccess("job.status", data, ctx);
690
- } catch (error2) {
1044
+ const data = await ctx.client.text(options.prompt, {
1045
+ model: options.model,
1046
+ max_tokens: options.maxTokens ? Number(options.maxTokens) : void 0
1047
+ });
1048
+ const localPath = await saveOutput(data, "text", options.outputDir);
1049
+ if (localPath) data.local_path = localPath;
1050
+ printSuccess("generate.text", data, ctx);
1051
+ } catch (error2) {
1052
+ printError("generate.text", error2);
1053
+ }
1054
+ })
1055
+ );
1056
+ command.addCommand(
1057
+ new Command3("sprite").description("Generate character animation spritesheet (AutoSprite)").requiredOption("--prompt <text>", "Character description").option("--input <path>", "Reference image for character consistency (local path or URL)").option("--animation-type <type>", "Animation type: walk, run, idle, jump, attack, death, cast, dance, wave, interact, or custom text", "walk").option("--style <style>", "Art style: 16-bit, hd-pixel, isometric, retro-8bit, anime, chibi, painterly, vector, or any text").option("--frame-count <n>", "Number of animation frames", "8").option("--frame-size <n>", "Frame size in pixels (square)", "256").option("--is-humanoid", "Character is humanoid (default true)", true).option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1058
+ try {
1059
+ const ctx = createContext(this);
1060
+ const params = {
1061
+ frame_count: Number(options.frameCount),
1062
+ frame_size: Number(options.frameSize),
1063
+ is_humanoid: options.isHumanoid
1064
+ };
1065
+ const data = await ctx.client.sprite(options.prompt, {
1066
+ input: toInputFile(options.input),
1067
+ animation_type: options.animationType,
1068
+ style: options.style,
1069
+ params: toJsonObject(params)
1070
+ });
1071
+ const localPath = await saveOutput(data, "sprite", options.outputDir);
1072
+ if (localPath) data.local_path = localPath;
1073
+ printSuccess("generate.sprite", data, ctx);
1074
+ } catch (error2) {
1075
+ printError("generate.sprite", error2);
1076
+ }
1077
+ })
1078
+ );
1079
+ command.addCommand(
1080
+ new Command3("world").description("Generate a 3D world/environment using WorldLabs Marble").option("--prompt <text>", "Text prompt describing the environment").option("--input <path>", "Input image (local path or URL) for image-to-world").option("--image <path>", "Input image (local path or URL) for image-to-world").option("--panorama <path>", "360 panorama image path or URL").option("--video <path>", "Input video path or URL").option("--multi-image <path>", "Multi-view image path or URL (repeatable)", collectValue, []).option("--quality <quality>", "World quality: low, high").option("--model <model>", "Model: marble-1.0-draft, marble-1.0, marble-1.1, marble-1.1-plus", "marble-1.1").option("--display-name <name>", "Display name for the generated world").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1081
+ try {
1082
+ const prompt = options.prompt ?? "";
1083
+ const multiImage = options.multiImage.map((value) => toInputFile(value)).filter((value) => Boolean(value));
1084
+ if (!prompt && !options.input && !options.image && !options.panorama && !options.video && multiImage.length === 0) {
1085
+ throw new Error("Provide at least one of --prompt, --image, --input, --panorama, --video, or --multi-image");
1086
+ }
1087
+ const ctx = createContext(this);
1088
+ const data = await ctx.client.world(prompt, {
1089
+ input: toInputFile(options.input ?? options.image),
1090
+ model: options.model,
1091
+ panorama_url: toInputFile(options.panorama),
1092
+ video_url: toInputFile(options.video),
1093
+ multi_image_url: multiImage.length > 0 ? multiImage : void 0,
1094
+ quality: options.quality,
1095
+ display_name: options.displayName
1096
+ });
1097
+ const localPath = await saveOutput(data, "world", options.outputDir);
1098
+ if (localPath) data.local_path = localPath;
1099
+ printSuccess("generate.world", data, ctx);
1100
+ } catch (error2) {
1101
+ printError("generate.world", error2);
1102
+ }
1103
+ })
1104
+ );
1105
+ command.addCommand(
1106
+ createGeneratedAssetCommand("animation", "animation", "animation", "Generate a 3D animation clip with Meshy AI")
1107
+ );
1108
+ command.addCommand(
1109
+ createGeneratedAssetCommand("material", "material", "material", "Resolve a PBR material from library packs")
1110
+ );
1111
+ command.addCommand(
1112
+ createGeneratedAssetCommand("hdri", "hdri", "hdri", "Resolve an HDRI environment from library packs")
1113
+ );
1114
+ command.addCommand(
1115
+ createGeneratedAssetCommand("vfx", "vfx", "vfx", "Resolve a Three.js VFX bundle from library packs")
1116
+ );
1117
+ command.addCommand(
1118
+ createGeneratedAssetCommand("lut", "lut", "lut", "Resolve a LUT cube from library packs")
1119
+ );
1120
+ command.addCommand(
1121
+ createGeneratedAssetCommand("shader", "shader", "shader", "Resolve a GLSL shader from library packs")
1122
+ );
1123
+ command.addCommand(
1124
+ createGeneratedAssetCommand("font-typeface", "fontTypeface", "font_typeface", "Resolve a Three.js typeface font from library packs")
1125
+ );
1126
+ command.addCommand(
1127
+ createGeneratedAssetCommand("skybox", "skybox", "skybox", "Generate an equirectangular skybox image")
1128
+ );
1129
+ command.addCommand(
1130
+ createGeneratedAssetCommand("decal", "decal", "decal", "Generate a transparent decal image")
1131
+ );
1132
+ command.addCommand(
1133
+ createGeneratedAssetCommand("heightmap", "heightmap", "heightmap", "Generate a grayscale terrain heightmap")
1134
+ );
1135
+ return command;
1136
+ }
1137
+
1138
+ // src/commands/job.ts
1139
+ import { Command as Command4 } from "commander";
1140
+ function createJobCommand() {
1141
+ const command = new Command4("job").description("Job management");
1142
+ command.addCommand(
1143
+ new Command4("list").description("List jobs").option("--status <status>", "Filter by status").option("--limit <n>", "Maximum number of jobs to return").action(async function(options) {
1144
+ try {
1145
+ const ctx = createContext(this);
1146
+ const data = await ctx.client.job.list({
1147
+ status: options.status,
1148
+ limit: options.limit ? Number(options.limit) : void 0
1149
+ });
1150
+ printSuccess("job.list", data, ctx);
1151
+ } catch (error2) {
1152
+ printError("job.list", error2);
1153
+ }
1154
+ })
1155
+ );
1156
+ command.addCommand(
1157
+ new Command4("status").description("Get job status").argument("<id>", "Job ID").action(async function(id) {
1158
+ try {
1159
+ const ctx = createContext(this);
1160
+ const data = await ctx.client.job.status(id);
1161
+ printSuccess("job.status", data, ctx);
1162
+ } catch (error2) {
691
1163
  printError("job.status", error2);
692
1164
  }
693
1165
  })
@@ -696,7 +1168,7 @@ function createJobCommand() {
696
1168
  new Command4("cancel").description("Cancel a job").argument("<id>", "Job ID").action(async function(id) {
697
1169
  try {
698
1170
  const ctx = createContext(this);
699
- const data = await ctx.client.post(`/api/jobs/${encodeURIComponent(id)}/cancel`);
1171
+ const data = await ctx.client.job.cancel(id);
700
1172
  printSuccess("job.cancel", data, ctx);
701
1173
  } catch (error2) {
702
1174
  printError("job.cancel", error2);
@@ -706,95 +1178,451 @@ function createJobCommand() {
706
1178
  return command;
707
1179
  }
708
1180
 
1181
+ // src/commands/library.ts
1182
+ import { readFile, writeFile } from "fs/promises";
1183
+ import { AssetForgeError as AssetForgeError4 } from "@doufunao123/assetforge-sdk";
1184
+ import { Command as Command5 } from "commander";
1185
+ function createLibraryCommand() {
1186
+ const command = new Command5("library").description(
1187
+ "Search and manage the asset library"
1188
+ );
1189
+ command.addCommand(
1190
+ new Command5("search").description("Search the asset library").argument("[query]", "Search query").option("-q, --query <query>", "Search query").option("-t, --type <type>", "Filter by asset type (audio, music, image, etc.)").option("--tags <tags>", "Filter by tags (comma-separated)").option("--source <source>", "Filter by source (manual, generated, imported)").option("--mode <mode>", "Search mode (fts, vector, hybrid)").option("--style <style>", "Filter by visual style").option("--composition <composition>", "Filter by composition").option("--lighting <lighting>", "Filter by lighting").option("--color-tone <tone>", "Filter by color tone").option("--mood <mood>", "Filter by mood (comma-separated)").option("--theme <theme>", "Filter by theme (comma-separated)").option("--era <era>", "Filter by era").option("--rig-name <rig>", "Filter by exact rig name").option("--compatible-rig <rig>", "Filter by compatible rig").option("--no-rerank", "Disable LLM reranking").option("-n, --limit <limit>", "Max results", "20").option("--offset <offset>", "Offset for pagination", "0").action(async function(query) {
1191
+ const ctx = createContext(this);
1192
+ const opts = this.opts();
1193
+ try {
1194
+ const data = await ctx.client.library.search({
1195
+ q: opts.query ?? query,
1196
+ type: opts.type,
1197
+ tags: opts.tags,
1198
+ source: opts.source,
1199
+ mode: opts.mode,
1200
+ style: opts.style,
1201
+ composition: opts.composition,
1202
+ lighting: opts.lighting,
1203
+ color_tone: opts.colorTone,
1204
+ mood: opts.mood,
1205
+ theme: opts.theme,
1206
+ era: opts.era,
1207
+ rig_name: opts.rigName,
1208
+ compatible_rig: opts.compatibleRig,
1209
+ rerank: opts.rerank,
1210
+ limit: Number(opts.limit),
1211
+ offset: Number(opts.offset)
1212
+ });
1213
+ printSuccess("library.search", data, ctx);
1214
+ } catch (error2) {
1215
+ printError("library.search", error2, ctx.human);
1216
+ }
1217
+ })
1218
+ );
1219
+ command.addCommand(
1220
+ new Command5("related").description("Find rig-compatible related assets").argument("<id>", "Library item ID").option("-t, --type <type>", "Filter related assets by type").option("-n, --limit <limit>", "Max results", "20").action(async function(id) {
1221
+ const ctx = createContext(this);
1222
+ const opts = this.opts();
1223
+ try {
1224
+ const data = await ctx.client.library.related(id, {
1225
+ type: opts.type,
1226
+ limit: Number(opts.limit)
1227
+ });
1228
+ printSuccess("library.related", data, ctx);
1229
+ } catch (error2) {
1230
+ printError("library.related", error2, ctx.human);
1231
+ }
1232
+ })
1233
+ );
1234
+ command.addCommand(
1235
+ new Command5("bundle").description("Download a zip bundle of library assets").argument("<ids...>", "Library item IDs").requiredOption("-o, --output <path>", "Output zip path").action(async function(ids) {
1236
+ const ctx = createContext(this);
1237
+ const opts = this.opts();
1238
+ try {
1239
+ const blob = await ctx.client.library.bundle({ asset_ids: ids, format: "zip" });
1240
+ const buffer = Buffer.from(await blob.arrayBuffer());
1241
+ await writeFile(opts.output, buffer);
1242
+ printSuccess(
1243
+ "library.bundle",
1244
+ { output: opts.output, bytes: buffer.length, asset_ids: ids },
1245
+ ctx
1246
+ );
1247
+ } catch (error2) {
1248
+ printError("library.bundle", error2, ctx.human);
1249
+ }
1250
+ })
1251
+ );
1252
+ command.addCommand(
1253
+ new Command5("ingest").description("Queue or run a library ingest job (admin only)").requiredOption("--source <source>", "Ingest source (manual, generated, pack, imported)").requiredOption("-t, --type <type>", "Asset type").option("--file-url <url>", "Public asset URL").option("--file <path>", "Local file to encode as base64").option("--thumbnail-url <url>", "Thumbnail URL").option("--name <name>", "Display name").option("--tag <tag>", "User tag (repeatable)", collect, []).option("--source-job-id <id>", "Source generate job ID").option("--pack-id <id>", "Pack ID").option("--pack-asset-key <key>", "Pack asset key").option("--sync", "Run ingest synchronously and return the library result").action(async function() {
1254
+ const ctx = createContext(this);
1255
+ const opts = this.opts();
1256
+ try {
1257
+ const request = {
1258
+ source: opts.source,
1259
+ asset_type: opts.type,
1260
+ file_url: opts.fileUrl,
1261
+ file_data_b64: opts.file ? await readBase64(opts.file) : void 0,
1262
+ thumbnail_url: opts.thumbnailUrl,
1263
+ name: opts.name,
1264
+ user_tags: opts.tag,
1265
+ source_job_id: opts.sourceJobId,
1266
+ pack_id: opts.packId,
1267
+ pack_asset_key: opts.packAssetKey
1268
+ };
1269
+ const data = opts.sync ? await ctx.client.library.ingestSync(request) : await ctx.client.library.ingest(request);
1270
+ printSuccess(opts.sync ? "library.ingest_sync" : "library.ingest", data, ctx);
1271
+ } catch (error2) {
1272
+ printError("library.ingest", error2, ctx.human);
1273
+ }
1274
+ })
1275
+ );
1276
+ command.addCommand(
1277
+ new Command5("ingest-status").description("Get a library ingest job status").argument("<id>", "Ingest job ID").action(async function(id) {
1278
+ const ctx = createContext(this);
1279
+ try {
1280
+ const data = await ctx.client.library.ingestStatus(id);
1281
+ printSuccess("library.ingest_status", data, ctx);
1282
+ } catch (error2) {
1283
+ printError("library.ingest_status", error2, ctx.human);
1284
+ }
1285
+ })
1286
+ );
1287
+ command.addCommand(
1288
+ new Command5("ingest-wait").description("Poll a library ingest job until completion").argument("<id>", "Ingest job ID").option("--interval-ms <ms>", "Polling interval", "1000").action(async function(id) {
1289
+ const ctx = createContext(this);
1290
+ const opts = this.opts();
1291
+ try {
1292
+ const data = await ctx.client.library.ingestWait(id, {
1293
+ intervalMs: Number(opts.intervalMs)
1294
+ });
1295
+ printSuccess("library.ingest_wait", data, ctx);
1296
+ } catch (error2) {
1297
+ printError("library.ingest_wait", error2, ctx.human);
1298
+ }
1299
+ })
1300
+ );
1301
+ command.addCommand(
1302
+ new Command5("add").description("Add an item to the asset library").requiredOption("--name <name>", "Display name").requiredOption("--url <url>", "File URL (public)").requiredOption("-t, --type <type>", "Asset type (audio, music, image, etc.)").option("-d, --description <desc>", "Description").option("--tags <tags>", "Comma-separated tags").option("--duration <seconds>", "Duration in seconds (for audio/video)").option("--source <source>", "Source (manual, generated, imported)", "manual").action(async function() {
1303
+ const ctx = createContext(this);
1304
+ const opts = this.opts();
1305
+ try {
1306
+ const body = {
1307
+ asset_type: opts.type,
1308
+ name: opts.name,
1309
+ file_url: opts.url,
1310
+ source: opts.source
1311
+ };
1312
+ if (opts.description) body.description = opts.description;
1313
+ if (opts.tags)
1314
+ body.tags = opts.tags.split(",").map((t) => t.trim());
1315
+ if (opts.duration)
1316
+ body.duration_seconds = parseFloat(opts.duration);
1317
+ const data = await ctx.client.library.add(body);
1318
+ printSuccess("library.add", data, ctx);
1319
+ } catch (error2) {
1320
+ printError("library.add", error2, ctx.human);
1321
+ }
1322
+ })
1323
+ );
1324
+ command.addCommand(
1325
+ new Command5("get").description("Get a library item by ID").argument("<id>", "Library item ID").action(async function(id) {
1326
+ const ctx = createContext(this);
1327
+ try {
1328
+ const data = await ctx.client.library.get(id);
1329
+ printSuccess("library.get", data, ctx);
1330
+ } catch (error2) {
1331
+ printError("library.get", error2, ctx.human);
1332
+ }
1333
+ })
1334
+ );
1335
+ command.addCommand(
1336
+ new Command5("delete").description("Delete a library item (admin only)").argument("<id>", "Library item ID").action(async function(id) {
1337
+ const ctx = createContext(this);
1338
+ try {
1339
+ const data = await ctx.client.library.delete(id);
1340
+ printSuccess("library.delete", data, ctx);
1341
+ } catch (error2) {
1342
+ printError("library.delete", error2, ctx.human);
1343
+ }
1344
+ })
1345
+ );
1346
+ command.addCommand(
1347
+ new Command5("catalog-jobs").description("Catalog completed generation jobs into the library (admin)").option("-t, --type <type>", "Filter jobs by asset type").option("--provider <id>", "Filter jobs by provider ID").option("--after <date>", "Only catalog jobs after this date").option("--dry-run", "Just count, don't actually catalog").action(async function() {
1348
+ const ctx = createContext(this);
1349
+ const opts = this.opts();
1350
+ try {
1351
+ const body = {};
1352
+ if (opts.type) body.asset_type = opts.type;
1353
+ if (opts.provider) body.provider_id = opts.provider;
1354
+ if (opts.after) body.after = opts.after;
1355
+ if (opts.dryRun) body.dry_run = true;
1356
+ const data = await ctx.client.library.catalogJobs(body);
1357
+ printSuccess("library.catalog_jobs", data, ctx);
1358
+ } catch (error2) {
1359
+ printError("library.catalog_jobs", error2, ctx.human);
1360
+ }
1361
+ })
1362
+ );
1363
+ command.addCommand(
1364
+ new Command5("backfill").description("Queue library embedding/enrichment backfill jobs (admin)").option("-t, --asset-type <type>", "Filter by asset type").option("--since <date>", "Only backfill assets created after this RFC3339 date").option("-n, --limit <limit>", "Max items to enqueue", "100").option("--dry-run", "Return matching IDs without enqueueing jobs").action(async function() {
1365
+ const ctx = createContext(this);
1366
+ const opts = this.opts();
1367
+ try {
1368
+ const request = {
1369
+ asset_type: opts.assetType,
1370
+ since: opts.since,
1371
+ limit: Number(opts.limit),
1372
+ dry_run: Boolean(opts.dryRun)
1373
+ };
1374
+ const data = await ctx.client.library.backfill(request);
1375
+ printSuccess("library.backfill", data, ctx);
1376
+ } catch (error2) {
1377
+ printError("library.backfill", error2, ctx.human);
1378
+ }
1379
+ })
1380
+ );
1381
+ command.addCommand(
1382
+ new Command5("reenrich").description("Queue library re-enrichment jobs (admin)").option("--id <id>", "Library item ID (repeatable)", collect, []).option("-t, --asset-type <type>", "Filter by asset type").option("--taxonomy-version-lt <version>", "Only items below this taxonomy version").option("--force-visual", "Force visual/textual analysis", true).option("--no-force-visual", "Do not force visual/textual analysis").option("--recompute-embedding", "Recompute embedding vectors").option("-n, --limit <limit>", "Max items to enqueue", "100").action(async function() {
1383
+ const ctx = createContext(this);
1384
+ const opts = this.opts();
1385
+ try {
1386
+ const request = {
1387
+ ids: opts.id,
1388
+ asset_type: opts.assetType,
1389
+ taxonomy_version_lt: opts.taxonomyVersionLt ? Number(opts.taxonomyVersionLt) : void 0,
1390
+ force_visual: opts.forceVisual,
1391
+ recompute_embedding: Boolean(opts.recomputeEmbedding),
1392
+ limit: Number(opts.limit)
1393
+ };
1394
+ const data = await ctx.client.library.reenrich(request);
1395
+ printSuccess("library.reenrich", data, ctx);
1396
+ } catch (error2) {
1397
+ printError("library.reenrich", error2, ctx.human);
1398
+ }
1399
+ })
1400
+ );
1401
+ return command;
1402
+ }
1403
+ function collect(value, previous) {
1404
+ previous.push(value);
1405
+ return previous;
1406
+ }
1407
+ async function readBase64(filePath) {
1408
+ try {
1409
+ return (await readFile(filePath)).toString("base64");
1410
+ } catch (error2) {
1411
+ throw new AssetForgeError4(
1412
+ `Failed to read file ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
1413
+ { code: "CONFIG_ERROR" }
1414
+ );
1415
+ }
1416
+ }
1417
+
1418
+ // src/commands/pack.ts
1419
+ import { readFile as readFile2 } from "fs/promises";
1420
+ import { createInterface } from "readline/promises";
1421
+ import { stdin as input, stdout as output2 } from "process";
1422
+ import { AssetForgeError as AssetForgeError5 } from "@doufunao123/assetforge-sdk";
1423
+ import { Command as Command6 } from "commander";
1424
+ function createPackCommand() {
1425
+ const command = new Command6("pack").description("Manage library asset packs");
1426
+ command.addCommand(
1427
+ new Command6("list").description("List installed library packs").action(async function() {
1428
+ const ctx = createContext(this);
1429
+ try {
1430
+ const data = await ctx.client.library.packs();
1431
+ printSuccess("pack.list", data, ctx);
1432
+ } catch (error2) {
1433
+ printError("pack.list", error2, ctx.human);
1434
+ }
1435
+ })
1436
+ );
1437
+ command.addCommand(
1438
+ new Command6("install").description("Install a library pack").argument("[source]", "Pack source (builtin, url, upload)").option("--source <source>", "Pack source (builtin, url, upload)").option("--id <id>", "Builtin pack ID").option("--version <version>", "Pack version").option("--url <url>", "HTTP tarball URL").option("--file <tarball>", "Local tarball to upload as base64").action(async function(sourceArg) {
1439
+ const ctx = createContext(this);
1440
+ const opts = this.opts();
1441
+ try {
1442
+ const source = opts.source ?? sourceArg;
1443
+ if (!source) {
1444
+ throw new AssetForgeError5("Pack source is required", { code: "CONFIG_ERROR" });
1445
+ }
1446
+ const request = {
1447
+ source,
1448
+ id: opts.id,
1449
+ version: opts.version,
1450
+ url: opts.url,
1451
+ tarball_b64: opts.file ? await readBase642(opts.file) : void 0
1452
+ };
1453
+ const data = await ctx.client.library.packsInstall(request);
1454
+ printSuccess("pack.install", data, ctx);
1455
+ } catch (error2) {
1456
+ printError("pack.install", error2, ctx.human);
1457
+ }
1458
+ })
1459
+ );
1460
+ command.addCommand(
1461
+ new Command6("delete").description("Delete a library pack (admin only)").argument("<id>", "Pack ID").option("-y, --yes", "Skip confirmation prompt").action(async function(id) {
1462
+ const ctx = createContext(this);
1463
+ const opts = this.opts();
1464
+ try {
1465
+ if (!opts.yes) {
1466
+ await confirmDelete(id);
1467
+ }
1468
+ const data = await ctx.client.library.packsDelete(id);
1469
+ printSuccess("pack.delete", data, ctx);
1470
+ } catch (error2) {
1471
+ printError("pack.delete", error2, ctx.human);
1472
+ }
1473
+ })
1474
+ );
1475
+ command.addCommand(
1476
+ new Command6("refresh").description("Refresh an installed library pack (admin only)").argument("<id>", "Pack ID").action(async function(id) {
1477
+ const ctx = createContext(this);
1478
+ try {
1479
+ const data = await ctx.client.library.packsRefresh(id);
1480
+ printSuccess("pack.refresh", data, ctx);
1481
+ } catch (error2) {
1482
+ printError("pack.refresh", error2, ctx.human);
1483
+ }
1484
+ })
1485
+ );
1486
+ return command;
1487
+ }
1488
+ async function readBase642(filePath) {
1489
+ try {
1490
+ return (await readFile2(filePath)).toString("base64");
1491
+ } catch (error2) {
1492
+ throw new AssetForgeError5(
1493
+ `Failed to read file ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
1494
+ { code: "CONFIG_ERROR" }
1495
+ );
1496
+ }
1497
+ }
1498
+ async function confirmDelete(id) {
1499
+ if (!input.isTTY) {
1500
+ throw new AssetForgeError5("Refusing to delete pack without confirmation. Pass --yes to confirm.", {
1501
+ code: "CONFIG_ERROR"
1502
+ });
1503
+ }
1504
+ const rl = createInterface({ input, output: output2 });
1505
+ try {
1506
+ const answer = await rl.question(`Delete pack ${id}? Type the pack ID to confirm: `);
1507
+ if (answer !== id) {
1508
+ throw new AssetForgeError5("Pack deletion cancelled", { code: "CONFIG_ERROR" });
1509
+ }
1510
+ } finally {
1511
+ rl.close();
1512
+ }
1513
+ }
1514
+
709
1515
  // src/commands/process.ts
710
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
711
- import { existsSync as existsSync3 } from "fs";
1516
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1517
+ import { existsSync as existsSync4 } from "fs";
712
1518
  import { join as join3 } from "path";
713
- import { Command as Command5 } from "commander";
714
- function readInputAsBase64(input) {
715
- if (existsSync3(input)) {
716
- const bytes = readFileSync2(input);
1519
+ import { Command as Command7 } from "commander";
1520
+ function readInputAsBase64(input2) {
1521
+ if (existsSync4(input2)) {
1522
+ const bytes = readFileSync3(input2);
717
1523
  return `data:image/png;base64,${bytes.toString("base64")}`;
718
1524
  }
719
- return input;
1525
+ return input2;
720
1526
  }
721
1527
  function saveProcessOutput(data, outputDir) {
722
- const outputData = data.output_data;
723
- if (typeof outputData !== "string" || !outputData) return null;
724
1528
  mkdirSync3(outputDir, { recursive: true });
725
1529
  const timestamp = Date.now();
1530
+ const outputs = data.outputs;
1531
+ if (Array.isArray(outputs) && outputs.length > 0) {
1532
+ const localPaths = [];
1533
+ for (let i = 0; i < outputs.length; i++) {
1534
+ const item = outputs[i];
1535
+ const b64 = item.output_data;
1536
+ if (typeof b64 !== "string" || !b64) continue;
1537
+ const filePath2 = join3(outputDir, `frame_${timestamp}_${String(i).padStart(4, "0")}.png`);
1538
+ writeFileSync3(filePath2, Buffer.from(b64, "base64"));
1539
+ localPaths.push(filePath2);
1540
+ }
1541
+ delete data.outputs;
1542
+ data.local_paths = localPaths;
1543
+ return localPaths[0] ?? null;
1544
+ }
1545
+ const outputData = data.output_data;
1546
+ if (typeof outputData !== "string" || !outputData) return null;
726
1547
  const filePath = join3(outputDir, `processed_${timestamp}.png`);
727
1548
  writeFileSync3(filePath, Buffer.from(outputData, "base64"));
728
1549
  delete data.output_data;
729
1550
  return filePath;
730
1551
  }
731
1552
  function createProcessCommand() {
732
- const command = new Command5("process").description("Post-process images (remove-bg, crop, resize, upscale)");
1553
+ const command = new Command7("process").description("Post-process images (crop, resize, compose, remove-bg)");
733
1554
  command.addCommand(
734
- new Command5("remove-bg").description("Remove background from an image (AI-powered, BiRefNet)").requiredOption("--input <path>", "Input image (file path or URL)").option("--smart-crop", "Also crop to power-of-2 after removing background").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1555
+ new Command7("crop").description("Smart crop an image (trim transparent borders)").requiredOption("--input <path>", "Input image (file path or URL)").option("--mode <mode>", "Crop mode: tightest or power_of2", "tightest").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
735
1556
  try {
736
1557
  const ctx = createContext(this);
737
- const ops = [{ op: "remove_bg" }];
738
- if (options.smartCrop) {
739
- ops.push({ op: "smart_crop", mode: "power_of2" });
740
- }
741
- const data = await ctx.client.post("/api/process", {
1558
+ const data = await ctx.client.process({
742
1559
  input: readInputAsBase64(options.input),
743
- operations: ops
1560
+ operations: [{ op: "smart_crop", mode: options.mode }]
744
1561
  });
745
1562
  const localPath = saveProcessOutput(data, options.outputDir);
746
1563
  if (localPath) data.local_path = localPath;
747
- printSuccess("process.remove-bg", data, ctx);
1564
+ printSuccess("process.crop", data, ctx);
748
1565
  } catch (error2) {
749
- printError("process.remove-bg", error2);
1566
+ printError("process.crop", error2);
750
1567
  }
751
1568
  })
752
1569
  );
753
1570
  command.addCommand(
754
- new Command5("crop").description("Smart crop an image (trim transparent borders)").requiredOption("--input <path>", "Input image (file path or URL)").option("--mode <mode>", "Crop mode: tightest or power_of2", "tightest").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1571
+ new Command7("resize").description("Resize an image to exact dimensions").requiredOption("--input <path>", "Input image (file path or URL)").requiredOption("--width <n>", "Target width").requiredOption("--height <n>", "Target height").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
755
1572
  try {
756
1573
  const ctx = createContext(this);
757
- const data = await ctx.client.post("/api/process", {
1574
+ const data = await ctx.client.process({
758
1575
  input: readInputAsBase64(options.input),
759
- operations: [{ op: "smart_crop", mode: options.mode }]
1576
+ operations: [{ op: "resize", width: Number(options.width), height: Number(options.height) }]
760
1577
  });
761
1578
  const localPath = saveProcessOutput(data, options.outputDir);
762
1579
  if (localPath) data.local_path = localPath;
763
- printSuccess("process.crop", data, ctx);
1580
+ printSuccess("process.resize", data, ctx);
764
1581
  } catch (error2) {
765
- printError("process.crop", error2);
1582
+ printError("process.resize", error2);
766
1583
  }
767
1584
  })
768
1585
  );
769
1586
  command.addCommand(
770
- new Command5("resize").description("Resize an image to exact dimensions").requiredOption("--input <path>", "Input image (file path or URL)").requiredOption("--width <n>", "Target width").requiredOption("--height <n>", "Target height").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1587
+ new Command7("compose").description("Compose multiple images into a sprite sheet").requiredOption("--input <paths...>", "Input images (files or URLs)").option("--direction <dir>", "Layout: horizontal, vertical, grid", "horizontal").option("--columns <n>", "Columns for grid layout").option("--padding <n>", "Padding between frames in px", "0").option("--frame-width <n>", "Normalize each frame to this width").option("--frame-height <n>", "Normalize each frame to this height").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
771
1588
  try {
772
1589
  const ctx = createContext(this);
773
- const data = await ctx.client.post("/api/process", {
774
- input: readInputAsBase64(options.input),
775
- operations: [{ op: "resize", width: Number(options.width), height: Number(options.height) }]
1590
+ const inputs = Array.isArray(options.input) ? options.input : [options.input];
1591
+ const data = await ctx.client.process({
1592
+ inputs: inputs.map(readInputAsBase64),
1593
+ operations: [{
1594
+ op: "compose",
1595
+ direction: options.direction,
1596
+ columns: options.columns ? Number(options.columns) : void 0,
1597
+ padding: Number(options.padding),
1598
+ frame_width: options.frameWidth ? Number(options.frameWidth) : void 0,
1599
+ frame_height: options.frameHeight ? Number(options.frameHeight) : void 0
1600
+ }]
776
1601
  });
777
1602
  const localPath = saveProcessOutput(data, options.outputDir);
778
1603
  if (localPath) data.local_path = localPath;
779
- printSuccess("process.resize", data, ctx);
1604
+ printSuccess("process.compose", data, ctx);
780
1605
  } catch (error2) {
781
- printError("process.resize", error2);
1606
+ printError("process.compose", error2);
782
1607
  }
783
1608
  })
784
1609
  );
785
1610
  command.addCommand(
786
- new Command5("upscale").description("AI upscale an image (2x or 4x via Real-ESRGAN)").requiredOption("--input <path>", "Input image (file path or URL)").option("--scale <n>", "Scale factor (2 or 4)", "4").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1611
+ new Command7("remove-bg").description("Remove background from image(s)").requiredOption("--input <paths...>", "Input images (files or URLs)").option("--bg-color <color>", "Background color hint for fallback (e.g. white, black)").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
787
1612
  try {
788
1613
  const ctx = createContext(this);
789
- const data = await ctx.client.post("/api/process", {
790
- input: readInputAsBase64(options.input),
791
- operations: [{ op: "upscale", scale: Number(options.scale) }]
1614
+ const inputs = Array.isArray(options.input) ? options.input : [options.input];
1615
+ const op = { op: "remove_bg" };
1616
+ if (options.bgColor) op.bg_color = options.bgColor;
1617
+ const data = await ctx.client.process({
1618
+ inputs: inputs.map(readInputAsBase64),
1619
+ operations: [op]
792
1620
  });
793
1621
  const localPath = saveProcessOutput(data, options.outputDir);
794
1622
  if (localPath) data.local_path = localPath;
795
- printSuccess("process.upscale", data, ctx);
1623
+ printSuccess("process.remove_bg", data, ctx);
796
1624
  } catch (error2) {
797
- printError("process.upscale", error2);
1625
+ printError("process.remove_bg", error2);
798
1626
  }
799
1627
  })
800
1628
  );
@@ -802,10 +1630,10 @@ function createProcessCommand() {
802
1630
  }
803
1631
 
804
1632
  // src/commands/process3d.ts
805
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1633
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
806
1634
  import { join as join4 } from "path";
807
- import { Command as Command6 } from "commander";
808
- function infer3dExtension(format) {
1635
+ import { Command as Command8 } from "commander";
1636
+ function infer3dExtension2(format) {
809
1637
  const map = {
810
1638
  fbx: "fbx",
811
1639
  usdz: "usdz",
@@ -817,11 +1645,32 @@ function infer3dExtension(format) {
817
1645
  };
818
1646
  return map[String(format ?? "glb").toLowerCase()] ?? "glb";
819
1647
  }
1648
+ function inferMimeType2(filePath) {
1649
+ const lower = filePath.toLowerCase();
1650
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
1651
+ if (lower.endsWith(".png")) return "image/png";
1652
+ if (lower.endsWith(".webp")) return "image/webp";
1653
+ if (lower.endsWith(".gif")) return "image/gif";
1654
+ return "application/octet-stream";
1655
+ }
1656
+ function toInputFile2(value) {
1657
+ if (!value) {
1658
+ return void 0;
1659
+ }
1660
+ if (!existsSync5(value)) {
1661
+ return value;
1662
+ }
1663
+ const b64 = readFileSync4(value).toString("base64");
1664
+ return `data:${inferMimeType2(value)};base64,${b64}`;
1665
+ }
1666
+ function toJsonObject2(value) {
1667
+ return value;
1668
+ }
820
1669
  async function saveProcess3dOutput(data, operation, outputDir, format) {
821
1670
  if (!data.output_url) {
822
1671
  return null;
823
1672
  }
824
- const ext = infer3dExtension(format);
1673
+ const ext = infer3dExtension2(format);
825
1674
  const timestamp = Date.now();
826
1675
  const filePath = join4(outputDir, `${operation}_${timestamp}.${ext}`);
827
1676
  mkdirSync4(outputDir, { recursive: true });
@@ -834,63 +1683,73 @@ async function saveProcess3dOutput(data, operation, outputDir, format) {
834
1683
  return filePath;
835
1684
  }
836
1685
  function createProcess3dCommand() {
837
- const command = new Command6("process3d").description("3D model post-processing via Tripo pipeline");
1686
+ const command = new Command8("process3d").description("3D model post-processing via Meshy AI");
838
1687
  command.addCommand(
839
- new Command6("convert").description("Convert 3D model format (FBX/USDZ/OBJ/STL/GLTF/3MF)").requiredOption("--task-id <id>", "Tripo task ID from generate model").requiredOption("--format <fmt>", "Target format: FBX, USDZ, OBJ, STL, GLTF, 3MF").option("--quad", "Enable quad remeshing").option("--face-limit <n>", "Max face count").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1688
+ new Command8("remesh").description("Remesh a model and optionally change format or polygon count").option("--task-id <id>", "Meshy task ID").option("--model-url <url>", "Model URL to remesh without an existing task").requiredOption("--format <fmt>", "Target format: glb, fbx, obj, usdz, stl, 3mf").option("--polycount <n>", "Target polygon count").option("--topology <type>", "Topology type: triangle or quad").option("--auto-size", "Estimate real-world size automatically").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
840
1689
  try {
1690
+ if (!options.taskId && !options.modelUrl) {
1691
+ throw new Error("Either --task-id or --model-url is required");
1692
+ }
841
1693
  const ctx = createContext(this);
842
1694
  const params = {
843
- format: options.format
1695
+ target_formats: [options.format]
844
1696
  };
845
- if (options.quad) params.quad = true;
846
- if (options.faceLimit) params.face_limit = Number(options.faceLimit);
847
- const data = await ctx.client.post("/api/process3d", {
848
- task_id: options.taskId,
849
- operation: "convert",
850
- params
1697
+ if (options.modelUrl) params.model_url = options.modelUrl;
1698
+ if (options.polycount) params.polycount = Number(options.polycount);
1699
+ if (options.topology) params.topology = options.topology;
1700
+ if (options.autoSize) params.auto_size = true;
1701
+ const data = await ctx.client.process3d({
1702
+ task_id: options.taskId ?? "",
1703
+ operation: "remesh",
1704
+ params: toJsonObject2(params)
851
1705
  });
852
- const localPath = await saveProcess3dOutput(data, "convert", options.outputDir, options.format);
1706
+ const localPath = await saveProcess3dOutput(data, "remesh", options.outputDir, options.format);
853
1707
  if (localPath) data.local_path = localPath;
854
- printSuccess("process3d.convert", data, ctx);
1708
+ printSuccess("process3d.remesh", data, ctx);
855
1709
  } catch (error2) {
856
- printError("process3d.convert", error2);
1710
+ printError("process3d.remesh", error2);
857
1711
  }
858
1712
  })
859
1713
  );
860
1714
  command.addCommand(
861
- new Command6("texture").description("Re-texture a 3D model with new materials").requiredOption("--task-id <id>", "Tripo task ID").option("--prompt <text>", "Texture description prompt").option("--pbr", "Enable PBR materials").option("--quality <q>", "Texture quality: standard or detailed").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1715
+ new Command8("retexture").description("Regenerate textures for an existing Meshy model").requiredOption("--task-id <id>", "Meshy task ID").option("--prompt <text>", "Text style prompt for the new material pass").option("--style-image <input>", "Texture style reference image URL or local file path").option("--pbr", "Enable PBR materials").option("--hd-texture", "Request 4K texture output").option("--ai-model <name>", "Meshy model: meshy-5, meshy-6, latest").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
862
1716
  try {
863
1717
  const ctx = createContext(this);
864
1718
  const params = {};
865
- if (options.prompt) params.prompt = options.prompt;
1719
+ if (options.prompt) params.text_style_prompt = options.prompt;
1720
+ if (options.styleImage) params.image_style_url = toInputFile2(options.styleImage);
866
1721
  if (options.pbr) params.pbr = true;
867
- if (options.quality) params.quality = options.quality;
868
- const data = await ctx.client.post("/api/process3d", {
1722
+ if (options.hdTexture) params.hd_texture = true;
1723
+ if (options.aiModel) params.ai_model = options.aiModel;
1724
+ const data = await ctx.client.process3d({
869
1725
  task_id: options.taskId,
870
- operation: "texture",
871
- params
1726
+ operation: "retexture",
1727
+ params: toJsonObject2(params)
872
1728
  });
873
- const localPath = await saveProcess3dOutput(data, "texture", options.outputDir);
1729
+ const localPath = await saveProcess3dOutput(data, "retexture", options.outputDir);
874
1730
  if (localPath) data.local_path = localPath;
875
- printSuccess("process3d.texture", data, ctx);
1731
+ printSuccess("process3d.retexture", data, ctx);
876
1732
  } catch (error2) {
877
- printError("process3d.texture", error2);
1733
+ printError("process3d.retexture", error2);
878
1734
  }
879
1735
  })
880
1736
  );
881
1737
  command.addCommand(
882
- new Command6("rig").description("Auto-rig a 3D model (add skeleton for animation)").requiredOption("--task-id <id>", "Tripo task ID").option("--format <fmt>", "Output format: glb or fbx", "glb").option("--spec <spec>", "Rig spec: mixamo or tripo", "mixamo").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1738
+ new Command8("rig").description("Rig a Meshy model for character animation").option("--task-id <id>", "Meshy task ID").option("--height <meters>", "Estimated character height in meters").option("--model-url <url>", "Model URL to rig without an existing task").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
883
1739
  try {
1740
+ if (!options.taskId && !options.modelUrl) {
1741
+ throw new Error("Either --task-id or --model-url is required");
1742
+ }
884
1743
  const ctx = createContext(this);
885
- const data = await ctx.client.post("/api/process3d", {
886
- task_id: options.taskId,
1744
+ const params = {};
1745
+ if (options.height) params.height_meters = Number(options.height);
1746
+ if (options.modelUrl) params.model_url = options.modelUrl;
1747
+ const data = await ctx.client.process3d({
1748
+ task_id: options.taskId ?? "",
887
1749
  operation: "rig",
888
- params: {
889
- format: options.format,
890
- spec: options.spec
891
- }
1750
+ params: toJsonObject2(params)
892
1751
  });
893
- const localPath = await saveProcess3dOutput(data, "rig", options.outputDir, options.format);
1752
+ const localPath = await saveProcess3dOutput(data, "rig", options.outputDir);
894
1753
  if (localPath) data.local_path = localPath;
895
1754
  printSuccess("process3d.rig", data, ctx);
896
1755
  } catch (error2) {
@@ -899,18 +1758,21 @@ function createProcess3dCommand() {
899
1758
  })
900
1759
  );
901
1760
  command.addCommand(
902
- new Command6("animate").description("Apply preset animation to a rigged model").requiredOption("--task-id <id>", "Tripo task ID (from rig step)").requiredOption("--animation <preset>", "Animation preset (e.g. preset:walk, preset:idle, preset:run)").option("--format <fmt>", "Output format: glb or fbx", "glb").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1761
+ new Command8("animate").description("Apply a preset animation to a rigged Meshy character").requiredOption("--task-id <id>", "Meshy rigging task ID").requiredOption("--action-id <n>", "Animation preset ID (see docs for full list)").option("--fps <n>", "Target frame rate: 24, 25, 30, or 60").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
903
1762
  try {
904
1763
  const ctx = createContext(this);
905
- const data = await ctx.client.post("/api/process3d", {
1764
+ const params = {
1765
+ action_id: Number(options.actionId)
1766
+ };
1767
+ if (options.fps) {
1768
+ params.fps = Number(options.fps);
1769
+ }
1770
+ const data = await ctx.client.process3d({
906
1771
  task_id: options.taskId,
907
1772
  operation: "animate",
908
- params: {
909
- animation: options.animation,
910
- format: options.format
911
- }
1773
+ params: toJsonObject2(params)
912
1774
  });
913
- const localPath = await saveProcess3dOutput(data, "animate", options.outputDir, options.format);
1775
+ const localPath = await saveProcess3dOutput(data, "animate", options.outputDir);
914
1776
  if (localPath) data.local_path = localPath;
915
1777
  printSuccess("process3d.animate", data, ctx);
916
1778
  } catch (error2) {
@@ -919,162 +1781,233 @@ function createProcess3dCommand() {
919
1781
  })
920
1782
  );
921
1783
  command.addCommand(
922
- new Command6("reduce").description("Reduce polygon count (high-poly to low-poly)").requiredOption("--task-id <id>", "Tripo task ID").option("--face-limit <n>", "Target face count").option("--quad", "Use quad topology").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1784
+ new Command8("refine").description("Refine a Meshy preview model into a higher quality result").requiredOption("--task-id <id>", "Meshy preview task ID").option("--pbr", "Enable PBR materials").option("--hd-texture", "Request 4K texture output").option("--texture-prompt <text>", "Optional texture prompt for the refinement pass").option("--ai-model <name>", "Meshy model: meshy-5, meshy-6, latest").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
923
1785
  try {
924
1786
  const ctx = createContext(this);
925
1787
  const params = {};
926
- if (options.faceLimit) params.face_limit = Number(options.faceLimit);
927
- if (options.quad) params.quad = true;
928
- const data = await ctx.client.post("/api/process3d", {
1788
+ if (options.pbr) params.pbr = true;
1789
+ if (options.hdTexture) params.hd_texture = true;
1790
+ if (options.texturePrompt) params.texture_prompt = options.texturePrompt;
1791
+ if (options.aiModel) params.ai_model = options.aiModel;
1792
+ const data = await ctx.client.process3d({
929
1793
  task_id: options.taskId,
930
- operation: "reduce",
931
- params
1794
+ operation: "refine",
1795
+ params: toJsonObject2(params)
932
1796
  });
933
- const localPath = await saveProcess3dOutput(data, "reduce", options.outputDir);
1797
+ const localPath = await saveProcess3dOutput(data, "refine", options.outputDir);
934
1798
  if (localPath) data.local_path = localPath;
935
- printSuccess("process3d.reduce", data, ctx);
1799
+ printSuccess("process3d.refine", data, ctx);
936
1800
  } catch (error2) {
937
- printError("process3d.reduce", error2);
1801
+ printError("process3d.refine", error2);
938
1802
  }
939
1803
  })
940
1804
  );
1805
+ return command;
1806
+ }
1807
+
1808
+ // src/commands/provider.ts
1809
+ import { Command as Command9 } from "commander";
1810
+ function createProviderCommand() {
1811
+ const command = new Command9("provider").description("Provider management");
941
1812
  command.addCommand(
942
- new Command6("stylize").description("Apply artistic style to a model").requiredOption("--task-id <id>", "Tripo task ID").requiredOption("--style <style>", "Style: lego, voxel, voronoi, minecraft").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1813
+ new Command9("list").description("List available providers").action(async function() {
943
1814
  try {
944
1815
  const ctx = createContext(this);
945
- const data = await ctx.client.post("/api/process3d", {
946
- task_id: options.taskId,
947
- operation: "stylize",
948
- params: {
949
- style: options.style
950
- }
951
- });
952
- const localPath = await saveProcess3dOutput(data, "stylize", options.outputDir);
953
- if (localPath) data.local_path = localPath;
954
- printSuccess("process3d.stylize", data, ctx);
1816
+ const data = await ctx.client.providers.list();
1817
+ printSuccess("provider.list", data, ctx);
955
1818
  } catch (error2) {
956
- printError("process3d.stylize", error2);
1819
+ printError("provider.list", error2);
957
1820
  }
958
1821
  })
959
1822
  );
960
1823
  command.addCommand(
961
- new Command6("segment").description("Segment mesh into logical parts").requiredOption("--task-id <id>", "Tripo task ID").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1824
+ new Command9("health").description("Check provider health").argument("[name]", "Specific provider name").action(async function(name) {
962
1825
  try {
963
1826
  const ctx = createContext(this);
964
- const data = await ctx.client.post("/api/process3d", {
965
- task_id: options.taskId,
966
- operation: "segment",
967
- params: {}
968
- });
969
- const localPath = await saveProcess3dOutput(data, "segment", options.outputDir);
970
- if (localPath) data.local_path = localPath;
971
- printSuccess("process3d.segment", data, ctx);
1827
+ const data = name ? await ctx.client.providers.health(name) : await ctx.client.providers.health();
1828
+ printSuccess("provider.health", data, ctx);
972
1829
  } catch (error2) {
973
- printError("process3d.segment", error2);
1830
+ printError("provider.health", error2);
974
1831
  }
975
1832
  })
976
1833
  );
1834
+ return command;
1835
+ }
1836
+
1837
+ // src/commands/upload.ts
1838
+ import { readFile as readFile3 } from "fs/promises";
1839
+ import { basename } from "path";
1840
+ import { AssetForgeError as AssetForgeError6 } from "@doufunao123/assetforge-sdk";
1841
+ import { Command as Command10 } from "commander";
1842
+ function createUploadCommand() {
1843
+ const command = new Command10("upload").description("Upload and manage assets");
977
1844
  command.addCommand(
978
- new Command6("prerigcheck").description("Check if a model can be rigged").requiredOption("--task-id <id>", "Tripo task ID").option("--output-dir <dir>", "Directory to save output", ".").action(async function(options) {
1845
+ new Command10("file").description("Upload a file and get a public URL").argument("<path>", "Path to file to upload").action(async function(filePath) {
1846
+ const ctx = createContext(this);
979
1847
  try {
980
- const ctx = createContext(this);
981
- const data = await ctx.client.post("/api/process3d", {
982
- task_id: options.taskId,
983
- operation: "prerigcheck",
984
- params: {}
985
- });
986
- const localPath = await saveProcess3dOutput(data, "prerigcheck", options.outputDir);
987
- if (localPath) data.local_path = localPath;
988
- printSuccess("process3d.prerigcheck", data, ctx);
1848
+ const content = await readLocalFile(filePath);
1849
+ const data = await ctx.client.assets.upload(content, { filename: basename(filePath) });
1850
+ printSuccess("asset.upload", data, ctx);
989
1851
  } catch (error2) {
990
- printError("process3d.prerigcheck", error2);
1852
+ printError("asset.upload", error2, ctx.human);
991
1853
  }
992
1854
  })
993
1855
  );
994
- return command;
995
- }
996
-
997
- // src/commands/provider.ts
998
- import { Command as Command7 } from "commander";
999
- function createProviderCommand() {
1000
- const command = new Command7("provider").description("Provider management");
1001
1856
  command.addCommand(
1002
- new Command7("list").description("List available providers").action(async function() {
1857
+ new Command10("list").description("List uploaded assets").action(async function() {
1858
+ const ctx = createContext(this);
1003
1859
  try {
1004
- const ctx = createContext(this);
1005
- const data = await ctx.client.get("/api/providers");
1006
- printSuccess("provider.list", data, ctx);
1860
+ const data = await ctx.client.assets.list();
1861
+ printSuccess("asset.list", data, ctx);
1007
1862
  } catch (error2) {
1008
- printError("provider.list", error2);
1863
+ printError("asset.list", error2, ctx.human);
1009
1864
  }
1010
1865
  })
1011
1866
  );
1012
1867
  command.addCommand(
1013
- new Command7("health").description("Check provider health").argument("[name]", "Specific provider name").action(async function(name) {
1868
+ new Command10("delete").description("Delete an uploaded asset (admin only)").argument("<filename>", "Filename to delete").action(async function(filename) {
1869
+ const ctx = createContext(this);
1014
1870
  try {
1015
- const ctx = createContext(this);
1016
- const path = name ? `/api/providers/${encodeURIComponent(name)}/health` : "/api/providers/health";
1017
- const data = await ctx.client.get(path);
1018
- printSuccess("provider.health", data, ctx);
1871
+ const data = await ctx.client.assets.delete(filename);
1872
+ printSuccess("asset.delete", data, ctx);
1019
1873
  } catch (error2) {
1020
- printError("provider.health", error2);
1874
+ printError("asset.delete", error2, ctx.human);
1021
1875
  }
1022
1876
  })
1023
1877
  );
1024
1878
  return command;
1025
1879
  }
1880
+ async function readLocalFile(filePath) {
1881
+ try {
1882
+ return await readFile3(filePath);
1883
+ } catch (error2) {
1884
+ throw new AssetForgeError6(
1885
+ `Failed to read file ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
1886
+ { code: "CONFIG_ERROR" }
1887
+ );
1888
+ }
1889
+ }
1026
1890
 
1027
- // src/commands/upload.ts
1028
- import { Command as Command8 } from "commander";
1029
- function createUploadCommand() {
1030
- const command = new Command8("upload").description("Upload and manage assets");
1891
+ // src/commands/voice.ts
1892
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
1893
+ import { dirname as dirname3, extname as extname2 } from "path";
1894
+ import { Command as Command11 } from "commander";
1895
+ function createVoiceCommand() {
1896
+ const command = new Command11("voice").description("Design, clone, list, and delete voices");
1031
1897
  command.addCommand(
1032
- new Command8("file").description("Upload a file and get a public URL").argument("<path>", "Path to file to upload").action(async function(filePath) {
1898
+ new Command11("design").description("Generate a MiMo voice from a text description").requiredOption("--voice-prompt <text>", "Voice description / style prompt").requiredOption("--preview-text <text>", "Text to synthesize for preview").option("--style <text>", "Additional director/style instruction").option("--name <name>", "Save generated voice with this name").option("--save-as <name>", "Alias for --name").option("--output <path>", "Write preview WAV to this path").action(async function(options) {
1033
1899
  const ctx = createContext(this);
1034
1900
  try {
1035
- const data = await ctx.client.uploadFile(filePath);
1036
- printSuccess("asset.upload", data, ctx);
1901
+ const request = {
1902
+ voice_prompt: options.voicePrompt,
1903
+ preview_text: options.previewText,
1904
+ style: options.style,
1905
+ name: options.name,
1906
+ save_as: options.saveAs
1907
+ };
1908
+ const data = await ctx.client.voice.design(request);
1909
+ const localPath = writeWavOutput(data, options.output);
1910
+ if (localPath) data.local_path = localPath;
1911
+ printSuccess("voice.design", data, ctx);
1037
1912
  } catch (error2) {
1038
- printError("asset.upload", error2, ctx.human);
1913
+ printError("voice.design", error2, ctx.human);
1039
1914
  }
1040
1915
  })
1041
1916
  );
1042
1917
  command.addCommand(
1043
- new Command8("list").description("List uploaded assets").action(async function() {
1918
+ new Command11("clone").description("Clone a MiMo voice from an mp3/wav sample").requiredOption("--audio <path-or-data-url>", "Voice sample mp3/wav file or data URL").requiredOption("--preview-text <text>", "Text to synthesize for preview").option("--audio-mime <mime>", "Audio MIME type when --audio is raw base64").option("--style <text>", "Director/style instruction").option("--name <name>", "Save cloned voice with this name").option("--save-as <name>", "Alias for --name").option("--output <path>", "Write preview WAV to this path").action(async function(options) {
1044
1919
  const ctx = createContext(this);
1045
1920
  try {
1046
- const data = await ctx.client.get("/api/assets");
1047
- printSuccess("asset.list", data, ctx);
1921
+ const sample = readVoiceSample(options.audio, options.audioMime);
1922
+ const request = {
1923
+ ...sample,
1924
+ preview_text: options.previewText,
1925
+ style: options.style,
1926
+ name: options.name,
1927
+ save_as: options.saveAs
1928
+ };
1929
+ const data = await ctx.client.voice.clone(request);
1930
+ const localPath = writeWavOutput(data, options.output);
1931
+ if (localPath) data.local_path = localPath;
1932
+ printSuccess("voice.clone", data, ctx);
1048
1933
  } catch (error2) {
1049
- printError("asset.list", error2, ctx.human);
1934
+ printError("voice.clone", error2, ctx.human);
1050
1935
  }
1051
1936
  })
1052
1937
  );
1053
1938
  command.addCommand(
1054
- new Command8("delete").description("Delete an uploaded asset (admin only)").argument("<filename>", "Filename to delete").action(async function(filename) {
1939
+ new Command11("list").description("List saved voices").option("--type <type>", "Voice type: vc or vd").action(async function(options) {
1055
1940
  const ctx = createContext(this);
1056
1941
  try {
1057
- const data = await ctx.client.delete(`/api/assets/${encodeURIComponent(filename)}`);
1058
- printSuccess("asset.delete", data, ctx);
1942
+ const data = await ctx.client.voice.list({ type: options.type });
1943
+ printSuccess("voice.list", data, ctx);
1059
1944
  } catch (error2) {
1060
- printError("asset.delete", error2, ctx.human);
1945
+ printError("voice.list", error2, ctx.human);
1946
+ }
1947
+ })
1948
+ );
1949
+ command.addCommand(
1950
+ new Command11("delete").description("Delete a saved voice").argument("<voice-id>", "Voice ID").option("--type <type>", "Voice type: vc or vd").action(async function(voiceId, options) {
1951
+ const ctx = createContext(this);
1952
+ try {
1953
+ const data = await ctx.client.voice.delete(voiceId, { type: options.type });
1954
+ printSuccess("voice.delete", data, ctx);
1955
+ } catch (error2) {
1956
+ printError("voice.delete", error2, ctx.human);
1061
1957
  }
1062
1958
  })
1063
1959
  );
1064
1960
  return command;
1065
1961
  }
1962
+ function readVoiceSample(input2, audioMime) {
1963
+ if (input2.startsWith("data:")) {
1964
+ return { sample_data_url: input2 };
1965
+ }
1966
+ if (existsSync6(input2)) {
1967
+ return {
1968
+ audio_base64: readFileSync5(input2).toString("base64"),
1969
+ audio_mime: audioMime ?? inferAudioMime(input2)
1970
+ };
1971
+ }
1972
+ return { audio_base64: input2, audio_mime: audioMime ?? "audio/wav" };
1973
+ }
1974
+ function inferAudioMime(filePath) {
1975
+ const ext = extname2(filePath).toLowerCase();
1976
+ if (ext === ".mp3") return "audio/mpeg";
1977
+ if (ext === ".wav") return "audio/wav";
1978
+ return "audio/wav";
1979
+ }
1980
+ function writeWavOutput(data, outputPath) {
1981
+ if (!outputPath) {
1982
+ return null;
1983
+ }
1984
+ const raw = data.wav_base64 ?? data.output_data;
1985
+ if (typeof raw !== "string" || !raw) {
1986
+ return null;
1987
+ }
1988
+ mkdirSync5(dirname3(outputPath), { recursive: true });
1989
+ writeFileSync5(outputPath, Buffer.from(stripDataUri2(raw), "base64"));
1990
+ return outputPath;
1991
+ }
1992
+ function stripDataUri2(data) {
1993
+ const idx = data.indexOf(";base64,");
1994
+ return idx >= 0 ? data.slice(idx + 8) : data;
1995
+ }
1066
1996
 
1067
1997
  // src/index.ts
1068
- var program = new Command9().name("asset-gateway").description("Universal asset generation gateway CLI").version(CLI_VERSION).option(
1998
+ var program = new Command12().name("asset-gateway").description("Universal asset generation gateway CLI").version(CLI_VERSION).option(
1069
1999
  "--gateway-url <url>",
1070
2000
  `Gateway URL (default: $ASSET_GATEWAY_URL, auth config, or ${DEFAULT_GATEWAY_URL})`
1071
2001
  ).option("--token <token>", "API token for authentication").option("--human", "Human-readable output instead of JSON").option("--fields <fields>", "Comma-separated list of output fields");
1072
2002
  program.addCommand(createAuthCommand());
1073
2003
  program.addCommand(createGenerateCommand());
2004
+ program.addCommand(createLibraryCommand());
2005
+ program.addCommand(createPackCommand());
1074
2006
  program.addCommand(createProcessCommand());
1075
2007
  program.addCommand(createProcess3dCommand());
1076
2008
  program.addCommand(createProviderCommand());
1077
2009
  program.addCommand(createUploadCommand());
1078
2010
  program.addCommand(createJobCommand());
2011
+ program.addCommand(createVoiceCommand());
1079
2012
  program.addCommand(createDescribeCommand());
1080
2013
  await program.parseAsync(process.argv);