tripo-cli 0.7.2 → 0.8.0

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 (59) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +16 -14
  3. package/dist/ai/experts.js +26 -20
  4. package/dist/ai/plan-card.js +73 -34
  5. package/dist/ai/wizard.js +19 -19
  6. package/dist/commands/account.js +2 -6
  7. package/dist/commands/ai.js +18 -16
  8. package/dist/commands/batch.js +58 -18
  9. package/dist/commands/completion.js +1 -1
  10. package/dist/commands/config-cmd.js +1 -1
  11. package/dist/commands/doctor.js +1 -3
  12. package/dist/commands/generate.js +50 -8
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +125 -182
  15. package/dist/commands/make.js +50 -25
  16. package/dist/commands/mcp.js +8 -3
  17. package/dist/commands/process-groups.js +29 -18
  18. package/dist/commands/shared.js +22 -13
  19. package/dist/commands/task.js +12 -11
  20. package/dist/commands/topup.js +6 -10
  21. package/dist/commands/update.js +23 -11
  22. package/dist/commands/view.js +3 -4
  23. package/dist/config/config.js +1 -1
  24. package/dist/core/analytics.js +39 -16
  25. package/dist/core/client.js +53 -21
  26. package/dist/core/device-auth.js +49 -1
  27. package/dist/core/download.js +2 -1
  28. package/dist/core/errors.js +24 -14
  29. package/dist/core/output.js +0 -4
  30. package/dist/core/requests.js +94 -31
  31. package/dist/core/task-service.js +21 -20
  32. package/dist/core/upload.js +23 -5
  33. package/dist/core/watch.js +3 -0
  34. package/dist/knowledge/chains.js +16 -17
  35. package/dist/knowledge/error-catalog.js +39 -37
  36. package/dist/knowledge/models.js +147 -27
  37. package/dist/knowledge/params.js +143 -32
  38. package/dist/knowledge/scenarios.js +32 -55
  39. package/dist/pipeline/make.js +38 -30
  40. package/dist/pipeline/runner.js +27 -8
  41. package/dist/pipeline/stdin.js +40 -3
  42. package/dist/program.js +3 -10
  43. package/dist/util/input-detect.js +10 -1
  44. package/package.json +3 -2
  45. package/skill/SKILL.md +52 -15
  46. package/skill/commands/account.md +8 -3
  47. package/skill/commands/batch.md +13 -3
  48. package/skill/commands/generate.md +57 -23
  49. package/skill/commands/make.md +43 -16
  50. package/skill/commands/process.md +29 -24
  51. package/skill/commands/task.md +4 -4
  52. package/skill/commands/view.md +1 -1
  53. package/skill/common-errors.md +14 -10
  54. package/skill/examples/animation.md +4 -2
  55. package/skill/examples/ar-web.md +6 -4
  56. package/skill/examples/game-asset.md +18 -7
  57. package/skill/examples/pipes.md +2 -2
  58. package/skill/examples/print.md +7 -3
  59. package/dist/i18n.js +0 -19
@@ -5,14 +5,13 @@
5
5
  */
6
6
  import * as p from '@clack/prompts';
7
7
  import { TripoClient } from '../core/client.js';
8
- import { AuthError, UsageError } from '../core/errors.js';
8
+ import { ArtifactDownloadError, AuthError, UsageError } from '../core/errors.js';
9
9
  import { emitResult, isInteractive, log, warn } from '../core/output.js';
10
10
  import { buildStepRequest } from '../core/requests.js';
11
- import { t } from '../i18n.js';
12
11
  import { ChainError, getStep } from '../knowledge/chains.js';
13
12
  import { EXIT } from '../knowledge/error-catalog.js';
14
13
  import { scenarioIds } from '../knowledge/scenarios.js';
15
- import { cardFromMakePlan, costNotes, renderPlanCard } from '../ai/plan-card.js';
14
+ import { cardFromMakePlan, costNotes, generationCreditsEstimate, renderPlanCard } from '../ai/plan-card.js';
16
15
  import { checkPlanChain, executeMake, planMake } from '../pipeline/make.js';
17
16
  import { detectInput, InputDetectError } from '../util/input-detect.js';
18
17
  import { newClient, parseChainFlag, printTaskResult, summarizeCredits, withTaskOptions } from './shared.js';
@@ -50,9 +49,9 @@ async function confirmPlan(plan, inputs, flags) {
50
49
  log(renderPlanCard(card));
51
50
  if (!isInteractive())
52
51
  return true;
53
- const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
52
+ const go = await p.confirm({ message: 'Run this plan?' });
54
53
  if (p.isCancel(go) || !go) {
55
- log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
54
+ log('cancelled; no credits spent');
56
55
  return false;
57
56
  }
58
57
  return true;
@@ -90,20 +89,14 @@ export async function runMake(inputs, flags) {
90
89
  return { results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly };
91
90
  }
92
91
  // -n candidates: N parallel generations with distinct seeds.
93
- if (n > 4)
94
- throw new UsageError('at most 4 candidates per run (-n 4)');
92
+ const preview = await planMake(client, { ...options, dryRun: true });
93
+ checkCandidatePlan(preview, n);
95
94
  const basePlan = await planMake(client, options);
96
- if (!basePlan.generation) {
97
- throw new UsageError('-n only applies to generation inputs (text/image), not task references');
98
- }
99
95
  const chain = basePlan.chain;
100
96
  const interactive = isInteractive();
101
- if (chain.length > 0 && !interactive) {
102
- throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
103
- }
104
97
  if (!(await confirmPlan(basePlan, inputs, flags)))
105
98
  return cancelled;
106
- log(t({ zh: `并行生成 ${n} 个候选…`, en: `generating ${n} candidates in parallel…` }));
99
+ log(`generating ${n} candidates in parallel…`);
107
100
  const seeds = Array.from({ length: n }, () => Math.floor(Math.random() * 2 ** 31));
108
101
  const runs = seeds.map((seed, i) => (async () => {
109
102
  const candidateOptions = {
@@ -121,6 +114,9 @@ export async function runMake(inputs, flags) {
121
114
  for (const s of settled) {
122
115
  if (s.status === 'fulfilled')
123
116
  results.push(s.value.primary);
117
+ // A finished (billed) candidate stays in the results even if its download failed.
118
+ else if (s.reason instanceof ArtifactDownloadError)
119
+ results.push({ ...s.reason.result, download_error: s.reason.message });
124
120
  else
125
121
  warn(String(s.reason instanceof Error ? s.reason.message : s.reason));
126
122
  }
@@ -134,7 +130,7 @@ export async function runMake(inputs, flags) {
134
130
  let stoppedEarly;
135
131
  if (chain.length > 0 && interactive) {
136
132
  const pick = await p.select({
137
- message: t({ zh: '选择一个候选继续加工链', en: 'pick a candidate to continue the chain' }),
133
+ message: 'pick a candidate to continue the chain',
138
134
  options: results.map((r, i) => ({
139
135
  value: r.task_id,
140
136
  label: `#${i + 1} ${r.task_id.slice(0, 8)} ${r.output_dir ?? ''}`,
@@ -158,6 +154,17 @@ export async function runMake(inputs, flags) {
158
154
  }
159
155
  return { results, chainResults, stoppedEarly };
160
156
  }
157
+ /** Shared by normal execution and dry-run; must run before upload. */
158
+ function checkCandidatePlan(plan, count) {
159
+ if (count > 4)
160
+ throw new UsageError('at most 4 candidates per run (-n 4)');
161
+ if (count > 1 && !plan.generation) {
162
+ throw new UsageError('-n only applies to generation inputs (text/image), not task references');
163
+ }
164
+ if (count > 1 && plan.chain.length > 0 && !isInteractive()) {
165
+ throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
166
+ }
167
+ }
161
168
  const UPSTREAM_PLACEHOLDER = '<upstream>';
162
169
  /** Dry runs never send a request, so a missing API key must not block them. */
163
170
  function dryRunClient() {
@@ -208,6 +215,7 @@ async function dryRunMake(inputs, flags, options) {
208
215
  candidates: flags.candidates,
209
216
  warnings,
210
217
  errors,
218
+ generation_credits_estimate: null,
211
219
  cost_notes: [],
212
220
  });
213
221
  return;
@@ -224,10 +232,13 @@ async function dryRunMake(inputs, flags, options) {
224
232
  if (plan.chain.length > 0 && !plan.generation && !plan.sourceType) {
225
233
  warnings.push(`chain legality unverified: upstream task type unknown (no local history for ${plan.sourceTaskId}); the real run checks it via the API`);
226
234
  }
227
- if (flags.candidates > 4)
228
- errors.push('at most 4 candidates per run (-n 4)');
229
- if (flags.candidates > 1 && !plan.generation) {
230
- errors.push('-n only applies to generation inputs (text/image), not task references');
235
+ try {
236
+ checkCandidatePlan(plan, flags.candidates);
237
+ }
238
+ catch (err) {
239
+ if (!(err instanceof UsageError))
240
+ throw err;
241
+ errors.push(err.message);
231
242
  }
232
243
  // Same card the real run shows; chain-step args are replaced by the
233
244
  // normalized payloads so the human sees the completed parameters too.
@@ -281,6 +292,7 @@ async function dryRunMake(inputs, flags, options) {
281
292
  candidates: flags.candidates,
282
293
  warnings,
283
294
  errors,
295
+ generation_credits_estimate: generationCreditsEstimate(card) ?? null,
284
296
  cost_notes: costNotes(card),
285
297
  }, renderPlanCard(card));
286
298
  }
@@ -288,18 +300,18 @@ async function dryRunMake(inputs, flags, options) {
288
300
  function emitDryRun(result, cardText) {
289
301
  const lines = cardText ? [cardText] : [];
290
302
  if (result.warnings.length > 0) {
291
- lines.push(t({ zh: '警告(已自动调整):', en: 'warnings (auto-adjusted):' }));
303
+ lines.push('warnings (auto-adjusted):');
292
304
  for (const w of result.warnings)
293
305
  lines.push(` ! ${w}`);
294
306
  }
295
307
  if (result.errors.length > 0) {
296
- lines.push(t({ zh: '错误(阻断):', en: 'errors (blocking):' }));
308
+ lines.push('errors (blocking):');
297
309
  for (const e of result.errors)
298
310
  lines.push(` ✗ ${e}`);
299
311
  }
300
312
  lines.push(result.valid
301
- ? t({ zh: 'dry run:方案有效;未提交任务,未消耗积分', en: 'dry run: plan is valid; nothing submitted, no credits spent' })
302
- : t({ zh: 'dry run:方案无效;修正上述错误后重试', en: 'dry run: plan is invalid; fix the errors above and retry' }));
313
+ ? 'dry run: plan is valid; nothing submitted, no credits spent'
314
+ : 'dry run: plan is invalid; fix the errors above and retry');
303
315
  emitResult(result, lines.join('\n'));
304
316
  if (!result.valid)
305
317
  process.exitCode = EXIT.USAGE;
@@ -315,6 +327,13 @@ export function emitMakeOutcome(outcome) {
315
327
  : primary;
316
328
  if (outcome.chainResults.length > 0)
317
329
  merged.chain = outcome.chainResults;
330
+ if (finalChain && primary.model_file) {
331
+ merged.source_output_dir = primary.output_dir;
332
+ merged.source_model_file = primary.model_file;
333
+ }
334
+ // Chain steps take a task id input, so the API omits normalize_scale on them; the model keeps the import's scale.
335
+ if (primary.normalize_scale !== undefined)
336
+ merged.normalize_scale ??= primary.normalize_scale;
318
337
  if (outcome.stoppedEarly)
319
338
  merged.stopped_early = outcome.stoppedEarly;
320
339
  if (credits.breakdown.length > 1) {
@@ -326,9 +345,9 @@ export function emitMakeOutcome(outcome) {
326
345
  printInlineImage(merged.preview);
327
346
  }
328
347
  else {
329
- const lines = outcome.results.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`);
348
+ const lines = outcome.results.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? (r.download_error ? 'download failed' : r.status)}`);
330
349
  if (credits.breakdown.length > 0) {
331
- lines.push(t({ zh: `消耗积分合计: ${credits.total}`, en: `credits consumed (total): ${credits.total}` }));
350
+ lines.push(`credits consumed (total): ${credits.total}`);
332
351
  for (const line of credits.breakdown)
333
352
  lines.push(` ${line.type} ${line.task_id} ${line.credits_consumed}`);
334
353
  }
@@ -339,4 +358,10 @@ export function emitMakeOutcome(outcome) {
339
358
  credits_breakdown: credits.breakdown,
340
359
  }, lines.join('\n'));
341
360
  }
361
+ const downloadFailed = outcome.results.filter((r) => r.download_error);
362
+ for (const r of downloadFailed) {
363
+ warn(`${r.task_id} succeeded but its download failed; do not regenerate, run "tripo task get ${r.task_id} --download"`);
364
+ }
365
+ if (downloadFailed.length > 0)
366
+ process.exitCode = EXIT.NETWORK;
342
367
  }
@@ -20,14 +20,17 @@ import { beginOperation, finishOperation } from '../core/analytics.js';
20
20
  const TOOLS = [
21
21
  {
22
22
  name: 'tripo_make',
23
- description: 'Generate a 3D asset from text, an image path/URL, or an existing task id. Blocks until finished; downloads artifacts locally and returns file paths (model_file, preview). Use scenario presets for domain defaults.',
23
+ description: 'Generate a 3D asset from text, an image path/URL, a model file path/URL, or an existing task id. Blocks until finished; downloads artifacts locally and returns file paths (model_file, preview). A model file is imported first and the result adds normalize_scale, the import normalization factor (original size = model size / normalize_scale). Use scenario presets for domain defaults.',
24
24
  inputSchema: {
25
25
  type: 'object',
26
26
  properties: {
27
- input: { type: 'string', description: 'prompt text, image file path, image URL, or task id' },
27
+ input: { type: 'string', description: 'prompt text, image file path or URL, model file path or URL (GLB/GLTF/FBX/OBJ/STL), or task id' },
28
28
  scenario: { type: 'string', enum: scenarioIds(), description: 'production scenario preset' },
29
29
  then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
30
- model: { type: 'string', description: 'force model: tripo-v3.1, tripo-p1 or tripo-p2 (P-series preview with quad)' },
30
+ model: {
31
+ type: 'string',
32
+ description: 'force model: tripo-v3.1 / v3.1-20260211, tripo-p1 / P1-20260311, or tripo-p2 / P2-20260801. Automatic routing prefers P2 for real-time/editable topology and uses H3.1 for dense or H-only geometry; aliases and dated versions are both accepted',
33
+ },
31
34
  output_dir: { type: 'string', description: 'directory for artifacts' },
32
35
  },
33
36
  required: ['input'],
@@ -165,7 +168,9 @@ async function callTool(params) {
165
168
  status: final.status,
166
169
  output_dir: final.output_dir ?? outcome.primary.output_dir,
167
170
  model_file: final.model_file ?? outcome.primary.model_file,
171
+ source_model_file: final === outcome.primary ? undefined : outcome.primary.model_file,
168
172
  preview: final.preview ?? outcome.primary.preview,
173
+ normalize_scale: final.normalize_scale ?? outcome.primary.normalize_scale,
169
174
  chain: outcome.chainResults.map((r) => ({ task_id: r.task_id, type: r.type })),
170
175
  stopped_early: outcome.stoppedEarly,
171
176
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Processing command groups: `tripo model`, `tripo anim`, `tripo mesh`.
3
- * Input resolution order: explicit argument > piped NDJSON from stdin > @last.
3
+ * Input resolution order: explicit argument > piped NDJSON from stdin > @last (interactive terminals only).
4
4
  * That makes both human implicit-context and shell pipelines work:
5
5
  * tripo make cat.png | tripo anim rig | tripo anim retarget --param animation=preset:walk
6
6
  */
@@ -8,34 +8,45 @@ import { UsageError } from '../core/errors.js';
8
8
  import { log, warn } from '../core/output.js';
9
9
  import { buildStepRequest } from '../core/requests.js';
10
10
  import { resolveInputValue, runTask } from '../core/task-service.js';
11
- import { t } from '../i18n.js';
12
11
  import { getStep, isMeshEditEnabled } from '../knowledge/chains.js';
13
12
  import { readPipedTask } from '../pipeline/stdin.js';
13
+ import { detectInput, IMAGE_EXTS, urlExtension } from '../util/input-detect.js';
14
14
  import { newClient, printTaskResult, withTaskOptions } from './shared.js';
15
15
  async function resolveStepInput(client, raw) {
16
- if (raw)
17
- return resolveInputValue(client, raw);
16
+ if (raw) {
17
+ const { value, sourceTaskType } = await resolveInputValue(client, raw);
18
+ return { value, sourceType: sourceTaskType };
19
+ }
18
20
  const piped = await readPipedTask();
19
21
  if (piped) {
20
- log(t({ zh: `使用管道输入任务 ${piped.task_id}`, en: `using piped task ${piped.task_id}` }));
22
+ log(`using piped task ${piped.task_id}`);
21
23
  return { value: piped.task_id, sourceType: piped.type };
22
24
  }
23
25
  const fallback = await resolveInputValue(client, '@last');
24
- log(t({ zh: `未指定输入,使用上一个成功任务 @last`, en: `no input given; defaulting to the last successful task (@last)` }));
25
- return fallback;
26
+ log(`no input given; defaulting to the last successful task (@last)`);
27
+ return { value: fallback.value, sourceType: fallback.sourceTaskType };
26
28
  }
27
29
  function makeStepAction(stepName, extraDefaults = {}) {
28
30
  return async (input, flags) => {
29
31
  const client = newClient();
32
+ const def = getStep(stepName);
33
+ const kind = input ? detectInput(input).kind : undefined;
34
+ // Task-only steps (retarget/complete): refuse a file/URL before uploading it.
35
+ if (!def.acceptsFile && kind && kind !== 'task-id' && kind !== 'history-ref' && kind !== 'text') {
36
+ throw new UsageError(`${def.name} takes a task id or @reference, not a file/URL`);
37
+ }
30
38
  const resolved = await resolveStepInput(client, input);
31
39
  // Local chain legality: catch e.g. retarget-on-non-rig before spending credits.
32
- const def = getStep(stepName);
33
40
  if (Array.isArray(def.accepts) && resolved.sourceType && !def.accepts.includes(resolved.sourceType)) {
34
41
  throw new UsageError(def.accepts.length === 0
35
42
  ? `${def.name} takes a file/URL input, not a task (input is a ${resolved.sourceType} task)`
36
43
  : `${def.name} only accepts ${def.accepts.join('/')} tasks (input is a ${resolved.sourceType} task)`);
37
44
  }
38
45
  const args = { ...extraDefaults, ...collectStepArgs(stepName, flags), ...flags.param };
46
+ if (stepName === 'smartsegment' && args.seg_type === undefined
47
+ && (kind === 'image-file' || (kind === 'url' && IMAGE_EXTS.has(urlExtension(input))))) {
48
+ args.seg_type = 'image';
49
+ }
39
50
  const built = buildStepRequest(stepName, resolved.value, args);
40
51
  for (const w of built.warnings)
41
52
  warn(w);
@@ -67,7 +78,7 @@ function collectStepArgs(step, flags) {
67
78
  };
68
79
  switch (step) {
69
80
  case 'convert':
70
- return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'auto_size', 'pivot_to_center_bottom', 'export_vertex_colors']);
81
+ return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'pivot_to_center_bottom', 'export_vertex_colors']);
71
82
  case 'texture':
72
83
  return pick(['model', 'texture_quality', 'texture_alignment', 'pbr']);
73
84
  case 'rig':
@@ -92,12 +103,12 @@ function collectStepArgs(step, flags) {
92
103
  }
93
104
  export function registerProcessCommands(program) {
94
105
  // ---- tripo model ----
95
- const model = program.command('model').description('model processing: refine / texture / convert / import');
96
- withTaskOptions(model.command('refine [input]').description('refine a draft text-to-model task (task id / @last)')).action(makeStepAction('refine'));
106
+ const model = program.command('model').description('model processing: texture / convert / import');
97
107
  withTaskOptions(model
98
108
  .command('texture [input]')
99
109
  .description('re-texture a model (task/@last/file/url)')
100
- .option('--texture-quality <q>', 'standard|detailed|extreme')
110
+ .option('--model <v>', 'v3.5-20260815|v3.0-20250812|v2.5-20250123 (default v3.0; fast/delight select v3.5)')
111
+ .option('--texture-quality <q>', 'fast|standard|detailed|extreme (fast needs v3.5)')
101
112
  .option('--texture-alignment <a>', 'original_image|geometry')).action(makeStepAction('texture'));
102
113
  withTaskOptions(model
103
114
  .command('convert [input]')
@@ -108,21 +119,21 @@ export function registerProcessCommands(program) {
108
119
  .option('--face-limit <n>', 'face limit', (v) => Number(v))
109
120
  .option('--quad', 'quad remesh')
110
121
  .option('--flatten-bottom', 'flatten the bottom (3D printing)')
111
- .option('--fbx-preset <p>', 'blender|3dsmax|mixamo')
122
+ .option('--fbx-preset <p>', 'blender|3dsmax|mixamo|bake_scale')
112
123
  .option('--export-orientation <o>', '+x|-x|+y|-y')
113
124
  .option('--scale-factor <n>', 'scale factor', (v) => Number(v))
114
- .option('--auto-size', 'scale to real-world size')
115
125
  .option('--pivot-to-center-bottom', 'pivot at center bottom')
116
126
  .option('--export-vertex-colors', 'include vertex colors (OBJ/GLTF)')).action(makeStepAction('convert'));
117
127
  withTaskOptions(model.command('import <input>').description('import an external model file/url (GLB/GLTF/FBX/OBJ/STL ≤150MB)')).action(makeStepAction('import'));
118
128
  // ---- tripo anim ----
119
129
  const anim = program.command('anim').description('animation: check (riggability) / rig / retarget');
120
- withTaskOptions(anim.command('check [input]').description('rig-check: is this model riggable? (GLB input)')).action(makeStepAction('rig-check'));
130
+ withTaskOptions(anim.command('check [input]').description('rig-check: is this model riggable? (GLB/FBX/OBJ/STL input)')).action(makeStepAction('rig-check'));
121
131
  withTaskOptions(anim
122
132
  .command('rig [input]')
123
133
  .description('rig a model (skeleton binding)')
124
- .option('--rig-type <type>', 'biped|quadruped|hexapod|octopod|avian|serpentine|aquatic')
125
- .option('--spec <spec>', 'tripo|mixamo')
134
+ .option('--model <v>', 'v3.0-20260909|v2.5-20260210|v1.0-20240301 (default v3.0; aliases rig-v3.0/rig-v2.5/rig-v1.0)')
135
+ .option('--rig-type <type>', 'biped|quadruped|hexapod|octopod|avian|serpentine|aquatic|others (others: v3.0)')
136
+ .option('--spec <spec>', 'tripo|mixamo|actorcore|unity|unreal|vrm (biped only; actorcore/unity/unreal/vrm: v3.0)')
126
137
  .option('--out-format <f>', 'glb|fbx')).action(makeStepAction('rig'));
127
138
  withTaskOptions(anim
128
139
  .command('retarget [input]')
@@ -180,7 +191,7 @@ export function registerProcessCommands(program) {
180
191
  withTaskOptions(mesh
181
192
  .command('smartsegment [input]')
182
193
  .description('smart segmentation (image or model pipeline)')
183
- .option('--seg-type <t>', 'image|model', 'model')
194
+ .option('--seg-type <t>', 'image|model (default: image for an image file/URL, else model)')
184
195
  .option('--granularity <g>', 'coarse|medium|fine')
185
196
  .option('--hint <text>', 'hint for the parts to find')).action(makeStepAction('smartsegment'));
186
197
  }
@@ -3,8 +3,7 @@ import { loadConfig } from '../config/config.js';
3
3
  import { TripoClient } from '../core/client.js';
4
4
  import { UsageError } from '../core/errors.js';
5
5
  import { emitResult } from '../core/output.js';
6
- import { t } from '../i18n.js';
7
- import { parseThenExpression } from '../knowledge/chains.js';
6
+ import { ChainError, parseThenExpression } from '../knowledge/chains.js';
8
7
  /** repeatable --param key=value collector */
9
8
  export function collectParam(value, previous) {
10
9
  const eq = value.indexOf('=');
@@ -49,7 +48,14 @@ export function newClient() {
49
48
  export function parseChainFlag(value) {
50
49
  if (!value)
51
50
  return undefined;
52
- return parseThenExpression(value);
51
+ try {
52
+ return parseThenExpression(value);
53
+ }
54
+ catch (error) {
55
+ if (error instanceof ChainError)
56
+ throw new UsageError(error.message);
57
+ throw error;
58
+ }
53
59
  }
54
60
  /**
55
61
  * Sum the spend of every task in a run and keep the per-task lines. Tasks
@@ -64,32 +70,35 @@ export function summarizeCredits(tasks) {
64
70
  breakdown.push({ task_id: task.task_id, type: task.type, credits_consumed: task.credits_consumed });
65
71
  total += task.credits_consumed;
66
72
  }
67
- return { total, breakdown };
73
+ // credits carry up to two decimals; round away float drift from the sum.
74
+ return { total: Math.round(total * 100) / 100, breakdown };
68
75
  }
69
76
  /** Standard result output for a finished task (human + JSON modes). */
70
77
  export function printTaskResult(result) {
71
78
  const lines = [];
72
- lines.push(t({
73
- zh: `任务 ${result.task_id} ${result.status === 'success' ? '完成' : result.status}`,
74
- en: `task ${result.task_id} ${result.status}`,
75
- }));
79
+ lines.push(`task ${result.task_id} ${result.status}`);
76
80
  if (result.credits_breakdown && result.credits_breakdown.length > 1) {
77
- lines.push(t({ zh: `消耗积分合计: ${result.credits_consumed}`, en: `credits consumed (total): ${result.credits_consumed}` }));
81
+ lines.push(`credits consumed (total): ${result.credits_consumed}`);
78
82
  const typeWidth = Math.max(...result.credits_breakdown.map((l) => l.type.length));
79
83
  for (const line of result.credits_breakdown) {
80
84
  lines.push(` ${line.type.padEnd(typeWidth)} ${line.task_id} ${line.credits_consumed}`);
81
85
  }
82
86
  }
83
87
  else if (result.credits_consumed !== undefined) {
84
- lines.push(t({ zh: `消耗积分: ${result.credits_consumed}`, en: `credits consumed: ${result.credits_consumed}` }));
88
+ lines.push(`credits consumed: ${result.credits_consumed}`);
85
89
  }
86
90
  if (result.output_dir) {
87
- lines.push(t({ zh: `产物目录: ${result.output_dir}`, en: `artifacts: ${result.output_dir}` }));
91
+ lines.push(`artifacts: ${result.output_dir}`);
88
92
  }
89
93
  if (result.model_file)
90
- lines.push(t({ zh: `模型文件: ${result.model_file}`, en: `model file: ${result.model_file}` }));
94
+ lines.push(`model file: ${result.model_file}`);
95
+ if (result.source_model_file)
96
+ lines.push(`generated model: ${result.source_model_file}`);
91
97
  if (result.preview)
92
- lines.push(t({ zh: `预览图: ${result.preview}`, en: `preview: ${result.preview}` }));
98
+ lines.push(`preview: ${result.preview}`);
99
+ if (result.normalize_scale !== undefined) {
100
+ lines.push(`import scale: ${result.normalize_scale} (original size = imported size ÷ ${result.normalize_scale})`);
101
+ }
93
102
  emitResult(result, lines.join('\n'));
94
103
  }
95
104
  export function llmConfigured() {
@@ -2,10 +2,9 @@
2
2
  import { readHistory, resolveReference, updateHistoryStatus } from '../config/history.js';
3
3
  import { TripoClient } from '../core/client.js';
4
4
  import { defaultOutputDir, downloadArtifacts } from '../core/download.js';
5
- import { emitNdjson, emitResult, isJsonMode, isQuiet, log } from '../core/output.js';
5
+ import { emitResult, isJsonMode, isQuiet, log } from '../core/output.js';
6
6
  import { watchTask } from '../core/watch.js';
7
7
  import { uploadLocalFile } from '../core/upload.js';
8
- import { t } from '../i18n.js';
9
8
  import { createSpinner } from '../util/spinner.js';
10
9
  import { newClient } from './shared.js';
11
10
  function resolveTaskId(target) {
@@ -21,12 +20,14 @@ function humanTask(task) {
21
20
  `${task.task_id} ${task.type} ${task.status}${typeof task.progress === 'number' ? ` ${task.progress}%` : ''}`,
22
21
  ];
23
22
  if (task.credits_consumed !== undefined)
24
- lines.push(t({ zh: `消耗积分 ${task.credits_consumed}`, en: `credits ${task.credits_consumed}` }));
23
+ lines.push(`credits ${task.credits_consumed}`);
25
24
  const output = task.output ?? {};
26
25
  for (const [key, value] of Object.entries(output)) {
27
26
  if (typeof value === 'string' && value.startsWith('http'))
28
27
  lines.push(`${key}: ${value}`);
29
28
  }
29
+ if (typeof output.normalize_scale === 'number')
30
+ lines.push(`normalize_scale: ${output.normalize_scale}`);
30
31
  return lines.join('\n');
31
32
  }
32
33
  export function registerTaskCommands(program) {
@@ -45,7 +46,7 @@ export function registerTaskCommands(program) {
45
46
  const entry = readHistory().find((e) => e.task_id === taskId);
46
47
  const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
47
48
  const artifacts = await downloadArtifacts(detail, dir);
48
- emitResult({ ...detail, output_dir: artifacts.outputDir, files: artifacts.files }, `${humanTask(detail)}\n${t({ zh: `产物目录: ${artifacts.outputDir}`, en: `artifacts: ${artifacts.outputDir}` })}`);
49
+ emitResult({ ...detail, output_dir: artifacts.outputDir, files: artifacts.files }, `${humanTask(detail)}\n${`artifacts: ${artifacts.outputDir}`}`);
49
50
  return;
50
51
  }
51
52
  emitResult(detail, humanTask(detail));
@@ -60,19 +61,19 @@ export function registerTaskCommands(program) {
60
61
  ? targets.map(resolveTaskId)
61
62
  : readHistory().slice(-opts.limit).map((e) => e.task_id);
62
63
  if (ids.length === 0) {
63
- emitResult([], t({ zh: '本地历史为空', en: 'no local history yet' }));
64
+ emitResult([], 'no local history yet');
64
65
  return;
65
66
  }
66
67
  const { tasks, missed } = await client.listTasks(ids);
67
68
  const lines = tasks.map((d) => `${d.task_id} ${(d.type ?? '').padEnd(24)} ${d.status}${typeof d.progress === 'number' ? ` ${d.progress}%` : ''}`);
68
69
  if (missed.length > 0) {
69
- lines.push(t({ zh: `未找到: ${missed.join(', ')}`, en: `missed: ${missed.join(', ')}` }));
70
+ lines.push(`missed: ${missed.join(', ')}`);
70
71
  }
71
72
  emitResult({ tasks, missed }, lines.join('\n'));
72
73
  });
73
74
  task
74
75
  .command('watch <task-id>')
75
- .description('block until a task finishes (streams NDJSON progress in --json mode)')
76
+ .description('block until a task finishes (progress on stderr; stdout carries only the final JSON)')
76
77
  .option('--download', 'download artifacts on success')
77
78
  .option('-o, --out <dir>', 'output directory for --download')
78
79
  .option('--timeout <seconds>', 'timeout in seconds', (v) => Number(v))
@@ -89,7 +90,7 @@ export function registerTaskCommands(program) {
89
90
  }
90
91
  else if (isJsonMode() && typeof d.progress === 'number' && d.progress !== lastProgress) {
91
92
  lastProgress = d.progress;
92
- emitNdjson({ event: 'progress', task_id: d.task_id, status: d.status, progress: d.progress });
93
+ log(`${d.type ?? 'task'} · ${d.status} ${d.progress}%`);
93
94
  }
94
95
  },
95
96
  });
@@ -98,9 +99,9 @@ export function registerTaskCommands(program) {
98
99
  if (opts.download) {
99
100
  const entry = readHistory().find((e) => e.task_id === taskId);
100
101
  const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
101
- log(t({ zh: '下载产物…', en: 'downloading artifacts…' }));
102
+ log('downloading artifacts…');
102
103
  const artifacts = await downloadArtifacts(detail, dir);
103
- emitResult({ ...detail, output_dir: artifacts.outputDir, files: artifacts.files, model_file: artifacts.modelFile, preview: artifacts.preview }, `${humanTask(detail)}\n${t({ zh: `产物目录: ${artifacts.outputDir}`, en: `artifacts: ${artifacts.outputDir}` })}`);
104
+ emitResult({ ...detail, output_dir: artifacts.outputDir, files: artifacts.files, model_file: artifacts.modelFile, preview: artifacts.preview }, `${humanTask(detail)}\n${`artifacts: ${artifacts.outputDir}`}`);
104
105
  return;
105
106
  }
106
107
  emitResult(detail, humanTask(detail));
@@ -123,6 +124,6 @@ export function registerTaskCommands(program) {
123
124
  const human = entries
124
125
  .map((e) => `${e.created_at.slice(0, 19)} ${(e.name ?? '').padEnd(24)} ${e.type.padEnd(22)} ${e.status.padEnd(9)} ${e.task_id}`)
125
126
  .join('\n');
126
- emitResult(entries, human || t({ zh: '本地历史为空', en: 'no local history yet' }));
127
+ emitResult(entries, human || 'no local history yet');
127
128
  });
128
129
  }
@@ -6,7 +6,6 @@
6
6
  import { sleep, TripoClient } from '../core/client.js';
7
7
  import { topupUrl } from '../core/task-service.js';
8
8
  import { emitResult, log, shouldOpen } from '../core/output.js';
9
- import { t } from '../i18n.js';
10
9
  import { createSpinner } from '../util/spinner.js';
11
10
  import { openExternal } from '../util/open.js';
12
11
  const POLL_INTERVAL_MS = 3000;
@@ -22,17 +21,17 @@ export function registerTopupCommand(program) {
22
21
  const url = topupUrl();
23
22
  if (shouldOpen()) {
24
23
  openExternal(url);
25
- log(t({ zh: `已打开充值页面: ${url}`, en: `opened the billing page: ${url}` }));
24
+ log(`opened the billing page: ${url}`);
26
25
  }
27
26
  else {
28
- log(t({ zh: `充值页面: ${url}`, en: `billing page: ${url}` }));
27
+ log(`billing page: ${url}`);
29
28
  }
30
- log(t({ zh: `当前余额 ${before.balance} 积分`, en: `current balance: ${before.balance} credits` }));
29
+ log(`current balance: ${before.balance} credits`);
31
30
  if (!opts.watch || !shouldOpen()) {
32
31
  emitResult({ billing_url: url, balance: before.balance }, url);
33
32
  return;
34
33
  }
35
- const spinner = createSpinner(t({ zh: '等待到账…(Ctrl+C 退出,不影响支付)', en: 'waiting for credits… (Ctrl+C is safe; payment continues in the browser)' }));
34
+ const spinner = createSpinner('waiting for credits… (Ctrl+C is safe; payment continues in the browser)');
36
35
  const deadline = Date.now() + POLL_TIMEOUT_MS;
37
36
  let current = Number(before.balance);
38
37
  while (Date.now() < deadline) {
@@ -41,10 +40,7 @@ export function registerTopupCommand(program) {
41
40
  const now = await client.getBalance();
42
41
  const value = Number(now.balance);
43
42
  if (value > current) {
44
- spinner.succeed(t({
45
- zh: `到账 +${value - current} 积分,当前余额 ${value}`,
46
- en: `+${value - current} credits arrived; balance is now ${value}`,
47
- }));
43
+ spinner.succeed(`+${value - current} credits arrived; balance is now ${value}`);
48
44
  emitResult({ billing_url: url, balance: value, delta: value - current });
49
45
  return;
50
46
  }
@@ -55,7 +51,7 @@ export function registerTopupCommand(program) {
55
51
  }
56
52
  }
57
53
  spinner.stop();
58
- log(t({ zh: '未检测到到账(超时 10 分钟);支付完成后余额会自动生效', en: 'no balance change detected within 10 minutes; credits apply automatically once payment completes' }));
54
+ log('no balance change detected within 10 minutes; credits apply automatically once payment completes');
59
55
  emitResult({ billing_url: url, balance: current });
60
56
  });
61
57
  }
@@ -8,13 +8,13 @@
8
8
  * so agents and pipes are unaffected.
9
9
  */
10
10
  import { spawn } from 'node:child_process';
11
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
13
14
  import pc from 'picocolors';
14
15
  import { tripoHome } from '../config/config.js';
15
16
  import { CliError, NetworkError } from '../core/errors.js';
16
17
  import { emitResult, isJsonMode, log, success } from '../core/output.js';
17
- import { t } from '../i18n.js';
18
18
  import { EXIT } from '../knowledge/error-catalog.js';
19
19
  import { cliVersion } from '../version.js';
20
20
  const PACKAGE_NAME = 'tripo-cli';
@@ -86,28 +86,40 @@ export function notifyIfOutdated() {
86
86
  const current = cliVersion();
87
87
  const cache = readCache();
88
88
  if (cache && isNewerVersion(cache.latest, current)) {
89
- process.stderr.write(pc.yellow(t({
90
- zh: `! 有新版本 tripo-cli ${cache.latest} 可用(当前 ${current}),运行 tripo update 升级`,
91
- en: `! a new version of tripo-cli is available: ${cache.latest} (current ${current}) — run "tripo update" to upgrade`,
92
- })) + '\n');
89
+ process.stderr.write(pc.yellow(`! a new version of tripo-cli is available: ${cache.latest} (current ${current}) — run "tripo update" to upgrade`) + '\n');
93
90
  }
94
91
  if (!cache || Date.now() - cache.checked_at > CHECK_INTERVAL_MS) {
95
92
  // Fire-and-forget; the result shows up on the next run.
96
93
  fetchLatestVersion().catch(() => { });
97
94
  }
98
95
  }
96
+ /**
97
+ * Prefix of the running global install: `<prefix>/lib/node_modules/tripo-cli` plus `<prefix>/bin/tripo`
98
+ * (`<prefix>/node_modules/tripo-cli` plus `<prefix>/tripo.cmd` on Windows). Version managers such as
99
+ * vite-plus and Volta keep `tripo` linked to the Node version it was installed under, so a bare
100
+ * `npm install -g` can land in the current Node's prefix and leave the `tripo` on PATH untouched.
101
+ */
102
+ function installPrefix() {
103
+ const pkgRoot = fileURLToPath(new URL('../../', import.meta.url));
104
+ const win = process.platform === 'win32';
105
+ const prefix = win ? join(pkgRoot, '..', '..') : join(pkgRoot, '..', '..', '..');
106
+ return existsSync(win ? join(prefix, 'tripo.cmd') : join(prefix, 'bin', 'tripo')) ? prefix : undefined;
107
+ }
99
108
  function runNpmInstall() {
109
+ const prefix = installPrefix();
110
+ const manual = `npm install -g ${prefix ? `--prefix "${prefix}" ` : ''}${PACKAGE_NAME}@latest`;
100
111
  return new Promise((resolve, reject) => {
101
112
  const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
102
113
  stdio: 'inherit',
103
114
  shell: process.platform === 'win32',
115
+ env: prefix ? { ...process.env, npm_config_prefix: prefix } : process.env,
104
116
  });
105
117
  child.on('error', (err) => reject(new CliError(`failed to run npm: ${err.message}`, EXIT.FATAL)));
106
118
  child.on('exit', (code) => {
107
119
  if (code === 0)
108
120
  resolve();
109
121
  else
110
- reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, t({ zh: `手动执行: npm install -g ${PACKAGE_NAME}@latest`, en: `run manually: npm install -g ${PACKAGE_NAME}@latest` })));
122
+ reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, `run manually: ${manual}`));
111
123
  });
112
124
  });
113
125
  }
@@ -121,18 +133,18 @@ export function registerUpdateCommand(program) {
121
133
  const latest = await fetchLatestVersion();
122
134
  const available = isNewerVersion(latest, current);
123
135
  if (!available) {
124
- success(t({ zh: `已是最新版本 ${current}`, en: `already up to date (${current})` }));
136
+ success(`already up to date (${current})`);
125
137
  emitResult({ current, latest, update_available: false, updated: false }, current);
126
138
  return;
127
139
  }
128
140
  if (opts.check) {
129
- log(t({ zh: `有新版本 ${latest} 可用(当前 ${current}),运行 tripo update 升级`, en: `new version ${latest} available (current ${current}); run "tripo update" to upgrade` }));
141
+ log(`new version ${latest} available (current ${current}); run "tripo update" to upgrade`);
130
142
  emitResult({ current, latest, update_available: true, updated: false }, `${current} → ${latest}`);
131
143
  return;
132
144
  }
133
- log(t({ zh: `正在升级 ${current} → ${latest}…`, en: `updating ${current} → ${latest}…` }));
145
+ log(`updating ${current} → ${latest}…`);
134
146
  await runNpmInstall();
135
- success(t({ zh: `已升级到 tripo-cli ${latest}`, en: `updated to tripo-cli ${latest}` }));
147
+ success(`updated to tripo-cli ${latest}`);
136
148
  emitResult({ current, latest, update_available: true, updated: true }, `${current} → ${latest}`);
137
149
  });
138
150
  }