tripo-cli 0.6.1 → 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 +13 -0
- package/dist/cli.js +7 -4
- package/dist/commands/login.js +13 -2
- package/dist/commands/mcp.js +14 -1
- package/dist/commands/task.js +1 -1
- package/dist/core/analytics.js +302 -0
- package/dist/core/client.js +13 -3
- package/dist/core/device-auth.js +6 -1
- package/dist/core/download.js +50 -40
- package/dist/core/task-service.js +3 -1
- package/dist/program.js +9 -0
- package/package.json +1 -1
- package/skill/SKILL.md +12 -4
package/README.md
CHANGED
|
@@ -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 usage analytics and the account lookup (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 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`) 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
|
+
|
|
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
|
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
|
-
|
|
5
|
-
|
|
6
|
-
process.
|
|
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);
|
package/dist/commands/login.js
CHANGED
|
@@ -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:
|
|
224
|
-
en:
|
|
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.
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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 };
|
package/dist/commands/task.js
CHANGED
|
@@ -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,302 @@
|
|
|
1
|
+
/** Agent analytics. Collection must never change a business result. */
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
+
import { loadConfig, updateConfig, resolveApiBaseUrl, resolveRegion, REGION_DEFAULTS } 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
|
+
/** 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
|
+
}
|
|
33
|
+
function collector(region = currentOperation()?.regionHint ?? resolveRegion(), apiUrl = resolveApiBaseUrl()) {
|
|
34
|
+
const operation = currentOperation();
|
|
35
|
+
if (!operation || operation.disabled)
|
|
36
|
+
return undefined;
|
|
37
|
+
const key = `${region}:${apiUrl}`;
|
|
38
|
+
let analytics = operation.collectors.get(key);
|
|
39
|
+
if (!analytics) {
|
|
40
|
+
analytics = new Analytics("tripo-cli", cliVersion(), region, apiUrl);
|
|
41
|
+
operation.collectors.set(key, analytics);
|
|
42
|
+
}
|
|
43
|
+
if (analytics.enabled && !installationId) {
|
|
44
|
+
try {
|
|
45
|
+
const config = loadConfig();
|
|
46
|
+
installationId = typeof config.analytics_distinct_id === "string" && /^[a-f0-9-]{36}$/.test(config.analytics_distinct_id)
|
|
47
|
+
? config.analytics_distinct_id : randomUUID();
|
|
48
|
+
if (config.analytics_distinct_id !== installationId)
|
|
49
|
+
updateConfig({ analytics_distinct_id: installationId });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
installationId = randomUUID();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (installationId)
|
|
56
|
+
operation.context.distinctId = installationId;
|
|
57
|
+
return analytics;
|
|
58
|
+
}
|
|
59
|
+
export function errorProperties(error) {
|
|
60
|
+
const category = error instanceof TaskFailedError ? error.status === 'timeout' ? 'waiting_timeout' : 'task_failed'
|
|
61
|
+
: error instanceof ApiError ? error.apiCode === 2010 ? 'balance' : 'upstream'
|
|
62
|
+
: error instanceof AuthError ? 'authorization' : error instanceof NetworkError ? 'network'
|
|
63
|
+
: error instanceof UsageError ? 'validation' : 'internal';
|
|
64
|
+
return { error_category: category, ...(error instanceof ApiError ? { http_status: error.httpStatus, api_code: error.apiCode } : {}) };
|
|
65
|
+
}
|
|
66
|
+
export function trackEvent(event, properties = {}, region, apiUrl) {
|
|
67
|
+
const operation = currentOperation();
|
|
68
|
+
// At most one authorization outcome per operation; later reports are dropped.
|
|
69
|
+
if (operation && event === 'agent_auth_started')
|
|
70
|
+
operation.authMethod = properties.auth_method;
|
|
71
|
+
if (operation && event === 'agent_auth_result') {
|
|
72
|
+
if (operation.authReported)
|
|
73
|
+
return;
|
|
74
|
+
operation.authReported = true;
|
|
75
|
+
}
|
|
76
|
+
const analytics = collector(region, apiUrl);
|
|
77
|
+
const context = analyticsContext();
|
|
78
|
+
if (analytics && context)
|
|
79
|
+
analytics.track(event, context, properties, operation?.identity?.value);
|
|
80
|
+
}
|
|
81
|
+
export async function flushAnalytics() {
|
|
82
|
+
const operation = currentOperation();
|
|
83
|
+
if (operation)
|
|
84
|
+
await Promise.all([...operation.collectors.values()].map((analytics) => analytics.flush()));
|
|
85
|
+
}
|
|
86
|
+
export async function finishOperation(result, error) {
|
|
87
|
+
const operation = currentOperation();
|
|
88
|
+
if (!operation || operation.finished)
|
|
89
|
+
return;
|
|
90
|
+
operation.finished = true;
|
|
91
|
+
if (operation.command === 'login' && result !== 'success') {
|
|
92
|
+
trackEvent('agent_auth_result', { result, auth_method: operation.authMethod ?? 'login', ...(error ? errorProperties(error) : {}) });
|
|
93
|
+
}
|
|
94
|
+
trackEvent('agent_tool_result', { command: operation.command,
|
|
95
|
+
...(operation.context.integration_mode === 'local_mcp' ? { tool_name: operation.command } : {}),
|
|
96
|
+
result, duration_ms: Date.now() - operation.started, ...(error ? errorProperties(error) : {}) });
|
|
97
|
+
await flushAnalytics();
|
|
98
|
+
}
|
|
99
|
+
export function taskOrigin(model) {
|
|
100
|
+
const context = analyticsContext();
|
|
101
|
+
return context ? { creation_client_app: context.client_app, creation_distribution_channel: context.distribution_channel,
|
|
102
|
+
creation_integration_mode: context.integration_mode, ...(typeof model === 'string' ? { model } : {}) } : undefined;
|
|
103
|
+
}
|
|
104
|
+
const observed = new Set();
|
|
105
|
+
export function observeTask(task, region, apiUrl) {
|
|
106
|
+
if (!collector(region, apiUrl)?.enabled || !['success', 'failed', 'cancelled', 'banned', 'expired'].includes(task.status))
|
|
107
|
+
return;
|
|
108
|
+
const key = `${region}:${task.task_id}`;
|
|
109
|
+
const history = readHistory().find((entry) => entry.task_id === task.task_id);
|
|
110
|
+
if (observed.has(key) || history?.analytics_finished)
|
|
111
|
+
return;
|
|
112
|
+
observed.add(key);
|
|
113
|
+
// Bound this cache for long-lived stdio hosts; history preserves known task markers.
|
|
114
|
+
if (observed.size > 1000)
|
|
115
|
+
observed.delete(observed.values().next().value);
|
|
116
|
+
trackEvent('agent_task_finished', { ...history?.analytics_origin,
|
|
117
|
+
creation_client_app: history?.analytics_origin?.creation_client_app ?? 'unknown',
|
|
118
|
+
creation_distribution_channel: history?.analytics_origin?.creation_distribution_channel ?? 'unknown',
|
|
119
|
+
origin_known: Boolean(history?.analytics_origin), task_id: task.task_id, task_type: task.type,
|
|
120
|
+
status: task.status, credits_consumed: task.credits_consumed,
|
|
121
|
+
generation_duration_ms: task.created_at && task.completed_at ? Math.max(0, Date.parse(task.completed_at) - Date.parse(task.created_at)) : undefined,
|
|
122
|
+
}, region, apiUrl);
|
|
123
|
+
if (history) {
|
|
124
|
+
try {
|
|
125
|
+
updateHistoryStatus(task.task_id, task.status, { analytics_finished: true });
|
|
126
|
+
}
|
|
127
|
+
catch { /* best effort marker */ }
|
|
128
|
+
}
|
|
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
|
+
}
|
|
135
|
+
export function clientApp(value) {
|
|
136
|
+
if (typeof value !== "string")
|
|
137
|
+
return "unknown";
|
|
138
|
+
const name = value.trim().toLowerCase();
|
|
139
|
+
for (const app of ["codex", "workbuddy", "zcode", "kimi"]) {
|
|
140
|
+
if (new RegExp(`(^|[\\s_./-])${app}($|[\\s_./-])`).test(name))
|
|
141
|
+
return app;
|
|
142
|
+
}
|
|
143
|
+
return "unknown";
|
|
144
|
+
}
|
|
145
|
+
export function attribution(explicit, distribution, info, oauthName) {
|
|
146
|
+
const tagged = clientApp(explicit);
|
|
147
|
+
const initialized = clientApp(info?.name);
|
|
148
|
+
const oauth = clientApp(oauthName);
|
|
149
|
+
const chosen = tagged !== "unknown" ? tagged : initialized !== "unknown" ? initialized : oauth;
|
|
150
|
+
const channels = ["codex_plugin", "workbuddy_plugin", "zcode_plugin", "kimi_plugin"];
|
|
151
|
+
return {
|
|
152
|
+
client_app: chosen,
|
|
153
|
+
distribution_channel: typeof distribution === "string" && channels.includes(distribution) ? distribution : "unknown",
|
|
154
|
+
attribution_method: tagged !== "unknown" ? "explicit_tag" : initialized !== "unknown" ? "mcp_client_info" : oauth !== "unknown" ? "oauth_client_name" : "unknown",
|
|
155
|
+
attribution_conflict: [tagged, initialized, oauth].some((app) => app !== "unknown" && chosen !== app),
|
|
156
|
+
...(typeof info?.version === "string" && /^[a-zA-Z0-9._+-]{1,64}$/.test(info.version) && !/tsk_|bearer|token/i.test(info.version) ? { client_version: info.version } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const FIELDS = new Set([
|
|
160
|
+
"tool_name", "command", "result", "status", "task_id", "parent_task_id", "model", "task_type",
|
|
161
|
+
"duration_ms", "generation_duration_ms", "credits_consumed", "error_category", "http_status", "api_code",
|
|
162
|
+
"auth_flow_id", "auth_method", "observation_source", "creation_client_app", "creation_distribution_channel",
|
|
163
|
+
"creation_integration_mode", "origin_known", "model_downloaded", "file_count", "format", "interrupted",
|
|
164
|
+
]);
|
|
165
|
+
export class Analytics {
|
|
166
|
+
app;
|
|
167
|
+
version;
|
|
168
|
+
enabled;
|
|
169
|
+
identityEnabled;
|
|
170
|
+
testing;
|
|
171
|
+
url;
|
|
172
|
+
appId;
|
|
173
|
+
environment;
|
|
174
|
+
queue = [];
|
|
175
|
+
pending;
|
|
176
|
+
scheduled = false;
|
|
177
|
+
stats = { sent: 0, failed: 0, dropped: 0 };
|
|
178
|
+
constructor(app, version, region, apiUrl, env = process.env) {
|
|
179
|
+
this.app = app;
|
|
180
|
+
this.version = version;
|
|
181
|
+
const receiver = env.TRIPO_ANALYTICS_SERVER_URL ?? ANALYTICS_SERVER_URL;
|
|
182
|
+
this.appId = env.TRIPO_ANALYTICS_APP_ID ?? ANALYTICS_APP_ID;
|
|
183
|
+
const environment = env.TRIPO_ANALYTICS_ENVIRONMENT ?? (env.NODE_ENV === "production" ? "production" : app === "tripo-cli" ? "production" : "development");
|
|
184
|
+
this.environment = ["production", "pre", "development"].includes(environment) ? environment : "development";
|
|
185
|
+
this.url = "";
|
|
186
|
+
let officialApi = false;
|
|
187
|
+
let localApi = false;
|
|
188
|
+
let testReceiver = false;
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL(receiver);
|
|
191
|
+
this.url = new URL("sync_json", url.href.endsWith("/") ? url : `${url.href}/`).href;
|
|
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);
|
|
195
|
+
testReceiver = (url.protocol === "http:" || url.protocol === "https:") && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
196
|
+
if (url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol))
|
|
197
|
+
this.url = "";
|
|
198
|
+
}
|
|
199
|
+
catch { /* invalid analytics configuration is disabled, never fatal */ }
|
|
200
|
+
const testing = this.testing = env.NODE_ENV === "test" || Boolean(env.VITEST);
|
|
201
|
+
this.enabled = region === "ov" && Boolean(this.url && this.appId) && env.TRIPO_ANALYTICS_ENABLED !== "0"
|
|
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
|
+
}
|
|
227
|
+
}
|
|
228
|
+
track(event, context, properties = {}, identity) {
|
|
229
|
+
if (!this.enabled || !/^agent_[a-z_]+$/.test(event))
|
|
230
|
+
return;
|
|
231
|
+
if (this.queue.length >= 100) {
|
|
232
|
+
this.stats.dropped++;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const safe = {};
|
|
236
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
237
|
+
if (!FIELDS.has(key))
|
|
238
|
+
continue;
|
|
239
|
+
if (typeof value === "number" && Number.isFinite(value) || typeof value === "boolean")
|
|
240
|
+
safe[key] = value;
|
|
241
|
+
else if (typeof value === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/.test(value) && !/tsk_|bearer|token/i.test(value))
|
|
242
|
+
safe[key] = value;
|
|
243
|
+
}
|
|
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
|
+
} } });
|
|
256
|
+
if (!this.scheduled) {
|
|
257
|
+
this.scheduled = true;
|
|
258
|
+
queueMicrotask(() => { this.scheduled = false; void this.flush(); });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async flush() {
|
|
262
|
+
if (this.pending)
|
|
263
|
+
return this.pending;
|
|
264
|
+
if (!this.queue.length)
|
|
265
|
+
return;
|
|
266
|
+
// ponytail: bounded best-effort delivery; no persistent spool or retries in v1.
|
|
267
|
+
const send = async () => {
|
|
268
|
+
const deadline = Date.now() + 2000;
|
|
269
|
+
while (this.queue.length && Date.now() < deadline) {
|
|
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
|
+
}));
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(this.url, {
|
|
279
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch.map(({ event }) => event)),
|
|
280
|
+
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
|
|
281
|
+
});
|
|
282
|
+
const body = await response.json();
|
|
283
|
+
if (!response.ok || body.code !== 0)
|
|
284
|
+
throw new Error("analytics rejected");
|
|
285
|
+
this.stats.sent += batch.length;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
this.stats.failed += batch.length;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this.stats.dropped += this.queue.length;
|
|
292
|
+
this.queue = [];
|
|
293
|
+
};
|
|
294
|
+
this.pending = send();
|
|
295
|
+
try {
|
|
296
|
+
await this.pending;
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
this.pending = undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
package/dist/core/client.js
CHANGED
|
@@ -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 { identifyUser, 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;
|
|
@@ -52,6 +55,7 @@ export class TripoClient {
|
|
|
52
55
|
return this.send('POST', '/v3/files', { body: form });
|
|
53
56
|
}
|
|
54
57
|
async send(method, path, init) {
|
|
58
|
+
identifyUser(this.apiKey, this.region, this.baseUrl);
|
|
55
59
|
let lastError;
|
|
56
60
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
57
61
|
if (attempt > 0) {
|
|
@@ -117,9 +121,11 @@ export class TripoClient {
|
|
|
117
121
|
return envelope.data;
|
|
118
122
|
}
|
|
119
123
|
// ---- typed convenience wrappers ----
|
|
120
|
-
getTask(taskId, step = false) {
|
|
124
|
+
async getTask(taskId, step = false) {
|
|
121
125
|
const suffix = step ? '?step=1' : '';
|
|
122
|
-
|
|
126
|
+
const task = await this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
|
|
127
|
+
observeTask(task, this.region, this.baseUrl);
|
|
128
|
+
return task;
|
|
123
129
|
}
|
|
124
130
|
/**
|
|
125
131
|
* Batch task query. The server returns `{tasks: {id: detail}, missed: [id]}`
|
|
@@ -128,6 +134,8 @@ export class TripoClient {
|
|
|
128
134
|
async listTasks(taskIds) {
|
|
129
135
|
const data = await this.request('POST', '/v3/tasks/list', { task_ids: taskIds });
|
|
130
136
|
const byId = data.tasks ?? {};
|
|
137
|
+
for (const [id, task] of Object.entries(byId))
|
|
138
|
+
observeTask({ ...task, task_id: task.task_id ?? id }, this.region, this.baseUrl);
|
|
131
139
|
const tasks = taskIds
|
|
132
140
|
.filter((id) => byId[id])
|
|
133
141
|
.map((id) => ({ ...byId[id], task_id: byId[id].task_id ?? id }));
|
|
@@ -140,9 +148,11 @@ export class TripoClient {
|
|
|
140
148
|
return this.request('GET', '/v3/account/usage');
|
|
141
149
|
}
|
|
142
150
|
/** Create a task; returns its id. Response may be {task_id} or a full detail. */
|
|
143
|
-
async createTask(endpoint, payload) {
|
|
151
|
+
async createTask(endpoint, payload, taskType) {
|
|
144
152
|
try {
|
|
145
153
|
const data = await this.request('POST', endpoint, payload, { idempotent: false });
|
|
154
|
+
trackEvent('agent_task_created', { ...taskOrigin(payload.model), task_id: data.task_id, task_type: taskType,
|
|
155
|
+
...([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
156
|
return data.task_id;
|
|
147
157
|
}
|
|
148
158
|
catch (err) {
|
package/dist/core/device-auth.js
CHANGED
|
@@ -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
|
/**
|
package/dist/core/download.js
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -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,
|
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
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
|
|
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
|
|