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.
- package/CHANGELOG.md +29 -0
- package/README.md +16 -14
- package/dist/ai/experts.js +26 -20
- package/dist/ai/plan-card.js +73 -34
- package/dist/ai/wizard.js +19 -19
- package/dist/commands/account.js +2 -6
- package/dist/commands/ai.js +18 -16
- package/dist/commands/batch.js +58 -18
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +1 -1
- package/dist/commands/doctor.js +1 -3
- package/dist/commands/generate.js +50 -8
- package/dist/commands/home.js +12 -13
- package/dist/commands/login.js +125 -182
- package/dist/commands/make.js +50 -25
- package/dist/commands/mcp.js +8 -3
- package/dist/commands/process-groups.js +29 -18
- package/dist/commands/shared.js +22 -13
- package/dist/commands/task.js +12 -11
- package/dist/commands/topup.js +6 -10
- package/dist/commands/update.js +23 -11
- package/dist/commands/view.js +3 -4
- package/dist/config/config.js +1 -1
- package/dist/core/analytics.js +39 -16
- package/dist/core/client.js +53 -21
- package/dist/core/device-auth.js +49 -1
- package/dist/core/download.js +2 -1
- package/dist/core/errors.js +24 -14
- package/dist/core/output.js +0 -4
- package/dist/core/requests.js +94 -31
- package/dist/core/task-service.js +21 -20
- package/dist/core/upload.js +23 -5
- package/dist/core/watch.js +3 -0
- package/dist/knowledge/chains.js +16 -17
- package/dist/knowledge/error-catalog.js +39 -37
- package/dist/knowledge/models.js +147 -27
- package/dist/knowledge/params.js +143 -32
- package/dist/knowledge/scenarios.js +32 -55
- package/dist/pipeline/make.js +38 -30
- package/dist/pipeline/runner.js +27 -8
- package/dist/pipeline/stdin.js +40 -3
- package/dist/program.js +3 -10
- package/dist/util/input-detect.js +10 -1
- package/package.json +3 -2
- package/skill/SKILL.md +52 -15
- package/skill/commands/account.md +8 -3
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +57 -23
- package/skill/commands/make.md +43 -16
- package/skill/commands/process.md +29 -24
- package/skill/commands/task.md +4 -4
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +14 -10
- package/skill/examples/animation.md +4 -2
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +18 -7
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/commands/make.js
CHANGED
|
@@ -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:
|
|
52
|
+
const go = await p.confirm({ message: 'Run this plan?' });
|
|
54
53
|
if (p.isCancel(go) || !go) {
|
|
55
|
-
log(
|
|
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
|
-
|
|
94
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
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(
|
|
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
|
-
?
|
|
302
|
-
:
|
|
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(
|
|
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
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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,
|
|
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: {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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', '
|
|
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:
|
|
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('--
|
|
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('--
|
|
125
|
-
.option('--
|
|
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
|
|
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
|
}
|
package/dist/commands/shared.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
88
|
+
lines.push(`credits consumed: ${result.credits_consumed}`);
|
|
85
89
|
}
|
|
86
90
|
if (result.output_dir) {
|
|
87
|
-
lines.push(
|
|
91
|
+
lines.push(`artifacts: ${result.output_dir}`);
|
|
88
92
|
}
|
|
89
93
|
if (result.model_file)
|
|
90
|
-
lines.push(
|
|
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(
|
|
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() {
|
package/dist/commands/task.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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${
|
|
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([],
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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${
|
|
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 ||
|
|
127
|
+
emitResult(entries, human || 'no local history yet');
|
|
127
128
|
});
|
|
128
129
|
}
|
package/dist/commands/topup.js
CHANGED
|
@@ -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(
|
|
24
|
+
log(`opened the billing page: ${url}`);
|
|
26
25
|
}
|
|
27
26
|
else {
|
|
28
|
-
log(
|
|
27
|
+
log(`billing page: ${url}`);
|
|
29
28
|
}
|
|
30
|
-
log(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
145
|
+
log(`updating ${current} → ${latest}…`);
|
|
134
146
|
await runNpmInstall();
|
|
135
|
-
success(
|
|
147
|
+
success(`updated to tripo-cli ${latest}`);
|
|
136
148
|
emitResult({ current, latest, update_available: true, updated: true }, `${current} → ${latest}`);
|
|
137
149
|
});
|
|
138
150
|
}
|