@assethub/cli 0.1.25 → 0.1.28

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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Pure, independently-testable pieces of `production run --image ...`
3
+ * (the image -> finished-asset one-shot command). Kept separate from
4
+ * index.ts so the tricky bits — cost estimation, batch-status polling, the
5
+ * V6 compose fallback's mesh<->image pairing, and error hints — can be unit
6
+ * tested without spinning up an HTTP server or spawning the built CLI.
7
+ *
8
+ * This module intentionally does not import from `./index.js`: the CLI
9
+ * command wiring (in index.ts) calls these functions and supplies whatever
10
+ * they need (flags already parsed, client calls already made) rather than
11
+ * this module reaching back into index.ts's private helpers, which would
12
+ * create a circular import between the two files.
13
+ */
14
+ import type { CanvasExecution, MeshComposerPart, ModelSummary, ProductionAutomationBatchStatusResult, ProductionOrderStatus } from '@assethub/api-client';
15
+ export type OneShotImageInput = {
16
+ kind: 'file';
17
+ path: string;
18
+ } | {
19
+ kind: 'resourceId';
20
+ resourceId: string;
21
+ };
22
+ /**
23
+ * `--image <file|assetId>` auto-detection: an existing local path is treated
24
+ * as `--file`; anything else (an asset/resource id) is treated as
25
+ * `--source-id`. `exists` is injectable so this stays a pure function in
26
+ * tests instead of touching the real filesystem.
27
+ */
28
+ export declare const detectOneShotImageInput: (value: string, exists: (path: string) => boolean) => OneShotImageInput;
29
+ export type ComposeMode = 'v6' | 'none';
30
+ /** Parses `--compose`. Absent stays absent (server default applies untouched). */
31
+ export declare const parseComposeFlag: (value: string | undefined) => ComposeMode | undefined;
32
+ /**
33
+ * The confirmed field name (assethub-web PR #8179, `web-cli-ux`) for
34
+ * requesting an explicit Composer V6 pass from `production automation`
35
+ * itself. Not `composeModel` — that was this module's pre-#8179 best guess.
36
+ */
37
+ export declare const PART_COMPOSER_V6_AGENT_VERSION = "part_composer_v6_auto_assemble";
38
+ /**
39
+ * Builds the `config` object for the `production automation` request.
40
+ *
41
+ * - `--compose` omitted: returns `undefined` so the request carries no
42
+ * `config.autoCompose` at all, leaving the server default in effect.
43
+ * - `--compose none`: disables automation's own compose stage entirely.
44
+ * - `--compose v6`: asks the server to run Composer V6 itself, in the same
45
+ * call, via `partComposerAgentVersion`. `autoCompose` is deliberately left
46
+ * unset (defaults to `true`) — the server 400s with
47
+ * `PART_COMPOSER_VERSION_REQUIRES_AUTO_COMPOSE` if both are sent together,
48
+ * so this function must never combine them. A server that predates #8179
49
+ * ignores the unrecognized field and runs its own default compose instead;
50
+ * `serverSupportsV6Compose` inspects the response afterward so the caller
51
+ * can still fall back to the client-side lineage-pairing compose
52
+ * (`buildComposerPartsFromMeshExecutions`) on that older server.
53
+ */
54
+ export declare const buildAutomationConfig: (compose: ComposeMode | undefined) => Record<string, unknown> | undefined;
55
+ /**
56
+ * `estimatedCostBreakdown` (added by #8179) only appears on a server new
57
+ * enough to also honor `partComposerAgentVersion` — its presence in the
58
+ * automation response is the signal that the server already did the V6
59
+ * compose server-side in that same call, so no client-side fallback compose
60
+ * is needed. Its absence means an older, currently-deployed server: it
61
+ * silently ignored `partComposerAgentVersion` and ran its own default
62
+ * compose (if any), so the caller must still run the explicit fallback.
63
+ */
64
+ export declare const serverSupportsV6Compose: (automationResult: Record<string, unknown>) => boolean;
65
+ export declare const DEFAULT_MAX_PARTS = 24;
66
+ export type CostLineItem = {
67
+ total: number | null;
68
+ note?: string;
69
+ };
70
+ export type OneShotCostBreakdown = {
71
+ split: CostLineItem & {
72
+ perImage: number | null;
73
+ };
74
+ meshGeneration: CostLineItem & {
75
+ perPart: number | null;
76
+ maxParts: number;
77
+ };
78
+ compose: CostLineItem & {
79
+ perLane: number | null;
80
+ lanesPerImage: number;
81
+ };
82
+ };
83
+ export type OneShotCostEstimate = {
84
+ breakdown: OneShotCostBreakdown;
85
+ totalCredits: number;
86
+ notes: string[];
87
+ };
88
+ /**
89
+ * Best-effort cost breakdown for `--estimate`, shaped like the server's own
90
+ * `estimatedCostBreakdown` (#8179: `split`/`meshGeneration`/`compose`, each
91
+ * with a per-unit price and a total). Nothing here calls `production
92
+ * automation`/`analyze` (that would start real, billable work); every
93
+ * number comes from a side-effect-free GET (`models` catalog, `mesh/compose`
94
+ * capabilities). Today's API has no dry-run cost for part extraction/split,
95
+ * so that line is reported as unknown rather than guessed. `maxParts`
96
+ * mirrors the server's own conservative constant for this math (24, per
97
+ * #8179) rather than a locally invented cap; `--max-parts` overrides it.
98
+ */
99
+ export declare const estimateOneShotCost: (options: {
100
+ meshModel: ModelSummary | undefined;
101
+ maxParts: number;
102
+ composeRequested: boolean;
103
+ composeCredits: number | null | undefined;
104
+ }) => OneShotCostEstimate;
105
+ /**
106
+ * Reads the server's own `estimatedTotalCredits`/`estimatedCostBreakdown`
107
+ * off a real (non-estimate) automation response, when present (#8179).
108
+ * These fields are not yet in `ProductionAutomationResult`'s shipped type,
109
+ * so this reads them optionally/untyped rather than guessing a schema;
110
+ * absent on a server that has not deployed #8179 yet.
111
+ */
112
+ export declare const readServerCostBreakdown: (automationResult: Record<string, unknown>) => {
113
+ estimatedTotalCredits: number;
114
+ estimatedCostBreakdown: unknown;
115
+ } | undefined;
116
+ /** Whether the estimated (or actual, server-reported) total would exceed `--max-cost`. */
117
+ export declare const exceedsMaxCost: (totalCredits: number, maxCostCredits: number | undefined) => boolean;
118
+ /**
119
+ * Human stage label for one image's `ProductionOrderStatus`. The API has no
120
+ * distinct "compose" status, so `ready`/`in_progress` are both reported as
121
+ * `mesh` (parts are being generated or are ready to be); there is no way to
122
+ * tell "meshing" apart from "composing" from this field alone.
123
+ */
124
+ export declare const stageLabelForOrderStatus: (status: ProductionOrderStatus) => string;
125
+ /**
126
+ * One line per per-image stage transition, plus a batch-level completion
127
+ * line when the completed/failed counts change. "mesh k/N" here means
128
+ * images processed out of the batch, not parts within one image — the batch
129
+ * status endpoint does not expose per-part progress.
130
+ */
131
+ export declare const batchProgressLines: (previous: ProductionAutomationBatchStatusResult | undefined, next: ProductionAutomationBatchStatusResult) => string[];
132
+ /**
133
+ * Polls `getStatus` (expected to be `client.v1.getProductionAutomationStatus`,
134
+ * which already retries 429s in its own request layer) until every image in
135
+ * the batch reaches a terminal outcome, printing one line per stage
136
+ * transition via `onProgress`. Mirrors the deadline/backoff shape of
137
+ * `pollProductionStatus`/`watchProductionRun` in index.ts.
138
+ */
139
+ export declare const pollAutomationBatch: (options: {
140
+ getStatus: () => Promise<ProductionAutomationBatchStatusResult>;
141
+ sleep: (ms: number) => Promise<void>;
142
+ intervalMs: number;
143
+ timeoutMs: number;
144
+ onProgress?: (lines: string[]) => void;
145
+ }) => Promise<ProductionAutomationBatchStatusResult>;
146
+ /**
147
+ * The V6 compose fallback's mesh<->image pairing. Reads the automation
148
+ * batch's own `mesh.generate` executions (fetched via `listCanvasRuns` and
149
+ * filtered to the target order) for each part's produced mesh asset
150
+ * (`execution.outputs`, `mediaType === 'mesh'`) and the part image it was
151
+ * generated from (`execution.inputAssets`, `mediaType === 'image'`) — the
152
+ * same `{assetId, partImageAssetId}` shape `composer run --part
153
+ * <mesh>:<image>` accepts, so no manual pairing is needed.
154
+ */
155
+ export declare const buildComposerPartsFromMeshExecutions: (executions: CanvasExecution[], orderId: string) => MeshComposerPart[];
156
+ /**
157
+ * The V6 compose fallback's `fullBodyImageAssetId`. Automation's own result
158
+ * does not return a durable asset id for the original image (only
159
+ * `orderId`/`projectId`/`url`/`runId`), so this reads it back off the part
160
+ * images' own `sourceAssetId` (each part image's own upstream source is the
161
+ * original full-body photo it was cropped from) — the same field
162
+ * `composerInputFromRun`'s mesh.generate part-field allowlist tracks.
163
+ */
164
+ export declare const resolveFullBodyImageAssetId: (executions: CanvasExecution[], orderId: string) => string | undefined;
165
+ export type KnownErrorLike = {
166
+ status?: number;
167
+ code?: string;
168
+ message: string;
169
+ details?: {
170
+ meshAssetId?: string;
171
+ assetId?: string;
172
+ actualType?: string;
173
+ expectedType?: string;
174
+ };
175
+ };
176
+ /**
177
+ * Actionable next steps appended to (never replacing) a known
178
+ * `AssetHubApiError`'s own message. Matches on `error.code`/`error.details`
179
+ * first — `PART_IMAGE_REQUIRED` and `ASSET_WRONG_MEDIA_TYPE` are confirmed
180
+ * `/mesh/compose` error codes (assethub-web PR #8179) with a structured
181
+ * `details` payload. Falls back to matching on message content for a server
182
+ * that has not shipped those codes yet (e.g. today's deployed API, where
183
+ * the same failures still surface as a plain `404 ASSET_NOT_FOUND` or a
184
+ * generic validation message — "Asset not found" is quoted directly from
185
+ * the originally reported failure). `ASSET_NOT_FOUND` itself (asset does
186
+ * not exist / isn't yours) is left alone; it is not one of these cases.
187
+ */
188
+ export declare const errorHintFor: (error: KnownErrorLike) => string | undefined;
189
+ /** Appends an error hint to `error.message` in place, if one applies. */
190
+ export declare const withErrorHint: <T extends Error & {
191
+ message: string;
192
+ }>(error: T) => T;
193
+ /** Condensed, human-readable lines for `runs get --summary`. */
194
+ export declare const formatExecutionSummaryLines: (execution: CanvasExecution) => string[];
195
+ //# sourceMappingURL=productionOneShot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"productionOneShot.d.ts","sourceRoot":"","sources":["../src/productionOneShot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,307 @@
1
+ /**
2
+ * `--image <file|assetId>` auto-detection: an existing local path is treated
3
+ * as `--file`; anything else (an asset/resource id) is treated as
4
+ * `--source-id`. `exists` is injectable so this stays a pure function in
5
+ * tests instead of touching the real filesystem.
6
+ */
7
+ export const detectOneShotImageInput = (value, exists) => exists(value) ? { kind: 'file', path: value } : { kind: 'resourceId', resourceId: value };
8
+ /** Parses `--compose`. Absent stays absent (server default applies untouched). */
9
+ export const parseComposeFlag = (value) => {
10
+ if (value == null)
11
+ return undefined;
12
+ if (value === 'v6' || value === 'none')
13
+ return value;
14
+ throw new Error('--compose must be "v6" or "none"');
15
+ };
16
+ /**
17
+ * The confirmed field name (assethub-web PR #8179, `web-cli-ux`) for
18
+ * requesting an explicit Composer V6 pass from `production automation`
19
+ * itself. Not `composeModel` — that was this module's pre-#8179 best guess.
20
+ */
21
+ export const PART_COMPOSER_V6_AGENT_VERSION = 'part_composer_v6_auto_assemble';
22
+ /**
23
+ * Builds the `config` object for the `production automation` request.
24
+ *
25
+ * - `--compose` omitted: returns `undefined` so the request carries no
26
+ * `config.autoCompose` at all, leaving the server default in effect.
27
+ * - `--compose none`: disables automation's own compose stage entirely.
28
+ * - `--compose v6`: asks the server to run Composer V6 itself, in the same
29
+ * call, via `partComposerAgentVersion`. `autoCompose` is deliberately left
30
+ * unset (defaults to `true`) — the server 400s with
31
+ * `PART_COMPOSER_VERSION_REQUIRES_AUTO_COMPOSE` if both are sent together,
32
+ * so this function must never combine them. A server that predates #8179
33
+ * ignores the unrecognized field and runs its own default compose instead;
34
+ * `serverSupportsV6Compose` inspects the response afterward so the caller
35
+ * can still fall back to the client-side lineage-pairing compose
36
+ * (`buildComposerPartsFromMeshExecutions`) on that older server.
37
+ */
38
+ export const buildAutomationConfig = (compose) => {
39
+ if (compose == null)
40
+ return undefined;
41
+ if (compose === 'none')
42
+ return { autoCompose: false };
43
+ return { partComposerAgentVersion: PART_COMPOSER_V6_AGENT_VERSION };
44
+ };
45
+ /**
46
+ * `estimatedCostBreakdown` (added by #8179) only appears on a server new
47
+ * enough to also honor `partComposerAgentVersion` — its presence in the
48
+ * automation response is the signal that the server already did the V6
49
+ * compose server-side in that same call, so no client-side fallback compose
50
+ * is needed. Its absence means an older, currently-deployed server: it
51
+ * silently ignored `partComposerAgentVersion` and ran its own default
52
+ * compose (if any), so the caller must still run the explicit fallback.
53
+ */
54
+ export const serverSupportsV6Compose = (automationResult) => automationResult.estimatedCostBreakdown != null;
55
+ export const DEFAULT_MAX_PARTS = 24;
56
+ /**
57
+ * Best-effort cost breakdown for `--estimate`, shaped like the server's own
58
+ * `estimatedCostBreakdown` (#8179: `split`/`meshGeneration`/`compose`, each
59
+ * with a per-unit price and a total). Nothing here calls `production
60
+ * automation`/`analyze` (that would start real, billable work); every
61
+ * number comes from a side-effect-free GET (`models` catalog, `mesh/compose`
62
+ * capabilities). Today's API has no dry-run cost for part extraction/split,
63
+ * so that line is reported as unknown rather than guessed. `maxParts`
64
+ * mirrors the server's own conservative constant for this math (24, per
65
+ * #8179) rather than a locally invented cap; `--max-parts` overrides it.
66
+ */
67
+ export const estimateOneShotCost = (options) => {
68
+ const notes = [];
69
+ const split = {
70
+ perImage: null,
71
+ total: null,
72
+ note: 'part extraction/split cost has no dry-run price source today; the actual cost is reported after the run (see `production status` or `runs get --summary`)',
73
+ };
74
+ notes.push(split.note);
75
+ const perPart = options.meshModel?.creditCost ?? options.meshModel?.defaultCreditCost ?? null;
76
+ const meshTotal = perPart == null ? null : perPart * options.maxParts;
77
+ const meshGeneration = {
78
+ perPart,
79
+ maxParts: options.maxParts,
80
+ total: meshTotal,
81
+ ...(perPart == null
82
+ ? {
83
+ note: options.meshModel
84
+ ? `mesh generation cost not included: no credit price found for model "${options.meshModel.id}"`
85
+ : 'mesh generation cost not included: mesh model was not found in the catalog',
86
+ }
87
+ : {
88
+ note: `assumes up to ${options.maxParts} parts at ${perPart} credits each; the CLI has no way to know the actual part count before splitting runs`,
89
+ }),
90
+ };
91
+ notes.push(meshGeneration.note);
92
+ const lanesPerImage = 1;
93
+ const compose = options.composeRequested
94
+ ? options.composeCredits == null
95
+ ? {
96
+ perLane: null,
97
+ lanesPerImage,
98
+ total: null,
99
+ note: 'compose cost not included: no quote found for the requested compose model',
100
+ }
101
+ : { perLane: options.composeCredits, lanesPerImage, total: options.composeCredits * lanesPerImage }
102
+ : {
103
+ perLane: null,
104
+ lanesPerImage,
105
+ total: 0,
106
+ note: 'compose disabled by --compose none',
107
+ };
108
+ if (compose.note)
109
+ notes.push(compose.note);
110
+ const totalCredits = (meshGeneration.total ?? 0) + (compose.total ?? 0);
111
+ return { breakdown: { split, meshGeneration, compose }, totalCredits, notes };
112
+ };
113
+ /**
114
+ * Reads the server's own `estimatedTotalCredits`/`estimatedCostBreakdown`
115
+ * off a real (non-estimate) automation response, when present (#8179).
116
+ * These fields are not yet in `ProductionAutomationResult`'s shipped type,
117
+ * so this reads them optionally/untyped rather than guessing a schema;
118
+ * absent on a server that has not deployed #8179 yet.
119
+ */
120
+ export const readServerCostBreakdown = (automationResult) => {
121
+ const total = automationResult.estimatedTotalCredits;
122
+ const breakdown = automationResult.estimatedCostBreakdown;
123
+ if (typeof total !== 'number' || breakdown == null)
124
+ return undefined;
125
+ return { estimatedTotalCredits: total, estimatedCostBreakdown: breakdown };
126
+ };
127
+ /** Whether the estimated (or actual, server-reported) total would exceed `--max-cost`. */
128
+ export const exceedsMaxCost = (totalCredits, maxCostCredits) => maxCostCredits != null && totalCredits > maxCostCredits;
129
+ /**
130
+ * Human stage label for one image's `ProductionOrderStatus`. The API has no
131
+ * distinct "compose" status, so `ready`/`in_progress` are both reported as
132
+ * `mesh` (parts are being generated or are ready to be); there is no way to
133
+ * tell "meshing" apart from "composing" from this field alone.
134
+ */
135
+ export const stageLabelForOrderStatus = (status) => {
136
+ switch (status) {
137
+ case 'draft':
138
+ return 'queued';
139
+ case 'analyzing':
140
+ return 'split';
141
+ case 'ready':
142
+ case 'in_progress':
143
+ return 'mesh';
144
+ case 'completed':
145
+ return 'done';
146
+ case 'failed':
147
+ return 'failed';
148
+ default:
149
+ return status;
150
+ }
151
+ };
152
+ /**
153
+ * One line per per-image stage transition, plus a batch-level completion
154
+ * line when the completed/failed counts change. "mesh k/N" here means
155
+ * images processed out of the batch, not parts within one image — the batch
156
+ * status endpoint does not expose per-part progress.
157
+ */
158
+ export const batchProgressLines = (previous, next) => {
159
+ const lines = [];
160
+ const previousStatusByOrder = new Map((previous?.images ?? []).map(image => [image.orderId, image.status]));
161
+ for (const image of next.images) {
162
+ const before = previousStatusByOrder.get(image.orderId);
163
+ if (before !== image.status) {
164
+ lines.push(`[batch] order=${image.orderId} stage=${stageLabelForOrderStatus(image.status)} (${image.status})`);
165
+ }
166
+ }
167
+ const previousSummary = previous?.summary;
168
+ const done = next.summary.completed + next.summary.failed;
169
+ if (previousSummary == null ||
170
+ previousSummary.completed !== next.summary.completed ||
171
+ previousSummary.failed !== next.summary.failed) {
172
+ lines.push(`[batch] mesh ${done}/${next.summary.total} done`);
173
+ }
174
+ return lines;
175
+ };
176
+ /**
177
+ * Polls `getStatus` (expected to be `client.v1.getProductionAutomationStatus`,
178
+ * which already retries 429s in its own request layer) until every image in
179
+ * the batch reaches a terminal outcome, printing one line per stage
180
+ * transition via `onProgress`. Mirrors the deadline/backoff shape of
181
+ * `pollProductionStatus`/`watchProductionRun` in index.ts.
182
+ */
183
+ export const pollAutomationBatch = async (options) => {
184
+ const deadline = Date.now() + options.timeoutMs;
185
+ let previous;
186
+ for (;;) {
187
+ const status = await options.getStatus();
188
+ const lines = batchProgressLines(previous, status);
189
+ if (lines.length && options.onProgress)
190
+ options.onProgress(lines);
191
+ previous = status;
192
+ if (status.summary.completed + status.summary.failed >= status.summary.total) {
193
+ return status;
194
+ }
195
+ if (Date.now() >= deadline) {
196
+ throw new Error(`production run --wait timed out after ${options.timeoutMs}ms for batch=${status.batchId}`);
197
+ }
198
+ await options.sleep(Math.min(options.intervalMs, Math.max(0, deadline - Date.now())));
199
+ }
200
+ };
201
+ /**
202
+ * The V6 compose fallback's mesh<->image pairing. Reads the automation
203
+ * batch's own `mesh.generate` executions (fetched via `listCanvasRuns` and
204
+ * filtered to the target order) for each part's produced mesh asset
205
+ * (`execution.outputs`, `mediaType === 'mesh'`) and the part image it was
206
+ * generated from (`execution.inputAssets`, `mediaType === 'image'`) — the
207
+ * same `{assetId, partImageAssetId}` shape `composer run --part
208
+ * <mesh>:<image>` accepts, so no manual pairing is needed.
209
+ */
210
+ export const buildComposerPartsFromMeshExecutions = (executions, orderId) => {
211
+ const parts = [];
212
+ for (const execution of executions) {
213
+ if (execution.operation !== 'mesh.generate')
214
+ continue;
215
+ if (!execution.orderIds.includes(orderId))
216
+ continue;
217
+ const meshOutput = execution.outputs.find(output => output.mediaType === 'mesh');
218
+ if (!meshOutput)
219
+ continue;
220
+ const partImage = execution.inputAssets?.find(asset => asset.mediaType === 'image');
221
+ parts.push({
222
+ assetId: meshOutput.assetId,
223
+ ...(partImage ? { partImageAssetId: partImage.assetId } : {}),
224
+ });
225
+ }
226
+ return parts;
227
+ };
228
+ /**
229
+ * The V6 compose fallback's `fullBodyImageAssetId`. Automation's own result
230
+ * does not return a durable asset id for the original image (only
231
+ * `orderId`/`projectId`/`url`/`runId`), so this reads it back off the part
232
+ * images' own `sourceAssetId` (each part image's own upstream source is the
233
+ * original full-body photo it was cropped from) — the same field
234
+ * `composerInputFromRun`'s mesh.generate part-field allowlist tracks.
235
+ */
236
+ export const resolveFullBodyImageAssetId = (executions, orderId) => {
237
+ for (const execution of executions) {
238
+ if (execution.operation !== 'mesh.generate')
239
+ continue;
240
+ if (!execution.orderIds.includes(orderId))
241
+ continue;
242
+ const partImage = execution.inputAssets?.find(asset => asset.mediaType === 'image');
243
+ if (partImage?.sourceAssetId)
244
+ return partImage.sourceAssetId;
245
+ }
246
+ return undefined;
247
+ };
248
+ /**
249
+ * Actionable next steps appended to (never replacing) a known
250
+ * `AssetHubApiError`'s own message. Matches on `error.code`/`error.details`
251
+ * first — `PART_IMAGE_REQUIRED` and `ASSET_WRONG_MEDIA_TYPE` are confirmed
252
+ * `/mesh/compose` error codes (assethub-web PR #8179) with a structured
253
+ * `details` payload. Falls back to matching on message content for a server
254
+ * that has not shipped those codes yet (e.g. today's deployed API, where
255
+ * the same failures still surface as a plain `404 ASSET_NOT_FOUND` or a
256
+ * generic validation message — "Asset not found" is quoted directly from
257
+ * the originally reported failure). `ASSET_NOT_FOUND` itself (asset does
258
+ * not exist / isn't yours) is left alone; it is not one of these cases.
259
+ */
260
+ export const errorHintFor = (error) => {
261
+ if (error.code === 'PART_IMAGE_REQUIRED') {
262
+ const meshAssetId = error.details?.meshAssetId ?? '<mesh-asset-id>';
263
+ return `pass --part ${meshAssetId}:<part-image-asset-id>, or regenerate with mesh generate --derived-from-run <split-run-id>`;
264
+ }
265
+ if (error.code === 'ASSET_WRONG_MEDIA_TYPE') {
266
+ const { assetId, actualType, expectedType } = error.details ?? {};
267
+ if (expectedType === 'mesh' && actualType === 'image') {
268
+ return `asset ${assetId ?? '<asset-id>'} is an image; run mesh generate (or production run --image) first`;
269
+ }
270
+ return `asset ${assetId ?? '<asset-id>'} has the wrong type: expected ${expectedType ?? 'unknown'}, got ${actualType ?? 'unknown'}`;
271
+ }
272
+ const message = error.message.toLowerCase();
273
+ if (message.includes('asset not found') || message.includes('not found')) {
274
+ return 'compose expects a mesh part asset id, not an image asset — run `mesh generate` (or `production run --image <file|assetId>`) first, then compose the resulting mesh asset ids';
275
+ }
276
+ if (message.includes('part image') ||
277
+ message.includes('partimageassetid') ||
278
+ message.includes('full body image') ||
279
+ message.includes('fullbodyimageassetid')) {
280
+ return "pass `--part <mesh-asset-id>:<part-image-asset-id>` on `composer run`/`composer refine` to attach each part's source image (or use `production run --image ... --compose v6`, which pairs them automatically)";
281
+ }
282
+ return undefined;
283
+ };
284
+ /** Appends an error hint to `error.message` in place, if one applies. */
285
+ export const withErrorHint = (error) => {
286
+ const hint = errorHintFor(error);
287
+ if (hint)
288
+ error.message = `${error.message} (${hint})`;
289
+ return error;
290
+ };
291
+ /** Condensed, human-readable lines for `runs get --summary`. */
292
+ export const formatExecutionSummaryLines = (execution) => {
293
+ const lines = [
294
+ `runId: ${execution.runId}`,
295
+ `operation: ${execution.operation}`,
296
+ `status: ${execution.status}`,
297
+ `canvas: ${execution.canvas.id}`,
298
+ ];
299
+ if (execution.orderIds.length)
300
+ lines.push(`orders: ${execution.orderIds.join(', ')}`);
301
+ lines.push(`credits: reserved=${execution.usage.reservedCredits ?? '-'} charged=${execution.usage.chargedCredits ?? '-'}`);
302
+ const output = execution.outputs[0];
303
+ lines.push(`output: ${output ? output.assetId : '-'}`);
304
+ if (execution.error)
305
+ lines.push(`error: ${execution.error.code}: ${execution.error.message}`);
306
+ return lines;
307
+ };
package/dist/setup.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2
2
  export declare const cliVersion: () => Promise<string>;
3
+ export declare const validatedBaseUrl: (baseUrl: string) => string;
3
4
  export declare const mcpConfig: (client: string, baseUrl: string, account?: boolean, workspaceId?: string) => string;
4
5
  type Check = {
5
6
  name: 'api' | 'mcp' | 'configuration';
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oCAAoC,CAAA;AAE5D,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,MAAM,CAKvC,CAAA;AAmBX,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,EAAE,iBAAe,EAAE,cAAc,MAAM,KAAG,MAsBlG,CAAA;AAED,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,eAAe,CAAA;IACrC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CACf,CAAA;AAED,KAAK,IAAI,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AA2DhI,eAAO,MAAM,QAAQ,GAAU,gEAM5B;IACD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;;;;;;;;;;;;;;;;;;;;;;EAkHA,CAAA"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oCAAoC,CAAA;AAE5D,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,MAAM,CAKvC,CAAA;AAEX,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,MAelD,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,EAAE,iBAAe,EAAE,cAAc,MAAM,KAAG,MAsBlG,CAAA;AAED,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,eAAe,CAAA;IACrC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CACf,CAAA;AAED,KAAK,IAAI,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AA2DhI,eAAO,MAAM,QAAQ,GAAU,gEAM5B;IACD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;;;;;;;;;;;;;;;;;;;;;;EAkHA,CAAA"}
package/dist/setup.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { AssetHubApiError, createAssetHubClient, createWorkspaceClient, WorkspaceClientError } from '@assethub/api-client';
3
3
  export const cliVersion = async () => JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
4
- const validatedBaseUrl = (baseUrl) => {
4
+ export const validatedBaseUrl = (baseUrl) => {
5
5
  const url = new URL(baseUrl);
6
6
  if (url.username || url.password || url.search || url.hash)
7
7
  throw new Error('Use an API URL without credentials, query, or fragment.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "skills",
20
21
  "README.md",
21
22
  "LICENSE"
22
23
  ],
@@ -30,7 +31,7 @@
30
31
  "access": "public"
31
32
  },
32
33
  "dependencies": {
33
- "@assethub/api-client": "^0.1.12",
34
+ "@assethub/api-client": "^0.1.13",
34
35
  "@modelcontextprotocol/sdk": "^1.30.0"
35
36
  },
36
37
  "repository": {