tripo-cli 0.1.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 +105 -0
- package/dist/ai/experts.js +115 -0
- package/dist/ai/llm.js +88 -0
- package/dist/ai/plan-card.js +79 -0
- package/dist/ai/wizard.js +105 -0
- package/dist/cli.js +7 -0
- package/dist/commands/account.js +27 -0
- package/dist/commands/ai.js +114 -0
- package/dist/commands/batch.js +148 -0
- package/dist/commands/completion.js +84 -0
- package/dist/commands/config-cmd.js +84 -0
- package/dist/commands/docs.js +30 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/generate.js +81 -0
- package/dist/commands/home.js +68 -0
- package/dist/commands/login.js +309 -0
- package/dist/commands/make.js +142 -0
- package/dist/commands/mcp.js +194 -0
- package/dist/commands/process-groups.js +166 -0
- package/dist/commands/shared.js +75 -0
- package/dist/commands/task.js +128 -0
- package/dist/commands/topup.js +61 -0
- package/dist/commands/view.js +97 -0
- package/dist/config/config.js +99 -0
- package/dist/config/context.js +44 -0
- package/dist/config/history.js +104 -0
- package/dist/core/client.js +129 -0
- package/dist/core/device-auth.js +141 -0
- package/dist/core/download.js +107 -0
- package/dist/core/errors.js +64 -0
- package/dist/core/output.js +72 -0
- package/dist/core/region.js +45 -0
- package/dist/core/requests.js +197 -0
- package/dist/core/task-service.js +167 -0
- package/dist/core/upload.js +42 -0
- package/dist/core/viewer-assets.js +24 -0
- package/dist/core/viewer.js +59 -0
- package/dist/core/watch.js +42 -0
- package/dist/i18n.js +19 -0
- package/dist/knowledge/chains.js +157 -0
- package/dist/knowledge/error-catalog.js +91 -0
- package/dist/knowledge/models.js +158 -0
- package/dist/knowledge/params.js +203 -0
- package/dist/knowledge/scenarios.js +112 -0
- package/dist/pipeline/make.js +233 -0
- package/dist/pipeline/runner.js +73 -0
- package/dist/pipeline/stdin.js +40 -0
- package/dist/program.js +147 -0
- package/dist/src/ai/experts.js +115 -0
- package/dist/src/ai/llm.js +88 -0
- package/dist/src/ai/plan-card.js +79 -0
- package/dist/src/ai/wizard.js +104 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/account.js +27 -0
- package/dist/src/commands/ai.js +113 -0
- package/dist/src/commands/batch.js +148 -0
- package/dist/src/commands/completion.js +84 -0
- package/dist/src/commands/config-cmd.js +84 -0
- package/dist/src/commands/docs.js +30 -0
- package/dist/src/commands/doctor.js +94 -0
- package/dist/src/commands/generate.js +81 -0
- package/dist/src/commands/home.js +68 -0
- package/dist/src/commands/login.js +225 -0
- package/dist/src/commands/make.js +142 -0
- package/dist/src/commands/mcp.js +194 -0
- package/dist/src/commands/process-groups.js +164 -0
- package/dist/src/commands/shared.js +75 -0
- package/dist/src/commands/task.js +127 -0
- package/dist/src/commands/topup.js +61 -0
- package/dist/src/commands/view.js +97 -0
- package/dist/src/config/config.js +79 -0
- package/dist/src/config/context.js +44 -0
- package/dist/src/config/history.js +104 -0
- package/dist/src/core/client.js +120 -0
- package/dist/src/core/download.js +107 -0
- package/dist/src/core/errors.js +64 -0
- package/dist/src/core/output.js +72 -0
- package/dist/src/core/requests.js +169 -0
- package/dist/src/core/task-service.js +161 -0
- package/dist/src/core/upload.js +36 -0
- package/dist/src/core/viewer-assets.js +24 -0
- package/dist/src/core/viewer.js +59 -0
- package/dist/src/core/watch.js +41 -0
- package/dist/src/i18n.js +19 -0
- package/dist/src/knowledge/chains.js +152 -0
- package/dist/src/knowledge/error-catalog.js +91 -0
- package/dist/src/knowledge/models.js +110 -0
- package/dist/src/knowledge/params.js +174 -0
- package/dist/src/knowledge/scenarios.js +112 -0
- package/dist/src/pipeline/make.js +233 -0
- package/dist/src/pipeline/runner.js +73 -0
- package/dist/src/pipeline/stdin.js +40 -0
- package/dist/src/program.js +147 -0
- package/dist/src/types.js +2 -0
- package/dist/src/util/input-detect.js +92 -0
- package/dist/src/util/notify.js +17 -0
- package/dist/src/util/open.js +18 -0
- package/dist/src/util/spinner.js +62 -0
- package/dist/src/util/term-image.js +28 -0
- package/dist/src/version.js +10 -0
- package/dist/types.js +2 -0
- package/dist/util/input-detect.js +93 -0
- package/dist/util/notify.js +17 -0
- package/dist/util/open.js +18 -0
- package/dist/util/spinner.js +62 -0
- package/dist/util/term-image.js +28 -0
- package/dist/version.js +10 -0
- package/package.json +63 -0
- package/skill/SKILL.md +70 -0
- package/skill/commands/account.md +41 -0
- package/skill/commands/batch.md +33 -0
- package/skill/commands/generate.md +34 -0
- package/skill/commands/make.md +47 -0
- package/skill/commands/process.md +50 -0
- package/skill/commands/task.md +27 -0
- package/skill/commands/view.md +25 -0
- package/skill/common-errors.md +38 -0
- package/skill/examples/animation.md +33 -0
- package/skill/examples/ar-web.md +23 -0
- package/skill/examples/film.md +22 -0
- package/skill/examples/game-asset.md +35 -0
- package/skill/examples/pipes.md +39 -0
- package/skill/examples/print.md +31 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding loop: `tripo login` (browser flow with paste fallback),
|
|
3
|
+
* `tripo logout`, `tripo whoami`. Key precedence: TRIPO_API_KEY env >
|
|
4
|
+
* ~/.tripo/config.json.
|
|
5
|
+
*/
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { createServer } from 'node:http';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
import * as p from '@clack/prompts';
|
|
10
|
+
import { deleteApiKey, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, updateConfig, } from '../config/config.js';
|
|
11
|
+
import { TripoClient } from '../core/client.js';
|
|
12
|
+
import { AuthError, CliError, UsageError } from '../core/errors.js';
|
|
13
|
+
import { emitResult, isInteractive, log, shouldOpen, success, warn } from '../core/output.js';
|
|
14
|
+
import { t } from '../i18n.js';
|
|
15
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
16
|
+
import { openExternal } from '../util/open.js';
|
|
17
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
|
+
export function registerAuthCommands(program) {
|
|
19
|
+
program
|
|
20
|
+
.command('login')
|
|
21
|
+
.description('sign in: browser flow (default) or paste an API key')
|
|
22
|
+
.option('--paste', 'paste an API key instead of using the browser')
|
|
23
|
+
.option('--key <key>', 'provide the API key directly (tsk_...)')
|
|
24
|
+
.option('--region <region>', 'ov (international) or cn (China mainland)')
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
await runLogin(opts);
|
|
27
|
+
});
|
|
28
|
+
program
|
|
29
|
+
.command('logout')
|
|
30
|
+
.description('remove the locally stored API key')
|
|
31
|
+
.action(() => {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
if (!config.api_key) {
|
|
34
|
+
log(t({ zh: '本地没有存储 API Key', en: 'no API key stored locally' }));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
deleteApiKey();
|
|
38
|
+
success(t({ zh: '已删除本地 API Key', en: 'local API key removed' }));
|
|
39
|
+
}
|
|
40
|
+
log(t({
|
|
41
|
+
zh: `如需吊销 Key 本身,请前往 ${resolvePlatformBaseUrl()} 的 API Keys 页面`,
|
|
42
|
+
en: `to revoke the key itself, visit the API Keys page at ${resolvePlatformBaseUrl()}`,
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command('whoami')
|
|
47
|
+
.description('show the active key (masked), region, and balance')
|
|
48
|
+
.action(async () => {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
const key = resolveApiKey(config);
|
|
51
|
+
if (!key)
|
|
52
|
+
throw new AuthError(t({ zh: '未配置 API Key', en: 'no API key configured' }));
|
|
53
|
+
const source = process.env.TRIPO_API_KEY ? 'env:TRIPO_API_KEY' : '~/.tripo/config.json';
|
|
54
|
+
const client = new TripoClient();
|
|
55
|
+
const balance = await client.getBalance();
|
|
56
|
+
emitResult({
|
|
57
|
+
key: maskKey(key),
|
|
58
|
+
source,
|
|
59
|
+
region: config.region ?? 'ov',
|
|
60
|
+
api_base_url: client.baseUrl,
|
|
61
|
+
balance: balance.balance,
|
|
62
|
+
frozen: balance.frozen,
|
|
63
|
+
}, [
|
|
64
|
+
`key ${maskKey(key)} (${source})`,
|
|
65
|
+
`region ${config.region ?? 'ov'}`,
|
|
66
|
+
`api ${client.baseUrl}`,
|
|
67
|
+
t({ zh: `余额 ${balance.balance} 积分(冻结 ${balance.frozen})`, en: `balance ${balance.balance} credits (${balance.frozen} frozen)` }),
|
|
68
|
+
].join('\n'));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function runLogin(opts) {
|
|
72
|
+
let region = normalizeRegion(opts.region);
|
|
73
|
+
if (opts.key) {
|
|
74
|
+
region ??= 'ov';
|
|
75
|
+
await validateAndSave(opts.key, region);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!isInteractive()) {
|
|
79
|
+
throw new UsageError(t({
|
|
80
|
+
zh: '非交互环境请用 TRIPO_API_KEY 环境变量或 tripo login --key tsk_...',
|
|
81
|
+
en: 'in non-interactive environments use the TRIPO_API_KEY env var or "tripo login --key tsk_..."',
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
p.intro('tripo login');
|
|
85
|
+
if (!region) {
|
|
86
|
+
const answer = await p.select({
|
|
87
|
+
message: t({ zh: '选择服务区域', en: 'Choose your region' }),
|
|
88
|
+
options: [
|
|
89
|
+
{ value: 'ov', label: t({ zh: '海外 (api: openapi.tripo3d.ai)', en: 'International (openapi.tripo3d.ai)' }) },
|
|
90
|
+
{ value: 'cn', label: t({ zh: '中国大陆 (api: openapi.tripo3d.com)', en: 'China mainland (openapi.tripo3d.com)' }) },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(answer))
|
|
94
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
95
|
+
region = answer;
|
|
96
|
+
}
|
|
97
|
+
const key = opts.paste ? await pasteFlow(region) : await browserFlow(region);
|
|
98
|
+
await validateAndSave(key, region);
|
|
99
|
+
p.outro(t({
|
|
100
|
+
zh: '登录完成!试试: tripo make "一只可爱的机器人"',
|
|
101
|
+
en: 'logged in! try: tripo make "a cute robot"',
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
function normalizeRegion(value) {
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (value === 'ov' || value === 'cn')
|
|
108
|
+
return value;
|
|
109
|
+
throw new UsageError('--region must be ov or cn');
|
|
110
|
+
}
|
|
111
|
+
async function pasteFlow(region) {
|
|
112
|
+
const platform = platformUrlFor(region);
|
|
113
|
+
log(t({
|
|
114
|
+
zh: `在 ${platform} 的「API Keys」页面创建 Key(新用户注册可领免费额度)`,
|
|
115
|
+
en: `create a key on the "API Keys" page at ${platform} (new accounts get free credits)`,
|
|
116
|
+
}));
|
|
117
|
+
const answer = await p.password({
|
|
118
|
+
message: t({ zh: '粘贴你的 API Key (tsk_...)', en: 'Paste your API key (tsk_...)' }),
|
|
119
|
+
validate: (v) => (v && v.trim().startsWith('tsk_') ? undefined : t({ zh: 'Key 应以 tsk_ 开头', en: 'keys start with tsk_' })),
|
|
120
|
+
});
|
|
121
|
+
if (p.isCancel(answer))
|
|
122
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
123
|
+
return answer.trim();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Browser flow (gh-auth style): open <platform>/cli-auth?state=...&callback=…
|
|
127
|
+
* and wait for the page to POST/GET the new key back to localhost. Races
|
|
128
|
+
* against manual paste so the flow still works before the platform page
|
|
129
|
+
* ships (graceful degradation, zero backend changes required).
|
|
130
|
+
*/
|
|
131
|
+
async function browserFlow(region) {
|
|
132
|
+
const platform = platformUrlFor(region);
|
|
133
|
+
const state = randomBytes(16).toString('hex');
|
|
134
|
+
const { server, port, keyPromise } = await startCallbackServer(state);
|
|
135
|
+
try {
|
|
136
|
+
const authUrl = `${platform}/cli-auth?state=${state}&callback=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}&name=${encodeURIComponent(`CLI - ${hostname()}`)}`;
|
|
137
|
+
if (shouldOpen()) {
|
|
138
|
+
openExternal(authUrl);
|
|
139
|
+
log(t({ zh: `已打开浏览器登录页(未弹出请手动访问):\n ${authUrl}`, en: `opened the browser login page (visit manually if it didn't open):\n ${authUrl}` }));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
log(t({ zh: `请访问: ${authUrl}`, en: `visit: ${authUrl}` }));
|
|
143
|
+
}
|
|
144
|
+
log(t({
|
|
145
|
+
zh: '等待浏览器完成登录…(也可以直接在下方粘贴 Key)',
|
|
146
|
+
en: 'waiting for the browser… (or paste a key below instead)',
|
|
147
|
+
}));
|
|
148
|
+
const pastePromise = p
|
|
149
|
+
.password({
|
|
150
|
+
message: t({ zh: '或直接粘贴 API Key (tsk_...)', en: 'or paste an API key (tsk_...)' }),
|
|
151
|
+
})
|
|
152
|
+
.then((v) => {
|
|
153
|
+
if (p.isCancel(v))
|
|
154
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
155
|
+
const trimmed = v.trim();
|
|
156
|
+
if (!trimmed.startsWith('tsk_'))
|
|
157
|
+
throw new UsageError(t({ zh: 'Key 应以 tsk_ 开头', en: 'keys start with tsk_' }));
|
|
158
|
+
return trimmed;
|
|
159
|
+
});
|
|
160
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new CliError(t({ zh: '登录超时(5 分钟)', en: 'login timed out (5 minutes)' }), EXIT.AUTH)), LOGIN_TIMEOUT_MS));
|
|
161
|
+
return await Promise.race([keyPromise, pastePromise, timeoutPromise]);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
server.close();
|
|
165
|
+
// Ensure the pending clack prompt does not hold the process open.
|
|
166
|
+
process.stdin.pause();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function startCallbackServer(state) {
|
|
170
|
+
let resolveKey;
|
|
171
|
+
const keyPromise = new Promise((resolve) => (resolveKey = resolve));
|
|
172
|
+
const server = createServer((req, res) => {
|
|
173
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
174
|
+
if (url.pathname !== '/callback') {
|
|
175
|
+
res.writeHead(404);
|
|
176
|
+
res.end();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const gotState = url.searchParams.get('state');
|
|
180
|
+
const key = url.searchParams.get('key') ?? url.searchParams.get('api_key');
|
|
181
|
+
if (gotState !== state || !key || !key.startsWith('tsk_')) {
|
|
182
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
183
|
+
res.end('<h3>Invalid login callback. Return to the terminal and try again.</h3>');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
187
|
+
res.end('<h3>✓ Tripo CLI login complete. You can close this tab and return to the terminal.</h3>');
|
|
188
|
+
resolveKey(key);
|
|
189
|
+
});
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
server.once('error', reject);
|
|
192
|
+
server.listen(0, '127.0.0.1', () => {
|
|
193
|
+
const address = server.address();
|
|
194
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
195
|
+
resolve({ server, port, keyPromise });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function validateAndSave(key, region) {
|
|
200
|
+
const config = updateConfig({ region });
|
|
201
|
+
const client = new TripoClient({ apiKey: key });
|
|
202
|
+
try {
|
|
203
|
+
const balance = await client.getBalance();
|
|
204
|
+
updateConfig({ ...config, api_key: key, region });
|
|
205
|
+
success(t({
|
|
206
|
+
zh: `Key 验证通过,当前余额 ${balance.balance} 积分(已保存到 ~/.tripo/config.json,权限 0600)`,
|
|
207
|
+
en: `key verified; balance ${balance.balance} credits (saved to ~/.tripo/config.json, mode 0600)`,
|
|
208
|
+
}));
|
|
209
|
+
if (Number(balance.balance) === 0) {
|
|
210
|
+
warn(t({
|
|
211
|
+
zh: '余额为 0:新账号可在控制台领取免费额度,或运行 tripo topup 充值',
|
|
212
|
+
en: 'balance is 0: claim free credits in the console for new accounts, or run "tripo topup"',
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (err instanceof CliError && err.exitCode === EXIT.AUTH) {
|
|
218
|
+
throw new AuthError(t({ zh: 'Key 验证失败:无效或已被删除', en: 'key validation failed: invalid or deleted' }));
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function platformUrlFor(region) {
|
|
224
|
+
return process.env.TRIPO_PLATFORM_BASE_URL || REGION_DEFAULTS[region].platform;
|
|
225
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tripo make` — the magic command. Give it anything (a sentence, an image,
|
|
3
|
+
* 2–4 views, a model file, or @last/task id) and it picks the endpoint, the
|
|
4
|
+
* model, the parameters, runs the chain, and downloads the artifacts.
|
|
5
|
+
*/
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import { UsageError } from '../core/errors.js';
|
|
8
|
+
import { emitResult, isInteractive, log, warn } from '../core/output.js';
|
|
9
|
+
import { t } from '../i18n.js';
|
|
10
|
+
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
11
|
+
import { executeMake, planMake } from '../pipeline/make.js';
|
|
12
|
+
import { newClient, parseChainFlag, printTaskResult, withTaskOptions } from './shared.js';
|
|
13
|
+
import { printInlineImage } from '../util/term-image.js';
|
|
14
|
+
export function registerMakeCommand(program) {
|
|
15
|
+
const cmd = program
|
|
16
|
+
.command('make')
|
|
17
|
+
.description('turn text / images / a model file / a task ref into 3D artifacts (the magic command)')
|
|
18
|
+
.argument('<input...>', 'prompt text, image file(s), model file, URL, task id, or @last')
|
|
19
|
+
.option('--for <scenario>', `scenario preset: ${scenarioIds().join('|')}`)
|
|
20
|
+
.option('--then <steps>', 'processing chain, e.g. "texture,rig,convert:fbx"')
|
|
21
|
+
.option('--model <model>', 'force model version (tripo-v3.1 / tripo-p1 / ...)')
|
|
22
|
+
.option('-n, --candidates <n>', 'generate N candidates in parallel', (v) => Number(v), 1)
|
|
23
|
+
.option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v));
|
|
24
|
+
withTaskOptions(cmd);
|
|
25
|
+
cmd.action(async (inputs, flags) => {
|
|
26
|
+
const result = await runMake(inputs, flags);
|
|
27
|
+
emitMakeOutcome(result);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function runMake(inputs, flags) {
|
|
31
|
+
const client = newClient();
|
|
32
|
+
const params = { ...flags.param };
|
|
33
|
+
if (flags.seed !== undefined)
|
|
34
|
+
params.model_seed = flags.seed;
|
|
35
|
+
const options = {
|
|
36
|
+
inputs,
|
|
37
|
+
scenario: flags.for,
|
|
38
|
+
chain: parseChainFlag(flags.then),
|
|
39
|
+
model: flags.model,
|
|
40
|
+
params,
|
|
41
|
+
name: flags.name,
|
|
42
|
+
outDir: flags.out,
|
|
43
|
+
noWait: flags.wait === false,
|
|
44
|
+
noDownload: flags.download === false,
|
|
45
|
+
notify: flags.notify,
|
|
46
|
+
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
47
|
+
};
|
|
48
|
+
const n = flags.candidates;
|
|
49
|
+
if (n <= 1) {
|
|
50
|
+
const plan = await planMake(client, options);
|
|
51
|
+
const outcome = await executeMake(client, plan, options);
|
|
52
|
+
return { results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly };
|
|
53
|
+
}
|
|
54
|
+
// -n candidates: N parallel generations with distinct seeds.
|
|
55
|
+
if (n > 4)
|
|
56
|
+
throw new UsageError('at most 4 candidates per run (-n 4)');
|
|
57
|
+
const basePlan = await planMake(client, options);
|
|
58
|
+
if (!basePlan.generation) {
|
|
59
|
+
throw new UsageError('-n only applies to generation inputs (text/image), not task references');
|
|
60
|
+
}
|
|
61
|
+
const chain = basePlan.chain;
|
|
62
|
+
const interactive = isInteractive();
|
|
63
|
+
if (chain.length > 0 && !interactive) {
|
|
64
|
+
throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
|
|
65
|
+
}
|
|
66
|
+
log(t({ zh: `并行生成 ${n} 个候选…`, en: `generating ${n} candidates in parallel…` }));
|
|
67
|
+
const seeds = Array.from({ length: n }, () => Math.floor(Math.random() * 2 ** 31));
|
|
68
|
+
const runs = seeds.map((seed, i) => (async () => {
|
|
69
|
+
const candidateOptions = {
|
|
70
|
+
...options,
|
|
71
|
+
chain: [],
|
|
72
|
+
params: { ...params, model_seed: seed },
|
|
73
|
+
name: `${flags.name ?? inputs[0] ?? 'make'}-c${i + 1}`,
|
|
74
|
+
};
|
|
75
|
+
const plan = await planMake(client, candidateOptions);
|
|
76
|
+
plan.chain = [];
|
|
77
|
+
return executeMake(client, plan, candidateOptions);
|
|
78
|
+
})());
|
|
79
|
+
const settled = await Promise.allSettled(runs);
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const s of settled) {
|
|
82
|
+
if (s.status === 'fulfilled')
|
|
83
|
+
results.push(s.value.primary);
|
|
84
|
+
else
|
|
85
|
+
warn(String(s.reason instanceof Error ? s.reason.message : s.reason));
|
|
86
|
+
}
|
|
87
|
+
if (results.length === 0)
|
|
88
|
+
throw new UsageError('all candidates failed');
|
|
89
|
+
for (const r of results) {
|
|
90
|
+
if (r.preview)
|
|
91
|
+
printInlineImage(r.preview);
|
|
92
|
+
}
|
|
93
|
+
let chainResults = [];
|
|
94
|
+
let stoppedEarly;
|
|
95
|
+
if (chain.length > 0 && interactive) {
|
|
96
|
+
const pick = await p.select({
|
|
97
|
+
message: t({ zh: '选择一个候选继续加工链', en: 'pick a candidate to continue the chain' }),
|
|
98
|
+
options: results.map((r, i) => ({
|
|
99
|
+
value: r.task_id,
|
|
100
|
+
label: `#${i + 1} ${r.task_id.slice(0, 8)} ${r.output_dir ?? ''}`,
|
|
101
|
+
})),
|
|
102
|
+
});
|
|
103
|
+
if (!p.isCancel(pick)) {
|
|
104
|
+
const winner = results.find((r) => r.task_id === pick);
|
|
105
|
+
const { runChain } = await import('../pipeline/runner.js');
|
|
106
|
+
const chainRun = await runChain({
|
|
107
|
+
client,
|
|
108
|
+
chain,
|
|
109
|
+
sourceTaskId: winner.task_id,
|
|
110
|
+
sourceType: winner.type,
|
|
111
|
+
namePrefix: flags.name ?? 'make',
|
|
112
|
+
outDir: flags.out,
|
|
113
|
+
timeoutMs: options.timeoutMs,
|
|
114
|
+
});
|
|
115
|
+
chainResults = chainRun.results;
|
|
116
|
+
stoppedEarly = chainRun.stoppedEarly;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { results, chainResults, stoppedEarly };
|
|
120
|
+
}
|
|
121
|
+
export function emitMakeOutcome(outcome) {
|
|
122
|
+
const finalChain = outcome.chainResults[outcome.chainResults.length - 1];
|
|
123
|
+
if (outcome.results.length === 1) {
|
|
124
|
+
const primary = outcome.results[0];
|
|
125
|
+
const merged = finalChain
|
|
126
|
+
? { ...finalChain, chain: undefined, source_task_id: primary.task_id }
|
|
127
|
+
: primary;
|
|
128
|
+
if (outcome.chainResults.length > 0)
|
|
129
|
+
merged.chain = outcome.chainResults;
|
|
130
|
+
if (outcome.stoppedEarly)
|
|
131
|
+
merged.stopped_early = outcome.stoppedEarly;
|
|
132
|
+
printTaskResult(merged);
|
|
133
|
+
if (merged.preview)
|
|
134
|
+
printInlineImage(merged.preview);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const human = outcome.results
|
|
138
|
+
.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
emitResult({ candidates: outcome.results, chain: outcome.chainResults }, human);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tripo mcp` — MCP (Model Context Protocol) server over stdio, exposing the
|
|
3
|
+
* CLI's capabilities as tools for Cursor / Claude Desktop / any MCP client.
|
|
4
|
+
*
|
|
5
|
+
* Implements the stdio transport (newline-delimited JSON-RPC 2.0):
|
|
6
|
+
* initialize, ping, tools/list, tools/call. Self-contained: no SDK
|
|
7
|
+
* dependency, no stdout pollution (stdout carries protocol frames only).
|
|
8
|
+
*/
|
|
9
|
+
import { createInterface } from 'node:readline';
|
|
10
|
+
import { readHistory } from '../config/history.js';
|
|
11
|
+
import { TripoClient } from '../core/client.js';
|
|
12
|
+
import { downloadArtifacts, defaultOutputDir } from '../core/download.js';
|
|
13
|
+
import { configureOutput } from '../core/output.js';
|
|
14
|
+
import { watchTask } from '../core/watch.js';
|
|
15
|
+
import { parseThenExpression } from '../knowledge/chains.js';
|
|
16
|
+
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
17
|
+
import { executeMake, planMake } from '../pipeline/make.js';
|
|
18
|
+
import { cliVersion } from '../version.js';
|
|
19
|
+
const TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: 'tripo_make',
|
|
22
|
+
description: 'Generate a 3D asset from text, an image path/URL, or an existing task id. Blocks until finished; downloads artifacts locally and returns file paths (model_file, preview). Use scenario presets for domain defaults.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
input: { type: 'string', description: 'prompt text, image file path, image URL, or task id' },
|
|
27
|
+
scenario: { type: 'string', enum: scenarioIds(), description: 'production scenario preset' },
|
|
28
|
+
then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
|
|
29
|
+
model: { type: 'string', description: 'force model: tripo-v3.1 or tripo-p1' },
|
|
30
|
+
output_dir: { type: 'string', description: 'directory for artifacts' },
|
|
31
|
+
},
|
|
32
|
+
required: ['input'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'tripo_task_get',
|
|
37
|
+
description: 'Get the status/details of a Tripo task by id.',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: { task_id: { type: 'string' } },
|
|
41
|
+
required: ['task_id'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'tripo_task_wait',
|
|
46
|
+
description: 'Block until a Tripo task finishes; optionally download artifacts.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
task_id: { type: 'string' },
|
|
51
|
+
download: { type: 'boolean', description: 'download artifacts on success (default true)' },
|
|
52
|
+
},
|
|
53
|
+
required: ['task_id'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'tripo_balance',
|
|
58
|
+
description: 'Get the account credit balance.',
|
|
59
|
+
inputSchema: { type: 'object', properties: {} },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'tripo_history',
|
|
63
|
+
description: 'List recent tasks created by this CLI (task ids, names, statuses, output dirs).',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: { limit: { type: 'number', description: 'max entries (default 10)' } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
export function registerMcpCommand(program) {
|
|
71
|
+
program
|
|
72
|
+
.command('mcp')
|
|
73
|
+
.description('run as an MCP server over stdio (tools: make/task/balance/history)')
|
|
74
|
+
.action(async () => {
|
|
75
|
+
configureOutput({ quiet: true, yes: true, noOpen: true, json: false });
|
|
76
|
+
await serveMcp();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function serveMcp() {
|
|
80
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
81
|
+
const write = (message) => {
|
|
82
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
83
|
+
};
|
|
84
|
+
for await (const line of rl) {
|
|
85
|
+
const text = line.trim();
|
|
86
|
+
if (!text)
|
|
87
|
+
continue;
|
|
88
|
+
let request;
|
|
89
|
+
try {
|
|
90
|
+
request = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (request.id === undefined || request.id === null) {
|
|
96
|
+
continue; // notification — nothing to answer
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const result = await dispatch(request.method, request.params ?? {});
|
|
100
|
+
if (result !== SKIP)
|
|
101
|
+
write({ jsonrpc: '2.0', id: request.id, result });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
write({
|
|
105
|
+
jsonrpc: '2.0',
|
|
106
|
+
id: request.id,
|
|
107
|
+
error: { code: -32000, message: err instanceof Error ? err.message : String(err) },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const SKIP = Symbol('skip');
|
|
113
|
+
async function dispatch(method, params) {
|
|
114
|
+
switch (method) {
|
|
115
|
+
case 'initialize':
|
|
116
|
+
return {
|
|
117
|
+
protocolVersion: params.protocolVersion ?? '2024-11-05',
|
|
118
|
+
capabilities: { tools: {} },
|
|
119
|
+
serverInfo: { name: 'tripo-cli', version: cliVersion() },
|
|
120
|
+
};
|
|
121
|
+
case 'ping':
|
|
122
|
+
return {};
|
|
123
|
+
case 'tools/list':
|
|
124
|
+
return { tools: TOOLS };
|
|
125
|
+
case 'tools/call':
|
|
126
|
+
return callTool(params);
|
|
127
|
+
default:
|
|
128
|
+
return SKIP;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function callTool(params) {
|
|
132
|
+
const args = params.arguments ?? {};
|
|
133
|
+
const asText = (value) => ({
|
|
134
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
switch (params.name) {
|
|
138
|
+
case 'tripo_make': {
|
|
139
|
+
const client = new TripoClient();
|
|
140
|
+
const options = {
|
|
141
|
+
inputs: [String(args.input ?? '')],
|
|
142
|
+
scenario: args.scenario,
|
|
143
|
+
chain: typeof args.then === 'string' ? parseThenExpression(args.then) : undefined,
|
|
144
|
+
model: args.model,
|
|
145
|
+
outDir: args.output_dir,
|
|
146
|
+
};
|
|
147
|
+
const plan = await planMake(client, options);
|
|
148
|
+
const outcome = await executeMake(client, plan, options);
|
|
149
|
+
const final = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
|
|
150
|
+
return asText({
|
|
151
|
+
task_id: final.task_id,
|
|
152
|
+
status: final.status,
|
|
153
|
+
output_dir: final.output_dir ?? outcome.primary.output_dir,
|
|
154
|
+
model_file: final.model_file ?? outcome.primary.model_file,
|
|
155
|
+
preview: final.preview ?? outcome.primary.preview,
|
|
156
|
+
chain: outcome.chainResults.map((r) => ({ task_id: r.task_id, type: r.type })),
|
|
157
|
+
stopped_early: outcome.stoppedEarly,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
case 'tripo_task_get': {
|
|
161
|
+
const client = new TripoClient();
|
|
162
|
+
return asText(await client.getTask(String(args.task_id)));
|
|
163
|
+
}
|
|
164
|
+
case 'tripo_task_wait': {
|
|
165
|
+
const client = new TripoClient();
|
|
166
|
+
const taskId = String(args.task_id);
|
|
167
|
+
const detail = await watchTask(client, taskId, { throwOnFailure: false });
|
|
168
|
+
if (detail.status === 'success' && args.download !== false) {
|
|
169
|
+
const entry = readHistory().find((e) => e.task_id === taskId);
|
|
170
|
+
const dir = entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'task', taskId);
|
|
171
|
+
const artifacts = await downloadArtifacts(detail, dir);
|
|
172
|
+
return asText({ ...detail, output_dir: artifacts.outputDir, model_file: artifacts.modelFile, preview: artifacts.preview });
|
|
173
|
+
}
|
|
174
|
+
return asText(detail);
|
|
175
|
+
}
|
|
176
|
+
case 'tripo_balance': {
|
|
177
|
+
const client = new TripoClient();
|
|
178
|
+
return asText(await client.getBalance());
|
|
179
|
+
}
|
|
180
|
+
case 'tripo_history': {
|
|
181
|
+
const limit = typeof args.limit === 'number' ? args.limit : 10;
|
|
182
|
+
return asText(readHistory().slice(-limit));
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return { content: [{ type: 'text', text: `unknown tool: ${params.name}` }], isError: true };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
191
|
+
isError: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|