tripo-cli 0.7.3 → 0.8.1

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 (54) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -7
  3. package/dist/ai/experts.js +25 -19
  4. package/dist/ai/plan-card.js +71 -33
  5. package/dist/ai/wizard.js +13 -17
  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 +35 -4
  13. package/dist/commands/home.js +12 -13
  14. package/dist/commands/login.js +72 -187
  15. package/dist/commands/make.js +47 -25
  16. package/dist/commands/mcp.js +2 -1
  17. package/dist/commands/models.js +47 -0
  18. package/dist/commands/process-groups.js +3 -4
  19. package/dist/commands/shared.js +9 -14
  20. package/dist/commands/task.js +10 -11
  21. package/dist/commands/topup.js +6 -10
  22. package/dist/commands/update.js +6 -10
  23. package/dist/commands/view.js +3 -4
  24. package/dist/config/config.js +1 -1
  25. package/dist/core/client.js +4 -13
  26. package/dist/core/errors.js +16 -11
  27. package/dist/core/output.js +0 -4
  28. package/dist/core/requests.js +61 -17
  29. package/dist/core/task-service.js +19 -20
  30. package/dist/knowledge/chains.js +12 -12
  31. package/dist/knowledge/error-catalog.js +32 -32
  32. package/dist/knowledge/models.js +119 -20
  33. package/dist/knowledge/params.js +96 -8
  34. package/dist/knowledge/scenarios.js +32 -55
  35. package/dist/pipeline/make.js +36 -28
  36. package/dist/pipeline/runner.js +7 -12
  37. package/dist/pipeline/stdin.js +40 -3
  38. package/dist/program.js +6 -11
  39. package/package.json +3 -2
  40. package/skill/SKILL.md +35 -8
  41. package/skill/commands/account.md +1 -1
  42. package/skill/commands/batch.md +13 -3
  43. package/skill/commands/generate.md +35 -10
  44. package/skill/commands/make.md +39 -13
  45. package/skill/commands/process.md +2 -1
  46. package/skill/commands/task.md +2 -2
  47. package/skill/commands/view.md +1 -1
  48. package/skill/common-errors.md +6 -3
  49. package/skill/examples/animation.md +3 -1
  50. package/skill/examples/ar-web.md +6 -4
  51. package/skill/examples/game-asset.md +17 -6
  52. package/skill/examples/pipes.md +2 -2
  53. package/skill/examples/print.md +7 -3
  54. 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,10 @@ 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
+ }
318
334
  // Chain steps take a task id input, so the API omits normalize_scale on them; the model keeps the import's scale.
319
335
  if (primary.normalize_scale !== undefined)
320
336
  merged.normalize_scale ??= primary.normalize_scale;
@@ -329,9 +345,9 @@ export function emitMakeOutcome(outcome) {
329
345
  printInlineImage(merged.preview);
330
346
  }
331
347
  else {
332
- 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)}`);
333
349
  if (credits.breakdown.length > 0) {
334
- lines.push(t({ zh: `消耗积分合计: ${credits.total}`, en: `credits consumed (total): ${credits.total}` }));
350
+ lines.push(`credits consumed (total): ${credits.total}`);
335
351
  for (const line of credits.breakdown)
336
352
  lines.push(` ${line.type} ${line.task_id} ${line.credits_consumed}`);
337
353
  }
@@ -342,4 +358,10 @@ export function emitMakeOutcome(outcome) {
342
358
  credits_breakdown: credits.breakdown,
343
359
  }, lines.join('\n'));
344
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;
345
367
  }
@@ -29,7 +29,7 @@ const TOOLS = [
29
29
  then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
30
30
  model: {
31
31
  type: 'string',
32
- description: 'force model: tripo-v3.1 / v3.1-20260211 (default), tripo-p1 / P1-20260311 (low-poly) or tripo-p2 / P2-20260801 (P-series Preview with quad); aliases and dated versions are both accepted',
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
33
  },
34
34
  output_dir: { type: 'string', description: 'directory for artifacts' },
35
35
  },
@@ -168,6 +168,7 @@ async function callTool(params) {
168
168
  status: final.status,
169
169
  output_dir: final.output_dir ?? outcome.primary.output_dir,
170
170
  model_file: final.model_file ?? outcome.primary.model_file,
171
+ source_model_file: final === outcome.primary ? undefined : outcome.primary.model_file,
171
172
  preview: final.preview ?? outcome.primary.preview,
172
173
  normalize_scale: final.normalize_scale ?? outcome.primary.normalize_scale,
173
174
  chain: outcome.chainResults.map((r) => ({ task_id: r.task_id, type: r.type })),
@@ -0,0 +1,47 @@
1
+ /** `tripo models` — list the model ids each capability accepts (local whitelist, no API call). */
2
+ import { emitResult } from '../core/output.js';
3
+ import { CAPABILITY_DEFAULTS, DECIMATE_MODELS, IMAGE_TO_IMAGE_MODELS, LEGACY_3D_MODELS, MODEL_ALIASES, MODEL_P1, MODEL_P2, MODEL_V31, RIG_MODEL_ALIASES, RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30, SEGMENT_MODELS, TEXTURE_MODELS, TEXT_TO_IMAGE_MODELS, } from '../knowledge/models.js';
4
+ /** Wire ids with the CLI aliases that normalize to each of them. */
5
+ function withAliases(ids, aliases) {
6
+ return ids.map((id) => {
7
+ const names = Object.keys(aliases).filter((alias) => aliases[alias] === id && alias !== id);
8
+ return names.length ? { id, aliases: names } : { id };
9
+ });
10
+ }
11
+ function plain(ids) {
12
+ return ids.map((id) => ({ id }));
13
+ }
14
+ export function listModels() {
15
+ return [
16
+ {
17
+ capability: '3d-generation (text/image/multiview-to-model)',
18
+ default: MODEL_V31,
19
+ models: withAliases([MODEL_V31, MODEL_P2, MODEL_P1, ...LEGACY_3D_MODELS], MODEL_ALIASES),
20
+ },
21
+ { capability: 'text-to-image', default: CAPABILITY_DEFAULTS.textToImage, models: plain(TEXT_TO_IMAGE_MODELS) },
22
+ { capability: 'image-to-image', default: CAPABILITY_DEFAULTS.imageToImage, models: plain(IMAGE_TO_IMAGE_MODELS) },
23
+ { capability: 'texture (model texture)', default: CAPABILITY_DEFAULTS.texture, models: plain(TEXTURE_MODELS) },
24
+ {
25
+ capability: 'rig (anim rig)',
26
+ default: CAPABILITY_DEFAULTS.rig,
27
+ models: withAliases([RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30], RIG_MODEL_ALIASES),
28
+ },
29
+ { capability: 'segment (mesh segment)', default: CAPABILITY_DEFAULTS.segment, models: plain(SEGMENT_MODELS) },
30
+ { capability: 'decimate (mesh decimate)', default: 'v2.0', models: plain(DECIMATE_MODELS) },
31
+ ];
32
+ }
33
+ export function registerModelsCommand(program) {
34
+ program
35
+ .command('models')
36
+ .description('list the model ids each capability accepts (local whitelist, no API call)')
37
+ .action(() => {
38
+ const data = listModels();
39
+ const human = data
40
+ .map((c) => [
41
+ `${c.capability} (default: ${c.default})`,
42
+ ...c.models.map((m) => ` ${m.id}${m.aliases ? ` (aliases: ${m.aliases.join(', ')})` : ''}`),
43
+ ].join('\n'))
44
+ .join('\n\n');
45
+ emitResult(data, human);
46
+ });
47
+ }
@@ -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,7 +8,6 @@ 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';
14
13
  import { detectInput, IMAGE_EXTS, urlExtension } from '../util/input-detect.js';
@@ -20,11 +19,11 @@ async function resolveStepInput(client, raw) {
20
19
  }
21
20
  const piped = await readPipedTask();
22
21
  if (piped) {
23
- log(t({ zh: `使用管道输入任务 ${piped.task_id}`, en: `using piped task ${piped.task_id}` }));
22
+ log(`using piped task ${piped.task_id}`);
24
23
  return { value: piped.task_id, sourceType: piped.type };
25
24
  }
26
25
  const fallback = await resolveInputValue(client, '@last');
27
- log(t({ zh: `未指定输入,使用上一个成功任务 @last`, en: `no input given; defaulting to the last successful task (@last)` }));
26
+ log(`no input given; defaulting to the last successful task (@last)`);
28
27
  return { value: fallback.value, sourceType: fallback.sourceTaskType };
29
28
  }
30
29
  function makeStepAction(stepName, extraDefaults = {}) {
@@ -3,7 +3,6 @@ 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
6
  import { ChainError, parseThenExpression } from '../knowledge/chains.js';
8
7
  /** repeatable --param key=value collector */
9
8
  export function collectParam(value, previous) {
@@ -77,32 +76,28 @@ export function summarizeCredits(tasks) {
77
76
  /** Standard result output for a finished task (human + JSON modes). */
78
77
  export function printTaskResult(result) {
79
78
  const lines = [];
80
- lines.push(t({
81
- zh: `任务 ${result.task_id} ${result.status === 'success' ? '完成' : result.status}`,
82
- en: `task ${result.task_id} ${result.status}`,
83
- }));
79
+ lines.push(`task ${result.task_id} ${result.status}`);
84
80
  if (result.credits_breakdown && result.credits_breakdown.length > 1) {
85
- lines.push(t({ zh: `消耗积分合计: ${result.credits_consumed}`, en: `credits consumed (total): ${result.credits_consumed}` }));
81
+ lines.push(`credits consumed (total): ${result.credits_consumed}`);
86
82
  const typeWidth = Math.max(...result.credits_breakdown.map((l) => l.type.length));
87
83
  for (const line of result.credits_breakdown) {
88
84
  lines.push(` ${line.type.padEnd(typeWidth)} ${line.task_id} ${line.credits_consumed}`);
89
85
  }
90
86
  }
91
87
  else if (result.credits_consumed !== undefined) {
92
- lines.push(t({ zh: `消耗积分: ${result.credits_consumed}`, en: `credits consumed: ${result.credits_consumed}` }));
88
+ lines.push(`credits consumed: ${result.credits_consumed}`);
93
89
  }
94
90
  if (result.output_dir) {
95
- lines.push(t({ zh: `产物目录: ${result.output_dir}`, en: `artifacts: ${result.output_dir}` }));
91
+ lines.push(`artifacts: ${result.output_dir}`);
96
92
  }
97
93
  if (result.model_file)
98
- 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}`);
99
97
  if (result.preview)
100
- lines.push(t({ zh: `预览图: ${result.preview}`, en: `preview: ${result.preview}` }));
98
+ lines.push(`preview: ${result.preview}`);
101
99
  if (result.normalize_scale !== undefined) {
102
- lines.push(t({
103
- zh: `导入缩放系数: ${result.normalize_scale}(原始尺寸 = 导入后尺寸 ÷ ${result.normalize_scale})`,
104
- en: `import scale: ${result.normalize_scale} (original size = imported size ÷ ${result.normalize_scale})`,
105
- }));
100
+ lines.push(`import scale: ${result.normalize_scale} (original size = imported size ÷ ${result.normalize_scale})`);
106
101
  }
107
102
  emitResult(result, lines.join('\n'));
108
103
  }
@@ -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,7 +20,7 @@ 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'))
@@ -47,7 +46,7 @@ export function registerTaskCommands(program) {
47
46
  const entry = readHistory().find((e) => e.task_id === taskId);
48
47
  const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
49
48
  const artifacts = await downloadArtifacts(detail, dir);
50
- 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}`}`);
51
50
  return;
52
51
  }
53
52
  emitResult(detail, humanTask(detail));
@@ -62,19 +61,19 @@ export function registerTaskCommands(program) {
62
61
  ? targets.map(resolveTaskId)
63
62
  : readHistory().slice(-opts.limit).map((e) => e.task_id);
64
63
  if (ids.length === 0) {
65
- emitResult([], t({ zh: '本地历史为空', en: 'no local history yet' }));
64
+ emitResult([], 'no local history yet');
66
65
  return;
67
66
  }
68
67
  const { tasks, missed } = await client.listTasks(ids);
69
68
  const lines = tasks.map((d) => `${d.task_id} ${(d.type ?? '').padEnd(24)} ${d.status}${typeof d.progress === 'number' ? ` ${d.progress}%` : ''}`);
70
69
  if (missed.length > 0) {
71
- lines.push(t({ zh: `未找到: ${missed.join(', ')}`, en: `missed: ${missed.join(', ')}` }));
70
+ lines.push(`missed: ${missed.join(', ')}`);
72
71
  }
73
72
  emitResult({ tasks, missed }, lines.join('\n'));
74
73
  });
75
74
  task
76
75
  .command('watch <task-id>')
77
- .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)')
78
77
  .option('--download', 'download artifacts on success')
79
78
  .option('-o, --out <dir>', 'output directory for --download')
80
79
  .option('--timeout <seconds>', 'timeout in seconds', (v) => Number(v))
@@ -91,7 +90,7 @@ export function registerTaskCommands(program) {
91
90
  }
92
91
  else if (isJsonMode() && typeof d.progress === 'number' && d.progress !== lastProgress) {
93
92
  lastProgress = d.progress;
94
- emitNdjson({ event: 'progress', task_id: d.task_id, status: d.status, progress: d.progress });
93
+ log(`${d.type ?? 'task'} · ${d.status} ${d.progress}%`);
95
94
  }
96
95
  },
97
96
  });
@@ -100,9 +99,9 @@ export function registerTaskCommands(program) {
100
99
  if (opts.download) {
101
100
  const entry = readHistory().find((e) => e.task_id === taskId);
102
101
  const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
103
- log(t({ zh: '下载产物…', en: 'downloading artifacts…' }));
102
+ log('downloading artifacts…');
104
103
  const artifacts = await downloadArtifacts(detail, dir);
105
- 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}`}`);
106
105
  return;
107
106
  }
108
107
  emitResult(detail, humanTask(detail));
@@ -125,6 +124,6 @@ export function registerTaskCommands(program) {
125
124
  const human = entries
126
125
  .map((e) => `${e.created_at.slice(0, 19)} ${(e.name ?? '').padEnd(24)} ${e.type.padEnd(22)} ${e.status.padEnd(9)} ${e.task_id}`)
127
126
  .join('\n');
128
- emitResult(entries, human || t({ zh: '本地历史为空', en: 'no local history yet' }));
127
+ emitResult(entries, human || 'no local history yet');
129
128
  });
130
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
  }
@@ -15,7 +15,6 @@ import pc from 'picocolors';
15
15
  import { tripoHome } from '../config/config.js';
16
16
  import { CliError, NetworkError } from '../core/errors.js';
17
17
  import { emitResult, isJsonMode, log, success } from '../core/output.js';
18
- import { t } from '../i18n.js';
19
18
  import { EXIT } from '../knowledge/error-catalog.js';
20
19
  import { cliVersion } from '../version.js';
21
20
  const PACKAGE_NAME = 'tripo-cli';
@@ -87,10 +86,7 @@ export function notifyIfOutdated() {
87
86
  const current = cliVersion();
88
87
  const cache = readCache();
89
88
  if (cache && isNewerVersion(cache.latest, current)) {
90
- process.stderr.write(pc.yellow(t({
91
- zh: `! 有新版本 tripo-cli ${cache.latest} 可用(当前 ${current}),运行 tripo update 升级`,
92
- en: `! a new version of tripo-cli is available: ${cache.latest} (current ${current}) — run "tripo update" to upgrade`,
93
- })) + '\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');
94
90
  }
95
91
  if (!cache || Date.now() - cache.checked_at > CHECK_INTERVAL_MS) {
96
92
  // Fire-and-forget; the result shows up on the next run.
@@ -123,7 +119,7 @@ function runNpmInstall() {
123
119
  if (code === 0)
124
120
  resolve();
125
121
  else
126
- reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, t({ zh: `手动执行: ${manual}`, en: `run manually: ${manual}` })));
122
+ reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, `run manually: ${manual}`));
127
123
  });
128
124
  });
129
125
  }
@@ -137,18 +133,18 @@ export function registerUpdateCommand(program) {
137
133
  const latest = await fetchLatestVersion();
138
134
  const available = isNewerVersion(latest, current);
139
135
  if (!available) {
140
- success(t({ zh: `已是最新版本 ${current}`, en: `already up to date (${current})` }));
136
+ success(`already up to date (${current})`);
141
137
  emitResult({ current, latest, update_available: false, updated: false }, current);
142
138
  return;
143
139
  }
144
140
  if (opts.check) {
145
- 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`);
146
142
  emitResult({ current, latest, update_available: true, updated: false }, `${current} → ${latest}`);
147
143
  return;
148
144
  }
149
- log(t({ zh: `正在升级 ${current} → ${latest}…`, en: `updating ${current} → ${latest}…` }));
145
+ log(`updating ${current} → ${latest}…`);
150
146
  await runNpmInstall();
151
- success(t({ zh: `已升级到 tripo-cli ${latest}`, en: `updated to tripo-cli ${latest}` }));
147
+ success(`updated to tripo-cli ${latest}`);
152
148
  emitResult({ current, latest, update_available: true, updated: true }, `${current} → ${latest}`);
153
149
  });
154
150
  }
@@ -10,7 +10,6 @@ import { downloadArtifacts, defaultOutputDir } from '../core/download.js';
10
10
  import { UsageError } from '../core/errors.js';
11
11
  import { emitResult, log, shouldOpen } from '../core/output.js';
12
12
  import { startViewer } from '../core/viewer.js';
13
- import { t } from '../i18n.js';
14
13
  import { runTask } from '../core/task-service.js';
15
14
  import { openExternal } from '../util/open.js';
16
15
  import { printTaskResult, newClient } from './shared.js';
@@ -26,11 +25,11 @@ export function registerViewCommands(program) {
26
25
  const handle = await startViewer(modelFile, target);
27
26
  if (shouldOpen()) {
28
27
  openExternal(handle.url);
29
- log(t({ zh: `预览已打开: ${handle.url}`, en: `preview opened: ${handle.url}` }));
28
+ log(`preview opened: ${handle.url}`);
30
29
  }
31
30
  emitResult({ url: handle.url, model_file: modelFile }, `${handle.url}\n${modelFile}`);
32
31
  if (opts.keep || shouldOpen()) {
33
- log(t({ zh: 'Ctrl+C 关闭预览服务', en: 'Ctrl+C to stop the preview server' }));
32
+ log('Ctrl+C to stop the preview server');
34
33
  await new Promise(() => { }); // hold until interrupted
35
34
  }
36
35
  else {
@@ -84,7 +83,7 @@ async function resolveModelFile(target) {
84
83
  const task = await client.getTask(taskId);
85
84
  if (task.status !== 'success')
86
85
  throw new UsageError(`task ${taskId} is ${task.status}, nothing to view yet`);
87
- log(t({ zh: '正在下载模型用于预览…', en: 'downloading the model for preview…' }));
86
+ log('downloading the model for preview…');
88
87
  const dir = entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'view', taskId);
89
88
  const artifacts = await downloadArtifacts(task, dir);
90
89
  if (!artifacts.modelFile || !VIEWABLE.has(extname(artifacts.modelFile).toLowerCase())) {
@@ -4,7 +4,7 @@
4
4
  * Multi-account model: credentials live in named *profiles* (a dict plus an
5
5
  * `active_profile` pointer — same shape as AWS CLI profiles / kubectl
6
6
  * contexts). Everything account-bound (key, region, URL overrides) is
7
- * per-profile; user preferences (language, llm, output dir) stay top-level.
7
+ * per-profile; user preferences (llm, output dir) stay top-level.
8
8
  *
9
9
  * v0.1.0 stored a single key at the top level. That format is normalized
10
10
  * into `profiles.default` on *every* load, so old files keep working
@@ -6,7 +6,6 @@
6
6
  * server's Retry-After / X-RateLimit-Reset hint, capped at 60s per wait.
7
7
  */
8
8
  import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
9
- import { t } from '../i18n.js';
10
9
  import { EXIT } from '../knowledge/error-catalog.js';
11
10
  import { ApiError, AuthError, NetworkError } from './errors.js';
12
11
  import { identifyUser, observeTask, trackEvent, taskOrigin } from './analytics.js';
@@ -28,15 +27,9 @@ export class TripoClient {
28
27
  const wanted = activeProfileName(config);
29
28
  const known = listProfiles(config).map((p) => p.name);
30
29
  if (wanted !== DEFAULT_PROFILE && !known.includes(wanted)) {
31
- throw new AuthError(t({
32
- zh: `没有叫「${wanted}」的 profile${known.length ? `(现有: ${known.join(', ')})` : ''}`,
33
- en: `profile "${wanted}" not found${known.length ? ` (have: ${known.join(', ')})` : ''}`,
34
- }), t({
35
- zh: `用 tripo login --profile ${wanted} 登录,或 tripo use 切换`,
36
- en: `run "tripo login --profile ${wanted}" to create it, or "tripo use" to switch`,
37
- }));
30
+ throw new AuthError(`profile "${wanted}" not found${known.length ? ` (have: ${known.join(', ')})` : ''}`, `run "tripo login --profile ${wanted}" to create it, or "tripo use" to switch`);
38
31
  }
39
- throw new AuthError(t({ zh: '未配置 API Key', en: 'no API key configured' }));
32
+ throw new AuthError('no API key configured');
40
33
  }
41
34
  this.apiKey = key;
42
35
  this.timeoutMs = options.timeoutMs ?? 60_000;
@@ -188,10 +181,8 @@ export class TripoClient {
188
181
  }
189
182
  catch (err) {
190
183
  if (err instanceof NetworkError || (err instanceof ApiError && err.exitCode === EXIT.NETWORK)) {
191
- err.suggestion = t({
192
- zh: '任务可能已在服务端创建并冻结积分;重试前先用 tripo balance 查看冻结额或到控制台确认,避免重复扣费',
193
- en: 'the task may already exist server-side (credits frozen); check "tripo balance" or the console before retrying, to avoid a duplicate charge',
194
- });
184
+ err.suggestion = 'the task may already exist server-side (credits frozen); check "tripo balance" or the console before retrying, to avoid a duplicate charge';
185
+ err.submissionUncertain = true;
195
186
  }
196
187
  throw err;
197
188
  }