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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task history at ~/.tripo/history.jsonl — powers implicit context (@last),
|
|
3
|
+
* positional references (@2), named references (@my-knight), and redo.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tripoHome } from './config.js';
|
|
8
|
+
function historyPath() {
|
|
9
|
+
return join(tripoHome(), 'history.jsonl');
|
|
10
|
+
}
|
|
11
|
+
export function appendHistory(entry) {
|
|
12
|
+
mkdirSync(tripoHome(), { recursive: true, mode: 0o700 });
|
|
13
|
+
appendFileSync(historyPath(), JSON.stringify(entry) + '\n');
|
|
14
|
+
}
|
|
15
|
+
export function readHistory() {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = readFileSync(historyPath(), 'utf8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const entries = [];
|
|
24
|
+
for (const line of raw.split('\n')) {
|
|
25
|
+
if (!line.trim())
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
entries.push(JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
// Later entries for the same task id override earlier ones (status updates).
|
|
33
|
+
const merged = new Map();
|
|
34
|
+
for (const e of entries)
|
|
35
|
+
merged.set(e.task_id, { ...merged.get(e.task_id), ...e });
|
|
36
|
+
return [...merged.values()];
|
|
37
|
+
}
|
|
38
|
+
export function updateHistoryStatus(taskId, status, patch) {
|
|
39
|
+
const existing = readHistory().find((e) => e.task_id === taskId);
|
|
40
|
+
if (!existing)
|
|
41
|
+
return;
|
|
42
|
+
appendHistory({ ...existing, ...patch, status, task_id: taskId });
|
|
43
|
+
}
|
|
44
|
+
export class RefError extends Error {
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a history reference:
|
|
48
|
+
* @last → most recent successful task
|
|
49
|
+
* @last:any → most recent task regardless of status
|
|
50
|
+
* @2 → 2nd most recent successful task
|
|
51
|
+
* @some-name → unique match on entry name (slug); ambiguity is an error
|
|
52
|
+
* Returns undefined when `ref` is not a reference at all.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveReference(ref) {
|
|
55
|
+
if (!ref.startsWith('@'))
|
|
56
|
+
return undefined;
|
|
57
|
+
const body = ref.slice(1);
|
|
58
|
+
const entries = readHistory();
|
|
59
|
+
const successes = entries.filter((e) => e.status === 'success');
|
|
60
|
+
if (body === 'last' || body === '') {
|
|
61
|
+
const hit = successes[successes.length - 1];
|
|
62
|
+
if (!hit)
|
|
63
|
+
throw new RefError('no successful task in history yet');
|
|
64
|
+
return hit;
|
|
65
|
+
}
|
|
66
|
+
if (body === 'last:any') {
|
|
67
|
+
const hit = entries[entries.length - 1];
|
|
68
|
+
if (!hit)
|
|
69
|
+
throw new RefError('history is empty');
|
|
70
|
+
return hit;
|
|
71
|
+
}
|
|
72
|
+
if (/^\d+$/.test(body)) {
|
|
73
|
+
const n = Number(body);
|
|
74
|
+
const hit = successes[successes.length - n];
|
|
75
|
+
if (!hit)
|
|
76
|
+
throw new RefError(`history has only ${successes.length} successful task(s), cannot resolve @${n}`);
|
|
77
|
+
return hit;
|
|
78
|
+
}
|
|
79
|
+
const matches = entries.filter((e) => e.name === body);
|
|
80
|
+
if (matches.length === 0) {
|
|
81
|
+
const fuzzy = entries.filter((e) => e.name?.includes(body));
|
|
82
|
+
if (fuzzy.length === 1)
|
|
83
|
+
return fuzzy[0];
|
|
84
|
+
if (fuzzy.length > 1) {
|
|
85
|
+
throw new RefError(`"@${body}" matches ${fuzzy.length} tasks: ${fuzzy.map((e) => `${e.name} (${e.task_id.slice(0, 8)})`).join(', ')} — use a task id instead`);
|
|
86
|
+
}
|
|
87
|
+
throw new RefError(`no task named "@${body}" in history`);
|
|
88
|
+
}
|
|
89
|
+
if (matches.length > 1) {
|
|
90
|
+
throw new RefError(`"@${body}" matches ${matches.length} tasks: ${matches.map((e) => e.task_id.slice(0, 8)).join(', ')} — use a task id instead`);
|
|
91
|
+
}
|
|
92
|
+
return matches[0];
|
|
93
|
+
}
|
|
94
|
+
/** Build a filesystem/name-safe slug from a prompt or filename. */
|
|
95
|
+
export function slugify(text, maxLen = 32) {
|
|
96
|
+
const slug = text
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.replace(/\.[a-z0-9]+$/i, '')
|
|
99
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/gi, '-')
|
|
100
|
+
.replace(/^-+|-+$/g, '')
|
|
101
|
+
.slice(0, maxLen)
|
|
102
|
+
.replace(/-+$/, '');
|
|
103
|
+
return slug || 'task';
|
|
104
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight V3 API client on top of global fetch (Node 20+).
|
|
3
|
+
* Handles the response envelope, error translation, timeouts, and
|
|
4
|
+
* exponential-backoff retries for transient failures (429/5xx/network).
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
|
|
7
|
+
import { ApiError, AuthError, NetworkError } from './errors.js';
|
|
8
|
+
export class TripoClient {
|
|
9
|
+
baseUrl;
|
|
10
|
+
apiKey;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
maxRetries;
|
|
13
|
+
onRetry;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
this.baseUrl = (options.baseUrl ?? resolveApiBaseUrl(config)).replace(/\/$/, '');
|
|
17
|
+
const key = options.apiKey ?? resolveApiKey(config);
|
|
18
|
+
if (!key) {
|
|
19
|
+
throw new AuthError('no API key configured');
|
|
20
|
+
}
|
|
21
|
+
this.apiKey = key;
|
|
22
|
+
this.timeoutMs = options.timeoutMs ?? 60_000;
|
|
23
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
24
|
+
this.onRetry = options.onRetry;
|
|
25
|
+
}
|
|
26
|
+
async request(method, path, body) {
|
|
27
|
+
return this.send(method, path, {
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async uploadFile(fileName, data) {
|
|
33
|
+
const form = new FormData();
|
|
34
|
+
form.append('file', data, fileName);
|
|
35
|
+
return this.send('POST', '/v3/files', { body: form });
|
|
36
|
+
}
|
|
37
|
+
async send(method, path, init) {
|
|
38
|
+
let lastError;
|
|
39
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
40
|
+
if (attempt > 0) {
|
|
41
|
+
const delay = Math.min(1000 * 2 ** (attempt - 1), 8000);
|
|
42
|
+
this.onRetry?.(attempt, delay, lastError?.message ?? 'retry');
|
|
43
|
+
await sleep(delay);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await this.sendOnce(method, path, init);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
lastError = err;
|
|
50
|
+
const retryable = err instanceof NetworkError || (err instanceof ApiError && err.retryable);
|
|
51
|
+
if (!retryable || attempt === this.maxRetries)
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/* c8 ignore next */
|
|
56
|
+
throw lastError ?? new NetworkError('request failed');
|
|
57
|
+
}
|
|
58
|
+
async sendOnce(method, path, init) {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetch(this.baseUrl + path, {
|
|
64
|
+
method,
|
|
65
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, ...init.headers },
|
|
66
|
+
body: init.body,
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
72
|
+
throw new NetworkError(controller.signal.aborted ? `request timed out after ${this.timeoutMs}ms: ${method} ${path}` : `network error: ${reason}`);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
let envelope;
|
|
78
|
+
try {
|
|
79
|
+
envelope = (await response.json());
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new ApiError({ httpStatus: response.status, message: `HTTP ${response.status} with non-JSON body` });
|
|
84
|
+
}
|
|
85
|
+
throw new NetworkError(`invalid JSON response from ${path}`);
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok || envelope.code !== 0) {
|
|
88
|
+
throw new ApiError({
|
|
89
|
+
apiCode: envelope.code !== 0 ? envelope.code : undefined,
|
|
90
|
+
httpStatus: response.status,
|
|
91
|
+
message: envelope.message,
|
|
92
|
+
suggestion: envelope.suggestion,
|
|
93
|
+
requestId: envelope.request_id,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return envelope.data;
|
|
97
|
+
}
|
|
98
|
+
// ---- typed convenience wrappers ----
|
|
99
|
+
getTask(taskId, step = false) {
|
|
100
|
+
const suffix = step ? '?step=1' : '';
|
|
101
|
+
return this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
|
|
102
|
+
}
|
|
103
|
+
listTasks(taskIds) {
|
|
104
|
+
return this.request('POST', '/v3/tasks/list', { task_ids: taskIds });
|
|
105
|
+
}
|
|
106
|
+
getBalance() {
|
|
107
|
+
return this.request('GET', '/v3/account/balance');
|
|
108
|
+
}
|
|
109
|
+
getUsage() {
|
|
110
|
+
return this.request('GET', '/v3/account/usage');
|
|
111
|
+
}
|
|
112
|
+
/** Create a task; returns its id. Response may be {task_id} or a full detail. */
|
|
113
|
+
async createTask(endpoint, payload) {
|
|
114
|
+
const data = await this.request('POST', endpoint, payload);
|
|
115
|
+
return data.task_id;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact download: walk task.output for URL-bearing fields, fetch each into
|
|
3
|
+
* the output directory, and write task.json (the reproducibility record).
|
|
4
|
+
* Output URLs expire in ~5 minutes, so downloads happen immediately after
|
|
5
|
+
* completion and URLs are never cached.
|
|
6
|
+
*/
|
|
7
|
+
import { createWriteStream } from 'node:fs';
|
|
8
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { basename, extname, join } from 'node:path';
|
|
10
|
+
import { Readable } from 'node:stream';
|
|
11
|
+
import { pipeline } from 'node:stream/promises';
|
|
12
|
+
import { NetworkError } from './errors.js';
|
|
13
|
+
const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply']);
|
|
14
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
15
|
+
/** Collect every downloadable URL in the output map (string or {url} object). */
|
|
16
|
+
export function collectOutputUrls(output) {
|
|
17
|
+
if (!output)
|
|
18
|
+
return [];
|
|
19
|
+
const found = [];
|
|
20
|
+
const visit = (value, path) => {
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
if (/^https?:\/\//.test(value))
|
|
23
|
+
found.push({ field: path, url: value });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
value.forEach((v, i) => visit(v, `${path}[${i}]`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (value && typeof value === 'object') {
|
|
31
|
+
for (const [k, v] of Object.entries(value))
|
|
32
|
+
visit(v, path ? `${path}.${k}` : k);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
visit(output, '');
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
function fileNameFor(field, url) {
|
|
39
|
+
const urlPath = new URL(url).pathname;
|
|
40
|
+
let ext = extname(urlPath).toLowerCase();
|
|
41
|
+
const base = field
|
|
42
|
+
.replace(/_url$/, '')
|
|
43
|
+
.replace(/[^a-z0-9_.\-[\]]/gi, '_')
|
|
44
|
+
.replace(/[[\]]/g, '_')
|
|
45
|
+
.replace(/_+/g, '_')
|
|
46
|
+
.replace(/^_|_$/g, '');
|
|
47
|
+
if (!ext) {
|
|
48
|
+
// rendered images are usually webp/png; models glb — guess from field name
|
|
49
|
+
ext = /image|preview|render/i.test(field) ? '.png' : '.bin';
|
|
50
|
+
}
|
|
51
|
+
return `${base || basename(urlPath) || 'artifact'}${ext}`;
|
|
52
|
+
}
|
|
53
|
+
async function downloadOne(url, dest) {
|
|
54
|
+
const response = await fetch(url);
|
|
55
|
+
if (!response.ok || !response.body) {
|
|
56
|
+
throw new NetworkError(`download failed (HTTP ${response.status}): ${dest}`);
|
|
57
|
+
}
|
|
58
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(dest));
|
|
59
|
+
}
|
|
60
|
+
/** Download all artifacts of a finished task + write task.json. */
|
|
61
|
+
export async function downloadArtifacts(task, outputDir, historyEntry, options = {}) {
|
|
62
|
+
await mkdir(outputDir, { recursive: true });
|
|
63
|
+
const urls = collectOutputUrls(task.output);
|
|
64
|
+
const files = [];
|
|
65
|
+
let modelFile;
|
|
66
|
+
let preview;
|
|
67
|
+
for (const { field, url } of urls) {
|
|
68
|
+
const name = fileNameFor(field, url);
|
|
69
|
+
const dest = join(outputDir, name);
|
|
70
|
+
options.onFile?.(name);
|
|
71
|
+
await downloadOne(url, dest);
|
|
72
|
+
files.push(dest);
|
|
73
|
+
const ext = extname(name).toLowerCase();
|
|
74
|
+
if (!modelFile && MODEL_EXTS.has(ext))
|
|
75
|
+
modelFile = dest;
|
|
76
|
+
if (!preview && (IMAGE_EXTS.has(ext) || /rendered|preview/i.test(field)))
|
|
77
|
+
preview = dest;
|
|
78
|
+
}
|
|
79
|
+
// Normalize the preview name so agents can rely on preview.png existing.
|
|
80
|
+
if (preview && basename(preview) !== 'preview.png') {
|
|
81
|
+
const { copyFile } = await import('node:fs/promises');
|
|
82
|
+
const canonical = join(outputDir, 'preview.png');
|
|
83
|
+
try {
|
|
84
|
+
await copyFile(preview, canonical);
|
|
85
|
+
files.push(canonical);
|
|
86
|
+
preview = canonical;
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
const record = {
|
|
91
|
+
task_id: task.task_id,
|
|
92
|
+
type: task.type,
|
|
93
|
+
status: task.status,
|
|
94
|
+
input: task.input ?? historyEntry?.payload ?? {},
|
|
95
|
+
output_fields: urls.map((u) => u.field),
|
|
96
|
+
credits_consumed: task.credits_consumed,
|
|
97
|
+
created_at: task.created_at,
|
|
98
|
+
completed_at: task.completed_at,
|
|
99
|
+
files: files.map((f) => basename(f)),
|
|
100
|
+
};
|
|
101
|
+
await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
|
|
102
|
+
return { outputDir, files, modelFile, preview };
|
|
103
|
+
}
|
|
104
|
+
/** Default output directory: ./tripo-out/<slug>-<id8>/ */
|
|
105
|
+
export function defaultOutputDir(baseDir, slug, taskId) {
|
|
106
|
+
return join(baseDir, 'tripo-out', `${slug}-${taskId.slice(0, 8)}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** CLI error types carrying stable exit codes and human translations. */
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
import { EXIT, exitCodeFor, isRetryable, lookupError } from '../knowledge/error-catalog.js';
|
|
4
|
+
export class CliError extends Error {
|
|
5
|
+
exitCode;
|
|
6
|
+
suggestion;
|
|
7
|
+
constructor(message, exitCode = EXIT.FATAL, suggestion) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.exitCode = exitCode;
|
|
10
|
+
this.suggestion = suggestion;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class UsageError extends CliError {
|
|
14
|
+
constructor(message, suggestion) {
|
|
15
|
+
super(message, EXIT.USAGE, suggestion);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class AuthError extends CliError {
|
|
19
|
+
constructor(message, suggestion) {
|
|
20
|
+
super(message, EXIT.AUTH, suggestion ?? t({ zh: '运行 tripo login 配置 API Key', en: 'run "tripo login" to configure an API key' }));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class ApiError extends CliError {
|
|
24
|
+
apiCode;
|
|
25
|
+
httpStatus;
|
|
26
|
+
requestId;
|
|
27
|
+
retryable;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
const info = opts.apiCode !== undefined ? lookupError(opts.apiCode) : undefined;
|
|
30
|
+
const translated = info ? t(info) : undefined;
|
|
31
|
+
const parts = [translated ?? opts.message ?? `API error (HTTP ${opts.httpStatus})`];
|
|
32
|
+
if (translated && opts.message && opts.message !== translated)
|
|
33
|
+
parts.push(`(${opts.message})`);
|
|
34
|
+
super(parts.join(' '), exitCodeFor(opts.apiCode, opts.httpStatus), opts.suggestion);
|
|
35
|
+
this.apiCode = opts.apiCode;
|
|
36
|
+
this.httpStatus = opts.httpStatus;
|
|
37
|
+
this.requestId = opts.requestId;
|
|
38
|
+
this.retryable = isRetryable(opts.apiCode, opts.httpStatus);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class NetworkError extends CliError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message, EXIT.NETWORK);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class TaskFailedError extends CliError {
|
|
47
|
+
taskId;
|
|
48
|
+
constructor(taskId, status, detail) {
|
|
49
|
+
const refundNote = t({
|
|
50
|
+
zh: '预扣积分已自动退还',
|
|
51
|
+
en: 'frozen credits have been refunded automatically',
|
|
52
|
+
});
|
|
53
|
+
super(`task ${taskId} ${status}${detail ? `: ${detail}` : ''} (${refundNote})`, EXIT.TASK_FAILED);
|
|
54
|
+
this.taskId = taskId;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Decorate insufficient-balance errors with the topup path and the gap. */
|
|
58
|
+
export function balanceSuggestion(needed, balance) {
|
|
59
|
+
const gap = needed !== undefined && balance !== undefined ? Math.max(0, needed - balance) : undefined;
|
|
60
|
+
return t({
|
|
61
|
+
zh: `运行 tripo topup 充值${gap !== undefined ? `(还差约 ${gap} 积分)` : ''}`,
|
|
62
|
+
en: `run "tripo topup" to add credits${gap !== undefined ? ` (about ${gap} more needed)` : ''}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output discipline (the iron rule):
|
|
3
|
+
* - stdout carries ONLY the final business result (JSON when piped/--json,
|
|
4
|
+
* human-readable text when interactive).
|
|
5
|
+
* - stderr carries all progress, logs, prompts, and decoration.
|
|
6
|
+
* Non-TTY environments automatically behave like --json --yes --no-open.
|
|
7
|
+
*/
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
const state = { json: false, yes: false, quiet: false, noOpen: false };
|
|
10
|
+
export function stdoutIsTTY() {
|
|
11
|
+
return Boolean(process.stdout.isTTY);
|
|
12
|
+
}
|
|
13
|
+
export function stdinIsTTY() {
|
|
14
|
+
return Boolean(process.stdin.isTTY);
|
|
15
|
+
}
|
|
16
|
+
export function configureOutput(flags) {
|
|
17
|
+
Object.assign(state, flags);
|
|
18
|
+
// Headless environments (pipes, agents, CI) get machine output + no prompts.
|
|
19
|
+
if (!stdoutIsTTY())
|
|
20
|
+
state.json = true;
|
|
21
|
+
if (!stdinIsTTY())
|
|
22
|
+
state.yes = state.yes || !stdoutIsTTY();
|
|
23
|
+
if (!stdoutIsTTY() || !stdinIsTTY())
|
|
24
|
+
state.noOpen = true;
|
|
25
|
+
}
|
|
26
|
+
export function isJsonMode() {
|
|
27
|
+
return state.json;
|
|
28
|
+
}
|
|
29
|
+
export function isYes() {
|
|
30
|
+
return state.yes;
|
|
31
|
+
}
|
|
32
|
+
export function isQuiet() {
|
|
33
|
+
return state.quiet;
|
|
34
|
+
}
|
|
35
|
+
export function shouldOpen() {
|
|
36
|
+
return !state.noOpen;
|
|
37
|
+
}
|
|
38
|
+
export function isInteractive() {
|
|
39
|
+
return stdinIsTTY() && stdoutIsTTY() && !state.yes;
|
|
40
|
+
}
|
|
41
|
+
/** Progress/log line → stderr. */
|
|
42
|
+
export function log(message) {
|
|
43
|
+
if (!state.quiet)
|
|
44
|
+
process.stderr.write(message + '\n');
|
|
45
|
+
}
|
|
46
|
+
export function warn(message) {
|
|
47
|
+
process.stderr.write(pc.yellow(`! ${message}`) + '\n');
|
|
48
|
+
}
|
|
49
|
+
export function errorLine(message) {
|
|
50
|
+
process.stderr.write(pc.red(`✗ ${message}`) + '\n');
|
|
51
|
+
}
|
|
52
|
+
export function success(message) {
|
|
53
|
+
if (!state.quiet)
|
|
54
|
+
process.stderr.write(pc.green(`✓ ${message}`) + '\n');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Final business result → stdout.
|
|
58
|
+
* JSON mode prints a single line (NDJSON-safe); human mode prints the
|
|
59
|
+
* formatted text (or a default rendering).
|
|
60
|
+
*/
|
|
61
|
+
export function emitResult(data, human) {
|
|
62
|
+
if (state.json) {
|
|
63
|
+
process.stdout.write(JSON.stringify(data) + '\n');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
process.stdout.write((human ?? JSON.stringify(data, null, 2)) + '\n');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Stream one NDJSON event to stdout (used by watch-style streaming output). */
|
|
70
|
+
export function emitNdjson(event) {
|
|
71
|
+
process.stdout.write(JSON.stringify(event) + '\n');
|
|
72
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request builders: turn (step + args + resolved input) into the exact
|
|
3
|
+
* endpoint + JSON payload, applying knowledge-base validation first.
|
|
4
|
+
* Unknown extra args pass through untouched (the server maps them into
|
|
5
|
+
* `parameters`), so new API options work without a CLI release.
|
|
6
|
+
*/
|
|
7
|
+
import { getStep } from '../knowledge/chains.js';
|
|
8
|
+
import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
9
|
+
import { CAPABILITY_DEFAULTS, normalizeModel } from '../knowledge/models.js';
|
|
10
|
+
import { UsageError } from './errors.js';
|
|
11
|
+
function ensure(result) {
|
|
12
|
+
if (result.errors.length > 0) {
|
|
13
|
+
throw new UsageError(result.errors.join('; '));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** 3D generation: text-to-model. */
|
|
17
|
+
export function buildTextToModel(prompt, model, params) {
|
|
18
|
+
const validated = validateGenerationParams(model, params);
|
|
19
|
+
ensure(validated);
|
|
20
|
+
return {
|
|
21
|
+
request: {
|
|
22
|
+
endpoint: '/v3/generation/text-to-model',
|
|
23
|
+
payload: { prompt, model: normalizeModel(model), ...validated.params },
|
|
24
|
+
},
|
|
25
|
+
taskType: 'text_to_model',
|
|
26
|
+
warnings: validated.warnings,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** 3D generation: image-to-model (input = file_token/url/task_id). */
|
|
30
|
+
export function buildImageToModel(input, model, params) {
|
|
31
|
+
const validated = validateGenerationParams(model, params);
|
|
32
|
+
ensure(validated);
|
|
33
|
+
return {
|
|
34
|
+
request: {
|
|
35
|
+
endpoint: '/v3/generation/image-to-model',
|
|
36
|
+
payload: { input, model: normalizeModel(model), ...validated.params },
|
|
37
|
+
},
|
|
38
|
+
taskType: 'image_to_model',
|
|
39
|
+
warnings: validated.warnings,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** 3D generation: multiview-to-model. Views ordered [front, left, back, right]. */
|
|
43
|
+
export function buildMultiviewToModel(views, model, params) {
|
|
44
|
+
const validated = validateGenerationParams(model, params);
|
|
45
|
+
ensure(validated);
|
|
46
|
+
const inputs = ['front', 'left', 'back', 'right']
|
|
47
|
+
.filter((v) => views[v])
|
|
48
|
+
.map((v) => ({ [v]: views[v] }));
|
|
49
|
+
if (!views.front)
|
|
50
|
+
throw new UsageError('multiview requires a front view');
|
|
51
|
+
if (inputs.length < 2)
|
|
52
|
+
throw new UsageError('multiview requires at least 2 views');
|
|
53
|
+
return {
|
|
54
|
+
request: {
|
|
55
|
+
endpoint: '/v3/generation/multiview-to-model',
|
|
56
|
+
payload: { inputs, model: normalizeModel(model), ...validated.params },
|
|
57
|
+
},
|
|
58
|
+
taskType: 'multiview_to_model',
|
|
59
|
+
warnings: validated.warnings,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Image generation: text-to-image / image-to-image. */
|
|
63
|
+
export function buildTextToImage(prompt, params) {
|
|
64
|
+
return {
|
|
65
|
+
request: {
|
|
66
|
+
endpoint: '/v3/generation/text-to-image',
|
|
67
|
+
payload: { prompt, model: params.model ?? CAPABILITY_DEFAULTS.textToImage, ...omit(params, ['model']) },
|
|
68
|
+
},
|
|
69
|
+
taskType: 'generate_image',
|
|
70
|
+
warnings: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function buildImageToImage(input, params) {
|
|
74
|
+
return {
|
|
75
|
+
request: {
|
|
76
|
+
endpoint: '/v3/generation/image-to-image',
|
|
77
|
+
payload: { input, model: params.model ?? CAPABILITY_DEFAULTS.imageToImage, ...omit(params, ['model']) },
|
|
78
|
+
},
|
|
79
|
+
taskType: 'image_to_image',
|
|
80
|
+
warnings: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function buildImageToMultiview(input, params) {
|
|
84
|
+
return {
|
|
85
|
+
request: { endpoint: '/v3/generation/image-to-multiview', payload: { input, ...params } },
|
|
86
|
+
taskType: 'generate_multiview_image',
|
|
87
|
+
warnings: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function buildEditMultiview(input, params) {
|
|
91
|
+
return {
|
|
92
|
+
request: { endpoint: '/v3/generation/edit-multiview', payload: { input, ...params } },
|
|
93
|
+
taskType: 'edit_multiview_image',
|
|
94
|
+
warnings: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function buildImageToSplat(input, params) {
|
|
98
|
+
return {
|
|
99
|
+
request: { endpoint: '/v3/generation/image-to-splat', payload: { input, ...params } },
|
|
100
|
+
taskType: 'image_to_splat',
|
|
101
|
+
warnings: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Processing step (texture/convert/rig/...): validate per step, then build.
|
|
106
|
+
* `input` must already be a task_id / file_token / URL.
|
|
107
|
+
*/
|
|
108
|
+
export function buildStepRequest(stepName, input, args) {
|
|
109
|
+
const def = getStep(stepName);
|
|
110
|
+
if (!def)
|
|
111
|
+
throw new UsageError(`unknown step "${stepName}"`);
|
|
112
|
+
let params = { ...args };
|
|
113
|
+
const warnings = [];
|
|
114
|
+
switch (def.name) {
|
|
115
|
+
case 'convert': {
|
|
116
|
+
const v = validateConvertParams(params);
|
|
117
|
+
ensure(v);
|
|
118
|
+
params = v.params;
|
|
119
|
+
warnings.push(...v.warnings);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'decimate': {
|
|
123
|
+
const v = validateDecimateParams(params);
|
|
124
|
+
ensure(v);
|
|
125
|
+
params = v.params;
|
|
126
|
+
warnings.push(...v.warnings);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'retarget': {
|
|
130
|
+
const v = validateRetargetParams(params);
|
|
131
|
+
ensure(v);
|
|
132
|
+
params = v.params;
|
|
133
|
+
warnings.push(...v.warnings);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'rig': {
|
|
137
|
+
if (params.model === undefined)
|
|
138
|
+
params.model = CAPABILITY_DEFAULTS.rig;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'texture': {
|
|
142
|
+
if (params.model === undefined)
|
|
143
|
+
params.model = CAPABILITY_DEFAULTS.texture;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'stylize': {
|
|
147
|
+
if (typeof params.style !== 'string') {
|
|
148
|
+
throw new UsageError('stylize requires style (lego/voxel/voronoi/minecraft)');
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'smartsegment': {
|
|
153
|
+
if (params.seg_type === undefined)
|
|
154
|
+
params.seg_type = 'model';
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
request: { endpoint: def.endpoint, payload: { input, ...params } },
|
|
160
|
+
taskType: def.produces,
|
|
161
|
+
warnings,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function omit(obj, keys) {
|
|
165
|
+
const out = { ...obj };
|
|
166
|
+
for (const k of keys)
|
|
167
|
+
delete out[k];
|
|
168
|
+
return out;
|
|
169
|
+
}
|