@helix3/helix-cli 0.1.13-helix3.65 → 0.1.13-helix3.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,9 +49,11 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
49
49
  | `helix thumbnail set <slug> <file>` | Upload a thumbnail or preview image through the CLI-owned media path. |
50
50
  | `helix assets search\|get\|versions\|track\|install\|update` | Search the typed Vault with explainable ranking; inspect/manage annotations and immutable versions; install by durable ID with verified SHA-256 receipts. |
51
51
  | `helix assets start\|status\|generate\|resume` | Start or poll generation without duplicates. Props/characters use the main Dreamer path; standalone assets use the shared broker and report the auto-published `vaultAssetId`. |
52
+ | `helix assets voices [--search <text>] [--category premade\|professional] [--page-size <n>] [--page-token <token>] [--json]` | Discover safe text-to-speech voices through the authenticated HELIX catalog, including language, labels, preview, default/recommended selection, and pagination. |
52
53
  | `helix assets generate-image\|generate-material\|generate-audio\|generate-environment` | Run the supported standalone generation adapters. Material is a validated PBR bundle (1K default, explicit 2K); splats declare object/environment scope. Animation remains an explicit unavailable boundary. |
53
54
  | `helix assets start-reference\|generate-reference` | Start or fully drive a character job from a local four-view PNG/JPEG/WebP. The backend validates and reuses the exact sheet; it skips image generation while preserving mesh, texture, rig, LOD, thumbnail, billing, and Vault publication. |
54
- | `helix assets materials\|material` | Discover the small built-in material palette and resolve its immutable map URLs before searching or generating. |
55
+ | `helix assets materials [--resolution <res>] [--human]` | Discover the small built-in material palette. `--resolution` keeps only materials carrying that texture resolution. |
56
+ | `helix assets material <id> [--resolution <res>] [--human]` | Resolve one material to its immutable map URLs at the resolution you pick (default: the pack's own default). |
55
57
  | `helix assets credits\|check-loaders` | Render asset credits from provenance and reject models requiring unsupported loaders. |
56
58
  | `helix character retarget-animation <clip.fbx\|clip.glb>` | Retarget a raw Mixamo/Meshy clip directly to `helix-humanoid@1`. FBX conversion is deterministic and runs inside the CLI; no hidden Blender/DCC pre-export is required. |
57
59
  | `helix world audit\|source-audit\|perf-gate\|prove-live` | Run the blocking world QA, performance, and deployed-build identity gates. |
@@ -145,11 +147,15 @@ helix assets generate-audio "hopeful orchestral exploration theme" \
145
147
  --mode music --duration-seconds 180 --force-instrumental
146
148
 
147
149
  # Text to speech
150
+ helix assets voices --search narrator --page-size 10
148
151
  helix assets generate-audio --mode text_to_speech \
149
- --text "Welcome to HELIX." --voice-id 21m00Tcm4TlvDq8ikWAM \
152
+ --text "Welcome to HELIX." --voice-id <id-from-catalog> \
150
153
  --language-code en --stability 0.5 --speaker-boost
151
154
  ```
152
155
 
156
+ Use the opaque `--page-token` printed by a page to continue. Voice cloning and
157
+ voice administration are deliberately not exposed.
158
+
153
159
  Use a deterministic four-view sheet when you already have approved FRONT, BACK,
154
160
  LEFT, and RIGHT character art and do not want Dreamer to generate another image:
155
161
 
@@ -164,6 +170,47 @@ The reference endpoint is character-only. The file is image-guarded, normalized,
164
170
  checksum-bound to the job, semantically validated, and mirrored before approval.
165
171
  Retries reuse those same bytes and never invoke or charge the image provider.
166
172
 
173
+ ## Material texture resolution
174
+
175
+ A platform material can ship more than one texture resolution. `--resolution` picks which one you
176
+ get; without it you get the pack's declared default, byte-for-byte what every earlier CLI returned.
177
+
178
+ ```bash
179
+ helix assets material brick-block # the pack's default resolution
180
+ helix assets material brick-block --resolution 2k # the 2K variant's map URLs
181
+ helix assets material brick-block --resolution 2048 # same thing — numeric spelling
182
+ helix assets materials --resolution 2k --human # only materials that carry 2K
183
+ ```
184
+
185
+ Accepted values are the keys the catalog itself declares (`1k`, `2k`, …), case-insensitively, plus
186
+ the numeric aliases `1024`/`2048`, which map onto the variant authored at that pixel edge.
187
+
188
+ Both commands print JSON by default (agents and the MCP delegation parse it); `--human` prints a
189
+ readable summary instead. Either way the output states **which resolution was resolved and which
190
+ ones the material offers**, under `resolution`:
191
+
192
+ ```jsonc
193
+ "resolution": {
194
+ "requested": "2k", "resolved": "2k", "pixels": 2048, "default": "1k",
195
+ "available": [{ "key": "1k", "pixels": 1024 }, { "key": "2k", "pixels": 2048 }],
196
+ "applicable": true
197
+ }
198
+ ```
199
+
200
+ Three rules make this safe to rely on:
201
+
202
+ - **No silent fallback, ever.** Asking for a resolution the material does not carry is an error that
203
+ names the ones it does. You never receive a different resolution than the one you asked for.
204
+ - **Old packs keep working.** A catalog with no `resolutions` block (`schemaVersion: 1`) is treated
205
+ as having exactly one resolution, keyed `default`, derived from its existing `maps`. Such a pack
206
+ never claims to be "1k" — it does not say how big its textures are, so neither do we.
207
+ - **Procedural materials are not an error.** `glass` and `procedural_water` carry no texture maps;
208
+ a resolution request against them is ignored and the output says so in `resolution.note`.
209
+
210
+ `helix assets install` has no resolution flag: Vault related-artifact roles are semantic
211
+ (`source.albedo`, `runtime.ktx2.normal`, `sky.backdrop`), never resolution-tagged, so a filter there
212
+ would filter nothing.
213
+
167
214
  ## Item publish flow
168
215
 
169
216
  `helix item publish` is a different shape from a world publish: one mesh, one round trip, an
package/dist/assets.d.ts CHANGED
@@ -14,6 +14,48 @@ export type DreamerAudioVoiceSettings = {
14
14
  speed?: number;
15
15
  speakerBoost?: boolean;
16
16
  };
17
+ export type DreamerVoiceCategory = 'premade' | 'professional';
18
+ export type DreamerVoiceLanguage = {
19
+ code: string;
20
+ locale: string | null;
21
+ accent: string | null;
22
+ previewUrl: string | null;
23
+ };
24
+ export type DreamerCatalogVoice = {
25
+ id: string;
26
+ name: string;
27
+ description: string | null;
28
+ category: DreamerVoiceCategory;
29
+ labels: {
30
+ accent: string | null;
31
+ age: string | null;
32
+ gender: string | null;
33
+ useCase: string | null;
34
+ };
35
+ languages: DreamerVoiceLanguage[];
36
+ previewUrl: string | null;
37
+ curated: true;
38
+ };
39
+ export type DreamerVoiceCatalogQuery = {
40
+ search?: string;
41
+ pageSize?: number;
42
+ pageToken?: string;
43
+ category?: DreamerVoiceCategory;
44
+ };
45
+ export type DreamerVoiceCatalogResponse = {
46
+ voices: DreamerCatalogVoice[];
47
+ selection: {
48
+ defaultVoiceId: string | null;
49
+ curatedVoiceIds: string[];
50
+ };
51
+ pageInfo: {
52
+ pageSize: number;
53
+ hasMore: boolean;
54
+ nextPageToken: string | null;
55
+ totalCount: number | null;
56
+ };
57
+ stale: boolean;
58
+ };
17
59
  export type DreamerAssetJob = {
18
60
  id: string;
19
61
  status: string;
@@ -229,25 +271,108 @@ export declare class UnavailableCapabilityError extends Error {
229
271
  readonly code = "CAPABILITY_UNAVAILABLE";
230
272
  constructor(capability: string, message: string);
231
273
  }
274
+ export type MaterialMaps = {
275
+ albedo: string;
276
+ normal: string;
277
+ orm: string;
278
+ };
279
+ /**
280
+ * One texture-resolution variant a catalog entry declares (pack catalog
281
+ * schemaVersion 2+). `pixels` is the authored square edge, e.g. 2048 for "2k".
282
+ */
283
+ export type MaterialResolutionVariant = {
284
+ pixels?: number;
285
+ maps: MaterialMaps;
286
+ };
232
287
  export type MaterialCatalogEntry = {
233
288
  id: string;
234
289
  name: string;
235
290
  category: string;
236
291
  kind: 'texture' | 'glass' | 'procedural_water';
237
292
  tags: string[];
238
- maps?: {
239
- albedo: string;
240
- normal: string;
241
- orm: string;
242
- };
293
+ /** The DEFAULT resolution's maps. Unchanged across schema versions. */
294
+ maps?: MaterialMaps;
295
+ /** Catalog schemaVersion 2+: names which `resolutions` key `maps` mirrors. */
296
+ defaultResolution?: string;
297
+ /** Catalog schemaVersion 2+: every texture resolution this material carries. */
298
+ resolutions?: Record<string, MaterialResolutionVariant>;
243
299
  [key: string]: unknown;
244
300
  };
301
+ /** One resolvable resolution of one material, normalized across schema versions. */
302
+ export type MaterialResolutionOption = {
303
+ key: string;
304
+ pixels: number | null;
305
+ maps: MaterialMaps;
306
+ };
307
+ /**
308
+ * What a command actually resolved, and what else it could have. Present on
309
+ * every material this CLI returns so a caller can SEE the choice it has
310
+ * instead of guessing — and so a silent downgrade is impossible.
311
+ */
312
+ export type MaterialResolutionReport = {
313
+ /** Exactly what the caller passed, or null when they did not ask. */
314
+ requested: string | null;
315
+ /** The catalog key whose maps were used; null when the material has none. */
316
+ resolved: string | null;
317
+ /** Authored square edge of the resolved variant when the pack declares it. */
318
+ pixels: number | null;
319
+ /** The key this material treats as its default. */
320
+ default: string | null;
321
+ available: Array<{
322
+ key: string;
323
+ pixels: number | null;
324
+ }>;
325
+ /** false for procedural materials — they carry no texture maps at all. */
326
+ applicable: boolean;
327
+ note?: string;
328
+ };
329
+ export type ResolvedMaterial = MaterialCatalogEntry & {
330
+ resolvedMaps?: Record<string, string>;
331
+ resolution: MaterialResolutionReport;
332
+ };
245
333
  export type MaterialCatalogResult = {
246
334
  slug: string;
247
335
  version: string;
336
+ /** The catalog's own schemaVersion, verbatim (1 = no per-resolution variants). */
337
+ schemaVersion: string | number | null;
248
338
  assetBaseUrl: string;
249
- materials: MaterialCatalogEntry[];
339
+ materials: ResolvedMaterial[];
340
+ resolution: {
341
+ requested: string | null;
342
+ /** Every resolution key offered by ANY material in this pack. */
343
+ packResolutions: Array<{
344
+ key: string;
345
+ pixels: number | null;
346
+ }>;
347
+ matched: number;
348
+ /** Texture materials dropped because they lack the requested resolution. */
349
+ skippedWithoutResolution: number;
350
+ /** Procedural materials dropped because they carry no texture maps. */
351
+ skippedProcedural: number;
352
+ };
250
353
  };
354
+ /**
355
+ * The label a schemaVersion-1 entry's single resolution gets. Such a pack never
356
+ * says how big its maps are, so naming it "1k" would be a guess presented as a
357
+ * fact — and a creator asking for "1k" would then be silently served whatever
358
+ * the pack happens to hold. "default" is the only honest label.
359
+ */
360
+ export declare const DEFAULT_RESOLUTION_KEY = "default";
361
+ /**
362
+ * Every resolution a material offers, in catalog-declared order, normalized so
363
+ * that an old single-resolution pack and a new multi-resolution pack are the
364
+ * same shape to every caller above this line.
365
+ */
366
+ export declare function materialResolutionOptions(material: MaterialCatalogEntry): MaterialResolutionOption[];
367
+ /** The key whose maps the pack mirrors at the top level — the default download. */
368
+ export declare function materialDefaultResolutionKey(material: MaterialCatalogEntry, options?: MaterialResolutionOption[]): string | null;
369
+ /**
370
+ * Match a requested resolution against what a material declares. Exact keys win
371
+ * (case-insensitively); `1024`/`2048` and `1k`/`2k` are accepted as aliases of
372
+ * each other so a caller never has to know which spelling a pack chose.
373
+ * Returns null when nothing matches — the caller MUST then fail loudly.
374
+ */
375
+ export declare function matchMaterialResolution(options: MaterialResolutionOption[], requested: string): MaterialResolutionOption | null;
251
376
  export declare function generateAndPublishAsset(creds: CliCredentials, input: GenerateAssetInput, options?: GenerateAssetOptions): Promise<DreamerAssetJob>;
252
377
  export declare function generateAndPublishAssetFromReference(creds: CliCredentials, input: GenerateAssetFromReferenceInput, options?: GenerateAssetOptions): Promise<DreamerAssetJob>;
253
378
  export declare function startDreamerAssetGeneration(creds: CliCredentials, input: GenerateAssetInput): Promise<DreamerAssetJob>;
@@ -293,6 +418,12 @@ export declare function generateAudio(creds: CliCredentials, input: GenerateAudi
293
418
  timeoutMs?: number;
294
419
  onProgress?: (job: StandaloneGeneratedAsset) => void;
295
420
  }): Promise<StandaloneGeneratedAsset>;
421
+ /**
422
+ * List/search the HELIX-safe text-to-speech voice catalog. The backend keeps
423
+ * vendor credentials and voice administration private; callers receive only
424
+ * the stable selection projection used by website, CLI, SDK, and MCP clients.
425
+ */
426
+ export declare function listDreamerVoices(creds: CliCredentials, query?: DreamerVoiceCatalogQuery): Promise<DreamerVoiceCatalogResponse>;
296
427
  export declare function searchVaultAssets(apiUrl: string, query?: VaultAssetSearch, creds?: CliCredentials): Promise<{
297
428
  items: VaultAssetSummary[];
298
429
  totalItems: number;
@@ -326,7 +457,8 @@ export declare function listMaterials(apiUrl: string, query?: {
326
457
  q?: string;
327
458
  category?: string;
328
459
  limit?: number;
460
+ resolution?: string;
329
461
  }, slug?: string): Promise<MaterialCatalogResult>;
330
- export declare function resolveMaterial(apiUrl: string, id: string, slug?: string): Promise<MaterialCatalogEntry & {
331
- resolvedMaps?: Record<string, string>;
332
- }>;
462
+ export declare function resolveMaterial(apiUrl: string, id: string, slug?: string, options?: {
463
+ resolution?: string;
464
+ }): Promise<ResolvedMaterial>;
package/dist/assets.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UnavailableCapabilityError = exports.AUDIO_GENERATION_TIMEOUT_MS = exports.VAULT_ASSET_KINDS = void 0;
3
+ exports.DEFAULT_RESOLUTION_KEY = exports.UnavailableCapabilityError = exports.AUDIO_GENERATION_TIMEOUT_MS = exports.VAULT_ASSET_KINDS = void 0;
4
+ exports.materialResolutionOptions = materialResolutionOptions;
5
+ exports.materialDefaultResolutionKey = materialDefaultResolutionKey;
6
+ exports.matchMaterialResolution = matchMaterialResolution;
4
7
  exports.generateAndPublishAsset = generateAndPublishAsset;
5
8
  exports.generateAndPublishAssetFromReference = generateAndPublishAssetFromReference;
6
9
  exports.startDreamerAssetGeneration = startDreamerAssetGeneration;
@@ -12,6 +15,7 @@ exports.startStandaloneAssetGeneration = startStandaloneAssetGeneration;
12
15
  exports.getStandaloneAssetGenerationJob = getStandaloneAssetGenerationJob;
13
16
  exports.generateImage = generateImage;
14
17
  exports.generateAudio = generateAudio;
18
+ exports.listDreamerVoices = listDreamerVoices;
15
19
  exports.searchVaultAssets = searchVaultAssets;
16
20
  exports.getVaultAsset = getVaultAsset;
17
21
  exports.getVaultAssetVersions = getVaultAssetVersions;
@@ -52,6 +56,184 @@ class UnavailableCapabilityError extends Error {
52
56
  }
53
57
  }
54
58
  exports.UnavailableCapabilityError = UnavailableCapabilityError;
59
+ /**
60
+ * The label a schemaVersion-1 entry's single resolution gets. Such a pack never
61
+ * says how big its maps are, so naming it "1k" would be a guess presented as a
62
+ * fact — and a creator asking for "1k" would then be silently served whatever
63
+ * the pack happens to hold. "default" is the only honest label.
64
+ */
65
+ exports.DEFAULT_RESOLUTION_KEY = 'default';
66
+ const isMapTable = (value) => !!value &&
67
+ typeof value === 'object' &&
68
+ !Array.isArray(value) &&
69
+ Object.values(value).every((entry) => typeof entry === 'string');
70
+ /**
71
+ * Every resolution a material offers, in catalog-declared order, normalized so
72
+ * that an old single-resolution pack and a new multi-resolution pack are the
73
+ * same shape to every caller above this line.
74
+ */
75
+ function materialResolutionOptions(material) {
76
+ const declared = material.resolutions;
77
+ if (declared && typeof declared === 'object' && !Array.isArray(declared)) {
78
+ const options = Object.entries(declared)
79
+ .filter(([key, variant]) => key.trim() !== '' && isMapTable(variant?.maps))
80
+ .map(([key, variant]) => ({
81
+ key,
82
+ pixels: typeof variant.pixels === 'number' && Number.isFinite(variant.pixels) ? variant.pixels : null,
83
+ maps: variant.maps,
84
+ }));
85
+ if (options.length)
86
+ return options;
87
+ }
88
+ // schemaVersion 1 (or an entry with no `resolutions`): exactly one resolution,
89
+ // derived from the top-level maps that every existing client already reads.
90
+ if (isMapTable(material.maps)) {
91
+ return [
92
+ {
93
+ key: (typeof material.defaultResolution === 'string' && material.defaultResolution.trim()) || exports.DEFAULT_RESOLUTION_KEY,
94
+ pixels: null,
95
+ maps: material.maps,
96
+ },
97
+ ];
98
+ }
99
+ return [];
100
+ }
101
+ const sameMaps = (a, b) => {
102
+ const left = Object.entries(a).sort(([x], [y]) => x.localeCompare(y));
103
+ const right = Object.entries(b).sort(([x], [y]) => x.localeCompare(y));
104
+ return (left.length === right.length &&
105
+ left.every(([key, value], index) => right[index][0] === key && right[index][1] === value));
106
+ };
107
+ /** The key whose maps the pack mirrors at the top level — the default download. */
108
+ function materialDefaultResolutionKey(material, options = materialResolutionOptions(material)) {
109
+ if (!options.length)
110
+ return null;
111
+ const declared = typeof material.defaultResolution === 'string' ? material.defaultResolution.trim().toLowerCase() : '';
112
+ if (declared) {
113
+ const named = options.find((option) => option.key.toLowerCase() === declared);
114
+ if (named)
115
+ return named.key;
116
+ }
117
+ if (isMapTable(material.maps)) {
118
+ const mirrored = options.find((option) => sameMaps(option.maps, material.maps));
119
+ if (mirrored)
120
+ return mirrored.key;
121
+ }
122
+ return options[0].key;
123
+ }
124
+ /**
125
+ * Match a requested resolution against what a material declares. Exact keys win
126
+ * (case-insensitively); `1024`/`2048` and `1k`/`2k` are accepted as aliases of
127
+ * each other so a caller never has to know which spelling a pack chose.
128
+ * Returns null when nothing matches — the caller MUST then fail loudly.
129
+ */
130
+ function matchMaterialResolution(options, requested) {
131
+ const wanted = requested.trim().toLowerCase();
132
+ if (!wanted)
133
+ return null;
134
+ const exact = options.find((option) => option.key.toLowerCase() === wanted);
135
+ if (exact)
136
+ return exact;
137
+ // Numeric spelling ("2048", "2048px") → the variant authored at that edge, or
138
+ // the conventional "2k" key when the pack declares no pixel counts.
139
+ const numeric = /^(\d+)(?:px)?$/.exec(wanted);
140
+ if (numeric) {
141
+ const pixels = Number(numeric[1]);
142
+ const byPixels = options.find((option) => option.pixels === pixels);
143
+ if (byPixels)
144
+ return byPixels;
145
+ if (pixels > 0 && pixels % 1024 === 0) {
146
+ const derived = `${pixels / 1024}k`;
147
+ const byDerivedKey = options.find((option) => option.key.toLowerCase() === derived);
148
+ if (byDerivedKey)
149
+ return byDerivedKey;
150
+ }
151
+ return null;
152
+ }
153
+ // "k" spelling → the variant authored at that edge, for a pack whose keys are
154
+ // named something else entirely but which declares pixels.
155
+ const kilo = /^(\d+(?:\.\d+)?)k$/.exec(wanted);
156
+ if (kilo) {
157
+ const pixels = Math.round(Number(kilo[1]) * 1024);
158
+ const byPixels = options.find((option) => option.pixels === pixels);
159
+ if (byPixels)
160
+ return byPixels;
161
+ }
162
+ return null;
163
+ }
164
+ const renderResolutionList = (options) => options.length
165
+ ? options.map((option) => (option.pixels ? `${option.key} (${option.pixels}px)` : option.key)).join(', ')
166
+ : '(none)';
167
+ const publicResolutions = (options) => options.map(({ key, pixels }) => ({ key, pixels }));
168
+ /** Union of every resolution key in the pack, in first-seen order. */
169
+ function packResolutionOptions(materials) {
170
+ const seen = new Map();
171
+ for (const material of materials) {
172
+ for (const option of materialResolutionOptions(material)) {
173
+ const id = option.key.toLowerCase();
174
+ const prior = seen.get(id);
175
+ if (!prior)
176
+ seen.set(id, { key: option.key, pixels: option.pixels });
177
+ else if (prior.pixels === null && option.pixels !== null)
178
+ prior.pixels = option.pixels;
179
+ }
180
+ }
181
+ return [...seen.values()];
182
+ }
183
+ function absoluteMaps(maps, assetBaseUrl) {
184
+ return Object.fromEntries(Object.entries(maps).map(([key, value]) => [
185
+ key,
186
+ /^https?:\/\//i.test(value) ? value : `${assetBaseUrl}/${value.replace(/^\/+/, '')}`,
187
+ ]));
188
+ }
189
+ /**
190
+ * Attach the resolution report (and, when asked, the resolved absolute map
191
+ * URLs) to one catalog entry. Throws when the caller asked for a resolution
192
+ * this material does not carry — never falls back to a different one.
193
+ */
194
+ function describeMaterialResolution(material, assetBaseUrl, requested) {
195
+ const options = materialResolutionOptions(material);
196
+ const available = publicResolutions(options);
197
+ const defaultKey = materialDefaultResolutionKey(material, options);
198
+ if (!options.length) {
199
+ // Procedural (glass / water): no maps exist, so a resolution is not an
200
+ // error — it simply does not apply, and we say so out loud.
201
+ return {
202
+ ...material,
203
+ resolution: {
204
+ requested,
205
+ resolved: null,
206
+ pixels: null,
207
+ default: null,
208
+ available,
209
+ applicable: false,
210
+ ...(requested
211
+ ? {
212
+ note: `"${material.id}" is a procedural ${material.kind} material with no texture maps — resolution "${requested}" does not apply and was ignored.`,
213
+ }
214
+ : {}),
215
+ },
216
+ };
217
+ }
218
+ const selected = requested
219
+ ? matchMaterialResolution(options, requested)
220
+ : options.find((option) => option.key === defaultKey) ?? options[0];
221
+ if (!selected) {
222
+ throw new Error(`Material "${material.id}" does not offer resolution "${requested}" — available: ${renderResolutionList(available)}`);
223
+ }
224
+ return {
225
+ ...material,
226
+ resolvedMaps: absoluteMaps(selected.maps, assetBaseUrl),
227
+ resolution: {
228
+ requested,
229
+ resolved: selected.key,
230
+ pixels: selected.pixels,
231
+ default: defaultKey,
232
+ available,
233
+ applicable: true,
234
+ },
235
+ };
236
+ }
55
237
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
56
238
  function isTerminalFailure(job) {
57
239
  return ['failed', 'blocked_insufficient_spark', 'cancelled'].includes(job.status);
@@ -199,6 +381,24 @@ function generateImage(creds, input, options = {}) {
199
381
  function generateAudio(creds, input, options = {}) {
200
382
  return generateStandaloneAsset(creds, { kind: 'audio', ...input }, { timeoutMs: exports.AUDIO_GENERATION_TIMEOUT_MS, ...options });
201
383
  }
384
+ /**
385
+ * List/search the HELIX-safe text-to-speech voice catalog. The backend keeps
386
+ * vendor credentials and voice administration private; callers receive only
387
+ * the stable selection projection used by website, CLI, SDK, and MCP clients.
388
+ */
389
+ function listDreamerVoices(creds, query = {}) {
390
+ const params = new URLSearchParams();
391
+ if (query.search)
392
+ params.set('search', query.search);
393
+ if (query.pageSize !== undefined)
394
+ params.set('pageSize', String(query.pageSize));
395
+ if (query.pageToken)
396
+ params.set('pageToken', query.pageToken);
397
+ if (query.category)
398
+ params.set('category', query.category);
399
+ const suffix = params.size ? `?${params}` : '';
400
+ return api_1.HelixApi.forCredentials(creds).request('GET', `/api/v1/dreamer/voices${suffix}`);
401
+ }
202
402
  async function searchVaultAssets(apiUrl, query = {}, creds) {
203
403
  const api = creds ? api_1.HelixApi.forCredentials(creds) : new api_1.HelixApi(apiUrl);
204
404
  const params = new URLSearchParams();
@@ -492,6 +692,13 @@ async function listMaterials(apiUrl, query = {}, slug = 'helix-materials') {
492
692
  const catalog = (await response.json());
493
693
  if (!Array.isArray(catalog.materials))
494
694
  throw new Error('Material catalog is invalid');
695
+ const requested = query.resolution?.trim() ? query.resolution.trim() : null;
696
+ const packResolutions = packResolutionOptions(catalog.materials);
697
+ // A resolution this pack does not carry ANYWHERE is a loud failure, not an
698
+ // empty result — an empty list reads as "no materials matched your search".
699
+ if (requested && !catalog.materials.some((material) => matchMaterialResolution(materialResolutionOptions(material), requested))) {
700
+ throw new Error(`Material pack "${catalog.pack?.slug ?? slug}"@${catalog.pack?.version ?? 'latest'} does not offer resolution "${requested}" — available: ${renderResolutionList(packResolutions)}`);
701
+ }
495
702
  const needle = query.q?.trim().toLowerCase() ?? '';
496
703
  const matched = catalog.materials.filter((material) => {
497
704
  if (query.category && material.category !== query.category)
@@ -502,29 +709,51 @@ async function listMaterials(apiUrl, query = {}, slug = 'helix-materials') {
502
709
  .toLowerCase()
503
710
  .includes(needle));
504
711
  });
505
- const materials = query.limit === undefined
506
- ? matched
507
- : matched.slice(0, Math.max(0, Math.floor(query.limit)));
712
+ let skippedWithoutResolution = 0;
713
+ let skippedProcedural = 0;
714
+ const carried = requested
715
+ ? matched.filter((material) => {
716
+ const options = materialResolutionOptions(material);
717
+ if (!options.length) {
718
+ skippedProcedural += 1;
719
+ return false;
720
+ }
721
+ if (!matchMaterialResolution(options, requested)) {
722
+ skippedWithoutResolution += 1;
723
+ return false;
724
+ }
725
+ return true;
726
+ })
727
+ : matched;
728
+ const limited = query.limit === undefined
729
+ ? carried
730
+ : carried.slice(0, Math.max(0, Math.floor(query.limit)));
731
+ const materials = limited.map((material) => describeMaterialResolution(material, assetBaseUrl, requested));
508
732
  return {
509
733
  slug: catalog.pack?.slug ?? slug,
510
734
  version: catalog.pack?.version ?? 'latest',
735
+ schemaVersion: catalog.schemaVersion ?? null,
511
736
  assetBaseUrl,
512
737
  materials,
738
+ resolution: {
739
+ requested,
740
+ packResolutions,
741
+ matched: materials.length,
742
+ skippedWithoutResolution,
743
+ skippedProcedural,
744
+ },
513
745
  };
514
746
  }
515
- async function resolveMaterial(apiUrl, id, slug = 'helix-materials') {
747
+ async function resolveMaterial(apiUrl, id, slug = 'helix-materials', options = {}) {
748
+ // Resolve against the UNFILTERED catalog: the resolution check belongs to the
749
+ // named material, so `helix assets material <id> --resolution 4k` must say
750
+ // what THAT material offers, not "not found".
516
751
  const catalog = await listMaterials(apiUrl, {}, slug);
517
752
  const material = catalog.materials.find((candidate) => candidate.id === id);
518
753
  if (!material)
519
754
  throw new Error(`Material "${id}" was not found in ${slug}`);
520
- const resolvedMaps = material.maps
521
- ? Object.fromEntries(Object.entries(material.maps).map(([key, value]) => [
522
- key,
523
- /^https?:\/\//i.test(value)
524
- ? value
525
- : `${catalog.assetBaseUrl}/${value.replace(/^\/+/, '')}`,
526
- ]))
527
- : undefined;
528
- return { ...material, ...(resolvedMaps ? { resolvedMaps } : {}) };
755
+ const requested = options.resolution?.trim() ? options.resolution.trim() : null;
756
+ const { resolvedMaps: _ignored, resolution: _prior, ...entry } = material;
757
+ return describeMaterialResolution(entry, catalog.assetBaseUrl, requested);
529
758
  }
530
759
  //# sourceMappingURL=assets.js.map