tripo-cli 0.7.0 → 0.7.2
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 +7 -7
- package/dist/core/analytics.js +73 -22
- package/dist/core/client.js +2 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
- **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`),
|
|
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
|
|
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` / `deepseek` (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`) 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
|
|
package/dist/core/analytics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
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,11 +127,16 @@ 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";
|
|
122
138
|
const name = value.trim().toLowerCase();
|
|
123
|
-
for (const app of ["codex", "workbuddy", "zcode", "kimi"]) {
|
|
139
|
+
for (const app of ["codex", "workbuddy", "zcode", "kimi", "deepseek"]) {
|
|
124
140
|
if (new RegExp(`(^|[\\s_./-])${app}($|[\\s_./-])`).test(name))
|
|
125
141
|
return app;
|
|
126
142
|
}
|
|
@@ -131,7 +147,7 @@ export function attribution(explicit, distribution, info, oauthName) {
|
|
|
131
147
|
const initialized = clientApp(info?.name);
|
|
132
148
|
const oauth = clientApp(oauthName);
|
|
133
149
|
const chosen = tagged !== "unknown" ? tagged : initialized !== "unknown" ? initialized : oauth;
|
|
134
|
-
const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin"];
|
|
150
|
+
const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin", "deepseek_plugin"];
|
|
135
151
|
return {
|
|
136
152
|
client_app: chosen,
|
|
137
153
|
distribution_channel: typeof distribution === "string" && channels.includes(distribution) ? distribution : "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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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();
|
package/dist/core/client.js
CHANGED
|
@@ -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) {
|