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
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `tripo batch run <manifest.yaml>` — bulk production with concurrency,
3
3
  * retries, and resumable state (skip jobs already succeeded on re-run).
4
+ * A job is never resubmitted once a paid task may exist for it: such jobs
5
+ * stop as needs_attention instead of retrying.
4
6
  *
5
7
  * Manifest:
6
8
  * concurrency: 2
@@ -14,10 +16,10 @@
14
16
  */
15
17
  import { readFileSync, writeFileSync } from 'node:fs';
16
18
  import { resolve } from 'node:path';
19
+ import pc from 'picocolors';
17
20
  import { parse as parseYaml } from 'yaml';
18
- import { UsageError } from '../core/errors.js';
19
- import { emitResult, errorLine, log, success } from '../core/output.js';
20
- import { t } from '../i18n.js';
21
+ import { CliError, TaskFailedError, UsageError } from '../core/errors.js';
22
+ import { emitResult, errorLine, log, success, warn } from '../core/output.js';
21
23
  import { parseThenExpression } from '../knowledge/chains.js';
22
24
  import { executeMake, planMake } from '../pipeline/make.js';
23
25
  import { newClient } from './shared.js';
@@ -27,7 +29,7 @@ export function registerBatchCommand(program) {
27
29
  .command('run <manifest>')
28
30
  .description('run all jobs (resumable: finished jobs are skipped on re-run)')
29
31
  .option('--concurrency <n>', 'parallel jobs (default from manifest or 2)', (v) => Number(v))
30
- .option('--retries <n>', 'retries per failed job', (v) => Number(v), 1)
32
+ .option('--retries <n>', 'retries per failed job (only when no paid task was left behind)', (v) => Number(v), 1)
31
33
  .option('--fresh', 'ignore previous state and re-run everything')
32
34
  .action(async (manifestPath, opts) => {
33
35
  const absPath = resolve(manifestPath);
@@ -46,12 +48,17 @@ export function registerBatchCommand(program) {
46
48
  seen.add(key);
47
49
  }
48
50
  const concurrency = Math.max(1, opts.concurrency ?? manifest.concurrency ?? 2);
49
- const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success');
50
- const skipped = jobs.length - queue.length;
51
+ const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success' && state.jobs[key]?.status !== 'needs_attention');
52
+ const attention = jobs.filter(({ key }) => state.jobs[key]?.status === 'needs_attention');
53
+ const skipped = jobs.length - queue.length - attention.length;
51
54
  if (skipped > 0) {
52
- log(t({ zh: `跳过 ${skipped} 个已完成任务(断点续跑)`, en: `skipping ${skipped} already-finished job(s) (resume)` }));
55
+ log(`skipping ${skipped} already-finished job(s) (resume)`);
53
56
  }
54
- log(t({ zh: `批量执行 ${queue.length} 个任务,并发 ${concurrency}`, en: `running ${queue.length} job(s), concurrency ${concurrency}` }));
57
+ for (const { key } of attention) {
58
+ const ids = (state.jobs[key].tasks ?? []).map((t) => t.task_id).join(', ') || 'unknown';
59
+ warn(`[${key}] needs attention, not resubmitted (tasks: ${ids}); delete its entry in ${statePath} or use --fresh to run it again`);
60
+ }
61
+ log(`running ${queue.length} job(s), concurrency ${concurrency}`);
55
62
  const client = newClient();
56
63
  let cursor = 0;
57
64
  const runNext = async () => {
@@ -64,20 +71,34 @@ export function registerBatchCommand(program) {
64
71
  const maxAttempts = 1 + opts.retries;
65
72
  for (let attempt = jobState.attempts + 1; attempt <= maxAttempts; attempt++) {
66
73
  jobState.attempts = attempt;
74
+ const created = [];
67
75
  try {
68
- const outcome = await runJob(client, item.key, item.spec);
76
+ const outcome = await runJob(client, item.key, item.spec, (task) => {
77
+ created.push(task);
78
+ (jobState.tasks ??= []).push(task);
79
+ saveState(statePath, state);
80
+ });
69
81
  jobState.status = 'success';
70
82
  jobState.task_id = outcome.taskId;
71
83
  jobState.output_dir = outcome.outputDir;
72
84
  delete jobState.error;
85
+ delete jobState.suggestion;
73
86
  success(`[${item.key}] done${outcome.outputDir ? ` → ${outcome.outputDir}` : ''}`);
74
87
  break;
75
88
  }
76
89
  catch (err) {
77
90
  const message = err instanceof Error ? err.message : String(err);
78
- jobState.status = 'failed';
91
+ const retryable = mayRetry(err, created);
92
+ jobState.status = retryable ? 'failed' : 'needs_attention';
79
93
  jobState.error = message;
80
- errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed: ${message}`);
94
+ jobState.suggestion = retryable
95
+ ? err instanceof CliError ? err.suggestion : undefined
96
+ : attentionSuggestion(err, jobState.tasks ?? []);
97
+ errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed${retryable ? '' : ' (needs attention: a paid task may exist, not retried)'}: ${message}`);
98
+ if (jobState.suggestion)
99
+ process.stderr.write(pc.dim(` → ${jobState.suggestion}\n`));
100
+ if (!retryable)
101
+ break;
81
102
  }
82
103
  finally {
83
104
  saveState(statePath, state);
@@ -90,19 +111,18 @@ export function registerBatchCommand(program) {
90
111
  total: jobs.length,
91
112
  success: Object.values(state.jobs).filter((j) => j.status === 'success').length,
92
113
  failed: Object.values(state.jobs).filter((j) => j.status === 'failed').length,
114
+ needs_attention: Object.values(state.jobs).filter((j) => j.status === 'needs_attention').length,
93
115
  state_file: statePath,
94
116
  jobs: state.jobs,
95
117
  };
96
- const failed = summary.failed;
97
- emitResult(summary, t({
98
- zh: `批量完成: 成功 ${summary.success}/${summary.total}${failed ? `,失败 ${failed}(重跑同一命令续跑)` : ''}`,
99
- en: `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}`,
100
- }));
101
- if (failed > 0)
118
+ const { failed, needs_attention: needsAttention } = summary;
119
+ emitResult(summary, `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}` +
120
+ `${needsAttention ? `, ${needsAttention} need attention (never resubmitted automatically; see "suggestion" in ${statePath})` : ''}`);
121
+ if (failed > 0 || needsAttention > 0)
102
122
  process.exitCode = 6;
103
123
  });
104
124
  }
105
- async function runJob(client, key, spec) {
125
+ async function runJob(client, key, spec, onTaskCreated) {
106
126
  const inputs = spec.inputs ?? (spec.input !== undefined ? [spec.input] : []);
107
127
  if (inputs.length === 0)
108
128
  throw new UsageError(`job "${key}" has no input`);
@@ -115,12 +135,32 @@ async function runJob(client, key, spec) {
115
135
  params: spec.params ?? {},
116
136
  name: spec.name ?? key,
117
137
  outDir: spec.out,
138
+ onTaskCreated,
118
139
  };
119
140
  const plan = await planMake(client, options);
120
141
  const outcome = await executeMake(client, plan, options);
121
142
  const finalResult = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
122
143
  return { taskId: finalResult.task_id, outputDir: finalResult.output_dir ?? outcome.primary.output_dir };
123
144
  }
145
+ /**
146
+ * Retrying is safe only when the failed attempt left no billed or still-running
147
+ * task behind: it failed before anything was submitted, or its only task failed
148
+ * server-side (failed/cancelled tasks are refunded).
149
+ */
150
+ function mayRetry(err, created) {
151
+ if (created.length === 0)
152
+ return !(err instanceof CliError && err.submissionUncertain);
153
+ return (created.length === 1 &&
154
+ err instanceof TaskFailedError &&
155
+ err.taskId === created[0].task_id &&
156
+ (err.status === 'failed' || err.status === 'cancelled'));
157
+ }
158
+ function attentionSuggestion(err, tasks) {
159
+ if (err instanceof CliError && err.suggestion)
160
+ return err.suggestion;
161
+ const list = tasks.map((t) => `${t.task_id} (${t.type})`).join(', ');
162
+ return `paid tasks already exist for this job: ${list}; do not rerun the whole job, continue from the last successful one (tripo make <task_id> --then <remaining steps>)`;
163
+ }
124
164
  function loadManifest(path) {
125
165
  let raw;
126
166
  try {
@@ -2,7 +2,7 @@
2
2
  import { UsageError } from '../core/errors.js';
3
3
  import { isMeshEditEnabled } from '../knowledge/chains.js';
4
4
  const COMMANDS = [
5
- 'login', 'logout', 'whoami', 'use', 'balance', 'usage', 'topup',
5
+ 'login', 'logout', 'whoami', 'use', 'balance', 'usage', 'models', 'topup',
6
6
  'make', 'view', 'redo', 'ai',
7
7
  'generate', 'model', 'anim', 'mesh', 'task', 'files', 'batch', 'history',
8
8
  'config', 'doctor', 'update', 'docs', 'completion', 'mcp',
@@ -7,7 +7,7 @@ import { coerceValue } from './shared.js';
7
7
  /** Account-bound settings — these live in the *active profile*. */
8
8
  const PROFILE_SETTABLE = new Set(['region', 'api_base_url', 'platform_base_url']);
9
9
  /** User preferences — top-level, shared by all profiles. */
10
- const GLOBAL_SETTABLE = new Set(['language', 'default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
10
+ const GLOBAL_SETTABLE = new Set(['default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
11
11
  const SETTABLE = [...PROFILE_SETTABLE, ...GLOBAL_SETTABLE];
12
12
  export function registerConfigCommands(program) {
13
13
  const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
@@ -2,7 +2,7 @@
2
2
  import { statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import pc from 'picocolors';
5
- import { configFileExists, describeKeySource, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
5
+ import { configFileExists, describeKeySource, listProfiles, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
6
6
  import { TripoClient } from '../core/client.js';
7
7
  import { CliError } from '../core/errors.js';
8
8
  import { detectRegionForKey } from '../core/region.js';
@@ -93,8 +93,6 @@ export function registerDoctorCommand(program) {
93
93
  detail: `${process.stdout.isTTY ? 'interactive TTY' : 'non-TTY (headless mode)'}${terminalSupportsInlineImages() ? ' · inline images supported' : ''}`,
94
94
  critical: false,
95
95
  });
96
- const language = loadConfig().language ?? 'auto';
97
- checks.push({ name: 'language', ok: true, detail: language, critical: false });
98
96
  const failedCritical = checks.some((c) => c.critical && !c.ok);
99
97
  const human = checks
100
98
  .map((c) => `${c.ok ? pc.green('✓') : c.critical ? pc.red('✗') : pc.yellow('!')} ${c.name.padEnd(34)} ${c.detail}`)
@@ -3,7 +3,7 @@ import { UsageError } from '../core/errors.js';
3
3
  import { warn } from '../core/output.js';
4
4
  import { buildEditMultiview, buildImageToImage, buildImageToModel, buildImageToMultiview, buildImageToSplat, buildMultiviewToModel, buildTextToImage, buildTextToModel, } from '../core/requests.js';
5
5
  import { resolveInputValue, runTask } from '../core/task-service.js';
6
- import { chooseModel } from '../knowledge/models.js';
6
+ import { chooseModel, detectDenseGeometryIntent, detectLowPolyIntent, detectRealtimeIntent } from '../knowledge/models.js';
7
7
  import { assignViews } from '../pipeline/make.js';
8
8
  import { detectInput } from '../util/input-detect.js';
9
9
  import { newClient, printTaskResult, withTaskOptions } from './shared.js';
@@ -39,16 +39,43 @@ export function registerGenerateCommands(program) {
39
39
  };
40
40
  add('text-to-model', 'text → 3D model', 'prompt', async (_c, inputs, flags) => {
41
41
  const prompt = inputs.join(' ');
42
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
42
+ const { model } = chooseModel({
43
+ explicit: flags.model,
44
+ faceLimit: numberParam(flags, 'face_limit'),
45
+ quad: flags.param.quad === true,
46
+ lowPoly: detectLowPolyIntent(prompt),
47
+ realtime: detectRealtimeIntent(prompt),
48
+ denseGeometry: detectDenseGeometryIntent(prompt),
49
+ params: flags.param,
50
+ });
43
51
  return buildTextToModel(prompt, model, flags.param);
44
52
  });
45
53
  add('image-to-model', 'single image → 3D model', 'single', async (client, inputs, flags) => {
54
+ const intent = flags.prompt ?? '';
55
+ const { model } = chooseModel({
56
+ explicit: flags.model,
57
+ faceLimit: numberParam(flags, 'face_limit'),
58
+ quad: flags.param.quad === true,
59
+ lowPoly: detectLowPolyIntent(intent),
60
+ realtime: detectRealtimeIntent(intent),
61
+ denseGeometry: detectDenseGeometryIntent(intent),
62
+ params: flags.param,
63
+ });
64
+ buildImageToModel(inputs[0], model, flags.param);
46
65
  const { value } = await resolveInputValue(client, inputs[0]);
47
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
48
66
  return buildImageToModel(value, model, flags.param);
49
67
  });
50
68
  add('multiview-to-model', '2–4 view images, or one image-to-multiview/edit-multiview task → 3D model', 'multi', async (client, inputs, flags) => {
51
- const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
69
+ const intent = flags.prompt ?? '';
70
+ const { model } = chooseModel({
71
+ explicit: flags.model,
72
+ faceLimit: numberParam(flags, 'face_limit'),
73
+ quad: flags.param.quad === true,
74
+ lowPoly: detectLowPolyIntent(intent),
75
+ realtime: detectRealtimeIntent(intent),
76
+ denseGeometry: detectDenseGeometryIntent(intent),
77
+ params: flags.param,
78
+ });
52
79
  const kind = inputs.length === 1 ? detectInput(inputs[0]).kind : undefined;
53
80
  if (kind === 'task-id' || kind === 'history-ref') {
54
81
  const { value, sourceTaskType } = await resolveInputValue(client, inputs[0]);
@@ -57,6 +84,8 @@ export function registerGenerateCommands(program) {
57
84
  }
58
85
  return buildMultiviewToModel({ task_id: value }, model, flags.param);
59
86
  }
87
+ const previewViews = await assignViews(client, inputs, { dryRun: true });
88
+ buildMultiviewToModel(previewViews, model, flags.param);
60
89
  const views = await assignViews(client, inputs);
61
90
  return buildMultiviewToModel(views, model, flags.param);
62
91
  });
@@ -68,6 +97,8 @@ export function registerGenerateCommands(program) {
68
97
  if (!params.prompt && !params.template) {
69
98
  throw new UsageError('image-to-image needs --prompt or --param template=...');
70
99
  }
100
+ // Validate deterministic parameters before resolving any local uploads.
101
+ buildImageToImage(inputs, params);
71
102
  const values = [];
72
103
  for (const input of inputs)
73
104
  values.push((await resolveInputValue(client, input)).value);
@@ -5,7 +5,6 @@ import { activeProfileName, listProfiles, loadConfig, resolveApiKey } from '../c
5
5
  import { readHistory } from '../config/history.js';
6
6
  import { TripoClient } from '../core/client.js';
7
7
  import { isInteractive, log } from '../core/output.js';
8
- import { t } from '../i18n.js';
9
8
  import { runLogin } from './login.js';
10
9
  export function registerHomeAction(program) {
11
10
  program.action(async () => {
@@ -16,30 +15,30 @@ export function registerHomeAction(program) {
16
15
  p.intro(pc.bold('tripo'));
17
16
  const key = resolveApiKey(loadConfig());
18
17
  if (!key) {
19
- log(t({ zh: '还没有配置 API Key,先登录吧(新用户可领免费额度)', en: 'no API key yet — log in first (new accounts get free credits)' }));
18
+ log('no API key yet — log in first (new accounts get free credits)');
20
19
  await runLogin({});
21
20
  return;
22
21
  }
23
22
  let balanceLine = '';
24
23
  try {
25
24
  const balance = await new TripoClient().getBalance();
26
- balanceLine = t({ zh: `余额 ${balance.balance} 积分`, en: `${balance.balance} credits` });
25
+ balanceLine = `${balance.balance} credits`;
27
26
  }
28
27
  catch {
29
- balanceLine = t({ zh: '余额获取失败', en: 'balance unavailable' });
28
+ balanceLine = 'balance unavailable';
30
29
  }
31
30
  const last = readHistory().filter((e) => e.status === 'success').pop();
32
31
  const config = loadConfig();
33
- const profileNote = listProfiles(config).length > 1 ? t({ zh: ` · 账号 ${activeProfileName(config)}`, en: ` · profile ${activeProfileName(config)}` }) : '';
34
- log(`${balanceLine}${profileNote}${last ? t({ zh: ` · 上次任务 @${last.name}`, en: ` · last task @${last.name}` }) : ''}`);
32
+ const profileNote = listProfiles(config).length > 1 ? ` · profile ${activeProfileName(config)}` : '';
33
+ log(`${balanceLine}${profileNote}${last ? ` · last task @${last.name}` : ''}`);
35
34
  const action = await p.select({
36
- message: t({ zh: '想做什么?', en: 'What do you want to do?' }),
35
+ message: 'What do you want to do?',
37
36
  options: [
38
- { value: 'ai', label: t({ zh: '描述需求,让专家帮我定方案 (tripo ai)', en: 'describe what I want, experts plan it (tripo ai)' }) },
39
- { value: 'make', label: t({ zh: '直接生成 (tripo make ...)', en: 'generate directly (tripo make ...)' }) },
40
- ...(last ? [{ value: 'view', label: t({ zh: '看看上一个结果 (tripo view)', en: 'view the last result (tripo view)' }) }] : []),
41
- { value: 'topup', label: t({ zh: '充值 (tripo topup)', en: 'top up credits (tripo topup)' }) },
42
- { value: 'help', label: t({ zh: '看全部命令', en: 'show all commands' }) },
37
+ { value: 'ai', label: 'describe what I want, experts plan it (tripo ai)' },
38
+ { value: 'make', label: 'generate directly (tripo make ...)' },
39
+ ...(last ? [{ value: 'view', label: 'view the last result (tripo view)' }] : []),
40
+ { value: 'topup', label: 'top up credits (tripo topup)' },
41
+ { value: 'help', label: 'show all commands' },
43
42
  ],
44
43
  });
45
44
  if (p.isCancel(action))
@@ -50,7 +49,7 @@ export function registerHomeAction(program) {
50
49
  break;
51
50
  case 'make': {
52
51
  const promptText = await p.text({
53
- message: t({ zh: '输入一句描述或文件路径', en: 'enter a description or file path' }),
52
+ message: 'enter a description or file path',
54
53
  });
55
54
  if (p.isCancel(promptText) || !promptText.trim())
56
55
  return;