tripo-cli 0.2.0 → 0.3.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 +2 -1
- package/dist/cli.js +2 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/login.js +126 -15
- package/dist/core/device-auth.js +8 -5
- package/dist/core/errors.js +2 -2
- package/dist/core/proxy.js +30 -0
- package/dist/core/region.js +20 -9
- package/package.json +2 -1
- package/skill/SKILL.md +13 -1
- package/skill/commands/account.md +5 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ tripo login # browser approves, key arrives automatically; or --paste /
|
|
|
15
15
|
tripo make knight.png --for game-mobile --then texture,rig,convert:fbx
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Works with both Tripo regions from the same install — international (`openapi.tripo3d.ai`, console `developers.tripo3d.ai`, email login + Stripe) and China mainland (`openapi.tripo3d.com`, console `developers.tripo3d.com`, SMS login + Alipay). `tripo login` runs an RFC 8628 device flow: the terminal shows a short code, the browser signs in (each console's own method) and approves, and the key lands in the CLI automatically — with graceful fallback to open-the-keys-page-and-paste when the device-auth backend is unavailable. You never pick a region when a key is in hand: the key is probed against both regions and whichever accepts it wins; `tripo topup` then opens the matching billing page (`/billing` on ov, `/billing/recharge` on cn). If a config was moved between machines, `tripo doctor` detects the mismatch and prints the one-line fix.
|
|
18
|
+
Works with both Tripo regions from the same install — international (`openapi.tripo3d.ai`, console `developers.tripo3d.ai`, email login + Stripe) and China mainland (`openapi.tripo3d.com`, console `developers.tripo3d.com`, SMS login + Alipay). `tripo login` runs an RFC 8628 device flow: the terminal shows a short code, the browser signs in (each console's own method) and approves, and the key lands in the CLI automatically — with graceful fallback to open-the-keys-page-and-paste when the device-auth backend is unavailable. The same flow works without a TTY: `tripo login --region ov|cn` from an agent or CI prints the verification URL + code and waits for the browser approval, so an AI agent can start the login and relay the code to its human. You never pick a region when a key is in hand: the key is probed against both regions and whichever accepts it wins; `tripo topup` then opens the matching billing page (`/billing` on ov, `/billing/recharge` on cn). If a config was moved between machines, `tripo doctor` detects the mismatch and prints the one-line fix.
|
|
19
19
|
|
|
20
20
|
## Why this CLI
|
|
21
21
|
|
|
@@ -101,6 +101,7 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
101
101
|
| `TRIPO_REGION` | `ov` or `cn` — normally unnecessary: `tripo login` probes both regions with your key and stores the right one automatically |
|
|
102
102
|
| `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
|
|
103
103
|
| `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
|
|
104
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | standard proxy variables, honored for all CLI traffic (plain Node fetch ignores them) |
|
|
104
105
|
| `TRIPO_LLM_BASE_URL` / `TRIPO_LLM_API_KEY` / `TRIPO_LLM_MODEL` | optional OpenAI-compatible LLM for `tripo ai` conversation mode |
|
|
105
106
|
|
|
106
107
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { installProxyFromEnv } from './core/proxy.js';
|
|
2
3
|
import { runCli } from './program.js';
|
|
3
4
|
process.on('SIGINT', () => {
|
|
4
5
|
process.stderr.write('\n');
|
|
5
6
|
process.exit(130);
|
|
6
7
|
});
|
|
8
|
+
await installProxyFromEnv();
|
|
7
9
|
void runCli(process.argv);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -67,7 +67,7 @@ export function registerDoctorCommand(program) {
|
|
|
67
67
|
// simply live in the other region. Probe and hand over the exact fix.
|
|
68
68
|
if (err instanceof CliError && err.exitCode === EXIT.AUTH && !process.env.TRIPO_API_BASE_URL) {
|
|
69
69
|
const detected = await detectRegionForKey(key);
|
|
70
|
-
if (detected && detected.region !== region) {
|
|
70
|
+
if (detected.ok && detected.region !== region) {
|
|
71
71
|
detail += ` — key belongs to region "${detected.region}"; run: tripo config set region ${detected.region}`;
|
|
72
72
|
}
|
|
73
73
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
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 { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval } from '../core/device-auth.js';
|
|
15
|
-
import { AuthError, CliError, UsageError } from '../core/errors.js';
|
|
14
|
+
import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
|
|
15
|
+
import { AuthError, CliError, NetworkError, UsageError } from '../core/errors.js';
|
|
16
16
|
import { detectRegionForKey } from '../core/region.js';
|
|
17
17
|
import { emitResult, isInteractive, log, shouldOpen, success, warn } from '../core/output.js';
|
|
18
18
|
import { t } from '../i18n.js';
|
|
@@ -26,7 +26,7 @@ export function registerAuthCommands(program) {
|
|
|
26
26
|
.description('sign in: browser authorizes, the key arrives automatically')
|
|
27
27
|
.option('--paste', 'skip the browser flow; just paste an API key')
|
|
28
28
|
.option('--key <key>', 'provide the API key directly (tsk_...); region is auto-detected')
|
|
29
|
-
.option('--region <region>', 'ov or cn —
|
|
29
|
+
.option('--region <region>', 'ov or cn — needed for the browser flow; required for non-interactive login')
|
|
30
30
|
.option('--profile <name>', 'save into a named profile (for a second account); becomes the active profile')
|
|
31
31
|
.action(async (opts) => {
|
|
32
32
|
await runLogin(opts);
|
|
@@ -169,10 +169,8 @@ export async function runLogin(opts) {
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
if (!isInteractive()) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
en: 'in non-interactive environments use the TRIPO_API_KEY env var or "tripo login --key tsk_..."',
|
|
175
|
-
}));
|
|
172
|
+
await runHeadlessLogin(region, profile, opts.paste);
|
|
173
|
+
return;
|
|
176
174
|
}
|
|
177
175
|
p.intro('tripo login');
|
|
178
176
|
// The browser/paste flows need a console URL *before* any key exists — the
|
|
@@ -204,6 +202,77 @@ export async function runLogin(opts) {
|
|
|
204
202
|
en: 'logged in! try: tripo make "a cute robot"',
|
|
205
203
|
}));
|
|
206
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Non-interactive login (agents, CI without a provisioned key): the same
|
|
207
|
+
* device flow, but with the verification URL and one-time code printed as
|
|
208
|
+
* plain text so the caller can relay them to a human, then a blocking poll
|
|
209
|
+
* until that human approves in a browser. The prompts of the interactive
|
|
210
|
+
* path are impossible here, so --region must be explicit (there is no key
|
|
211
|
+
* yet to probe, and the two regions have different console hosts) and
|
|
212
|
+
* --paste is rejected outright.
|
|
213
|
+
*/
|
|
214
|
+
async function runHeadlessLogin(region, profile, paste) {
|
|
215
|
+
if (paste) {
|
|
216
|
+
throw new UsageError(t({
|
|
217
|
+
zh: '--paste 需要交互终端;非交互环境请用设备码登录(tripo login --region ov|cn)或 TRIPO_API_KEY / --key',
|
|
218
|
+
en: '--paste needs an interactive terminal — use device login ("tripo login --region ov|cn"), TRIPO_API_KEY, or --key instead',
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (!region) {
|
|
222
|
+
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_..."',
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
let grant;
|
|
228
|
+
try {
|
|
229
|
+
grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (err instanceof DeviceAuthUnavailableError) {
|
|
233
|
+
if (err.kind === 'unreachable') {
|
|
234
|
+
throw new CliError(t({
|
|
235
|
+
zh: `连不上授权服务(${err.message})— 请检查本机网络/防火墙,出网走 HTTP 代理时设置 HTTPS_PROXY;或改用 TRIPO_API_KEY / tripo login --key tsk_...`,
|
|
236
|
+
en: `cannot reach the authorization service (${err.message}) — check this machine's network/firewall (set HTTPS_PROXY when behind a proxy), or use TRIPO_API_KEY / "tripo login --key tsk_..."`,
|
|
237
|
+
}), EXIT.NETWORK);
|
|
238
|
+
}
|
|
239
|
+
// No paste fallback without a TTY — fail with every remaining option spelled out.
|
|
240
|
+
throw new CliError(t({
|
|
241
|
+
zh: `浏览器授权服务不可用(${err.message});请设置 TRIPO_API_KEY,运行 tripo login --key tsk_...,或在交互终端里运行 tripo login`,
|
|
242
|
+
en: `device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`,
|
|
243
|
+
}), EXIT.AUTH);
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
const minutes = Math.max(1, Math.round(grant.expires_in / 60));
|
|
248
|
+
log(t({
|
|
249
|
+
zh: [
|
|
250
|
+
'需要在浏览器完成授权;本命令会一直等待,直到授权完成或过期。请转告用户:',
|
|
251
|
+
` 1. 打开: ${grant.verification_url_complete}`,
|
|
252
|
+
' (页面如果提示登录,先登录或注册 Tripo 账号 — 新账号有免费额度)',
|
|
253
|
+
` 2. 在「Verification code」输入框填入终端码: ${grant.user_code}`,
|
|
254
|
+
' 3. 账号已有多把 API Key 时页面会让用户选择一把;一把没有则自动创建',
|
|
255
|
+
` 4. 点击「Authorize」。此码 ${minutes} 分钟内有效`,
|
|
256
|
+
].join('\n'),
|
|
257
|
+
en: [
|
|
258
|
+
'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:',
|
|
259
|
+
` 1. Open: ${grant.verification_url_complete}`,
|
|
260
|
+
' (if the page asks to sign in, sign in or sign up first — new accounts get free credits)',
|
|
261
|
+
` 2. Enter this one-time code in the "Verification code" field: ${grant.user_code}`,
|
|
262
|
+
' 3. An account with several API keys is asked to pick one; with none, a key is created automatically',
|
|
263
|
+
` 4. Click "Authorize". The code expires in ${minutes} minutes`,
|
|
264
|
+
].join('\n'),
|
|
265
|
+
}));
|
|
266
|
+
const result = await waitForDeviceApproval(region, grant);
|
|
267
|
+
if (result.status === 'denied') {
|
|
268
|
+
throw new CliError(t({ zh: '授权被拒绝;重新运行 tripo login 再试', en: 'authorization denied — run "tripo login" again to retry' }), EXIT.AUTH);
|
|
269
|
+
}
|
|
270
|
+
if (result.status !== 'approved') {
|
|
271
|
+
throw new CliError(t({ zh: '授权超时或已过期;重新运行 tripo login 获取新的终端码', en: 'authorization expired — run "tripo login" again for a fresh code' }), EXIT.AUTH);
|
|
272
|
+
}
|
|
273
|
+
const saved = await validateAndSave(result.apikey, region, profile);
|
|
274
|
+
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
|
+
}
|
|
207
276
|
/**
|
|
208
277
|
* Preferred path: RFC 8628 device flow — the browser does the sign-in and a
|
|
209
278
|
* one-click approve, the key arrives over the CLI↔console-API channel, and
|
|
@@ -228,6 +297,14 @@ async function deviceFlowWithFallback(region, profile) {
|
|
|
228
297
|
en: `device authorization backend error (${err.message}) — please report this; falling back to paste login`,
|
|
229
298
|
}));
|
|
230
299
|
}
|
|
300
|
+
else if (err.kind === 'unreachable') {
|
|
301
|
+
// The console API could not be reached at all — the same network
|
|
302
|
+
// problem will likely hit key validation next, so say it loudly now.
|
|
303
|
+
warn(t({
|
|
304
|
+
zh: `连不上授权服务(${err.message})— 疑似网络/代理问题(出网走 HTTP 代理时请设置 HTTPS_PROXY);已改用粘贴方式登录`,
|
|
305
|
+
en: `cannot reach the authorization service (${err.message}) — likely a network/proxy issue (set HTTPS_PROXY if this machine needs a proxy); falling back to paste login`,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
231
308
|
else {
|
|
232
309
|
log(t({ zh: '浏览器授权服务暂未开通,改用粘贴方式登录', en: 'device authorization not available yet — falling back to paste login' }));
|
|
233
310
|
}
|
|
@@ -312,7 +389,10 @@ function safeTimeZone() {
|
|
|
312
389
|
* email-first; cn: +86 SMS-first) live entirely on the website either way.
|
|
313
390
|
*/
|
|
314
391
|
async function keyFromConsole(region, openBrowser) {
|
|
315
|
-
|
|
392
|
+
// The console SPA nests every page under a locale segment (/:lang/keys);
|
|
393
|
+
// a bare /keys is swallowed by the ":lang" route and lands on the home
|
|
394
|
+
// page. Both regions' backends use /en in their canonical URLs.
|
|
395
|
+
const keysUrl = `${platformUrlFor(region)}/en/keys`;
|
|
316
396
|
if (openBrowser && shouldOpen()) {
|
|
317
397
|
openExternal(keysUrl);
|
|
318
398
|
log(t({ zh: `已打开控制台 API Keys 页面(未弹出请手动访问):\n ${keysUrl}`, en: `opened the console API Keys page (visit manually if it didn't open):\n ${keysUrl}` }));
|
|
@@ -357,19 +437,17 @@ async function validateAndSave(key, selectedRegion, profile) {
|
|
|
357
437
|
const client = new TripoClient({ apiKey: key });
|
|
358
438
|
try {
|
|
359
439
|
const balance = await client.getBalance();
|
|
360
|
-
|
|
440
|
+
const region = selectedRegion ?? 'ov';
|
|
441
|
+
finishSave(key, region, balance.balance, profile);
|
|
442
|
+
return { region, balance: balance.balance };
|
|
361
443
|
}
|
|
362
444
|
catch (err) {
|
|
363
445
|
throw asAuthError(err);
|
|
364
446
|
}
|
|
365
|
-
return;
|
|
366
447
|
}
|
|
367
448
|
const detected = await detectRegionForKey(key);
|
|
368
|
-
if (!detected) {
|
|
369
|
-
throw
|
|
370
|
-
zh: 'Key 验证失败:国内(openapi.tripo3d.com)和海外(openapi.tripo3d.ai)都不接受这把 Key — 无效或已被删除',
|
|
371
|
-
en: 'key validation failed: rejected by both cn (openapi.tripo3d.com) and ov (openapi.tripo3d.ai) — invalid or deleted',
|
|
372
|
-
}));
|
|
449
|
+
if (!detected.ok) {
|
|
450
|
+
throw regionDetectionFailure(detected.failures);
|
|
373
451
|
}
|
|
374
452
|
if (selectedRegion && selectedRegion !== detected.region) {
|
|
375
453
|
warn(t({
|
|
@@ -378,6 +456,39 @@ async function validateAndSave(key, selectedRegion, profile) {
|
|
|
378
456
|
}));
|
|
379
457
|
}
|
|
380
458
|
finishSave(key, detected.region, detected.balance.balance, profile);
|
|
459
|
+
return { region: detected.region, balance: detected.balance.balance };
|
|
460
|
+
}
|
|
461
|
+
const REGION_HOSTS = { ov: 'openapi.tripo3d.ai', cn: 'openapi.tripo3d.com' };
|
|
462
|
+
/**
|
|
463
|
+
* Turn the per-region probe failures into an honest error. "Invalid or
|
|
464
|
+
* deleted" is only ever claimed when both regions *answered* and rejected
|
|
465
|
+
* the key; any unreachable side turns the verdict into a network problem —
|
|
466
|
+
* the classic case being a firewalled/proxy-only server where a perfectly
|
|
467
|
+
* good key used to be reported as dead.
|
|
468
|
+
*/
|
|
469
|
+
function regionDetectionFailure(failures) {
|
|
470
|
+
if (failures.ov === 'rejected' && failures.cn === 'rejected') {
|
|
471
|
+
return new AuthError(t({
|
|
472
|
+
zh: 'Key 验证失败:国内(openapi.tripo3d.com)和海外(openapi.tripo3d.ai)都不接受这把 Key — 无效或已被删除',
|
|
473
|
+
en: 'key validation failed: rejected by both cn (openapi.tripo3d.com) and ov (openapi.tripo3d.ai) — invalid or deleted',
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
const networkHint = t({
|
|
477
|
+
zh: '检查本机网络/防火墙;出网需要 HTTP 代理时,设置 HTTPS_PROXY 环境变量后重试;tripo doctor 可自检',
|
|
478
|
+
en: 'check this machine\'s network/firewall; if outbound traffic needs an HTTP proxy, set HTTPS_PROXY and retry; "tripo doctor" helps diagnose',
|
|
479
|
+
});
|
|
480
|
+
if (failures.ov === 'unreachable' && failures.cn === 'unreachable') {
|
|
481
|
+
return new NetworkError(t({
|
|
482
|
+
zh: '无法验证 Key:海外(openapi.tripo3d.ai)和国内(openapi.tripo3d.com)都连接失败 — 这是网络问题,不代表 Key 无效',
|
|
483
|
+
en: 'could not validate the key: both ov (openapi.tripo3d.ai) and cn (openapi.tripo3d.com) are unreachable — a network problem, not proof of a bad key',
|
|
484
|
+
}), networkHint);
|
|
485
|
+
}
|
|
486
|
+
const down = failures.ov === 'unreachable' ? 'ov' : 'cn';
|
|
487
|
+
const up = down === 'ov' ? 'cn' : 'ov';
|
|
488
|
+
return new NetworkError(t({
|
|
489
|
+
zh: `Key 验证失败:${up === 'cn' ? '国内' : '海外'}站(${REGION_HOSTS[up]})拒绝了这把 Key,而${down === 'cn' ? '国内' : '海外'}站(${REGION_HOSTS[down]})无法连接 — 如果 Key 属于${down === 'cn' ? '国内' : '海外'}站,请先解决网络问题`,
|
|
490
|
+
en: `key validation failed: ${up} (${REGION_HOSTS[up]}) rejected the key and ${down} (${REGION_HOSTS[down]}) is unreachable — if the key belongs to ${down}, fix the network first`,
|
|
491
|
+
}), networkHint);
|
|
381
492
|
}
|
|
382
493
|
function finishSave(key, region, balance, profile) {
|
|
383
494
|
// Saving activates the profile: "the account I just logged into is the
|
package/dist/core/device-auth.js
CHANGED
|
@@ -19,11 +19,14 @@ export const DEVICE_CODE_DENIED = 4292;
|
|
|
19
19
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
20
20
|
/**
|
|
21
21
|
* Thrown when the device-auth endpoints are missing or unusable — callers
|
|
22
|
-
* fall back to paste. `kind` separates
|
|
22
|
+
* fall back to paste. `kind` separates three very different situations:
|
|
23
23
|
* 'not_deployed' (404 — feature flag off, a normal rollout state, fall back
|
|
24
|
-
* quietly)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* quietly), 'backend_error' (5xx — the feature is deployed but broken; fall
|
|
25
|
+
* back too, but surface the backend's message + request_id loudly so the
|
|
26
|
+
* failure is reportable instead of silently masked), and 'unreachable'
|
|
27
|
+
* (fetch itself failed — firewall/DNS/proxy; the same network problem will
|
|
28
|
+
* hit key validation next, so callers must warn about the network, not
|
|
29
|
+
* pretend the feature simply is not rolled out yet).
|
|
27
30
|
*/
|
|
28
31
|
export class DeviceAuthUnavailableError extends Error {
|
|
29
32
|
kind;
|
|
@@ -149,7 +152,7 @@ async function post(url, payload) {
|
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
154
|
catch {
|
|
152
|
-
throw new DeviceAuthUnavailableError('
|
|
155
|
+
throw new DeviceAuthUnavailableError('unreachable', `cannot reach ${url}`);
|
|
153
156
|
}
|
|
154
157
|
if (response.status === 404 || response.status === 405) {
|
|
155
158
|
throw new DeviceAuthUnavailableError('not_deployed', 'device auth endpoints not deployed');
|
package/dist/core/errors.js
CHANGED
|
@@ -39,8 +39,8 @@ export class ApiError extends CliError {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
export class NetworkError extends CliError {
|
|
42
|
-
constructor(message) {
|
|
43
|
-
super(message, EXIT.NETWORK);
|
|
42
|
+
constructor(message, suggestion) {
|
|
43
|
+
super(message, EXIT.NETWORK, suggestion);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
export class TaskFailedError extends CliError {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node's built-in fetch (undici) ignores the HTTP_PROXY / HTTPS_PROXY /
|
|
3
|
+
* NO_PROXY environment variables, so on proxy-only networks (a common
|
|
4
|
+
* server setup) every CLI request fails with a network error while curl on
|
|
5
|
+
* the same machine works fine. When a proxy is configured in the
|
|
6
|
+
* environment, swap global fetch for undici's, driven by an
|
|
7
|
+
* EnvHttpProxyAgent — it routes through HTTP(S)_PROXY and honors NO_PROXY
|
|
8
|
+
* exclusions. Without proxy env vars this is a no-op, so the zero-config
|
|
9
|
+
* path stays on the built-in fetch.
|
|
10
|
+
*/
|
|
11
|
+
export async function installProxyFromEnv() {
|
|
12
|
+
const { env } = process;
|
|
13
|
+
if (!(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy))
|
|
14
|
+
return false;
|
|
15
|
+
// Imported lazily so undici only loads when a proxy is actually configured.
|
|
16
|
+
const { EnvHttpProxyAgent, fetch: undiciFetch } = await import('undici');
|
|
17
|
+
// The constructor emits an "EnvHttpProxyAgent is experimental" warning on
|
|
18
|
+
// stderr — noise on every command for proxy users; swallow just that.
|
|
19
|
+
const emitWarning = process.emitWarning;
|
|
20
|
+
process.emitWarning = () => { };
|
|
21
|
+
let dispatcher;
|
|
22
|
+
try {
|
|
23
|
+
dispatcher = new EnvHttpProxyAgent();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
process.emitWarning = emitWarning;
|
|
27
|
+
}
|
|
28
|
+
globalThis.fetch = ((input, init) => undiciFetch(input, { ...init, dispatcher }));
|
|
29
|
+
return true;
|
|
30
|
+
}
|
package/dist/core/region.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
* Region auto-detection: a Tripo API key is valid in exactly one region
|
|
3
3
|
* (ov = openapi.tripo3d.ai, cn = openapi.tripo3d.com), so instead of asking
|
|
4
4
|
* the user "which region?" we probe both with the key and let the facts
|
|
5
|
-
* decide. First successful probe wins
|
|
5
|
+
* decide. First successful probe wins. When neither succeeds, the failure
|
|
6
|
+
* kind is reported per region: only an explicit auth rejection from a
|
|
7
|
+
* server that answered proves anything about the key — an unreachable host
|
|
8
|
+
* (firewall, DNS, missing proxy) must never be blamed on the key.
|
|
6
9
|
*/
|
|
7
10
|
import { REGION_DEFAULTS } from '../config/config.js';
|
|
8
11
|
import { TripoClient } from './client.js';
|
|
12
|
+
import { CliError } from './errors.js';
|
|
13
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
9
14
|
const PROBE_TIMEOUT_MS = 12_000;
|
|
10
15
|
export function defaultProbeBases() {
|
|
11
16
|
return { ov: REGION_DEFAULTS.ov.api, cn: REGION_DEFAULTS.cn.api };
|
|
@@ -14,7 +19,6 @@ export function defaultProbeBases() {
|
|
|
14
19
|
* Probe both regions concurrently; resolve as soon as one accepts the key
|
|
15
20
|
* (the other side may be slow/unreachable from this network — e.g. the
|
|
16
21
|
* overseas host from mainland China — and must not delay the answer).
|
|
17
|
-
* Returns undefined when both regions reject or are unreachable.
|
|
18
22
|
*/
|
|
19
23
|
export async function detectRegionForKey(key, bases = defaultProbeBases()) {
|
|
20
24
|
const probe = async (region) => {
|
|
@@ -27,18 +31,25 @@ export async function detectRegionForKey(key, bases = defaultProbeBases()) {
|
|
|
27
31
|
});
|
|
28
32
|
return { region, balance: await client.getBalance() };
|
|
29
33
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
34
|
+
catch (err) {
|
|
35
|
+
// Only an authentication error is a verdict on the key; everything
|
|
36
|
+
// else (NetworkError, 5xx, malformed response) means "no answer".
|
|
37
|
+
return err instanceof CliError && err.exitCode === EXIT.AUTH ? 'rejected' : 'unreachable';
|
|
32
38
|
}
|
|
33
39
|
};
|
|
34
40
|
return new Promise((resolve) => {
|
|
41
|
+
const failures = {};
|
|
35
42
|
let pending = 2;
|
|
36
43
|
for (const region of ['ov', 'cn']) {
|
|
37
|
-
void probe(region).then((
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
void probe(region).then((outcome) => {
|
|
45
|
+
if (typeof outcome === 'string') {
|
|
46
|
+
failures[region] = outcome;
|
|
47
|
+
if (--pending === 0)
|
|
48
|
+
resolve({ ok: false, failures });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve({ ok: true, ...outcome });
|
|
52
|
+
}
|
|
42
53
|
});
|
|
43
54
|
}
|
|
44
55
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tripo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@clack/prompts": "^1.7.0",
|
|
49
49
|
"commander": "^15.0.0",
|
|
50
50
|
"picocolors": "^1.1.1",
|
|
51
|
+
"undici": "^6.28.0",
|
|
51
52
|
"yaml": "^2.9.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
package/skill/SKILL.md
CHANGED
|
@@ -11,7 +11,19 @@ export TRIPO_API_KEY=tsk_... # highest-priority auth; no interaction needed
|
|
|
11
11
|
tripo doctor # verify key, network, balance
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
If there is no key,
|
|
14
|
+
If there is no key, run the device login **yourself** — it works without a TTY:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
tripo login --region ov # ov = international, cn = China mainland
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It prints a verification URL and a one-time code, then blocks until the human
|
|
21
|
+
approves in a browser (up to ~15 minutes — do not kill it early). Relay the URL
|
|
22
|
+
and code to the human and tell them: sign in on that page first if it asks
|
|
23
|
+
(sign-up gives free credits), enter the code, pick an API key if the account has
|
|
24
|
+
several (one is created automatically if there are none), click Authorize. The
|
|
25
|
+
key is then saved locally; never ask the human to paste a key into the chat. If
|
|
26
|
+
the code expires (exit 3), just run the same command again.
|
|
15
27
|
|
|
16
28
|
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`).
|
|
17
29
|
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
## Auth
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
export TRIPO_API_KEY=tsk_... #
|
|
7
|
-
tripo login # humans
|
|
6
|
+
export TRIPO_API_KEY=tsk_... # zero interaction when a key is already provisioned
|
|
7
|
+
tripo login # humans in a terminal: interactive browser flow
|
|
8
|
+
tripo login --region ov|cn # agents/CI without a key: headless device flow — prints a
|
|
9
|
+
# verification URL + one-time code on stderr, waits for the
|
|
10
|
+
# human to authorize in a browser (≤15 min), saves the key
|
|
8
11
|
tripo login --key tsk_... --region ov|cn
|
|
9
12
|
tripo whoami # masked key + active profile + region + balance
|
|
10
13
|
tripo logout # removes the local key of the active profile only
|