tripo-cli 0.1.0 → 0.2.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.
- package/README.md +20 -4
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +32 -17
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/home.js +4 -2
- package/dist/commands/login.js +201 -39
- package/dist/config/config.js +103 -14
- package/dist/core/client.js +16 -2
- package/dist/core/device-auth.js +41 -9
- package/dist/program.js +21 -1
- package/package.json +2 -2
- package/skill/SKILL.md +13 -1
- package/skill/commands/account.md +19 -5
- package/dist/src/ai/experts.js +0 -115
- package/dist/src/ai/llm.js +0 -88
- package/dist/src/ai/plan-card.js +0 -79
- package/dist/src/ai/wizard.js +0 -104
- package/dist/src/cli.js +0 -7
- package/dist/src/commands/account.js +0 -27
- package/dist/src/commands/ai.js +0 -113
- package/dist/src/commands/batch.js +0 -148
- package/dist/src/commands/completion.js +0 -84
- package/dist/src/commands/config-cmd.js +0 -84
- package/dist/src/commands/docs.js +0 -30
- package/dist/src/commands/doctor.js +0 -94
- package/dist/src/commands/generate.js +0 -81
- package/dist/src/commands/home.js +0 -68
- package/dist/src/commands/login.js +0 -225
- package/dist/src/commands/make.js +0 -142
- package/dist/src/commands/mcp.js +0 -194
- package/dist/src/commands/process-groups.js +0 -164
- package/dist/src/commands/shared.js +0 -75
- package/dist/src/commands/task.js +0 -127
- package/dist/src/commands/topup.js +0 -61
- package/dist/src/commands/view.js +0 -97
- package/dist/src/config/config.js +0 -79
- package/dist/src/config/context.js +0 -44
- package/dist/src/config/history.js +0 -104
- package/dist/src/core/client.js +0 -120
- package/dist/src/core/download.js +0 -107
- package/dist/src/core/errors.js +0 -64
- package/dist/src/core/output.js +0 -72
- package/dist/src/core/requests.js +0 -169
- package/dist/src/core/task-service.js +0 -161
- package/dist/src/core/upload.js +0 -36
- package/dist/src/core/viewer-assets.js +0 -24
- package/dist/src/core/viewer.js +0 -59
- package/dist/src/core/watch.js +0 -41
- package/dist/src/i18n.js +0 -19
- package/dist/src/knowledge/chains.js +0 -152
- package/dist/src/knowledge/error-catalog.js +0 -91
- package/dist/src/knowledge/models.js +0 -110
- package/dist/src/knowledge/params.js +0 -174
- package/dist/src/knowledge/scenarios.js +0 -112
- package/dist/src/pipeline/make.js +0 -233
- package/dist/src/pipeline/runner.js +0 -73
- package/dist/src/pipeline/stdin.js +0 -40
- package/dist/src/program.js +0 -147
- package/dist/src/types.js +0 -2
- package/dist/src/util/input-detect.js +0 -92
- package/dist/src/util/notify.js +0 -17
- package/dist/src/util/open.js +0 -18
- package/dist/src/util/spinner.js +0 -62
- package/dist/src/util/term-image.js +0 -28
- package/dist/src/version.js +0 -10
package/dist/src/commands/mcp.js
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo mcp` — MCP (Model Context Protocol) server over stdio, exposing the
|
|
3
|
-
* CLI's capabilities as tools for Cursor / Claude Desktop / any MCP client.
|
|
4
|
-
*
|
|
5
|
-
* Implements the stdio transport (newline-delimited JSON-RPC 2.0):
|
|
6
|
-
* initialize, ping, tools/list, tools/call. Self-contained: no SDK
|
|
7
|
-
* dependency, no stdout pollution (stdout carries protocol frames only).
|
|
8
|
-
*/
|
|
9
|
-
import { createInterface } from 'node:readline';
|
|
10
|
-
import { readHistory } from '../config/history.js';
|
|
11
|
-
import { TripoClient } from '../core/client.js';
|
|
12
|
-
import { downloadArtifacts, defaultOutputDir } from '../core/download.js';
|
|
13
|
-
import { configureOutput } from '../core/output.js';
|
|
14
|
-
import { watchTask } from '../core/watch.js';
|
|
15
|
-
import { parseThenExpression } from '../knowledge/chains.js';
|
|
16
|
-
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
17
|
-
import { executeMake, planMake } from '../pipeline/make.js';
|
|
18
|
-
import { cliVersion } from '../version.js';
|
|
19
|
-
const TOOLS = [
|
|
20
|
-
{
|
|
21
|
-
name: 'tripo_make',
|
|
22
|
-
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
|
-
inputSchema: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
input: { type: 'string', description: 'prompt text, image file path, image URL, or task id' },
|
|
27
|
-
scenario: { type: 'string', enum: scenarioIds(), description: 'production scenario preset' },
|
|
28
|
-
then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
|
|
29
|
-
model: { type: 'string', description: 'force model: tripo-v3.1 or tripo-p1' },
|
|
30
|
-
output_dir: { type: 'string', description: 'directory for artifacts' },
|
|
31
|
-
},
|
|
32
|
-
required: ['input'],
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: 'tripo_task_get',
|
|
37
|
-
description: 'Get the status/details of a Tripo task by id.',
|
|
38
|
-
inputSchema: {
|
|
39
|
-
type: 'object',
|
|
40
|
-
properties: { task_id: { type: 'string' } },
|
|
41
|
-
required: ['task_id'],
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'tripo_task_wait',
|
|
46
|
-
description: 'Block until a Tripo task finishes; optionally download artifacts.',
|
|
47
|
-
inputSchema: {
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: {
|
|
50
|
-
task_id: { type: 'string' },
|
|
51
|
-
download: { type: 'boolean', description: 'download artifacts on success (default true)' },
|
|
52
|
-
},
|
|
53
|
-
required: ['task_id'],
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'tripo_balance',
|
|
58
|
-
description: 'Get the account credit balance.',
|
|
59
|
-
inputSchema: { type: 'object', properties: {} },
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: 'tripo_history',
|
|
63
|
-
description: 'List recent tasks created by this CLI (task ids, names, statuses, output dirs).',
|
|
64
|
-
inputSchema: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
properties: { limit: { type: 'number', description: 'max entries (default 10)' } },
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
];
|
|
70
|
-
export function registerMcpCommand(program) {
|
|
71
|
-
program
|
|
72
|
-
.command('mcp')
|
|
73
|
-
.description('run as an MCP server over stdio (tools: make/task/balance/history)')
|
|
74
|
-
.action(async () => {
|
|
75
|
-
configureOutput({ quiet: true, yes: true, noOpen: true, json: false });
|
|
76
|
-
await serveMcp();
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
async function serveMcp() {
|
|
80
|
-
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
81
|
-
const write = (message) => {
|
|
82
|
-
process.stdout.write(JSON.stringify(message) + '\n');
|
|
83
|
-
};
|
|
84
|
-
for await (const line of rl) {
|
|
85
|
-
const text = line.trim();
|
|
86
|
-
if (!text)
|
|
87
|
-
continue;
|
|
88
|
-
let request;
|
|
89
|
-
try {
|
|
90
|
-
request = JSON.parse(text);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (request.id === undefined || request.id === null) {
|
|
96
|
-
continue; // notification — nothing to answer
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const result = await dispatch(request.method, request.params ?? {});
|
|
100
|
-
if (result !== SKIP)
|
|
101
|
-
write({ jsonrpc: '2.0', id: request.id, result });
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
write({
|
|
105
|
-
jsonrpc: '2.0',
|
|
106
|
-
id: request.id,
|
|
107
|
-
error: { code: -32000, message: err instanceof Error ? err.message : String(err) },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const SKIP = Symbol('skip');
|
|
113
|
-
async function dispatch(method, params) {
|
|
114
|
-
switch (method) {
|
|
115
|
-
case 'initialize':
|
|
116
|
-
return {
|
|
117
|
-
protocolVersion: params.protocolVersion ?? '2024-11-05',
|
|
118
|
-
capabilities: { tools: {} },
|
|
119
|
-
serverInfo: { name: 'tripo-cli', version: cliVersion() },
|
|
120
|
-
};
|
|
121
|
-
case 'ping':
|
|
122
|
-
return {};
|
|
123
|
-
case 'tools/list':
|
|
124
|
-
return { tools: TOOLS };
|
|
125
|
-
case 'tools/call':
|
|
126
|
-
return callTool(params);
|
|
127
|
-
default:
|
|
128
|
-
return SKIP;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
async function callTool(params) {
|
|
132
|
-
const args = params.arguments ?? {};
|
|
133
|
-
const asText = (value) => ({
|
|
134
|
-
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
135
|
-
});
|
|
136
|
-
try {
|
|
137
|
-
switch (params.name) {
|
|
138
|
-
case 'tripo_make': {
|
|
139
|
-
const client = new TripoClient();
|
|
140
|
-
const options = {
|
|
141
|
-
inputs: [String(args.input ?? '')],
|
|
142
|
-
scenario: args.scenario,
|
|
143
|
-
chain: typeof args.then === 'string' ? parseThenExpression(args.then) : undefined,
|
|
144
|
-
model: args.model,
|
|
145
|
-
outDir: args.output_dir,
|
|
146
|
-
};
|
|
147
|
-
const plan = await planMake(client, options);
|
|
148
|
-
const outcome = await executeMake(client, plan, options);
|
|
149
|
-
const final = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
|
|
150
|
-
return asText({
|
|
151
|
-
task_id: final.task_id,
|
|
152
|
-
status: final.status,
|
|
153
|
-
output_dir: final.output_dir ?? outcome.primary.output_dir,
|
|
154
|
-
model_file: final.model_file ?? outcome.primary.model_file,
|
|
155
|
-
preview: final.preview ?? outcome.primary.preview,
|
|
156
|
-
chain: outcome.chainResults.map((r) => ({ task_id: r.task_id, type: r.type })),
|
|
157
|
-
stopped_early: outcome.stoppedEarly,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
case 'tripo_task_get': {
|
|
161
|
-
const client = new TripoClient();
|
|
162
|
-
return asText(await client.getTask(String(args.task_id)));
|
|
163
|
-
}
|
|
164
|
-
case 'tripo_task_wait': {
|
|
165
|
-
const client = new TripoClient();
|
|
166
|
-
const taskId = String(args.task_id);
|
|
167
|
-
const detail = await watchTask(client, taskId, { throwOnFailure: false });
|
|
168
|
-
if (detail.status === 'success' && args.download !== false) {
|
|
169
|
-
const entry = readHistory().find((e) => e.task_id === taskId);
|
|
170
|
-
const dir = entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
|
|
171
|
-
const artifacts = await downloadArtifacts(detail, dir);
|
|
172
|
-
return asText({ ...detail, output_dir: artifacts.outputDir, model_file: artifacts.modelFile, preview: artifacts.preview });
|
|
173
|
-
}
|
|
174
|
-
return asText(detail);
|
|
175
|
-
}
|
|
176
|
-
case 'tripo_balance': {
|
|
177
|
-
const client = new TripoClient();
|
|
178
|
-
return asText(await client.getBalance());
|
|
179
|
-
}
|
|
180
|
-
case 'tripo_history': {
|
|
181
|
-
const limit = typeof args.limit === 'number' ? args.limit : 10;
|
|
182
|
-
return asText(readHistory().slice(-limit));
|
|
183
|
-
}
|
|
184
|
-
default:
|
|
185
|
-
return { content: [{ type: 'text', text: `unknown tool: ${params.name}` }], isError: true };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
return {
|
|
190
|
-
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
191
|
-
isError: true,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Processing command groups: `tripo model`, `tripo anim`, `tripo mesh`.
|
|
3
|
-
* Input resolution order: explicit argument > piped NDJSON from stdin > @last.
|
|
4
|
-
* That makes both human implicit-context and shell pipelines work:
|
|
5
|
-
* tripo make cat.png | tripo anim rig | tripo anim retarget --param animation=preset:walk
|
|
6
|
-
*/
|
|
7
|
-
import { UsageError } from '../core/errors.js';
|
|
8
|
-
import { log, warn } from '../core/output.js';
|
|
9
|
-
import { buildStepRequest } from '../core/requests.js';
|
|
10
|
-
import { resolveInputValue, runTask } from '../core/task-service.js';
|
|
11
|
-
import { t } from '../i18n.js';
|
|
12
|
-
import { getStep } from '../knowledge/chains.js';
|
|
13
|
-
import { readPipedTask } from '../pipeline/stdin.js';
|
|
14
|
-
import { newClient, printTaskResult, withTaskOptions } from './shared.js';
|
|
15
|
-
async function resolveStepInput(client, raw) {
|
|
16
|
-
if (raw)
|
|
17
|
-
return resolveInputValue(client, raw);
|
|
18
|
-
const piped = await readPipedTask();
|
|
19
|
-
if (piped) {
|
|
20
|
-
log(t({ zh: `使用管道输入任务 ${piped.task_id}`, en: `using piped task ${piped.task_id}` }));
|
|
21
|
-
return { value: piped.task_id, sourceType: piped.type };
|
|
22
|
-
}
|
|
23
|
-
const fallback = await resolveInputValue(client, '@last');
|
|
24
|
-
log(t({ zh: `未指定输入,使用上一个成功任务 @last`, en: `no input given; defaulting to the last successful task (@last)` }));
|
|
25
|
-
return fallback;
|
|
26
|
-
}
|
|
27
|
-
function makeStepAction(stepName, extraDefaults = {}) {
|
|
28
|
-
return async (input, flags) => {
|
|
29
|
-
const client = newClient();
|
|
30
|
-
const resolved = await resolveStepInput(client, input);
|
|
31
|
-
// Local chain legality: catch e.g. retarget-on-non-rig before spending credits.
|
|
32
|
-
const def = getStep(stepName);
|
|
33
|
-
if (Array.isArray(def.accepts) && resolved.sourceType && !def.accepts.includes(resolved.sourceType)) {
|
|
34
|
-
throw new UsageError(`${def.name} only accepts ${def.accepts.join('/')} tasks (input is a ${resolved.sourceType} task)`);
|
|
35
|
-
}
|
|
36
|
-
const args = { ...extraDefaults, ...collectStepArgs(stepName, flags), ...flags.param };
|
|
37
|
-
const built = buildStepRequest(stepName, resolved.value, args);
|
|
38
|
-
for (const w of built.warnings)
|
|
39
|
-
warn(w);
|
|
40
|
-
const result = await runTask({
|
|
41
|
-
client,
|
|
42
|
-
request: built.request,
|
|
43
|
-
taskType: built.taskType,
|
|
44
|
-
name: flags.name ?? `${stepName}`,
|
|
45
|
-
outDir: flags.out,
|
|
46
|
-
noWait: flags.wait === false,
|
|
47
|
-
noDownload: flags.download === false,
|
|
48
|
-
notify: flags.notify,
|
|
49
|
-
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
50
|
-
});
|
|
51
|
-
printTaskResult(result);
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** Map first-class flags (e.g. --format) into step args. */
|
|
55
|
-
function collectStepArgs(step, flags) {
|
|
56
|
-
const pick = (keys) => {
|
|
57
|
-
const out = {};
|
|
58
|
-
for (const key of keys) {
|
|
59
|
-
const flagKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
60
|
-
const v = flags[flagKey] ?? flags[key];
|
|
61
|
-
if (v !== undefined)
|
|
62
|
-
out[key] = v;
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
};
|
|
66
|
-
switch (step) {
|
|
67
|
-
case 'convert':
|
|
68
|
-
return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'auto_size', 'pivot_to_center_bottom', 'export_vertex_colors']);
|
|
69
|
-
case 'texture':
|
|
70
|
-
return pick(['model', 'texture_quality', 'texture_alignment', 'pbr']);
|
|
71
|
-
case 'stylize':
|
|
72
|
-
return pick(['style', 'block_size']);
|
|
73
|
-
case 'rig':
|
|
74
|
-
return pick(['model', 'rig_type', 'spec', 'out_format']);
|
|
75
|
-
case 'retarget':
|
|
76
|
-
return pick(['animation', 'out_format', 'animate_in_place']);
|
|
77
|
-
case 'decimate':
|
|
78
|
-
return pick(['face_limit', 'quad', 'bake']);
|
|
79
|
-
case 'segment':
|
|
80
|
-
return pick(['model']);
|
|
81
|
-
case 'complete':
|
|
82
|
-
return pick(['completion_mode']);
|
|
83
|
-
case 'smartsegment':
|
|
84
|
-
return pick(['seg_type', 'granularity', 'hint']);
|
|
85
|
-
default:
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
export function registerProcessCommands(program) {
|
|
90
|
-
// ---- tripo model ----
|
|
91
|
-
const model = program.command('model').description('model processing: refine / texture / stylize / convert / import');
|
|
92
|
-
withTaskOptions(model.command('refine [input]').description('refine a draft text-to-model task (task id / @last)')).action(makeStepAction('refine'));
|
|
93
|
-
withTaskOptions(model
|
|
94
|
-
.command('texture [input]')
|
|
95
|
-
.description('re-texture a model (task/@last/file/url)')
|
|
96
|
-
.option('--texture-quality <q>', 'standard|detailed|extreme')
|
|
97
|
-
.option('--texture-alignment <a>', 'original_image|geometry')).action(makeStepAction('texture'));
|
|
98
|
-
withTaskOptions(model
|
|
99
|
-
.command('stylize [input]')
|
|
100
|
-
.description('stylize a model (lego/voxel/voronoi/minecraft)')
|
|
101
|
-
.option('--style <style>', 'lego|voxel|voronoi|minecraft')
|
|
102
|
-
.option('--block-size <n>', 'minecraft block size 32-128', (v) => Number(v))).action(makeStepAction('stylize'));
|
|
103
|
-
withTaskOptions(model
|
|
104
|
-
.command('convert [input]')
|
|
105
|
-
.description('convert format (GLTF/USDZ/FBX/OBJ/STL/3MF)')
|
|
106
|
-
.option('--format <format>', 'target format', 'GLTF')
|
|
107
|
-
.option('--texture-size <n>', 'texture size in px', (v) => Number(v))
|
|
108
|
-
.option('--texture-format <f>', 'JPEG/PNG/OPEN_EXR/...')
|
|
109
|
-
.option('--face-limit <n>', 'face limit', (v) => Number(v))
|
|
110
|
-
.option('--quad', 'quad remesh')
|
|
111
|
-
.option('--flatten-bottom', 'flatten the bottom (3D printing)')
|
|
112
|
-
.option('--fbx-preset <p>', 'blender|3dsmax|mixamo')
|
|
113
|
-
.option('--export-orientation <o>', '+x|-x|+y|-y')
|
|
114
|
-
.option('--scale-factor <n>', 'scale factor', (v) => Number(v))
|
|
115
|
-
.option('--auto-size', 'scale to real-world size')
|
|
116
|
-
.option('--pivot-to-center-bottom', 'pivot at center bottom')
|
|
117
|
-
.option('--export-vertex-colors', 'include vertex colors (OBJ/GLTF)')).action(makeStepAction('convert'));
|
|
118
|
-
withTaskOptions(model.command('import <input>').description('import an external model file/url (GLB/GLTF/FBX/OBJ/STL ≤150MB)')).action(makeStepAction('import'));
|
|
119
|
-
// ---- tripo anim ----
|
|
120
|
-
const anim = program.command('anim').description('animation: check (riggability) / rig / retarget');
|
|
121
|
-
withTaskOptions(anim.command('check [input]').description('rig-check: is this model riggable? (GLB input)')).action(makeStepAction('rig-check'));
|
|
122
|
-
withTaskOptions(anim
|
|
123
|
-
.command('rig [input]')
|
|
124
|
-
.description('rig a model (skeleton binding)')
|
|
125
|
-
.option('--rig-type <type>', 'biped|quadruped|hexapod|octopod|avian|serpentine|aquatic')
|
|
126
|
-
.option('--spec <spec>', 'tripo|mixamo')
|
|
127
|
-
.option('--out-format <f>', 'glb|fbx')).action(makeStepAction('rig'));
|
|
128
|
-
withTaskOptions(anim
|
|
129
|
-
.command('retarget [input]')
|
|
130
|
-
.description('apply preset animations to a rigged task')
|
|
131
|
-
.option('--animation <name...>', 'preset:walk preset:run ... (≤5, billed per animation)')
|
|
132
|
-
.option('--out-format <f>', 'glb|fbx')
|
|
133
|
-
.option('--animate-in-place', 'no root motion')).action(async (input, flags) => {
|
|
134
|
-
// --animation may be a list → map to animations[]
|
|
135
|
-
const anims = flags.animation;
|
|
136
|
-
if (Array.isArray(anims) && anims.length > 1) {
|
|
137
|
-
flags.param = { ...flags.param, animations: anims };
|
|
138
|
-
delete flags.animation;
|
|
139
|
-
}
|
|
140
|
-
else if (Array.isArray(anims)) {
|
|
141
|
-
flags.animation = anims[0];
|
|
142
|
-
}
|
|
143
|
-
await makeStepAction('retarget')(input, flags);
|
|
144
|
-
});
|
|
145
|
-
// ---- tripo mesh ----
|
|
146
|
-
const mesh = program.command('mesh').description('mesh ops: segment / complete / decimate / smartsegment');
|
|
147
|
-
withTaskOptions(mesh.command('segment [input]').description('segment a model into parts').option('--model <v>', 'v1.0-20250506|v2.0-20260430')).action(makeStepAction('segment'));
|
|
148
|
-
withTaskOptions(mesh
|
|
149
|
-
.command('complete [input]')
|
|
150
|
-
.description('complete/fill segmented parts (needs a segment task)')
|
|
151
|
-
.option('--completion-mode <m>', 'ai_completion|quick_cap')).action(makeStepAction('complete'));
|
|
152
|
-
withTaskOptions(mesh
|
|
153
|
-
.command('decimate [input]')
|
|
154
|
-
.description('high-poly → low-poly (bakes textures by default)')
|
|
155
|
-
.option('--face-limit <n>', 'target faces 500–20000', (v) => Number(v))
|
|
156
|
-
.option('--quad', 'quad output')
|
|
157
|
-
.option('--no-bake', 'skip baking textures onto the low-poly')).action(makeStepAction('decimate'));
|
|
158
|
-
withTaskOptions(mesh
|
|
159
|
-
.command('smartsegment [input]')
|
|
160
|
-
.description('smart segmentation (image or model pipeline)')
|
|
161
|
-
.option('--seg-type <t>', 'image|model', 'model')
|
|
162
|
-
.option('--granularity <g>', 'coarse|medium|fine')
|
|
163
|
-
.option('--hint <text>', 'hint for the parts to find')).action(makeStepAction('smartsegment'));
|
|
164
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/** Helpers shared by task-producing commands: flag parsing and result output. */
|
|
2
|
-
import { loadConfig } from '../config/config.js';
|
|
3
|
-
import { TripoClient } from '../core/client.js';
|
|
4
|
-
import { UsageError } from '../core/errors.js';
|
|
5
|
-
import { emitResult } from '../core/output.js';
|
|
6
|
-
import { t } from '../i18n.js';
|
|
7
|
-
import { parseThenExpression } from '../knowledge/chains.js';
|
|
8
|
-
/** repeatable --param key=value collector */
|
|
9
|
-
export function collectParam(value, previous) {
|
|
10
|
-
const eq = value.indexOf('=');
|
|
11
|
-
if (eq <= 0)
|
|
12
|
-
throw new UsageError(`--param expects key=value (got "${value}")`);
|
|
13
|
-
const key = value.slice(0, eq).trim();
|
|
14
|
-
const raw = value.slice(eq + 1).trim();
|
|
15
|
-
previous[key] = coerceValue(raw);
|
|
16
|
-
return previous;
|
|
17
|
-
}
|
|
18
|
-
export function coerceValue(raw) {
|
|
19
|
-
if (raw === 'true')
|
|
20
|
-
return true;
|
|
21
|
-
if (raw === 'false')
|
|
22
|
-
return false;
|
|
23
|
-
if (/^-?\d+$/.test(raw))
|
|
24
|
-
return Number(raw);
|
|
25
|
-
if (/^-?\d*\.\d+$/.test(raw))
|
|
26
|
-
return Number(raw);
|
|
27
|
-
if ((raw.startsWith('[') && raw.endsWith(']')) || (raw.startsWith('{') && raw.endsWith('}'))) {
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(raw);
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
|
-
}
|
|
33
|
-
return raw;
|
|
34
|
-
}
|
|
35
|
-
/** Attach the common generation/processing option set to a command. */
|
|
36
|
-
export function withTaskOptions(cmd) {
|
|
37
|
-
return cmd
|
|
38
|
-
.option('-o, --out <dir>', 'output directory (default ./tripo-out/...)')
|
|
39
|
-
.option('--no-wait', 'submit only; do not wait for completion')
|
|
40
|
-
.option('--no-download', 'wait but skip downloading artifacts')
|
|
41
|
-
.option('--notify', 'desktop notification when done')
|
|
42
|
-
.option('--timeout <seconds>', 'watch timeout in seconds', (v) => Number(v))
|
|
43
|
-
.option('--name <name>', 'history name for @references')
|
|
44
|
-
.option('-p, --param <key=value>', 'extra API parameter (repeatable)', collectParam, {});
|
|
45
|
-
}
|
|
46
|
-
export function newClient() {
|
|
47
|
-
return new TripoClient({ baseUrl: undefined, apiKey: undefined });
|
|
48
|
-
}
|
|
49
|
-
export function parseChainFlag(value) {
|
|
50
|
-
if (!value)
|
|
51
|
-
return undefined;
|
|
52
|
-
return parseThenExpression(value);
|
|
53
|
-
}
|
|
54
|
-
/** Standard result output for a finished task (human + JSON modes). */
|
|
55
|
-
export function printTaskResult(result) {
|
|
56
|
-
const lines = [];
|
|
57
|
-
lines.push(t({
|
|
58
|
-
zh: `任务 ${result.task_id} ${result.status === 'success' ? '完成' : result.status}`,
|
|
59
|
-
en: `task ${result.task_id} ${result.status}`,
|
|
60
|
-
}));
|
|
61
|
-
if (result.credits_consumed !== undefined) {
|
|
62
|
-
lines.push(t({ zh: `消耗积分: ${result.credits_consumed}`, en: `credits consumed: ${result.credits_consumed}` }));
|
|
63
|
-
}
|
|
64
|
-
if (result.output_dir) {
|
|
65
|
-
lines.push(t({ zh: `产物目录: ${result.output_dir}`, en: `artifacts: ${result.output_dir}` }));
|
|
66
|
-
}
|
|
67
|
-
if (result.model_file)
|
|
68
|
-
lines.push(t({ zh: `模型文件: ${result.model_file}`, en: `model file: ${result.model_file}` }));
|
|
69
|
-
if (result.preview)
|
|
70
|
-
lines.push(t({ zh: `预览图: ${result.preview}`, en: `preview: ${result.preview}` }));
|
|
71
|
-
emitResult(result, lines.join('\n'));
|
|
72
|
-
}
|
|
73
|
-
export function llmConfigured() {
|
|
74
|
-
return Boolean(loadConfig().llm?.base_url && loadConfig().llm?.model);
|
|
75
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/** `tripo task get|list|watch` and `tripo files upload`. */
|
|
2
|
-
import { readHistory, resolveReference, updateHistoryStatus } from '../config/history.js';
|
|
3
|
-
import { TripoClient } from '../core/client.js';
|
|
4
|
-
import { defaultOutputDir, downloadArtifacts } from '../core/download.js';
|
|
5
|
-
import { emitNdjson, emitResult, isJsonMode, isQuiet, log } from '../core/output.js';
|
|
6
|
-
import { watchTask } from '../core/watch.js';
|
|
7
|
-
import { uploadLocalFile } from '../core/upload.js';
|
|
8
|
-
import { t } from '../i18n.js';
|
|
9
|
-
import { createSpinner } from '../util/spinner.js';
|
|
10
|
-
import { newClient } from './shared.js';
|
|
11
|
-
function resolveTaskId(target) {
|
|
12
|
-
if (target.startsWith('@')) {
|
|
13
|
-
const entry = resolveReference(target);
|
|
14
|
-
if (entry)
|
|
15
|
-
return entry.task_id;
|
|
16
|
-
}
|
|
17
|
-
return target;
|
|
18
|
-
}
|
|
19
|
-
function humanTask(task) {
|
|
20
|
-
const lines = [
|
|
21
|
-
`${task.task_id} ${task.type} ${task.status}${typeof task.progress === 'number' ? ` ${task.progress}%` : ''}`,
|
|
22
|
-
];
|
|
23
|
-
if (task.credits_consumed !== undefined)
|
|
24
|
-
lines.push(t({ zh: `消耗积分 ${task.credits_consumed}`, en: `credits ${task.credits_consumed}` }));
|
|
25
|
-
const output = task.output ?? {};
|
|
26
|
-
for (const [key, value] of Object.entries(output)) {
|
|
27
|
-
if (typeof value === 'string' && value.startsWith('http'))
|
|
28
|
-
lines.push(`${key}: ${value}`);
|
|
29
|
-
}
|
|
30
|
-
return lines.join('\n');
|
|
31
|
-
}
|
|
32
|
-
export function registerTaskCommands(program) {
|
|
33
|
-
const task = program.command('task').description('query, list, and watch tasks');
|
|
34
|
-
task
|
|
35
|
-
.command('get <task-id>')
|
|
36
|
-
.description('fetch one task (accepts @last/@name)')
|
|
37
|
-
.option('--step', 'include pipeline step details')
|
|
38
|
-
.option('--download', 'download artifacts if successful')
|
|
39
|
-
.option('-o, --out <dir>', 'output directory for --download')
|
|
40
|
-
.action(async (target, opts) => {
|
|
41
|
-
const client = new TripoClient();
|
|
42
|
-
const taskId = resolveTaskId(target);
|
|
43
|
-
const detail = await client.getTask(taskId, opts.step);
|
|
44
|
-
if (opts.download && detail.status === 'success') {
|
|
45
|
-
const entry = readHistory().find((e) => e.task_id === taskId);
|
|
46
|
-
const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
|
|
47
|
-
const artifacts = await downloadArtifacts(detail, dir);
|
|
48
|
-
emitResult({ ...detail, output_dir: artifacts.outputDir, files: artifacts.files }, `${humanTask(detail)}\n${t({ zh: `产物目录: ${artifacts.outputDir}`, en: `artifacts: ${artifacts.outputDir}` })}`);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
emitResult(detail, humanTask(detail));
|
|
52
|
-
});
|
|
53
|
-
task
|
|
54
|
-
.command('list [task-ids...]')
|
|
55
|
-
.description('batch-query tasks (default: recent history)')
|
|
56
|
-
.option('--limit <n>', 'how many from history', (v) => Number(v), 10)
|
|
57
|
-
.action(async (targets, opts) => {
|
|
58
|
-
const client = new TripoClient();
|
|
59
|
-
const ids = targets.length > 0
|
|
60
|
-
? targets.map(resolveTaskId)
|
|
61
|
-
: readHistory().slice(-opts.limit).map((e) => e.task_id);
|
|
62
|
-
if (ids.length === 0) {
|
|
63
|
-
emitResult([], t({ zh: '本地历史为空', en: 'no local history yet' }));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const list = await client.listTasks(ids);
|
|
67
|
-
const human = list
|
|
68
|
-
.map((d) => `${d.task_id} ${(d.type ?? '').padEnd(24)} ${d.status}${typeof d.progress === 'number' ? ` ${d.progress}%` : ''}`)
|
|
69
|
-
.join('\n');
|
|
70
|
-
emitResult(list, human);
|
|
71
|
-
});
|
|
72
|
-
task
|
|
73
|
-
.command('watch <task-id>')
|
|
74
|
-
.description('block until a task finishes (streams NDJSON progress in --json mode)')
|
|
75
|
-
.option('--download', 'download artifacts on success')
|
|
76
|
-
.option('-o, --out <dir>', 'output directory for --download')
|
|
77
|
-
.option('--timeout <seconds>', 'timeout in seconds', (v) => Number(v))
|
|
78
|
-
.action(async (target, opts) => {
|
|
79
|
-
const client = new TripoClient();
|
|
80
|
-
const taskId = resolveTaskId(target);
|
|
81
|
-
const spinner = isJsonMode() ? undefined : createSpinner(`watch ${taskId}`, isQuiet());
|
|
82
|
-
let lastProgress = -1;
|
|
83
|
-
const detail = await watchTask(client, taskId, {
|
|
84
|
-
timeoutMs: opts.timeout ? opts.timeout * 1000 : undefined,
|
|
85
|
-
onProgress: (d) => {
|
|
86
|
-
if (spinner) {
|
|
87
|
-
spinner.update(`${d.type ?? 'task'} · ${d.status}${typeof d.progress === 'number' ? ` ${d.progress}%` : ''}`);
|
|
88
|
-
}
|
|
89
|
-
else if (isJsonMode() && typeof d.progress === 'number' && d.progress !== lastProgress) {
|
|
90
|
-
lastProgress = d.progress;
|
|
91
|
-
emitNdjson({ event: 'progress', task_id: d.task_id, status: d.status, progress: d.progress });
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
spinner?.succeed(`${detail.type} · success`);
|
|
96
|
-
updateHistoryStatus(taskId, detail.status, { credits_consumed: detail.credits_consumed });
|
|
97
|
-
if (opts.download) {
|
|
98
|
-
const entry = readHistory().find((e) => e.task_id === taskId);
|
|
99
|
-
const dir = opts.out ?? entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
|
|
100
|
-
log(t({ zh: '下载产物…', en: 'downloading artifacts…' }));
|
|
101
|
-
const artifacts = await downloadArtifacts(detail, dir);
|
|
102
|
-
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}` })}`);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
emitResult(detail, humanTask(detail));
|
|
106
|
-
});
|
|
107
|
-
const files = program.command('files').description('file uploads');
|
|
108
|
-
files
|
|
109
|
-
.command('upload <path>')
|
|
110
|
-
.description('upload a local image/model and print the file_token')
|
|
111
|
-
.action(async (path) => {
|
|
112
|
-
const client = newClient();
|
|
113
|
-
const token = await uploadLocalFile(client, path);
|
|
114
|
-
emitResult({ file_token: token }, token);
|
|
115
|
-
});
|
|
116
|
-
program
|
|
117
|
-
.command('history')
|
|
118
|
-
.description('recent tasks created by this CLI (@name references)')
|
|
119
|
-
.option('--limit <n>', 'rows', (v) => Number(v), 20)
|
|
120
|
-
.action((opts) => {
|
|
121
|
-
const entries = readHistory().slice(-opts.limit);
|
|
122
|
-
const human = entries
|
|
123
|
-
.map((e) => `${e.created_at.slice(0, 19)} ${(e.name ?? '').padEnd(24)} ${e.type.padEnd(22)} ${e.status.padEnd(9)} ${e.task_id}`)
|
|
124
|
-
.join('\n');
|
|
125
|
-
emitResult(entries, human || t({ zh: '本地历史为空', en: 'no local history yet' }));
|
|
126
|
-
});
|
|
127
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo topup` — payment stays in the browser (Stripe/Alipay pages already
|
|
3
|
-
* handle it); the CLI opens the billing page and then polls the balance so
|
|
4
|
-
* the user sees "credits arrived" without leaving the terminal.
|
|
5
|
-
*/
|
|
6
|
-
import { resolvePlatformBaseUrl } from '../config/config.js';
|
|
7
|
-
import { sleep, TripoClient } from '../core/client.js';
|
|
8
|
-
import { emitResult, log, shouldOpen } from '../core/output.js';
|
|
9
|
-
import { t } from '../i18n.js';
|
|
10
|
-
import { createSpinner } from '../util/spinner.js';
|
|
11
|
-
import { openExternal } from '../util/open.js';
|
|
12
|
-
const POLL_INTERVAL_MS = 3000;
|
|
13
|
-
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
14
|
-
export function registerTopupCommand(program) {
|
|
15
|
-
program
|
|
16
|
-
.command('topup')
|
|
17
|
-
.description('open the billing page and watch for the credits to arrive')
|
|
18
|
-
.option('--no-watch', 'just open the page; skip balance polling')
|
|
19
|
-
.action(async (opts) => {
|
|
20
|
-
const client = new TripoClient();
|
|
21
|
-
const before = await client.getBalance();
|
|
22
|
-
const url = `${resolvePlatformBaseUrl()}/billing`;
|
|
23
|
-
if (shouldOpen()) {
|
|
24
|
-
openExternal(url);
|
|
25
|
-
log(t({ zh: `已打开充值页面: ${url}`, en: `opened the billing page: ${url}` }));
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
log(t({ zh: `充值页面: ${url}`, en: `billing page: ${url}` }));
|
|
29
|
-
}
|
|
30
|
-
log(t({ zh: `当前余额 ${before.balance} 积分`, en: `current balance: ${before.balance} credits` }));
|
|
31
|
-
if (!opts.watch || !shouldOpen()) {
|
|
32
|
-
emitResult({ billing_url: url, balance: before.balance }, url);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const spinner = createSpinner(t({ zh: '等待到账…(Ctrl+C 退出,不影响支付)', en: 'waiting for credits… (Ctrl+C is safe; payment continues in the browser)' }));
|
|
36
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
37
|
-
let current = Number(before.balance);
|
|
38
|
-
while (Date.now() < deadline) {
|
|
39
|
-
await sleep(POLL_INTERVAL_MS);
|
|
40
|
-
try {
|
|
41
|
-
const now = await client.getBalance();
|
|
42
|
-
const value = Number(now.balance);
|
|
43
|
-
if (value > current) {
|
|
44
|
-
spinner.succeed(t({
|
|
45
|
-
zh: `到账 +${value - current} 积分,当前余额 ${value}`,
|
|
46
|
-
en: `+${value - current} credits arrived; balance is now ${value}`,
|
|
47
|
-
}));
|
|
48
|
-
emitResult({ billing_url: url, balance: value, delta: value - current });
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
current = value;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// transient polling errors are fine; keep waiting
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
spinner.stop();
|
|
58
|
-
log(t({ zh: '未检测到到账(超时 10 分钟);支付完成后余额会自动生效', en: 'no balance change detected within 10 minutes; credits apply automatically once payment completes' }));
|
|
59
|
-
emitResult({ billing_url: url, balance: current });
|
|
60
|
-
});
|
|
61
|
-
}
|