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.
Files changed (65) hide show
  1. package/README.md +20 -4
  2. package/dist/commands/completion.js +1 -1
  3. package/dist/commands/config-cmd.js +32 -17
  4. package/dist/commands/doctor.js +6 -5
  5. package/dist/commands/home.js +4 -2
  6. package/dist/commands/login.js +201 -39
  7. package/dist/config/config.js +103 -14
  8. package/dist/core/client.js +16 -2
  9. package/dist/core/device-auth.js +41 -9
  10. package/dist/program.js +21 -1
  11. package/package.json +2 -2
  12. package/skill/SKILL.md +13 -1
  13. package/skill/commands/account.md +19 -5
  14. package/dist/src/ai/experts.js +0 -115
  15. package/dist/src/ai/llm.js +0 -88
  16. package/dist/src/ai/plan-card.js +0 -79
  17. package/dist/src/ai/wizard.js +0 -104
  18. package/dist/src/cli.js +0 -7
  19. package/dist/src/commands/account.js +0 -27
  20. package/dist/src/commands/ai.js +0 -113
  21. package/dist/src/commands/batch.js +0 -148
  22. package/dist/src/commands/completion.js +0 -84
  23. package/dist/src/commands/config-cmd.js +0 -84
  24. package/dist/src/commands/docs.js +0 -30
  25. package/dist/src/commands/doctor.js +0 -94
  26. package/dist/src/commands/generate.js +0 -81
  27. package/dist/src/commands/home.js +0 -68
  28. package/dist/src/commands/login.js +0 -225
  29. package/dist/src/commands/make.js +0 -142
  30. package/dist/src/commands/mcp.js +0 -194
  31. package/dist/src/commands/process-groups.js +0 -164
  32. package/dist/src/commands/shared.js +0 -75
  33. package/dist/src/commands/task.js +0 -127
  34. package/dist/src/commands/topup.js +0 -61
  35. package/dist/src/commands/view.js +0 -97
  36. package/dist/src/config/config.js +0 -79
  37. package/dist/src/config/context.js +0 -44
  38. package/dist/src/config/history.js +0 -104
  39. package/dist/src/core/client.js +0 -120
  40. package/dist/src/core/download.js +0 -107
  41. package/dist/src/core/errors.js +0 -64
  42. package/dist/src/core/output.js +0 -72
  43. package/dist/src/core/requests.js +0 -169
  44. package/dist/src/core/task-service.js +0 -161
  45. package/dist/src/core/upload.js +0 -36
  46. package/dist/src/core/viewer-assets.js +0 -24
  47. package/dist/src/core/viewer.js +0 -59
  48. package/dist/src/core/watch.js +0 -41
  49. package/dist/src/i18n.js +0 -19
  50. package/dist/src/knowledge/chains.js +0 -152
  51. package/dist/src/knowledge/error-catalog.js +0 -91
  52. package/dist/src/knowledge/models.js +0 -110
  53. package/dist/src/knowledge/params.js +0 -174
  54. package/dist/src/knowledge/scenarios.js +0 -112
  55. package/dist/src/pipeline/make.js +0 -233
  56. package/dist/src/pipeline/runner.js +0 -73
  57. package/dist/src/pipeline/stdin.js +0 -40
  58. package/dist/src/program.js +0 -147
  59. package/dist/src/types.js +0 -2
  60. package/dist/src/util/input-detect.js +0 -92
  61. package/dist/src/util/notify.js +0 -17
  62. package/dist/src/util/open.js +0 -18
  63. package/dist/src/util/spinner.js +0 -62
  64. package/dist/src/util/term-image.js +0 -28
  65. package/dist/src/version.js +0 -10
@@ -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
- }