tripo-cli 0.2.0 → 0.2.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
@@ -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
 
@@ -11,7 +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 { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval } from '../core/device-auth.js';
14
+ import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
15
15
  import { AuthError, CliError, 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';
@@ -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 — only needed for the browser flow; auto-detected once a key exists')
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
- throw new UsageError(t({
173
- zh: '非交互环境请用 TRIPO_API_KEY 环境变量或 tripo login --key tsk_...',
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,71 @@ 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
+ // No paste fallback without a TTY — fail with every remaining option spelled out.
234
+ throw new CliError(t({
235
+ zh: `浏览器授权服务不可用(${err.message});请设置 TRIPO_API_KEY,运行 tripo login --key tsk_...,或在交互终端里运行 tripo login`,
236
+ en: `device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`,
237
+ }), EXIT.AUTH);
238
+ }
239
+ throw err;
240
+ }
241
+ const minutes = Math.max(1, Math.round(grant.expires_in / 60));
242
+ log(t({
243
+ zh: [
244
+ '需要在浏览器完成授权;本命令会一直等待,直到授权完成或过期。请转告用户:',
245
+ ` 1. 打开: ${grant.verification_url_complete}`,
246
+ ' (页面如果提示登录,先登录或注册 Tripo 账号 — 新账号有免费额度)',
247
+ ` 2. 在「Verification code」输入框填入终端码: ${grant.user_code}`,
248
+ ' 3. 账号已有多把 API Key 时页面会让用户选择一把;一把没有则自动创建',
249
+ ` 4. 点击「Authorize」。此码 ${minutes} 分钟内有效`,
250
+ ].join('\n'),
251
+ en: [
252
+ 'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:',
253
+ ` 1. Open: ${grant.verification_url_complete}`,
254
+ ' (if the page asks to sign in, sign in or sign up first — new accounts get free credits)',
255
+ ` 2. Enter this one-time code in the "Verification code" field: ${grant.user_code}`,
256
+ ' 3. An account with several API keys is asked to pick one; with none, a key is created automatically',
257
+ ` 4. Click "Authorize". The code expires in ${minutes} minutes`,
258
+ ].join('\n'),
259
+ }));
260
+ const result = await waitForDeviceApproval(region, grant);
261
+ if (result.status === 'denied') {
262
+ throw new CliError(t({ zh: '授权被拒绝;重新运行 tripo login 再试', en: 'authorization denied — run "tripo login" again to retry' }), EXIT.AUTH);
263
+ }
264
+ if (result.status !== 'approved') {
265
+ throw new CliError(t({ zh: '授权超时或已过期;重新运行 tripo login 获取新的终端码', en: 'authorization expired — run "tripo login" again for a fresh code' }), EXIT.AUTH);
266
+ }
267
+ const saved = await validateAndSave(result.apikey, region, profile);
268
+ 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})` }));
269
+ }
207
270
  /**
208
271
  * Preferred path: RFC 8628 device flow — the browser does the sign-in and a
209
272
  * one-click approve, the key arrives over the CLI↔console-API channel, and
@@ -357,12 +420,13 @@ async function validateAndSave(key, selectedRegion, profile) {
357
420
  const client = new TripoClient({ apiKey: key });
358
421
  try {
359
422
  const balance = await client.getBalance();
360
- finishSave(key, selectedRegion ?? 'ov', balance.balance, profile);
423
+ const region = selectedRegion ?? 'ov';
424
+ finishSave(key, region, balance.balance, profile);
425
+ return { region, balance: balance.balance };
361
426
  }
362
427
  catch (err) {
363
428
  throw asAuthError(err);
364
429
  }
365
- return;
366
430
  }
367
431
  const detected = await detectRegionForKey(key);
368
432
  if (!detected) {
@@ -378,6 +442,7 @@ async function validateAndSave(key, selectedRegion, profile) {
378
442
  }));
379
443
  }
380
444
  finishSave(key, detected.region, detected.balance.balance, profile);
445
+ return { region: detected.region, balance: detected.balance.balance };
381
446
  }
382
447
  function finishSave(key, region, balance, profile) {
383
448
  // Saving activates the profile: "the account I just logged into is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",
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, ask the human to run `tripo login` or to provide `TRIPO_API_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_... # best for agents/CI — zero interaction, bypasses profiles
7
- tripo login # humans: browser flow (or --paste / --key tsk_...)
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