tripo-cli 0.7.0 → 0.7.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 CHANGED
@@ -105,7 +105,7 @@ 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
+ | `TRIPO_ANALYTICS_ENABLED` | set to `0` to turn off usage analytics and the account lookup (see [Telemetry](#telemetry)) |
109
109
  | `TRIPO_NPM_REGISTRY` | npm registry used by `tripo update` and the version check (default `https://registry.npmjs.org`; e.g. a mirror) |
110
110
  | `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | standard proxy variables, honored for all CLI traffic (plain Node fetch ignores them) |
111
111
  | `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
@@ -114,13 +114,13 @@ Setting `TRIPO_API_KEY` per shell — bash/zsh: `export TRIPO_API_KEY=tsk_...`
114
114
 
115
115
  ## Telemetry
116
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.
117
+ Since 0.7.0 the CLI sends 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
118
 
119
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`.
120
+ - **Identity:** a random installation id (`analytics_distinct_id` in `~/.tripo/config.json`) and, since 0.7.1, your Tripo account id (`platform_user_id`) when it can be resolved. It is looked up for the API key actually in use with `POST https://apiplatform.tripo3d.ai/verify`, once per command or MCP tool call, alongside the first API request rather than before it; the lookup gives up after 1 second, is never retried, and the id is never written to disk — on failure events simply go without it. No email or billing details are sent.
121
+ - **Never sent:** prompts, file paths, input images or models, API keys, download URLs (the key itself only goes to `/verify` for the account lookup).
122
+ - **Nothing is sent, and no account lookup is made, 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` (also skips the account lookup).
124
124
 
125
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
126
 
@@ -1,7 +1,7 @@
1
- /** Anonymous Agent analytics. Collection must never change a business result. */
2
- import { randomUUID } from "node:crypto";
1
+ /** Agent analytics. Collection must never change a business result. */
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
- import { loadConfig, updateConfig, resolveApiBaseUrl, resolveRegion } from '../config/config.js';
4
+ import { loadConfig, updateConfig, resolveApiBaseUrl, resolveRegion, REGION_DEFAULTS } from '../config/config.js';
5
5
  import { readHistory, updateHistoryStatus } from '../config/history.js';
6
6
  import { cliVersion } from '../version.js';
7
7
  import { ApiError, AuthError, NetworkError, TaskFailedError, UsageError } from './errors.js';
@@ -19,6 +19,17 @@ export function beginOperation(command, disabled = false, info, regionHint) {
19
19
  operations.enterWith(activeOperation);
20
20
  }
21
21
  export function analyticsContext() { return currentOperation()?.context; }
22
+ /** Resolve only the key actually used by this operation, without delaying its requests; never persist account identities. */
23
+ export function identifyUser(apiKey, region, apiUrl) {
24
+ const operation = currentOperation();
25
+ const analytics = collector(region, apiUrl);
26
+ if (!operation || !analytics?.identityEnabled)
27
+ return;
28
+ const consoleApi = process.env.TRIPO_CONSOLE_API_BASE_URL ?? REGION_DEFAULTS[region].consoleApi;
29
+ const key = createHash('sha256').update(JSON.stringify([region, apiUrl, consoleApi, apiKey])).digest('hex');
30
+ if (operation.identity?.key !== key)
31
+ operation.identity = { key, value: analytics.identify(apiKey, consoleApi) };
32
+ }
22
33
  function collector(region = currentOperation()?.regionHint ?? resolveRegion(), apiUrl = resolveApiBaseUrl()) {
23
34
  const operation = currentOperation();
24
35
  if (!operation || operation.disabled)
@@ -65,7 +76,7 @@ export function trackEvent(event, properties = {}, region, apiUrl) {
65
76
  const analytics = collector(region, apiUrl);
66
77
  const context = analyticsContext();
67
78
  if (analytics && context)
68
- analytics.track(event, context, properties);
79
+ analytics.track(event, context, properties, operation?.identity?.value);
69
80
  }
70
81
  export async function flushAnalytics() {
71
82
  const operation = currentOperation();
@@ -116,6 +127,11 @@ export function observeTask(task, region, apiUrl) {
116
127
  catch { /* best effort marker */ }
117
128
  }
118
129
  }
130
+ function platformUserId(value) {
131
+ if (typeof value === 'number')
132
+ return Number.isSafeInteger(value) && value > 0 ? String(value) : undefined;
133
+ return typeof value === 'string' && /^[1-9]\d{0,18}$/.test(value) ? value : undefined;
134
+ }
119
135
  export function clientApp(value) {
120
136
  if (typeof value !== "string")
121
137
  return "unknown";
@@ -150,6 +166,8 @@ export class Analytics {
150
166
  app;
151
167
  version;
152
168
  enabled;
169
+ identityEnabled;
170
+ testing;
153
171
  url;
154
172
  appId;
155
173
  environment;
@@ -166,21 +184,48 @@ export class Analytics {
166
184
  this.environment = ["production", "pre", "development"].includes(environment) ? environment : "development";
167
185
  this.url = "";
168
186
  let officialApi = false;
187
+ let localApi = false;
169
188
  let testReceiver = false;
170
189
  try {
171
190
  const url = new URL(receiver);
172
191
  this.url = new URL("sync_json", url.href.endsWith("/") ? url : `${url.href}/`).href;
173
- officialApi = new URL(apiUrl).hostname === "openapi.tripo3d.ai";
192
+ const api = new URL(apiUrl);
193
+ officialApi = api.protocol === "https:" && api.hostname === "openapi.tripo3d.ai";
194
+ localApi = ["localhost", "127.0.0.1", "[::1]"].includes(api.hostname);
174
195
  testReceiver = (url.protocol === "http:" || url.protocol === "https:") && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
175
196
  if (url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol))
176
197
  this.url = "";
177
198
  }
178
199
  catch { /* invalid analytics configuration is disabled, never fatal */ }
179
- const testing = env.NODE_ENV === "test" || Boolean(env.VITEST);
200
+ const testing = this.testing = env.NODE_ENV === "test" || Boolean(env.VITEST);
180
201
  this.enabled = region === "ov" && Boolean(this.url && this.appId) && env.TRIPO_ANALYTICS_ENABLED !== "0"
181
202
  && (testing ? env.TRIPO_ANALYTICS_ENABLED === "1" && testReceiver : officialApi || testReceiver);
203
+ this.identityEnabled = this.enabled && (testing ? localApi : officialApi);
204
+ }
205
+ async identify(apiKey, consoleApi) {
206
+ if (!this.identityEnabled)
207
+ return undefined;
208
+ try {
209
+ const url = new URL(`${consoleApi.replace(/\/$/, '')}/verify`);
210
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
211
+ if (url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol)
212
+ || (this.testing ? !local : url.protocol !== "https:" || url.hostname !== "apiplatform.tripo3d.ai"))
213
+ return undefined;
214
+ const response = await fetch(url, {
215
+ method: "POST", headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ apikey: apiKey, with_detail: true }),
217
+ redirect: "error", signal: AbortSignal.timeout(1000),
218
+ });
219
+ if (!response.ok)
220
+ return undefined;
221
+ const body = await response.json();
222
+ return body?.code === 0 && body.data?.valid === true ? platformUserId(body.data.detail?.owner) : undefined;
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
182
227
  }
183
- track(event, context, properties = {}) {
228
+ track(event, context, properties = {}, identity) {
184
229
  if (!this.enabled || !/^agent_[a-z_]+$/.test(event))
185
230
  return;
186
231
  if (this.queue.length >= 100) {
@@ -196,18 +241,18 @@ export class Analytics {
196
241
  else if (typeof value === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value) && !/tsk_|bearer|token/i.test(value))
197
242
  safe[key] = value;
198
243
  }
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
- } });
244
+ this.queue.push({ identity, event: { appid: this.appId, data: {
245
+ "#type": "track", "#event_name": event, "#distinct_id": context.distinctId,
246
+ "#time": new Date().toISOString().replace("T", " ").replace("Z", ""),
247
+ properties: {
248
+ ...safe, ...attribution(context.client_app, context.distribution_channel, { version: context.client_version }),
249
+ attribution_method: context.attribution_method, attribution_conflict: context.attribution_conflict ?? false,
250
+ app: this.app, event_source: this.app === "tripo-mcp" ? "server" : "cli", site_region: "global",
251
+ environment: this.environment, schema_version: 2, agent_schema_version: 1, app_version: this.version,
252
+ integration_mode: context.integration_mode, operation_id: context.operation_id, event_id: randomUUID(),
253
+ identity_type: this.app === "tripo-mcp" ? "authorization" : "installation", observation_source: "client", "#zone_offset": 0,
254
+ },
255
+ } } });
211
256
  if (!this.scheduled) {
212
257
  this.scheduled = true;
213
258
  queueMicrotask(() => { this.scheduled = false; void this.flush(); });
@@ -223,9 +268,15 @@ export class Analytics {
223
268
  const deadline = Date.now() + 2000;
224
269
  while (this.queue.length && Date.now() < deadline) {
225
270
  const batch = this.queue.splice(0, 100);
271
+ // Each lookup began before its event and gives up after 1 s, so this wait leaves the send at least 1 s of the budget.
272
+ await Promise.all(batch.map(async ({ event, identity }) => {
273
+ const userId = await identity;
274
+ if (userId)
275
+ event.data.properties.platform_user_id = userId;
276
+ }));
226
277
  try {
227
278
  const response = await fetch(this.url, {
228
- method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch),
279
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch.map(({ event }) => event)),
229
280
  signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
230
281
  });
231
282
  const body = await response.json();
@@ -8,7 +8,7 @@ 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';
11
+ import { identifyUser, observeTask, trackEvent, taskOrigin } from './analytics.js';
12
12
  import { resolveRegion } from '../config/config.js';
13
13
  export class TripoClient {
14
14
  region = resolveRegion();
@@ -55,6 +55,7 @@ export class TripoClient {
55
55
  return this.send('POST', '/v3/files', { body: form });
56
56
  }
57
57
  async send(method, path, init) {
58
+ identifyUser(this.apiKey, this.region, this.baseUrl);
58
59
  let lastError;
59
60
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
60
61
  if (attempt > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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",