tripo-cli 0.1.0 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +19 -3
  2. package/dist/commands/completion.js +1 -1
  3. package/dist/commands/config-cmd.js +32 -17
  4. package/dist/commands/doctor.js +6 -5
  5. package/dist/commands/home.js +4 -2
  6. package/dist/commands/login.js +129 -32
  7. package/dist/config/config.js +103 -14
  8. package/dist/core/client.js +16 -2
  9. package/dist/core/device-auth.js +41 -9
  10. package/dist/program.js +21 -1
  11. package/package.json +2 -2
  12. package/skill/commands/account.md +15 -4
  13. package/dist/src/ai/experts.js +0 -115
  14. package/dist/src/ai/llm.js +0 -88
  15. package/dist/src/ai/plan-card.js +0 -79
  16. package/dist/src/ai/wizard.js +0 -104
  17. package/dist/src/cli.js +0 -7
  18. package/dist/src/commands/account.js +0 -27
  19. package/dist/src/commands/ai.js +0 -113
  20. package/dist/src/commands/batch.js +0 -148
  21. package/dist/src/commands/completion.js +0 -84
  22. package/dist/src/commands/config-cmd.js +0 -84
  23. package/dist/src/commands/docs.js +0 -30
  24. package/dist/src/commands/doctor.js +0 -94
  25. package/dist/src/commands/generate.js +0 -81
  26. package/dist/src/commands/home.js +0 -68
  27. package/dist/src/commands/login.js +0 -225
  28. package/dist/src/commands/make.js +0 -142
  29. package/dist/src/commands/mcp.js +0 -194
  30. package/dist/src/commands/process-groups.js +0 -164
  31. package/dist/src/commands/shared.js +0 -75
  32. package/dist/src/commands/task.js +0 -127
  33. package/dist/src/commands/topup.js +0 -61
  34. package/dist/src/commands/view.js +0 -97
  35. package/dist/src/config/config.js +0 -79
  36. package/dist/src/config/context.js +0 -44
  37. package/dist/src/config/history.js +0 -104
  38. package/dist/src/core/client.js +0 -120
  39. package/dist/src/core/download.js +0 -107
  40. package/dist/src/core/errors.js +0 -64
  41. package/dist/src/core/output.js +0 -72
  42. package/dist/src/core/requests.js +0 -169
  43. package/dist/src/core/task-service.js +0 -161
  44. package/dist/src/core/upload.js +0 -36
  45. package/dist/src/core/viewer-assets.js +0 -24
  46. package/dist/src/core/viewer.js +0 -59
  47. package/dist/src/core/watch.js +0 -41
  48. package/dist/src/i18n.js +0 -19
  49. package/dist/src/knowledge/chains.js +0 -152
  50. package/dist/src/knowledge/error-catalog.js +0 -91
  51. package/dist/src/knowledge/models.js +0 -110
  52. package/dist/src/knowledge/params.js +0 -174
  53. package/dist/src/knowledge/scenarios.js +0 -112
  54. package/dist/src/pipeline/make.js +0 -233
  55. package/dist/src/pipeline/runner.js +0 -73
  56. package/dist/src/pipeline/stdin.js +0 -40
  57. package/dist/src/program.js +0 -147
  58. package/dist/src/types.js +0 -2
  59. package/dist/src/util/input-detect.js +0 -92
  60. package/dist/src/util/notify.js +0 -17
  61. package/dist/src/util/open.js +0 -18
  62. package/dist/src/util/spinner.js +0 -62
  63. package/dist/src/util/term-image.js +0 -28
  64. package/dist/src/version.js +0 -10
@@ -7,8 +7,11 @@
7
7
  * The flow is optional by design — when the backend feature flag is off the
8
8
  * /cli/device endpoints 404 and the caller falls back to paste login.
9
9
  */
10
+ import { randomBytes } from 'node:crypto';
11
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
12
  import { hostname } from 'node:os';
11
- import { REGION_DEFAULTS } from '../config/config.js';
13
+ import { join } from 'node:path';
14
+ import { REGION_DEFAULTS, tripoHome } from '../config/config.js';
12
15
  import { sleep } from './client.js';
13
16
  export const DEVICE_CODE_EXPIRED = 4290;
14
17
  export const DEVICE_CODE_SLOW_DOWN = 4291;
@@ -32,19 +35,48 @@ export class DeviceAuthUnavailableError extends Error {
32
35
  export function consoleApiBaseUrl(region) {
33
36
  return process.env.TRIPO_CONSOLE_API_BASE_URL || REGION_DEFAULTS[region].consoleApi;
34
37
  }
38
+ /**
39
+ * A persistent per-machine fingerprint (4 hex chars in ~/.tripo/device-id).
40
+ * The backend rotates keys *by name*, and the name used to be derived from
41
+ * the hostname alone — but default hostnames collide constantly (every
42
+ * other Mac is "MacBook-Pro.local"), and a collision means machine B's
43
+ * login silently revokes machine A's key. The fingerprint makes "same
44
+ * machine" mean "same fingerprint", not "same hostname by luck".
45
+ */
46
+ export function deviceId() {
47
+ const file = join(tripoHome(), 'device-id');
48
+ try {
49
+ const existing = readFileSync(file, 'utf8').trim();
50
+ if (/^[0-9a-f]{4}$/.test(existing))
51
+ return existing;
52
+ }
53
+ catch { }
54
+ const fresh = randomBytes(2).toString('hex');
55
+ try {
56
+ mkdirSync(tripoHome(), { recursive: true, mode: 0o700 });
57
+ writeFileSync(file, fresh + '\n');
58
+ }
59
+ catch { } // unwritable home: fall back to a per-login id — worst case is key buildup, never cross-machine revocation
60
+ return fresh;
61
+ }
35
62
  /**
36
63
  * The backend whitelists key names to [a-zA-Z0-9 _.-]{1,32} — hostnames with
37
64
  * anything else (e.g. non-ASCII machine names) would get a hard 400, so strip
38
- * offending characters up front and fall back to a constant when nothing
39
- * survives.
65
+ * offending characters up front. The backend also *rotates* keys by name
66
+ * (same name → old key revoked), so the name must be unique per (machine,
67
+ * profile): the device fingerprint separates same-named machines, the
68
+ * profile separates accounts on one machine. The hostname is only there for
69
+ * humans reading the /keys page; it is the part that gets truncated first.
40
70
  */
41
- export function deviceKeyName(host = hostname()) {
42
- const cleaned = `CLI - ${host}`.replace(/[^a-zA-Z0-9 _.-]/g, '').replace(/\s+/g, ' ').trim();
43
- const capped = cleaned.slice(0, 32).trim();
44
- return capped.length >= 7 ? capped : 'Tripo CLI';
71
+ export function deviceKeyName(profile, host = hostname(), id = deviceId()) {
72
+ const prefix = profile ? `CLI ${profile}` : 'CLI';
73
+ const cleanedHost = host.replace(/[^a-zA-Z0-9 _.-]/g, '').replace(/\s+/g, ' ').trim();
74
+ const budget = 32 - `${prefix} - ${id}`.length;
75
+ const shownHost = cleanedHost.slice(0, Math.max(budget, 0)).trim();
76
+ return `${prefix} - ${shownHost} ${id}`.replace(/\s+/g, ' ').slice(0, 32).trim();
45
77
  }
46
- export async function requestDeviceCode(region) {
47
- const body = await post(`${consoleApiBaseUrl(region)}/cli/device/code`, { name: deviceKeyName() });
78
+ export async function requestDeviceCode(region, profile) {
79
+ const body = await post(`${consoleApiBaseUrl(region)}/cli/device/code`, { name: deviceKeyName(profile) });
48
80
  const grant = body.data;
49
81
  if (!grant ||
50
82
  typeof grant.device_code !== 'string' ||
package/dist/program.js CHANGED
@@ -37,6 +37,7 @@ export function buildProgram() {
37
37
  .option('--yes', 'skip confirmations (auto-enabled when headless)')
38
38
  .option('--quiet', 'suppress progress logs on stderr')
39
39
  .option('--no-open', 'never auto-open the browser/viewer')
40
+ .option('--profile <name>', 'use a named account profile for this command (like TRIPO_PROFILE)')
40
41
  .enablePositionalOptions()
41
42
  .showSuggestionAfterError(true);
42
43
  program.hook('preAction', (_thisCommand, actionCommand) => {
@@ -46,6 +47,11 @@ export function buildProgram() {
46
47
  quiet: resolveGlobalFlag(actionCommand, 'quiet', false),
47
48
  noOpen: !resolveGlobalFlag(actionCommand, 'open', true),
48
49
  });
50
+ // --profile rides the TRIPO_PROFILE env var: config resolution already
51
+ // reads it, so one assignment covers every code path.
52
+ const profile = resolveGlobalString(actionCommand, 'profile');
53
+ if (profile)
54
+ process.env.TRIPO_PROFILE = profile;
49
55
  const lang = loadConfig().language;
50
56
  if (lang)
51
57
  setLocale(lang);
@@ -70,7 +76,7 @@ export function buildProgram() {
70
76
  return program;
71
77
  }
72
78
  /**
73
- * The 4 global flags live on every leaf command so `tripo balance --json`
79
+ * The global flags live on every leaf command so `tripo balance --json`
74
80
  * works the way people type it (prefix `tripo --json balance` works too).
75
81
  */
76
82
  function attachGlobalFlags(cmd) {
@@ -87,6 +93,9 @@ function attachGlobalFlags(cmd) {
87
93
  cmd.option('--quiet', 'suppress progress logs on stderr');
88
94
  if (!has('--no-open'))
89
95
  cmd.option('--no-open', 'never auto-open the browser/viewer');
96
+ // login/logout keep their own --profile (same semantics, different help text).
97
+ if (!has('--profile'))
98
+ cmd.option('--profile <name>', 'use a named account profile');
90
99
  }
91
100
  /**
92
101
  * Resolve a flag along the command chain, innermost explicit value first.
@@ -101,6 +110,17 @@ function resolveGlobalFlag(cmd, key, def) {
101
110
  }
102
111
  return def;
103
112
  }
113
+ /** Same walk for string-valued global options (--profile). */
114
+ function resolveGlobalString(cmd, key) {
115
+ for (let c = cmd; c; c = c.parent) {
116
+ const source = c.getOptionValueSource(key);
117
+ if (source !== undefined && source !== 'default') {
118
+ const value = c.getOptionValue(key);
119
+ return typeof value === 'string' ? value : undefined;
120
+ }
121
+ }
122
+ return undefined;
123
+ }
104
124
  /** Run the CLI; converts errors into the stable exit-code contract. */
105
125
  export async function runCli(argv) {
106
126
  const program = buildProgram();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tripo-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -34,7 +34,7 @@
34
34
  "url": "https://github.com/vast-enterprise/Tripo-API-CLI/issues"
35
35
  },
36
36
  "scripts": {
37
- "build": "tsc -p tsconfig.build.json && node scripts/postbuild.mjs",
37
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/postbuild.mjs",
38
38
  "dev": "tsx src/cli.ts",
39
39
  "typecheck": "tsc --noEmit",
40
40
  "lint": "eslint src tests",
@@ -3,15 +3,25 @@
3
3
  ## Auth
4
4
 
5
5
  ```bash
6
- export TRIPO_API_KEY=tsk_... # best for agents/CI — zero interaction
6
+ export TRIPO_API_KEY=tsk_... # best for agents/CI — zero interaction, bypasses profiles
7
7
  tripo login # humans: browser flow (or --paste / --key tsk_...)
8
8
  tripo login --key tsk_... --region ov|cn
9
- tripo whoami # masked key + region + balance
10
- tripo logout # removes the local key only
9
+ tripo whoami # masked key + active profile + region + balance
10
+ tripo logout # removes the local key of the active profile only
11
11
  ```
12
12
 
13
13
  Region decides the API host: ov → `openapi.tripo3d.ai`, cn → `openapi.tripo3d.com`.
14
14
 
15
+ ## Multiple accounts (profiles)
16
+
17
+ ```bash
18
+ tripo login --profile work-cn # log a second account into its own profile (becomes active)
19
+ tripo use work-cn # switch the active profile (no arg = interactive picker)
20
+ tripo balance --profile work-cn # one-shot override on any command; TRIPO_PROFILE env works too
21
+ ```
22
+
23
+ One profile = one account (key + region). Unknown `--profile` fails with exit 3 and the fix in the message — do not retry blindly.
24
+
15
25
  ## Credits
16
26
 
17
27
  ```bash
@@ -34,7 +44,8 @@ tripo config set llm.model gpt-4o-mini
34
44
  tripo config context default_scenario=game-mobile output_dir=./assets # per-directory .tripo/context.json
35
45
  ```
36
46
 
37
- Env overrides: `TRIPO_API_KEY` `TRIPO_REGION` `TRIPO_API_BASE_URL` `TRIPO_LLM_BASE_URL` `TRIPO_LLM_API_KEY` `TRIPO_LLM_MODEL`.
47
+ Env overrides: `TRIPO_API_KEY` `TRIPO_PROFILE` `TRIPO_REGION` `TRIPO_API_BASE_URL` `TRIPO_LLM_BASE_URL` `TRIPO_LLM_API_KEY` `TRIPO_LLM_MODEL`.
48
+ `region`/`api_base_url`/`platform_base_url` are stored per profile; `language`/`llm.*`/`default_output_dir` are shared.
38
49
 
39
50
  ## tripo doctor
40
51
 
@@ -1,115 +0,0 @@
1
- /**
2
- * Multi-expert agent orchestration (conversation mode).
3
- *
4
- * Producer (router) → domain specialist(s) → plan card. The LLM only parses
5
- * intent and phrases advice; every decision it may make is constrained to the
6
- * knowledge base (scenario ids, step names, parameter windows), and the
7
- * result is re-validated locally before anything is spent.
8
- */
9
- import { getLocale, t, tList } from '../i18n.js';
10
- import { parseThenExpression, validateChain, STEPS } from '../knowledge/chains.js';
11
- import { chooseModel, detectLowPolyIntent } from '../knowledge/models.js';
12
- import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
13
- import { chatJson } from './llm.js';
14
- const SCENARIO_TABLE = SCENARIOS.map((s) => `- ${s.id}: ${s.description.en} | model=${s.model} | chain=${s.chain.map((c) => c.step).join('→') || 'none'} | deliver=${s.deliverables.join('/')}`).join('\n');
15
- const STEP_TABLE = STEPS.map((s) => s.name).join(', ');
16
- const PRODUCER_SYSTEM = `You are the producer/router of the Tripo 3D generation CLI.
17
- Classify the user's request into exactly one scenario and extract a clean English 3D generation prompt.
18
-
19
- Scenarios:
20
- ${SCENARIO_TABLE}
21
-
22
- Rules:
23
- - "prompt" must describe the object to generate (translate to English, keep it concrete, <= 200 chars).
24
- - Set "face_limit" only when the user states a face/polygon budget.
25
- - Set "needs_animation" true only when the user wants skeleton/animations.
26
- - If one critical piece of information is missing and materially changes the plan, set "question" (one short question in the user's language). Otherwise omit it.
27
- Respond with JSON only: {"scenario","prompt","face_limit","needs_animation","question","notes"}`;
28
- const SPECIALIST_PROMPTS = {
29
- 'game-mobile': `You are a senior game technical artist (mobile). Budgets: 500–20000 faces, 1–2K textures, clean topology, FBX/GLB delivery. P1 model is used (no quad/parts/geometry_quality).`,
30
- 'game-pc': `You are a senior game technical artist (PC/console). PBR detailed textures, optional decimate for LODs (bake=true), FBX delivery with engine presets.`,
31
- film: `You are a film/offline-rendering artist. quad=true subdivision-friendly topology (FBX output), geometry_quality=detailed, texture_quality=detailed, 4K textures, USD/OBJ handoff via convert.`,
32
- print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via scale_factor/auto_size.`,
33
- 'ar-web': `You are a realtime AR/Web 3D engineer. Keep faces <= 20000 via decimate, 2K textures, deliver GLB + USDZ (iOS QuickLook).`,
34
- anim: `You are a character animation TD. Chain: rig-check → rig (rig-v2.0, spec tripo|mixamo) → retarget (<=5 preset animations, billed per animation). Mixamo/Unity Humanoid → spec=mixamo.`,
35
- toy: `You are a stylized collectible designer. P1 low-poly or stylize steps (lego/voxel/voronoi/minecraft). STL via convert for physical prints.`,
36
- };
37
- function specialistSystem(scenarioId) {
38
- const persona = SPECIALIST_PROMPTS[scenarioId] ?? SPECIALIST_PROMPTS['game-mobile'];
39
- return `${persona}
40
- You fine-tune a Tripo generation plan. You may adjust generation params and the processing chain.
41
- Valid chain steps: ${STEP_TABLE}. Chain syntax: "step:key=value,step2" (e.g. "texture,rig,convert:format=FBX").
42
- Constraints: pbr=>texture; generate_parts excludes texture/pbr/quad; P1 face_limit 48-20000 and forbids quad/smart_low_poly/generate_parts/geometry_quality; retarget <= 5 animations.
43
- Respond with JSON only: {"params":{...},"chain":"...","notes":["short practical notes in ${getLocale() === 'zh' ? 'Chinese' : 'English'}"]}. Omit fields you don't want to change.`;
44
- }
45
- /**
46
- * Run the expert pipeline. `askUser` is called when the producer needs one
47
- * clarification (interactive mode only); return undefined to skip.
48
- */
49
- export async function buildExpertPlan(request, askUser) {
50
- const messages = [
51
- { role: 'system', content: PRODUCER_SYSTEM },
52
- { role: 'user', content: request },
53
- ];
54
- let producer = await chatJson(messages);
55
- if (producer.question && askUser) {
56
- const answer = await askUser(producer.question);
57
- if (answer) {
58
- messages.push({ role: 'assistant', content: JSON.stringify(producer) });
59
- messages.push({ role: 'user', content: `Additional info: ${answer}. Re-classify with the complete picture; do not ask again.` });
60
- producer = await chatJson(messages);
61
- }
62
- }
63
- const scenario = getScenario(producer.scenario) ?? getScenario('game-mobile');
64
- const specialist = await chatJson([
65
- { role: 'system', content: specialistSystem(scenario.id) },
66
- {
67
- role: 'user',
68
- content: `User request: ${request}\nPrompt: ${producer.prompt}\nBase plan: model=${scenario.model} params=${JSON.stringify(scenario.genParams)} chain=${scenario.chain.map((c) => c.step).join('→') || 'none'}\nneeds_animation=${producer.needs_animation ?? false} face_limit=${producer.face_limit ?? 'default'}`,
69
- },
70
- ]).catch(() => ({}));
71
- // Merge + re-validate everything the LLM produced.
72
- const params = { ...scenario.genParams, ...(specialist.params ?? {}) };
73
- if (producer.face_limit)
74
- params.face_limit = producer.face_limit;
75
- let chain = scenario.chain;
76
- if (specialist.chain) {
77
- try {
78
- chain = parseThenExpression(specialist.chain);
79
- }
80
- catch {
81
- chain = scenario.chain; // discard invalid expert chain, keep the preset
82
- }
83
- }
84
- if (producer.needs_animation && !chain.some((s) => s.step === 'rig')) {
85
- chain = [
86
- ...chain.filter((s) => s.step !== 'convert'),
87
- { step: 'rig-check', args: {} },
88
- { step: 'rig', args: { model: 'rig-v2.0' } },
89
- { step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
90
- ];
91
- }
92
- const choice = chooseModel({
93
- explicit: scenario.model,
94
- faceLimit: typeof params.face_limit === 'number' ? params.face_limit : undefined,
95
- lowPoly: detectLowPolyIntent(request),
96
- });
97
- try {
98
- validateChain(chain, 'text_to_model');
99
- }
100
- catch {
101
- chain = scenario.chain;
102
- }
103
- const card = {
104
- inputSummary: producer.prompt,
105
- scenario: scenario.id,
106
- model: choice.model,
107
- modelReason: t({ zh: '制片人路由 + 场景预设', en: 'producer routing + scenario preset' }),
108
- genParams: params,
109
- chain,
110
- deliverables: scenario.deliverables,
111
- notes: tList(scenario.notes),
112
- expertNotes: [...(producer.notes ?? []), ...(specialist.notes ?? [])],
113
- };
114
- return { card, scenarioId: scenario.id, prompt: producer.prompt, chain, params };
115
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * BYO-LLM client: any OpenAI-compatible /chat/completions endpoint.
3
- * Configure via `tripo config set llm.base_url/llm.api_key/llm.model`
4
- * or env TRIPO_LLM_BASE_URL / TRIPO_LLM_API_KEY / TRIPO_LLM_MODEL.
5
- */
6
- import { loadConfig } from '../config/config.js';
7
- import { CliError } from '../core/errors.js';
8
- import { EXIT } from '../knowledge/error-catalog.js';
9
- export function resolveLlmConfig() {
10
- const env = {
11
- base_url: process.env.TRIPO_LLM_BASE_URL,
12
- api_key: process.env.TRIPO_LLM_API_KEY,
13
- model: process.env.TRIPO_LLM_MODEL,
14
- };
15
- if (env.base_url && env.model)
16
- return { base_url: env.base_url, api_key: env.api_key, model: env.model };
17
- const config = loadConfig().llm;
18
- if (config?.base_url && config.model)
19
- return config;
20
- return undefined;
21
- }
22
- export async function chatCompletion(messages, options = {}) {
23
- const config = resolveLlmConfig();
24
- if (!config)
25
- throw new CliError('no LLM configured', EXIT.USAGE);
26
- const url = `${config.base_url.replace(/\/$/, '')}/chat/completions`;
27
- const controller = new AbortController();
28
- const timer = setTimeout(() => controller.abort(), 60_000);
29
- let response;
30
- try {
31
- response = await fetch(url, {
32
- method: 'POST',
33
- headers: {
34
- 'Content-Type': 'application/json',
35
- ...(config.api_key ? { Authorization: `Bearer ${config.api_key}` } : {}),
36
- },
37
- body: JSON.stringify({
38
- model: config.model,
39
- messages,
40
- temperature: 0.2,
41
- ...(options.json ? { response_format: { type: 'json_object' } } : {}),
42
- }),
43
- signal: controller.signal,
44
- });
45
- }
46
- catch (err) {
47
- throw new CliError(`LLM request failed: ${err instanceof Error ? err.message : String(err)}`, EXIT.NETWORK);
48
- }
49
- finally {
50
- clearTimeout(timer);
51
- }
52
- if (!response.ok) {
53
- const body = await response.text().catch(() => '');
54
- throw new CliError(`LLM endpoint returned HTTP ${response.status}: ${body.slice(0, 300)}`, EXIT.NETWORK);
55
- }
56
- const data = (await response.json());
57
- const content = data.choices?.[0]?.message?.content;
58
- if (!content)
59
- throw new CliError('LLM returned an empty response', EXIT.NETWORK);
60
- return content;
61
- }
62
- /** Chat expecting a JSON object back; tolerates markdown fences. */
63
- export async function chatJson(messages) {
64
- const raw = await chatCompletion(messages, { json: true }).catch(async (err) => {
65
- // Some endpoints reject response_format; retry without it.
66
- if (err instanceof CliError && err.exitCode === EXIT.NETWORK) {
67
- return chatCompletion(messages, { json: false });
68
- }
69
- throw err;
70
- });
71
- return extractJson(raw);
72
- }
73
- export function extractJson(raw) {
74
- const trimmed = raw.trim();
75
- const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
76
- const candidate = fenced ? fenced[1] : trimmed;
77
- const start = candidate.indexOf('{');
78
- const end = candidate.lastIndexOf('}');
79
- if (start === -1 || end === -1 || end < start) {
80
- throw new CliError(`LLM did not return JSON: ${raw.slice(0, 200)}`, EXIT.NETWORK);
81
- }
82
- try {
83
- return JSON.parse(candidate.slice(start, end + 1));
84
- }
85
- catch {
86
- throw new CliError(`LLM returned invalid JSON: ${candidate.slice(0, 200)}`, EXIT.NETWORK);
87
- }
88
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * The plan card: one shared "here is what I will do and what it costs"
3
- * confirmation format used by both the wizard (zero-LLM) and the expert
4
- * conversation mode. Decisions come from the knowledge base either way.
5
- */
6
- import pc from 'picocolors';
7
- import { t } from '../i18n.js';
8
- import { getScenario } from '../knowledge/scenarios.js';
9
- /** Cost-impact annotations derived from the parameter set. */
10
- export function costNotes(card) {
11
- const notes = [];
12
- const params = card.genParams;
13
- if (params.texture === false) {
14
- notes.push(t({ zh: '白模生成:不消耗贴图积分', en: 'bare geometry: no texture credits' }));
15
- }
16
- if (params.texture_quality === 'detailed' || params.texture_quality === 'extreme') {
17
- notes.push(t({ zh: `贴图质量 ${params.texture_quality}:价签更高`, en: `texture_quality ${params.texture_quality}: higher price tier` }));
18
- }
19
- if (params.geometry_quality === 'detailed') {
20
- notes.push(t({ zh: 'geometry detailed(Ultra):加收 HD credit', en: 'geometry detailed (Ultra): extra HD credit' }));
21
- }
22
- if (params.quad === true) {
23
- notes.push(t({ zh: 'quad 四边面:价签更高,输出 FBX', en: 'quad topology: higher price tier, FBX output' }));
24
- }
25
- if (params.generate_parts === true) {
26
- notes.push(t({ zh: '分件生成:价签更高,出裸几何', en: 'part generation: higher price tier, bare geometry' }));
27
- }
28
- for (const step of card.chain) {
29
- if (step.step === 'convert' && Object.keys(step.args).length > 1) {
30
- notes.push(t({ zh: 'convert 非默认参数:按 complex 价签计费', en: 'non-default convert options bill as complex convert' }));
31
- }
32
- if (step.step === 'retarget') {
33
- const count = Array.isArray(step.args.animations) ? step.args.animations.length : 1;
34
- notes.push(t({ zh: `retarget ${count} 条动画:按条计费`, en: `retarget ${count} animation(s): billed per animation` }));
35
- }
36
- }
37
- return notes;
38
- }
39
- /** Render the card for terminal display (stderr). */
40
- export function renderPlanCard(card) {
41
- const scenario = card.scenario ? getScenario(card.scenario) : undefined;
42
- const lines = [];
43
- const title = t({ zh: '方案卡', en: 'Plan' });
44
- lines.push(pc.bold(`── ${title} ──────────────────────────`));
45
- lines.push(`${t({ zh: '输入', en: 'input' })}: ${card.inputSummary}`);
46
- if (scenario)
47
- lines.push(`${t({ zh: '场景', en: 'scenario' })}: ${scenario.id} (${t(scenario.label)})`);
48
- if (card.model) {
49
- lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
50
- }
51
- const params = Object.entries(card.genParams);
52
- if (params.length > 0) {
53
- lines.push(`${t({ zh: '参数', en: 'params' })}: ${params.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
54
- }
55
- if (card.chain.length > 0) {
56
- const chainStr = card.chain
57
- .map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
58
- .join(' → ');
59
- lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
60
- }
61
- if (card.deliverables.length > 0) {
62
- lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
63
- }
64
- const cost = costNotes(card);
65
- if (cost.length > 0) {
66
- lines.push(`${t({ zh: '成本影响', en: 'cost impact' })}:`);
67
- for (const note of cost)
68
- lines.push(` · ${note}`);
69
- }
70
- for (const note of card.notes)
71
- lines.push(pc.dim(` ${note}`));
72
- if (card.expertNotes && card.expertNotes.length > 0) {
73
- lines.push(`${t({ zh: '专家意见', en: 'expert notes' })}:`);
74
- for (const note of card.expertNotes)
75
- lines.push(` · ${note}`);
76
- }
77
- lines.push(pc.bold('──────────────────────────────────'));
78
- return lines.join('\n');
79
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * Wizard mode (zero-LLM): 3–4 multiple-choice questions drive the same
3
- * knowledge-base decision tree that experts use. Every user gets 80% of the
4
- * intelligence with no LLM configured.
5
- */
6
- import * as p from '@clack/prompts';
7
- import { CliError, UsageError } from '../core/errors.js';
8
- import { isInteractive } from '../core/output.js';
9
- import { t, tList } from '../i18n.js';
10
- import { EXIT } from '../knowledge/error-catalog.js';
11
- import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
12
- export async function runWizard(initialPrompt) {
13
- if (!isInteractive()) {
14
- throw new UsageError(t({
15
- zh: '向导需要交互终端;无头环境请直接用 tripo make --for <场景> 或配置 LLM 后用 tripo ai --yes',
16
- en: 'the wizard needs an interactive terminal; headless callers should use "tripo make --for <scenario>" instead',
17
- }));
18
- }
19
- p.intro('tripo ai');
20
- let prompt = initialPrompt?.trim() ?? '';
21
- if (!prompt) {
22
- const answer = await p.text({
23
- message: t({ zh: '想做个什么?一句话描述(或图片路径)', en: 'What do you want to make? one sentence (or an image path)' }),
24
- validate: (v) => (v && v.trim().length > 0 ? undefined : t({ zh: '说点什么吧', en: 'say something' })),
25
- });
26
- if (p.isCancel(answer))
27
- throw new CliError('cancelled', EXIT.FATAL);
28
- prompt = answer.trim();
29
- }
30
- const scenarioAnswer = await p.select({
31
- message: t({ zh: '用在什么地方?', en: 'What is it for?' }),
32
- options: SCENARIOS.map((s) => ({ value: s.id, label: `${t(s.label)} — ${t(s.description)}` })),
33
- });
34
- if (p.isCancel(scenarioAnswer))
35
- throw new CliError('cancelled', EXIT.FATAL);
36
- const scenario = getScenario(scenarioAnswer);
37
- let engine;
38
- if (scenarioAnswer === 'game-mobile' || scenarioAnswer === 'game-pc') {
39
- const engineAnswer = await p.select({
40
- message: t({ zh: '目标引擎/软件?', en: 'Target engine/software?' }),
41
- options: [
42
- { value: 'unity', label: 'Unity' },
43
- { value: 'unreal', label: 'Unreal' },
44
- { value: 'blender', label: 'Blender' },
45
- { value: 'any', label: t({ zh: '不确定/通用', en: 'not sure / generic' }) },
46
- ],
47
- });
48
- if (p.isCancel(engineAnswer))
49
- throw new CliError('cancelled', EXIT.FATAL);
50
- engine = engineAnswer;
51
- }
52
- let animations = false;
53
- if (scenarioAnswer !== 'print' && scenarioAnswer !== 'film') {
54
- const animAnswer = await p.confirm({
55
- message: t({ zh: '需要骨骼/动画吗?', en: 'Do you need a skeleton / animations?' }),
56
- initialValue: scenarioAnswer === 'anim',
57
- });
58
- if (p.isCancel(animAnswer))
59
- throw new CliError('cancelled', EXIT.FATAL);
60
- animations = animAnswer;
61
- }
62
- const { chain, notes } = adjustChain(scenario.id, engine, animations);
63
- const card = {
64
- inputSummary: prompt,
65
- scenario: scenario.id,
66
- model: scenario.model,
67
- modelReason: t({ zh: '场景预设', en: 'scenario preset' }),
68
- genParams: scenario.genParams,
69
- chain,
70
- deliverables: scenario.deliverables,
71
- notes: [...tList(scenario.notes), ...notes],
72
- };
73
- return { card, options: { prompt, scenario: scenario.id, chain } };
74
- }
75
- /** Engine/animation adjustments on top of the scenario chain. */
76
- export function adjustChain(scenarioId, engine, animations) {
77
- const scenario = getScenario(scenarioId);
78
- let chain = scenario.chain.map((s) => ({ step: s.step, args: { ...s.args } }));
79
- const notes = [];
80
- if (animations && !chain.some((s) => s.step === 'rig')) {
81
- const rigArgs = { model: 'rig-v2.0', out_format: 'fbx' };
82
- if (engine === 'unity') {
83
- rigArgs.spec = 'mixamo';
84
- notes.push(t({ zh: 'Unity Humanoid → spec=mixamo', en: 'Unity Humanoid → spec=mixamo' }));
85
- }
86
- chain = [
87
- ...chain.filter((s) => s.step !== 'convert'),
88
- { step: 'rig-check', args: {} },
89
- { step: 'rig', args: rigArgs },
90
- { step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'], out_format: 'fbx' } },
91
- ];
92
- notes.push(t({ zh: '动画链输出 FBX(骨骼+动画)', en: 'animation chain outputs FBX (skeleton + animations)' }));
93
- }
94
- if (engine && !animations) {
95
- const convert = chain.find((s) => s.step === 'convert');
96
- if (convert && convert.args.format === 'FBX') {
97
- if (engine === 'blender')
98
- convert.args.fbx_preset = 'blender';
99
- if (engine === 'unreal' || engine === 'unity')
100
- convert.args.export_orientation = '+x';
101
- }
102
- }
103
- return { chain, notes };
104
- }
package/dist/src/cli.js DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCli } from './program.js';
3
- process.on('SIGINT', () => {
4
- process.stderr.write('\n');
5
- process.exit(130);
6
- });
7
- void runCli(process.argv);
@@ -1,27 +0,0 @@
1
- /** `tripo balance` / `tripo usage` — account visibility. */
2
- import { TripoClient } from '../core/client.js';
3
- import { emitResult } from '../core/output.js';
4
- import { t } from '../i18n.js';
5
- export function registerAccountCommands(program) {
6
- program
7
- .command('balance')
8
- .description('show available and frozen credits')
9
- .action(async () => {
10
- const data = await new TripoClient().getBalance();
11
- emitResult(data, t({
12
- zh: `余额 ${data.balance} 积分(冻结 ${data.frozen})`,
13
- en: `balance: ${data.balance} credits (${data.frozen} frozen)`,
14
- }));
15
- });
16
- program
17
- .command('usage')
18
- .description('recent task credit usage')
19
- .option('--limit <n>', 'max rows to show', (v) => Number(v), 20)
20
- .action(async (opts) => {
21
- const items = (await new TripoClient().getUsage()).slice(0, opts.limit);
22
- const human = items
23
- .map((i) => `${(i.created_at ?? '').slice(0, 19).padEnd(20)} ${i.type.padEnd(24)} ${String(i.credits_consumed ?? 0).padStart(6)} cr ${i.task_id}`)
24
- .join('\n');
25
- emitResult(items, human || t({ zh: '暂无用量记录', en: 'no usage yet' }));
26
- });
27
- }