tripo-cli 0.6.0 → 0.7.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/README.md CHANGED
@@ -35,7 +35,7 @@ Works with both Tripo regions from the same install — international (`openapi.
35
35
  | `tripo login / logout / whoami / use` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json`; `use` switches accounts |
36
36
  | `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
37
37
  | `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
38
- | `tripo model / anim / mesh <step>` | refine, texture, stylize, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
38
+ | `tripo model / anim / mesh <step>` | refine, texture, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
39
39
  | `tripo task get/list/watch` | query and stream task progress (NDJSON in `--json` mode) |
40
40
  | `tripo files upload` | upload a file, print the `file_token` |
41
41
  | `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
@@ -105,12 +105,25 @@ tripo batch run assets.yaml --concurrency 2
105
105
  | `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
106
106
  | `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
107
107
  | `TRIPO_NO_UPDATE_CHECK` | disable the startup "new version available" hint (also off when `CI` is set, headless, or `--json`) |
108
+ | `TRIPO_ANALYTICS_ENABLED` | set to `0` to turn off anonymous usage analytics (see [Telemetry](#telemetry)) |
108
109
  | `TRIPO_NPM_REGISTRY` | npm registry used by `tripo update` and the version check (default `https://registry.npmjs.org`; e.g. a mirror) |
109
110
  | `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | standard proxy variables, honored for all CLI traffic (plain Node fetch ignores them) |
110
111
  | `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
111
112
 
112
113
  Setting `TRIPO_API_KEY` per shell — bash/zsh: `export TRIPO_API_KEY=tsk_...` · PowerShell: `$env:TRIPO_API_KEY = "tsk_..."` (persist for future sessions with `setx TRIPO_API_KEY "tsk_..."`) · CMD: `set TRIPO_API_KEY=tsk_...`
113
114
 
115
+ ## Telemetry
116
+
117
+ Since 0.7.0 the CLI sends anonymous usage events to Tripo (`https://sshu-api.tripo3d.ai`) for calls to the international API (`openapi.tripo3d.ai`). It is on by default, needs no setup, prints nothing, and never changes stdout, exit codes, or credit behavior.
118
+
119
+ - **Sent:** command or MCP tool name and its result, sign-in method and result, error category with HTTP status / API code, task id (and parent task id for chains), task type, model, status, credits consumed, durations, downloaded format and file count, CLI version, and the calling agent with its version — `codex` / `workbuddy` / `zcode` / `kimi` (otherwise `unknown`), from `TRIPO_CLIENT_APP` / `TRIPO_DISTRIBUTION_CHANNEL` (set by agent plugins) or the MCP host's client info.
120
+ - **Identity:** a random installation id (`analytics_distinct_id` in `~/.tripo/config.json`), never your account.
121
+ - **Never sent:** prompts, file paths, input images or models, API keys, download URLs.
122
+ - **Nothing is sent for:** China-region (`cn`) calls, custom API endpoints, `--dry-run`, help/version, `docs`, `completion`, `config`, `update`, `use`, `logout`, and the bare `tripo` home screen.
123
+ - **Opt out:** `TRIPO_ANALYTICS_ENABLED=0`.
124
+
125
+ Delivery is best-effort: each send (including the flush on exit) is capped at 2 seconds, the in-memory queue holds at most 100 events, and nothing is retried or written to disk. Maintainers can redirect or tag events with `TRIPO_ANALYTICS_SERVER_URL`, `TRIPO_ANALYTICS_APP_ID`, and `TRIPO_ANALYTICS_ENVIRONMENT=production|pre|development`; tests send nothing unless `TRIPO_ANALYTICS_ENABLED=1` and a local receiver are both set.
126
+
114
127
  ## Development
115
128
 
116
129
  ```bash
@@ -32,7 +32,7 @@ const SPECIALIST_PROMPTS = {
32
32
  print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via scale_factor/auto_size.`,
33
33
  'ar-web': `You are a realtime AR/Web 3D engineer. Keep faces <= 20000 via decimate, 2K textures, deliver GLB + USDZ (iOS QuickLook).`,
34
34
  anim: `You are a character animation TD. Chain: rig-check → rig (v2.5 rig, spec tripo|mixamo) → retarget (<=5 preset animations, billed per animation). Mixamo/Unity Humanoid → spec=mixamo.`,
35
- toy: `You are a stylized collectible designer. P1 low-poly or stylize steps (lego/voxel/voronoi/minecraft). STL via convert for physical prints.`,
35
+ toy: `You are a stylized collectible designer. P1 low-poly. STL via convert for physical prints.`,
36
36
  };
37
37
  function specialistSystem(scenarioId) {
38
38
  const persona = SPECIALIST_PROMPTS[scenarioId] ?? SPECIALIST_PROMPTS['game-mobile'];
package/dist/cli.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { installProxyFromEnv } from './core/proxy.js';
3
3
  import { runCli } from './program.js';
4
- process.on('SIGINT', () => {
5
- process.stderr.write('\n');
6
- process.exit(130);
7
- });
4
+ import { finishOperation } from './core/analytics.js';
5
+ for (const signal of ['SIGINT', 'SIGTERM']) {
6
+ process.on(signal, () => {
7
+ process.stderr.write('\n');
8
+ void finishOperation('interrupted').finally(() => process.exit(signal === 'SIGINT' ? 130 : 143));
9
+ });
10
+ }
8
11
  await installProxyFromEnv();
9
12
  void runCli(process.argv);
@@ -9,7 +9,7 @@ const COMMANDS = [
9
9
  ];
10
10
  const SUBS = {
11
11
  generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
12
- model: ['refine', 'texture', 'stylize', 'convert', 'import'],
12
+ model: ['refine', 'texture', 'convert', 'import'],
13
13
  anim: ['check', 'rig', 'retarget'],
14
14
  mesh: ['segment', 'complete', 'decimate', 'uv', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
15
15
  task: ['get', 'list', 'watch'],
@@ -11,6 +11,7 @@
11
11
  import * as p from '@clack/prompts';
12
12
  import { activeProfileName, DEFAULT_PROFILE, deleteProfile, describeKeySource, isValidProfileName, listProfiles, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, resolveRegion, setActiveProfile, updateProfile, } from '../config/config.js';
13
13
  import { TripoClient } from '../core/client.js';
14
+ import { trackEvent } from '../core/analytics.js';
14
15
  import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
15
16
  import { AuthError, CliError, NetworkError, UsageError } from '../core/errors.js';
16
17
  import { detectRegionForKey } from '../core/region.js';
@@ -220,8 +221,11 @@ async function runHeadlessLogin(region, profile, paste) {
220
221
  }
221
222
  if (!region) {
222
223
  throw new UsageError(t({
223
- zh: '非交互登录请指定 --region ov(海外站)或 cn(国内站),例如: tripo login --region ov;或改用 TRIPO_API_KEY / tripo login --key tsk_...',
224
- en: 'non-interactive login needs an explicit --region ov (international) or cn (China mainland), e.g. "tripo login --region ov" — or use TRIPO_API_KEY / "tripo login --key tsk_..."',
224
+ zh: `非交互登录需要 --region:先问用户账号在哪个站,不要猜(授权只在该站有效)— cn = ${siteLabel('cn')};ov = ${siteLabel('ov')};还没有账号:中国大陆选 cn,其它地区选 ov`,
225
+ en: `non-interactive login needs --region: ask the human which site their account is on, do not guess (authorization only works on that site) — cn = ${siteLabel('cn')}; ov = ${siteLabel('ov')}; no account yet: mainland China → cn, elsewhere → ov`,
226
+ }), t({
227
+ zh: 'tripo login --region cn|ov;已有 Key 时用 tripo login --key tsk_...(自动识别站点)',
228
+ en: '"tripo login --region cn|ov" — or, with a key in hand: "tripo login --key tsk_..." (site auto-detected)',
225
229
  }));
226
230
  }
227
231
  let grant;
@@ -273,6 +277,12 @@ async function runHeadlessLogin(region, profile, paste) {
273
277
  const saved = await validateAndSave(result.apikey, region, profile);
274
278
  emitResult({ status: 'logged_in', profile, region: saved.region, key: maskKey(result.apikey), balance: saved.balance }, t({ zh: `登录完成(profile=${profile}, region=${saved.region})`, en: `logged in (profile=${profile}, region=${saved.region})` }));
275
279
  }
280
+ /** What a human recognizes a console by — they know their sign-in method and how they pay, rarely the region code. */
281
+ function siteLabel(region) {
282
+ return region === 'cn'
283
+ ? t({ zh: '国内站 developers.tripo3d.com(手机号登录,支付宝)', en: 'China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)' })
284
+ : t({ zh: '海外站 developers.tripo3d.ai(邮箱登录,Stripe)', en: 'international site developers.tripo3d.ai (email sign-in, Stripe)' });
285
+ }
276
286
  /**
277
287
  * Preferred path: RFC 8628 device flow — the browser does the sign-in and a
278
288
  * one-click approve, the key arrives over the CLI↔console-API channel, and
@@ -494,6 +504,7 @@ function finishSave(key, region, balance, profile) {
494
504
  // Saving activates the profile: "the account I just logged into is the
495
505
  // account I am using" — no second command needed for the common case.
496
506
  updateProfile(profile, { api_key: key, region });
507
+ trackEvent('agent_auth_result', { result: 'success', auth_method: 'login' }, region, REGION_DEFAULTS[region].api);
497
508
  const where = profile === DEFAULT_PROFILE ? '~/.tripo/config.json' : `~/.tripo/config.json · profile "${profile}"`;
498
509
  // "mode 0600" is only true where POSIX modes exist; on Windows the file
499
510
  // relies on the user profile's NTFS ACLs, so the claim would be wrong.
@@ -16,6 +16,7 @@ import { parseThenExpression } from '../knowledge/chains.js';
16
16
  import { scenarioIds } from '../knowledge/scenarios.js';
17
17
  import { executeMake, planMake } from '../pipeline/make.js';
18
18
  import { cliVersion } from '../version.js';
19
+ import { beginOperation, finishOperation } from '../core/analytics.js';
19
20
  const TOOLS = [
20
21
  {
21
22
  name: 'tripo_make',
@@ -77,6 +78,7 @@ export function registerMcpCommand(program) {
77
78
  });
78
79
  }
79
80
  async function serveMcp() {
81
+ let clientInfo = {};
80
82
  const rl = createInterface({ input: process.stdin, terminal: false });
81
83
  const write = (message) => {
82
84
  process.stdout.write(JSON.stringify(message) + '\n');
@@ -96,11 +98,22 @@ async function serveMcp() {
96
98
  continue; // notification — nothing to answer
97
99
  }
98
100
  try {
101
+ if (request.method === 'initialize' && request.params?.clientInfo && typeof request.params.clientInfo === 'object') {
102
+ clientInfo = request.params.clientInfo;
103
+ }
104
+ if (request.method === 'tools/call')
105
+ beginOperation(String(request.params?.name ?? 'unknown'), false, clientInfo);
99
106
  const result = await dispatch(request.method, request.params ?? {});
100
107
  if (result !== SKIP)
101
108
  write({ jsonrpc: '2.0', id: request.id, result });
109
+ if (request.method === 'tools/call') {
110
+ const failed = typeof result === 'object' && result !== null && 'isError' in result && result.isError === true;
111
+ await finishOperation(failed ? 'failed' : 'success');
112
+ }
102
113
  }
103
114
  catch (err) {
115
+ if (request.method === 'tools/call')
116
+ await finishOperation('failed', err);
104
117
  write({
105
118
  jsonrpc: '2.0',
106
119
  id: request.id,
@@ -179,7 +192,7 @@ async function callTool(params) {
179
192
  }
180
193
  case 'tripo_history': {
181
194
  const limit = typeof args.limit === 'number' ? args.limit : 10;
182
- return asText(readHistory().slice(-limit));
195
+ return asText(readHistory().slice(-limit).map(({ analytics_origin: _origin, analytics_finished: _finished, ...entry }) => entry));
183
196
  }
184
197
  default:
185
198
  return { content: [{ type: 'text', text: `unknown tool: ${params.name}` }], isError: true };
@@ -70,8 +70,6 @@ function collectStepArgs(step, flags) {
70
70
  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']);
71
71
  case 'texture':
72
72
  return pick(['model', 'texture_quality', 'texture_alignment', 'pbr']);
73
- case 'stylize':
74
- return pick(['style', 'block_size']);
75
73
  case 'rig':
76
74
  return pick(['model', 'rig_type', 'spec', 'out_format']);
77
75
  case 'retarget':
@@ -94,18 +92,13 @@ function collectStepArgs(step, flags) {
94
92
  }
95
93
  export function registerProcessCommands(program) {
96
94
  // ---- tripo model ----
97
- const model = program.command('model').description('model processing: refine / texture / stylize / convert / import');
95
+ const model = program.command('model').description('model processing: refine / texture / convert / import');
98
96
  withTaskOptions(model.command('refine [input]').description('refine a draft text-to-model task (task id / @last)')).action(makeStepAction('refine'));
99
97
  withTaskOptions(model
100
98
  .command('texture [input]')
101
99
  .description('re-texture a model (task/@last/file/url)')
102
100
  .option('--texture-quality <q>', 'standard|detailed|extreme')
103
101
  .option('--texture-alignment <a>', 'original_image|geometry')).action(makeStepAction('texture'));
104
- withTaskOptions(model
105
- .command('stylize [input]')
106
- .description('stylize a model (lego/voxel/voronoi/minecraft)')
107
- .option('--style <style>', 'lego|voxel|voronoi|minecraft')
108
- .option('--block-size <n>', 'minecraft block size 32-128', (v) => Number(v))).action(makeStepAction('stylize'));
109
102
  withTaskOptions(model
110
103
  .command('convert [input]')
111
104
  .description('convert format (GLTF/USDZ/FBX/OBJ/STL/3MF)')
@@ -119,7 +119,7 @@ export function registerTaskCommands(program) {
119
119
  .description('recent tasks created by this CLI (@name references)')
120
120
  .option('--limit <n>', 'rows', (v) => Number(v), 20)
121
121
  .action((opts) => {
122
- const entries = readHistory().slice(-opts.limit);
122
+ const entries = readHistory().slice(-opts.limit).map(({ analytics_origin: _origin, analytics_finished: _finished, ...entry }) => entry);
123
123
  const human = entries
124
124
  .map((e) => `${e.created_at.slice(0, 19)} ${(e.name ?? '').padEnd(24)} ${e.type.padEnd(22)} ${e.status.padEnd(9)} ${e.task_id}`)
125
125
  .join('\n');
@@ -0,0 +1,251 @@
1
+ /** Anonymous Agent analytics. Collection must never change a business result. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { loadConfig, updateConfig, resolveApiBaseUrl, resolveRegion } from '../config/config.js';
5
+ import { readHistory, updateHistoryStatus } from '../config/history.js';
6
+ import { cliVersion } from '../version.js';
7
+ import { ApiError, AuthError, NetworkError, TaskFailedError, UsageError } from './errors.js';
8
+ export const ANALYTICS_SERVER_URL = "https://sshu-api.tripo3d.ai";
9
+ export const ANALYTICS_APP_ID = "2908364badd34996a1cd44a5f7a71223";
10
+ const operations = new AsyncLocalStorage();
11
+ let activeOperation;
12
+ let installationId;
13
+ const currentOperation = () => operations.getStore() ?? activeOperation;
14
+ export function beginOperation(command, disabled = false, info, regionHint) {
15
+ activeOperation = { command, started: Date.now(), disabled, finished: false, collectors: new Map(), regionHint, context: {
16
+ ...attribution(process.env.TRIPO_CLIENT_APP, process.env.TRIPO_DISTRIBUTION_CHANNEL, info),
17
+ distinctId: randomUUID(), operation_id: randomUUID(), integration_mode: info ? "local_mcp" : "cli",
18
+ } };
19
+ operations.enterWith(activeOperation);
20
+ }
21
+ export function analyticsContext() { return currentOperation()?.context; }
22
+ function collector(region = currentOperation()?.regionHint ?? resolveRegion(), apiUrl = resolveApiBaseUrl()) {
23
+ const operation = currentOperation();
24
+ if (!operation || operation.disabled)
25
+ return undefined;
26
+ const key = `${region}:${apiUrl}`;
27
+ let analytics = operation.collectors.get(key);
28
+ if (!analytics) {
29
+ analytics = new Analytics("tripo-cli", cliVersion(), region, apiUrl);
30
+ operation.collectors.set(key, analytics);
31
+ }
32
+ if (analytics.enabled && !installationId) {
33
+ try {
34
+ const config = loadConfig();
35
+ installationId = typeof config.analytics_distinct_id === "string" && /^[a-f0-9-]{36}$/.test(config.analytics_distinct_id)
36
+ ? config.analytics_distinct_id : randomUUID();
37
+ if (config.analytics_distinct_id !== installationId)
38
+ updateConfig({ analytics_distinct_id: installationId });
39
+ }
40
+ catch {
41
+ installationId = randomUUID();
42
+ }
43
+ }
44
+ if (installationId)
45
+ operation.context.distinctId = installationId;
46
+ return analytics;
47
+ }
48
+ export function errorProperties(error) {
49
+ const category = error instanceof TaskFailedError ? error.status === 'timeout' ? 'waiting_timeout' : 'task_failed'
50
+ : error instanceof ApiError ? error.apiCode === 2010 ? 'balance' : 'upstream'
51
+ : error instanceof AuthError ? 'authorization' : error instanceof NetworkError ? 'network'
52
+ : error instanceof UsageError ? 'validation' : 'internal';
53
+ return { error_category: category, ...(error instanceof ApiError ? { http_status: error.httpStatus, api_code: error.apiCode } : {}) };
54
+ }
55
+ export function trackEvent(event, properties = {}, region, apiUrl) {
56
+ const operation = currentOperation();
57
+ // At most one authorization outcome per operation; later reports are dropped.
58
+ if (operation && event === 'agent_auth_started')
59
+ operation.authMethod = properties.auth_method;
60
+ if (operation && event === 'agent_auth_result') {
61
+ if (operation.authReported)
62
+ return;
63
+ operation.authReported = true;
64
+ }
65
+ const analytics = collector(region, apiUrl);
66
+ const context = analyticsContext();
67
+ if (analytics && context)
68
+ analytics.track(event, context, properties);
69
+ }
70
+ export async function flushAnalytics() {
71
+ const operation = currentOperation();
72
+ if (operation)
73
+ await Promise.all([...operation.collectors.values()].map((analytics) => analytics.flush()));
74
+ }
75
+ export async function finishOperation(result, error) {
76
+ const operation = currentOperation();
77
+ if (!operation || operation.finished)
78
+ return;
79
+ operation.finished = true;
80
+ if (operation.command === 'login' && result !== 'success') {
81
+ trackEvent('agent_auth_result', { result, auth_method: operation.authMethod ?? 'login', ...(error ? errorProperties(error) : {}) });
82
+ }
83
+ trackEvent('agent_tool_result', { command: operation.command,
84
+ ...(operation.context.integration_mode === 'local_mcp' ? { tool_name: operation.command } : {}),
85
+ result, duration_ms: Date.now() - operation.started, ...(error ? errorProperties(error) : {}) });
86
+ await flushAnalytics();
87
+ }
88
+ export function taskOrigin(model) {
89
+ const context = analyticsContext();
90
+ return context ? { creation_client_app: context.client_app, creation_distribution_channel: context.distribution_channel,
91
+ creation_integration_mode: context.integration_mode, ...(typeof model === 'string' ? { model } : {}) } : undefined;
92
+ }
93
+ const observed = new Set();
94
+ export function observeTask(task, region, apiUrl) {
95
+ if (!collector(region, apiUrl)?.enabled || !['success', 'failed', 'cancelled', 'banned', 'expired'].includes(task.status))
96
+ return;
97
+ const key = `${region}:${task.task_id}`;
98
+ const history = readHistory().find((entry) => entry.task_id === task.task_id);
99
+ if (observed.has(key) || history?.analytics_finished)
100
+ return;
101
+ observed.add(key);
102
+ // Bound this cache for long-lived stdio hosts; history preserves known task markers.
103
+ if (observed.size > 1000)
104
+ observed.delete(observed.values().next().value);
105
+ trackEvent('agent_task_finished', { ...history?.analytics_origin,
106
+ creation_client_app: history?.analytics_origin?.creation_client_app ?? 'unknown',
107
+ creation_distribution_channel: history?.analytics_origin?.creation_distribution_channel ?? 'unknown',
108
+ origin_known: Boolean(history?.analytics_origin), task_id: task.task_id, task_type: task.type,
109
+ status: task.status, credits_consumed: task.credits_consumed,
110
+ generation_duration_ms: task.created_at && task.completed_at ? Math.max(0, Date.parse(task.completed_at) - Date.parse(task.created_at)) : undefined,
111
+ }, region, apiUrl);
112
+ if (history) {
113
+ try {
114
+ updateHistoryStatus(task.task_id, task.status, { analytics_finished: true });
115
+ }
116
+ catch { /* best effort marker */ }
117
+ }
118
+ }
119
+ export function clientApp(value) {
120
+ if (typeof value !== "string")
121
+ return "unknown";
122
+ const name = value.trim().toLowerCase();
123
+ for (const app of ["codex", "workbuddy", "zcode", "kimi"]) {
124
+ if (new RegExp(`(^|[\\s_./-])${app}($|[\\s_./-])`).test(name))
125
+ return app;
126
+ }
127
+ return "unknown";
128
+ }
129
+ export function attribution(explicit, distribution, info, oauthName) {
130
+ const tagged = clientApp(explicit);
131
+ const initialized = clientApp(info?.name);
132
+ const oauth = clientApp(oauthName);
133
+ const chosen = tagged !== "unknown" ? tagged : initialized !== "unknown" ? initialized : oauth;
134
+ const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin"];
135
+ return {
136
+ client_app: chosen,
137
+ distribution_channel: typeof distribution === "string" && channels.includes(distribution) ? distribution : "unknown",
138
+ attribution_method: tagged !== "unknown" ? "explicit_tag" : initialized !== "unknown" ? "mcp_client_info" : oauth !== "unknown" ? "oauth_client_name" : "unknown",
139
+ attribution_conflict: [tagged, initialized, oauth].some((app) => app !== "unknown" && chosen !== app),
140
+ ...(typeof info?.version === "string" && /^[a-zA-Z0-9._+-]{1,64}$/.test(info.version) && !/tsk_|bearer|token/i.test(info.version) ? { client_version: info.version } : {}),
141
+ };
142
+ }
143
+ const FIELDS = new Set([
144
+ "tool_name", "command", "result", "status", "task_id", "parent_task_id", "model", "task_type",
145
+ "duration_ms", "generation_duration_ms", "credits_consumed", "error_category", "http_status", "api_code",
146
+ "auth_flow_id", "auth_method", "observation_source", "creation_client_app", "creation_distribution_channel",
147
+ "creation_integration_mode", "origin_known", "model_downloaded", "file_count", "format", "interrupted",
148
+ ]);
149
+ export class Analytics {
150
+ app;
151
+ version;
152
+ enabled;
153
+ url;
154
+ appId;
155
+ environment;
156
+ queue = [];
157
+ pending;
158
+ scheduled = false;
159
+ stats = { sent: 0, failed: 0, dropped: 0 };
160
+ constructor(app, version, region, apiUrl, env = process.env) {
161
+ this.app = app;
162
+ this.version = version;
163
+ const receiver = env.TRIPO_ANALYTICS_SERVER_URL ?? ANALYTICS_SERVER_URL;
164
+ this.appId = env.TRIPO_ANALYTICS_APP_ID ?? ANALYTICS_APP_ID;
165
+ const environment = env.TRIPO_ANALYTICS_ENVIRONMENT ?? (env.NODE_ENV === "production" ? "production" : app === "tripo-cli" ? "production" : "development");
166
+ this.environment = ["production", "pre", "development"].includes(environment) ? environment : "development";
167
+ this.url = "";
168
+ let officialApi = false;
169
+ let testReceiver = false;
170
+ try {
171
+ const url = new URL(receiver);
172
+ this.url = new URL("sync_json", url.href.endsWith("/") ? url : `${url.href}/`).href;
173
+ officialApi = new URL(apiUrl).hostname === "openapi.tripo3d.ai";
174
+ testReceiver = (url.protocol === "http:" || url.protocol === "https:") && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
175
+ if (url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol))
176
+ this.url = "";
177
+ }
178
+ catch { /* invalid analytics configuration is disabled, never fatal */ }
179
+ const testing = env.NODE_ENV === "test" || Boolean(env.VITEST);
180
+ this.enabled = region === "ov" && Boolean(this.url && this.appId) && env.TRIPO_ANALYTICS_ENABLED !== "0"
181
+ && (testing ? env.TRIPO_ANALYTICS_ENABLED === "1" && testReceiver : officialApi || testReceiver);
182
+ }
183
+ track(event, context, properties = {}) {
184
+ if (!this.enabled || !/^agent_[a-z_]+$/.test(event))
185
+ return;
186
+ if (this.queue.length >= 100) {
187
+ this.stats.dropped++;
188
+ return;
189
+ }
190
+ const safe = {};
191
+ for (const [key, value] of Object.entries(properties)) {
192
+ if (!FIELDS.has(key))
193
+ continue;
194
+ if (typeof value === "number" && Number.isFinite(value) || typeof value === "boolean")
195
+ safe[key] = value;
196
+ else if (typeof value === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value) && !/tsk_|bearer|token/i.test(value))
197
+ safe[key] = value;
198
+ }
199
+ this.queue.push({ appid: this.appId, data: {
200
+ "#type": "track", "#event_name": event, "#distinct_id": context.distinctId,
201
+ "#time": new Date().toISOString().replace("T", " ").replace("Z", ""),
202
+ properties: {
203
+ ...safe, ...attribution(context.client_app, context.distribution_channel, { version: context.client_version }),
204
+ attribution_method: context.attribution_method, attribution_conflict: context.attribution_conflict ?? false,
205
+ app: this.app, event_source: this.app === "tripo-mcp" ? "server" : "cli", site_region: "global",
206
+ environment: this.environment, schema_version: 2, agent_schema_version: 1, app_version: this.version,
207
+ integration_mode: context.integration_mode, operation_id: context.operation_id, event_id: randomUUID(),
208
+ identity_type: this.app === "tripo-mcp" ? "authorization" : "installation", observation_source: "client", "#zone_offset": 0,
209
+ },
210
+ } });
211
+ if (!this.scheduled) {
212
+ this.scheduled = true;
213
+ queueMicrotask(() => { this.scheduled = false; void this.flush(); });
214
+ }
215
+ }
216
+ async flush() {
217
+ if (this.pending)
218
+ return this.pending;
219
+ if (!this.queue.length)
220
+ return;
221
+ // ponytail: bounded best-effort delivery; no persistent spool or retries in v1.
222
+ const send = async () => {
223
+ const deadline = Date.now() + 2000;
224
+ while (this.queue.length && Date.now() < deadline) {
225
+ const batch = this.queue.splice(0, 100);
226
+ try {
227
+ const response = await fetch(this.url, {
228
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch),
229
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
230
+ });
231
+ const body = await response.json();
232
+ if (!response.ok || body.code !== 0)
233
+ throw new Error("analytics rejected");
234
+ this.stats.sent += batch.length;
235
+ }
236
+ catch {
237
+ this.stats.failed += batch.length;
238
+ }
239
+ }
240
+ this.stats.dropped += this.queue.length;
241
+ this.queue = [];
242
+ };
243
+ this.pending = send();
244
+ try {
245
+ await this.pending;
246
+ }
247
+ finally {
248
+ this.pending = undefined;
249
+ }
250
+ }
251
+ }
@@ -8,7 +8,10 @@ import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveAp
8
8
  import { t } from '../i18n.js';
9
9
  import { EXIT } from '../knowledge/error-catalog.js';
10
10
  import { ApiError, AuthError, NetworkError } from './errors.js';
11
+ import { observeTask, trackEvent, taskOrigin } from './analytics.js';
12
+ import { resolveRegion } from '../config/config.js';
11
13
  export class TripoClient {
14
+ region = resolveRegion();
12
15
  baseUrl;
13
16
  apiKey;
14
17
  timeoutMs;
@@ -117,9 +120,11 @@ export class TripoClient {
117
120
  return envelope.data;
118
121
  }
119
122
  // ---- typed convenience wrappers ----
120
- getTask(taskId, step = false) {
123
+ async getTask(taskId, step = false) {
121
124
  const suffix = step ? '?step=1' : '';
122
- return this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
125
+ const task = await this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
126
+ observeTask(task, this.region, this.baseUrl);
127
+ return task;
123
128
  }
124
129
  /**
125
130
  * Batch task query. The server returns `{tasks: {id: detail}, missed: [id]}`
@@ -128,6 +133,8 @@ export class TripoClient {
128
133
  async listTasks(taskIds) {
129
134
  const data = await this.request('POST', '/v3/tasks/list', { task_ids: taskIds });
130
135
  const byId = data.tasks ?? {};
136
+ for (const [id, task] of Object.entries(byId))
137
+ observeTask({ ...task, task_id: task.task_id ?? id }, this.region, this.baseUrl);
131
138
  const tasks = taskIds
132
139
  .filter((id) => byId[id])
133
140
  .map((id) => ({ ...byId[id], task_id: byId[id].task_id ?? id }));
@@ -140,9 +147,11 @@ export class TripoClient {
140
147
  return this.request('GET', '/v3/account/usage');
141
148
  }
142
149
  /** Create a task; returns its id. Response may be {task_id} or a full detail. */
143
- async createTask(endpoint, payload) {
150
+ async createTask(endpoint, payload, taskType) {
144
151
  try {
145
152
  const data = await this.request('POST', endpoint, payload, { idempotent: false });
153
+ trackEvent('agent_task_created', { ...taskOrigin(payload.model), task_id: data.task_id, task_type: taskType,
154
+ ...([payload.original_model_task_id, payload.task_id, payload.input].some((value) => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value)) ? { parent_task_id: [payload.original_model_task_id, payload.task_id, payload.input].find((value) => typeof value === 'string' && /^[a-f0-9-]{36}$/.test(value)) } : {}) }, this.region, this.baseUrl);
146
155
  return data.task_id;
147
156
  }
148
157
  catch (err) {
@@ -13,6 +13,7 @@ import { hostname } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import { REGION_DEFAULTS, tripoHome } from '../config/config.js';
15
15
  import { sleep } from './client.js';
16
+ import { trackEvent } from './analytics.js';
16
17
  export const DEVICE_CODE_EXPIRED = 4290;
17
18
  export const DEVICE_CODE_SLOW_DOWN = 4291;
18
19
  export const DEVICE_CODE_DENIED = 4292;
@@ -79,6 +80,7 @@ export function deviceKeyName(profile, host = hostname(), id = deviceId()) {
79
80
  return `${prefix} - ${shownHost} ${id}`.replace(/\s+/g, ' ').slice(0, 32).trim();
80
81
  }
81
82
  export async function requestDeviceCode(region, profile) {
83
+ trackEvent('agent_auth_started', { auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
82
84
  const body = await post(`${consoleApiBaseUrl(region)}/cli/device/code`, { name: deviceKeyName(profile) });
83
85
  const grant = body.data;
84
86
  if (!grant ||
@@ -138,9 +140,12 @@ export async function waitForDeviceApproval(region, grant, onTick) {
138
140
  intervalMs += 5000;
139
141
  continue;
140
142
  }
141
- if (poll.status !== 'pending')
143
+ if (poll.status !== 'pending') {
144
+ trackEvent('agent_auth_result', { result: poll.status === 'approved' ? 'success' : poll.status, auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
142
145
  return poll;
146
+ }
143
147
  }
148
+ trackEvent('agent_auth_result', { result: 'expired', auth_method: 'device' }, region, REGION_DEFAULTS[region].api);
144
149
  return { status: 'expired' };
145
150
  }
146
151
  /**
@@ -10,6 +10,7 @@ import { basename, extname, join } from 'node:path';
10
10
  import { Readable } from 'node:stream';
11
11
  import { pipeline } from 'node:stream/promises';
12
12
  import { NetworkError } from './errors.js';
13
+ import { trackEvent, errorProperties } from './analytics.js';
13
14
  const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply']);
14
15
  const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
15
16
  /** Collect every downloadable URL in the output map (string or {url} object). */
@@ -59,48 +60,57 @@ async function downloadOne(url, dest) {
59
60
  }
60
61
  /** Download all artifacts of a finished task + write task.json. */
61
62
  export async function downloadArtifacts(task, outputDir, historyEntry, options = {}) {
62
- await mkdir(outputDir, { recursive: true });
63
- const urls = collectOutputUrls(task.output);
64
- const files = [];
65
- let modelFile;
66
- let preview;
67
- for (const { field, url } of urls) {
68
- const name = fileNameFor(field, url);
69
- const dest = join(outputDir, name);
70
- options.onFile?.(name);
71
- await downloadOne(url, dest);
72
- files.push(dest);
73
- const ext = extname(name).toLowerCase();
74
- if (!modelFile && MODEL_EXTS.has(ext))
75
- modelFile = dest;
76
- // uv_render (mesh/uv) is a flat UV atlas, not a render of the model.
77
- if (!preview && !/uv_render/.test(field) && (IMAGE_EXTS.has(ext) || /rendered|preview/i.test(field)))
78
- preview = dest;
79
- }
80
- // Normalize the preview name so agents can rely on preview.png existing.
81
- if (preview && basename(preview) !== 'preview.png') {
82
- const { copyFile } = await import('node:fs/promises');
83
- const canonical = join(outputDir, 'preview.png');
84
- try {
85
- await copyFile(preview, canonical);
86
- files.push(canonical);
87
- preview = canonical;
63
+ const started = Date.now();
64
+ try {
65
+ await mkdir(outputDir, { recursive: true });
66
+ const urls = collectOutputUrls(task.output);
67
+ const files = [];
68
+ let modelFile;
69
+ let preview;
70
+ for (const { field, url } of urls) {
71
+ const name = fileNameFor(field, url);
72
+ const dest = join(outputDir, name);
73
+ options.onFile?.(name);
74
+ await downloadOne(url, dest);
75
+ files.push(dest);
76
+ const ext = extname(name).toLowerCase();
77
+ if (!modelFile && MODEL_EXTS.has(ext))
78
+ modelFile = dest;
79
+ // uv_render (mesh/uv) is a flat UV atlas, not a render of the model.
80
+ if (!preview && !/uv_render/.test(field) && (IMAGE_EXTS.has(ext) || /rendered|preview/i.test(field)))
81
+ preview = dest;
82
+ }
83
+ // Normalize the preview name so agents can rely on preview.png existing.
84
+ if (preview && basename(preview) !== 'preview.png') {
85
+ const { copyFile } = await import('node:fs/promises');
86
+ const canonical = join(outputDir, 'preview.png');
87
+ try {
88
+ await copyFile(preview, canonical);
89
+ files.push(canonical);
90
+ preview = canonical;
91
+ }
92
+ catch { }
88
93
  }
89
- catch { }
94
+ const record = {
95
+ task_id: task.task_id,
96
+ type: task.type,
97
+ status: task.status,
98
+ input: task.input ?? historyEntry?.payload ?? {},
99
+ output_fields: urls.map((u) => u.field),
100
+ credits_consumed: task.credits_consumed,
101
+ created_at: task.created_at,
102
+ completed_at: task.completed_at,
103
+ files: files.map((f) => basename(f)),
104
+ };
105
+ await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
106
+ trackEvent('agent_download_result', { task_id: task.task_id, result: 'success', model_downloaded: Boolean(modelFile),
107
+ file_count: files.length, duration_ms: Date.now() - started, format: modelFile ? extname(modelFile).slice(1) : undefined });
108
+ return { outputDir, files, modelFile, preview };
109
+ }
110
+ catch (error) {
111
+ trackEvent('agent_download_result', { task_id: task.task_id, result: 'failed', duration_ms: Date.now() - started, ...errorProperties(error) });
112
+ throw error;
90
113
  }
91
- const record = {
92
- task_id: task.task_id,
93
- type: task.type,
94
- status: task.status,
95
- input: task.input ?? historyEntry?.payload ?? {},
96
- output_fields: urls.map((u) => u.field),
97
- credits_consumed: task.credits_consumed,
98
- created_at: task.created_at,
99
- completed_at: task.completed_at,
100
- files: files.map((f) => basename(f)),
101
- };
102
- await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
103
- return { outputDir, files, modelFile, preview };
104
114
  }
105
115
  /** Default output directory: ./tripo-out/<slug>-<id8>/ */
106
116
  export function defaultOutputDir(baseDir, slug, taskId) {
@@ -165,12 +165,6 @@ export function buildStepRequest(stepName, input, args) {
165
165
  }
166
166
  break;
167
167
  }
168
- case 'stylize': {
169
- if (typeof params.style !== 'string') {
170
- throw new UsageError('stylize requires style (lego/voxel/voronoi/minecraft)');
171
- }
172
- break;
173
- }
174
168
  case 'smartsegment': {
175
169
  if (params.seg_type === undefined)
176
170
  params.seg_type = 'model';
@@ -13,6 +13,7 @@ import { detectInput } from '../util/input-detect.js';
13
13
  import { notifyDone } from '../util/notify.js';
14
14
  import { createSpinner } from '../util/spinner.js';
15
15
  import { defaultOutputDir, downloadArtifacts } from './download.js';
16
+ import { taskOrigin } from './analytics.js';
16
17
  import { ApiError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
17
18
  import { isQuiet, log, warn } from './output.js';
18
19
  import { uploadLocalFile } from './upload.js';
@@ -85,9 +86,10 @@ export async function runTask(options) {
85
86
  const { client, request, taskType } = options;
86
87
  if (options.precheckBalance !== false)
87
88
  await precheckBalance(client);
88
- const taskId = await client.createTask(request.endpoint, request.payload);
89
+ const taskId = await client.createTask(request.endpoint, request.payload, taskType);
89
90
  const name = uniqueName(slugify(options.name));
90
91
  const entry = {
92
+ analytics_origin: taskOrigin(request.payload.model),
91
93
  task_id: taskId,
92
94
  type: taskType,
93
95
  name,
@@ -24,7 +24,6 @@ export const MODEL_PRODUCING_TYPES = new Set([
24
24
  export const STEPS = [
25
25
  { name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false, describe: { zh: '精修文生模型的几何细节', en: 'refine geometry of a text-to-model result' } },
26
26
  { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '重新生成贴图 / PBR 材质', en: 'regenerate textures / PBR materials' } },
27
- { name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '按风格重塑模型(lego/voxel/...)', en: 'restyle the model (lego/voxel/...)' } },
28
27
  { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: { zh: '转换输出格式 / 贴图尺寸 / 朝向', en: 'convert format / texture size / orientation' } },
29
28
  { name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: { zh: '导入本地模型文件作为加工起点', en: 'import a local model file as the chain source' } },
30
29
  { name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe: { zh: '检查是否可绑骨(不产出模型,不可绑则止损)', en: 'check riggability (no model output; stops the chain if not riggable)' } },
@@ -71,7 +70,7 @@ export function getStep(name) {
71
70
  * Parse a --then expression: "texture,rig,convert:fbx" or
72
71
  * "convert:format=fbx,texture_size=2048". A bare value after ":" is sugar for
73
72
  * the step's primary argument (convert→format, retarget→animation,
74
- * stylize→style, decimate→face_limit).
73
+ * decimate→face_limit).
75
74
  */
76
75
  export function parseThenExpression(expr) {
77
76
  const out = [];
@@ -116,7 +115,6 @@ function applyPrimaryArg(step, args, value) {
116
115
  const primary = {
117
116
  convert: 'format',
118
117
  retarget: 'animation',
119
- stylize: 'style',
120
118
  decimate: 'face_limit',
121
119
  texture: 'texture_quality',
122
120
  rig: 'rig_type',
@@ -139,7 +139,6 @@ export const TEXTURE_FORMATS = [
139
139
  ];
140
140
  export const FBX_PRESETS = ['blender', '3dsmax', 'mixamo'];
141
141
  export const EXPORT_ORIENTATIONS = ['+x', '-x', '+y', '-y'];
142
- export const STYLIZE_STYLES = ['lego', 'voxel', 'voronoi', 'minecraft'];
143
142
  export const TEXTURE_QUALITIES = ['standard', 'detailed', 'extreme'];
144
143
  /** Normalize any accepted alias to the canonical wire value; unknown values pass through. */
145
144
  export function normalizeModel(input) {
@@ -92,14 +92,14 @@ export const SCENARIOS = [
92
92
  {
93
93
  id: 'toy',
94
94
  label: { zh: '风格化摆件', en: 'Stylized toy / desk piece' },
95
- description: { zh: '低模可爱风或积木/体素风格化', en: 'Cute low-poly or brick/voxel stylization' },
95
+ description: { zh: '低模可爱风摆件', en: 'Cute low-poly desk piece' },
96
96
  model: MODEL_P1,
97
97
  genParams: { texture: true, pbr: true, face_limit: 20000 },
98
98
  chain: [],
99
99
  deliverables: ['GLB', 'STL via convert'],
100
100
  notes: {
101
- zh: ['P1 出干净低模', '积木/体素风追加 --then stylize:lego 或 stylize:voxel', '要打印实物追加 --then convert:stl'],
102
- en: ['P1 produces clean low-poly', 'append --then stylize:lego or stylize:voxel for brick/voxel looks', 'append --then convert:stl to print it'],
101
+ zh: ['P1 出干净低模', '要打印实物追加 --then convert:stl'],
102
+ en: ['P1 produces clean low-poly', 'append --then convert:stl to print it'],
103
103
  },
104
104
  },
105
105
  ];
package/dist/program.js CHANGED
@@ -25,6 +25,7 @@ import { configureOutput, errorLine, isJsonMode } from './core/output.js';
25
25
  import { setLocale, t } from './i18n.js';
26
26
  import { EXIT } from './knowledge/error-catalog.js';
27
27
  import { cliVersion } from './version.js';
28
+ import { beginOperation, finishOperation } from './core/analytics.js';
28
29
  export function buildProgram() {
29
30
  const program = new Command();
30
31
  program
@@ -56,11 +57,18 @@ export function buildProgram() {
56
57
  const lang = loadConfig().language;
57
58
  if (lang)
58
59
  setLocale(lang);
60
+ const path = [];
61
+ for (let command = actionCommand; command && command !== program; command = command.parent)
62
+ path.unshift(command.name());
63
+ const excluded = ['mcp', 'docs', 'completion', 'config', 'update', 'use', 'logout', 'home'];
64
+ if (actionCommand.name() !== 'mcp')
65
+ beginOperation(path.join(':'), path.length === 0 || excluded.includes(path[0]) || actionCommand.opts().dryRun === true, undefined, actionCommand.opts().region === 'cn' || actionCommand.opts().region === 'ov' ? actionCommand.opts().region : undefined);
59
66
  // Startup version hint (cached, non-blocking). `update` reports on its
60
67
  // own; `mcp` speaks JSON-RPC over stdio and must stay silent.
61
68
  if (actionCommand.name() !== 'update' && actionCommand.name() !== 'mcp')
62
69
  notifyIfOutdated();
63
70
  });
71
+ program.hook('postAction', async () => { await finishOperation('success'); });
64
72
  registerMakeCommand(program);
65
73
  registerAiCommand(program);
66
74
  registerViewCommands(program);
@@ -134,6 +142,7 @@ export async function runCli(argv) {
134
142
  await program.parseAsync(argv);
135
143
  }
136
144
  catch (err) {
145
+ await finishOperation('failed', err);
137
146
  handleError(err);
138
147
  }
139
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Tripo 3D intelligent CLI — turn a prompt or an image into engine-ready 3D assets from your terminal (Tripo V3 API)",
5
5
  "keywords": [
6
6
  "tripo",
package/skill/SKILL.md CHANGED
@@ -9,13 +9,20 @@ One command turns a text prompt or an image into local files: a 3D model (glb/fb
9
9
  npm install -g tripo-cli # or: npx tripo-cli ...
10
10
  export TRIPO_API_KEY=tsk_... # highest-priority auth; no interaction needed
11
11
  # (PowerShell: $env:TRIPO_API_KEY = "tsk_...")
12
+ # China-site key? also export TRIPO_REGION=cn (env keys default to ov)
12
13
  tripo doctor # verify key, network, balance
13
14
  ```
14
15
 
15
- If there is no key, run the device login **yourself** — it works without a TTY:
16
+ If there is no key, run the device login **yourself** — it works without a TTY.
17
+ First ask the human which Tripo site their account is on, and do not guess: the
18
+ login only works on the site it was started for.
19
+
20
+ - `cn` — China mainland site developers.tripo3d.com (phone/SMS sign-in, Alipay)
21
+ - `ov` — international site developers.tripo3d.ai (email sign-in, Stripe)
22
+ - no account yet: mainland China → `cn`, everywhere else → `ov`
16
23
 
17
24
  ```bash
18
- tripo login --region ov # ov = international, cn = China mainland
25
+ tripo login --region <cn|ov> # the site the human named
19
26
  ```
20
27
 
21
28
  It prints a verification URL and a one-time code, then blocks until the human
@@ -24,9 +31,10 @@ and code to the human and tell them: sign in on that page first if it asks
24
31
  (sign-up gives free credits), enter the code, pick an API key if the account has
25
32
  several (one is created automatically if there are none), click Authorize. The
26
33
  key is then saved locally; never ask the human to paste a key into the chat. If
27
- the code expires (exit 3), just run the same command again.
34
+ the code expires (exit 3), just run the same command again; if the human says
35
+ the page is the wrong site, stop it and rerun with the other `--region`.
28
36
 
29
- Regions (ov = international, cn = China mainland) share one CLI: `tripo login --key tsk_...` auto-detects which region the key belongs to. On exit code 3 (auth), run `tripo doctor` — it diagnoses key-vs-region mismatches and prints the exact fix (`tripo config set region cn|ov`).
37
+ Regions (ov = international, cn = China mainland) share one CLI: `tripo login --key tsk_...` auto-detects which region the key belongs to. Already logged into the wrong site? `tripo login --region <other>` replaces the active profile's key and region. On exit code 3 (auth), run `tripo doctor` — it diagnoses key-vs-region mismatches and prints the exact fix (`tripo config set region cn|ov`).
30
38
 
31
39
  ## Behavior rules (read first)
32
40
 
@@ -50,8 +58,8 @@ tripo make @last --then convert:fbx --json --yes # continue from last t
50
58
  ```
51
59
 
52
60
  - `--for` scenario presets: `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`
53
- - `--then` chain steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate uv smartsegment`
54
- - step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `stylize:lego`, `decimate:5000`)
61
+ - `--then` chain steps: `refine texture convert import rig-check rig retarget segment complete decimate uv smartsegment`
62
+ - step args: `convert:format=FBX,texture_size=2048` (bare value = primary arg: `convert:fbx`, `decimate:5000`)
55
63
  - Output JSON: `{"task_id","type","status","credits_consumed","output_dir","files",["model_file"],["preview"],["chain"],["credits_breakdown"]}` — with `--then`, `credits_consumed` is the **total** for the run and `credits_breakdown` itemizes every task (`[{task_id,type,credits_consumed}]`, generation first)
56
64
 
57
65
  ## Pipes (composing steps yourself)
@@ -40,7 +40,7 @@ In a TTY it then asks `Run this plan?`; answering no exits 0 with `cancelled; no
40
40
  ## Options
41
41
 
42
42
  - `--for <scenario>` — `game-mobile` `game-pc` `film` `print` `ar-web` `anim` `toy`. Sets model+params+chain+format from the domain knowledge base.
43
- - `--then <steps>` — processing chain, comma-separated. Steps: `refine texture stylize convert import rig-check rig retarget segment complete decimate uv smartsegment`. Args: `step:key=value,...`; bare value maps to the step's primary arg (`convert:fbx` → format, `stylize:lego` → style, `decimate:5000` → face_limit, `retarget:preset:walk` → animation). Overrides the scenario chain.
43
+ - `--then <steps>` — processing chain, comma-separated. Steps: `refine texture convert import rig-check rig retarget segment complete decimate uv smartsegment`. Args: `step:key=value,...`; bare value maps to the step's primary arg (`convert:fbx` → format, `decimate:5000` → face_limit, `retarget:preset:walk` → animation). Overrides the scenario chain.
44
44
  - `--model <m>` — force `tripo-v3.1` (high fidelity, default), `tripo-p1` (low-poly; face_limit 50–20000; quad/parts/geometry_quality unsupported and stripped) or `tripo-p2` (P-series preview; adds quad, face_limit 48–50000 tri / 48–25000 quad; never auto-selected). The CLI normalizes these aliases to the wire versions (`v3.1-20260211` / `P1-20260311` / `P2-20260801`) the server actually accepts. Full table with status (GA / Preview / Legacy) and official links: `tripo docs --topic commands/generate` → "Models & Versions".
45
45
  - `-n, --candidates <n>` — up to 4 parallel candidates with different seeds (interactive pick when chaining).
46
46
  - `--seed <n>` — fixed model_seed for reproducible geometry.
@@ -9,7 +9,6 @@ Any API parameter without a first-class flag can be passed with `-p key=value` (
9
9
  ```bash
10
10
  tripo model refine [input] # only accepts text_to_model tasks
11
11
  tripo model texture [input] --texture-quality detailed --texture-alignment geometry [-p pbr=true]
12
- tripo model stylize [input] --style lego|voxel|voronoi|minecraft [--block-size 80]
13
12
  tripo model convert [input] --format GLTF|USDZ|FBX|OBJ|STL|3MF \
14
13
  [--texture-size 2048] [--texture-format JPEG|PNG|OPEN_EXR|...] [--face-limit N] \
15
14
  [--quad] [--flatten-bottom] [--fbx-preset blender|3dsmax|mixamo] \
@@ -22,7 +21,6 @@ tripo model import <file-or-url> # GLB/GLTF/FBX/OBJ/STL ≤150MB
22
21
  | --- | --- | --- |
23
22
  | `refine` | successful `text_to_model` task only | `model` (default `tripo-v3.1`) |
24
23
  | `texture` | task / file / URL (file/URL is imported implicitly) | `model` `v3.0-20250812` (default) / `v2.5-20250123`; `texture_quality` standard/detailed/extreme; `pbr` (default true); `texture_seed`; `texture_alignment` original_image/geometry |
25
- | `stylize` | task / file (no URL) | `style` lego/voxel/voronoi/minecraft; `block_size` 32–128 (minecraft only, default 80) |
26
24
  | `convert` | task / file | see table below; `format` required |
27
25
  | `import` | file / URL | GLB / GLTF / FBX / OBJ / STL ≤150 MB. `.gltf` references external files, so it only works as a URL — upload `.glb` |
28
26