museav-cli 2.4.1 → 2.5.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/AGENTS.md CHANGED
@@ -87,7 +87,9 @@ museav products
87
87
  museav assets
88
88
 
89
89
  # Check who you're logged in as and whether the account is affiliated with a tenant
90
- # (personal login only apiKey callers get an error, they're already acting as the tenant)
90
+ # Works for both personal login and apiKey (platform account or tenant). For apiKey:
91
+ # platform-account → 账户: nickname + 邮箱 + 累计出图 + credits
92
+ # tenant → 租户: tenant_id + name + logo
91
93
  museav whoami
92
94
  ```
93
95
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 (2026-08-17)
4
+
5
+ ### 修复
6
+ - **`whoami` 不再拒绝 apiKey 模式**:之前会直接抛错「只支持个人 login 身份」。实际上 `/api/me` 对两种 apiKey 都返回真实数据——
7
+ - 平台账户 apiKey(`STUDIO_API_KEY` 指向 `accounts.sk-*`)→ 账户身份:邮箱 + nickname + 累计出图次数 + credits
8
+ - 租户 apiKey(指向 `api_tenants.tenant_key`)→ 业务身份:tenant_id + name + nickname + logo
9
+ 修复后三种身份都能 `museav whoami` 查清楚当前是谁、关联到哪个租户。
10
+
11
+ ### 文档
12
+ - AGENTS.md `whoami` 说明:去掉「apiKey 不可用」的旧断言。
13
+
3
14
  ## 2.4.0 · 2026-08-17
4
15
 
5
16
  **本地图像工具箱补全(免登录、零成本、macOS/Windows 通用)。** 均为轻量级工具,无大模型常驻内存。
package/README.md CHANGED
@@ -214,6 +214,10 @@ curl -o poster.png "$URL"
214
214
  # 先查有哪些模板(自己租户建的 + 平台共享的)
215
215
  museav templates
216
216
  museav templates --category 电商白底图 # 按分类过滤
217
+ museav templates --mine # 只看我这个人建的
218
+ museav templates --tenant # 只看本租户专属的
219
+ museav templates --platform # 只看平台共享的
220
+ museav templates --type image # 按类型过滤(image / article)
217
221
 
218
222
  # 没有占位符的模板,直接用
219
223
  museav gen --template <模板id>
package/dist/client.d.ts CHANGED
@@ -71,6 +71,10 @@ export interface TemplateOption {
71
71
  sample_cover_image?: string | null;
72
72
  /** 归属:自己租户建的 vs 平台共享的(tenant_id 为空) */
73
73
  tenant_id: string | null;
74
+ /** 中台下发的归属标记:mine=本租户建的 / platform=平台共享 / personal=我这个人建的 */
75
+ source?: 'mine' | 'platform' | 'personal';
76
+ /** 创建人(平台管理员个人建的模板会带邮箱;租户建的为 null) */
77
+ created_by?: string | null;
74
78
  generation_configs: Array<{
75
79
  model: string;
76
80
  prompt_template: string;
@@ -319,8 +323,9 @@ export declare class StudioClient {
319
323
  /** 可用技能清单:私有 + 所属租户专属模板 + 公共库,服务端已按调用者权限过滤 */
320
324
  skills(): Promise<SkillOption[]>;
321
325
  /** 可用图片/文字模板清单:自己租户建的 + 平台共享的,服务端已按调用者权限过滤。
322
- * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。 */
323
- templates(type?: 'image' | 'article'): Promise<TemplateOption[]>;
326
+ * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。
327
+ * source=mine|platform|personal|all(默认 all;mine=本租户,platform=平台共享,personal=我这个人建的)。 */
328
+ templates(type?: 'image' | 'article', source?: 'mine' | 'platform' | 'personal' | 'all'): Promise<TemplateOption[]>;
324
329
  /** 视频模板清单(POST /api/videos 用 template_id)。结构同图片模板的 generation_configs 形态 */
325
330
  videoTemplates(): Promise<TemplateOption[]>;
326
331
  /** 新建图片模板。归属(是否关联租户)由服务端根据鉴权身份决定,见 CreateTemplateInput 注释 */
package/dist/client.js CHANGED
@@ -90,9 +90,15 @@ export class StudioClient {
90
90
  return Array.isArray(r) ? r : [];
91
91
  }
92
92
  /** 可用图片/文字模板清单:自己租户建的 + 平台共享的,服务端已按调用者权限过滤。
93
- * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。 */
94
- async templates(type) {
95
- const qs = type ? `?type=${type}` : '';
93
+ * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。
94
+ * source=mine|platform|personal|all(默认 all;mine=本租户,platform=平台共享,personal=我这个人建的)。 */
95
+ async templates(type, source) {
96
+ const params = new URLSearchParams();
97
+ if (type)
98
+ params.set('type', type);
99
+ if (source && source !== 'all')
100
+ params.set('source', source);
101
+ const qs = params.toString() ? `?${params}` : '';
96
102
  const r = await this.request(`templates${qs}`);
97
103
  return Array.isArray(r) ? r : [];
98
104
  }
@@ -20,9 +20,12 @@ export async function projects(client) {
20
20
  process.stderr.write('还没有工作区(museav projects create --name 新建)\n');
21
21
  return;
22
22
  }
23
+ // 每个项目的素材数(模板不挂项目,这里统计的是项目素材库,不是模板)
24
+ const assetCounts = await Promise.all(list.map(async (w) => ({ id: w.id, n: (await client.workspaceAssets(w.id).catch(() => [])).length })));
25
+ const assetOf = Object.fromEntries(assetCounts.map((x) => [x.id, x.n]));
23
26
  process.stderr.write(`工作区(${list.length} 个):\n`);
24
27
  for (const w of list) {
25
- process.stderr.write(` ${w.id} ${w.name.padEnd(16)} 出图 ${w.gen_done ?? 0}/${w.gen_total ?? 0}${w.brand ? ` brand:${w.brand}` : ''}\n`);
28
+ process.stderr.write(` ${w.id} ${w.name.padEnd(16)} 出图 ${w.gen_done ?? 0}/${w.gen_total ?? 0} 素材 ${assetOf[w.id] ?? 0}${w.brand ? ` brand:${w.brand}` : ''}\n`);
26
29
  }
27
30
  process.stderr.write(`\n素材库: museav projects assets --project <id|名>\n出图归档: museav gen --project <id|名> ...\n`);
28
31
  // stdout 只出 id,便于脚本解析
@@ -1,9 +1,14 @@
1
- /** museav templates —— 查可用图片/文字模板(自己租户建的 + 平台共享的)
2
- * --type image|article 可过滤(中台 templates 表同时装两种,不传则都列并标注类型) */
1
+ /** museav templates —— 查可用图片/文字模板。
2
+ * --type image|article 按类型过滤
3
+ * --mine 只看本租户建的;--platform 只看平台共享的;都不传则全部列出
4
+ * --category 按分类过滤 */
3
5
  import type { StudioClient } from '../client.js';
4
6
  export declare function templates(client: StudioClient, opts?: {
5
7
  category?: string;
6
8
  type?: string;
9
+ mine?: boolean;
10
+ tenant?: boolean;
11
+ platform?: boolean;
7
12
  }): Promise<void>;
8
13
  interface CreateTemplateOpts {
9
14
  name: string;
@@ -1,5 +1,22 @@
1
1
  export async function templates(client, opts = {}) {
2
- let list = await client.templates((opts.type === 'image' || opts.type === 'article') ? opts.type : undefined);
2
+ const type = opts.type === 'image' || opts.type === 'article' ? opts.type : undefined;
3
+ // 三个归属维度都走服务端 source 参数(正式 API,不再客户端猜):
4
+ // --mine → source=personal(created_by = 当前账户邮箱,我这个人建的)
5
+ // --tenant → source=mine(本租户专属)
6
+ // --platform → source=platform(平台共享)
7
+ let list;
8
+ if (opts.mine) {
9
+ list = await client.templates(type, 'personal');
10
+ }
11
+ else if (opts.tenant) {
12
+ list = await client.templates(type, 'mine');
13
+ }
14
+ else if (opts.platform) {
15
+ list = await client.templates(type, 'platform');
16
+ }
17
+ else {
18
+ list = await client.templates(type);
19
+ }
3
20
  if (opts.category) {
4
21
  const kw = opts.category.toLowerCase();
5
22
  list = list.filter((t) => (t.category || '').toLowerCase().includes(kw));
@@ -8,7 +25,13 @@ export async function templates(client, opts = {}) {
8
25
  process.stderr.write(opts.category ? `没有匹配「${opts.category}」的模板\n` : '没有可用模板\n');
9
26
  return;
10
27
  }
11
- const tag = (t) => (t.tenant_id ? '' : '[平台]');
28
+ const tag = (t) => {
29
+ if (t.source === 'personal')
30
+ return '[个人]';
31
+ if (t.tenant_id)
32
+ return '[租户]';
33
+ return '[平台]';
34
+ };
12
35
  const typeTag = (t) => (t.template_type === 'article' ? '[文字]' : t.template_type === 'image' ? '[图片]' : '');
13
36
  process.stderr.write(`可用模板(${list.length} 个):\n`);
14
37
  for (const t of list) {
@@ -19,7 +42,7 @@ export async function templates(client, opts = {}) {
19
42
  process.stderr.write(` ${t.id.padEnd(38)} ${(t.zh_name || '').padEnd(16)} ${(t.category || '').padEnd(10)} ${(t.ratio || '').padEnd(6)} ${typeTag(t).padEnd(8)} ${fieldHint.padEnd(20)} ${tag(t)}\n`);
20
43
  }
21
44
  process.stderr.write(`\n出图: museav gen --template <模板id> [--fields '{"key":"值"}']\n`);
22
- process.stderr.write(`按类型过滤: museav templates --type image|article\n`);
45
+ process.stderr.write(`筛选: --mine(我建的) --tenant(本租户) --platform(平台共享) --type image|article --category <分类>\n`);
23
46
  // stdout 只出 id,便于脚本与 agent 解析
24
47
  console.log(list.map((t) => t.id).join('\n'));
25
48
  }
@@ -1,3 +1,9 @@
1
- /** museav whoami —— 查当前登录账户 + 租户归属 */
1
+ /** museav whoami —— 查当前身份 + 关联信息。
2
+ *
3
+ * apiKey 模式下也支持(2026-08-17 修复):服务端 /api/me 对两种 apiKey 都返回真实数据——
4
+ * - 平台账户 apiKey(STUDIO_API_KEY 指向 accounts.sk-*) → 走个人身份,含 credits / gen 统计
5
+ * - 租户 apiKey(指向 api_tenants.tenant_key) → 走业务身份,只下发 tenant 信息
6
+ * 个人 login(museav login)走完整 me,输出不变。
7
+ */
2
8
  import type { StudioClient } from '../client.js';
3
9
  export declare function whoami(client: StudioClient): Promise<void>;
@@ -1,14 +1,19 @@
1
- import { loadConfig } from '../config.js';
2
1
  export async function whoami(client) {
3
- // apiKey 是系统接入方/服务身份,不是个人账户,/api/me 不适用——直接提示,别等服务端 401
4
- const cfg = loadConfig();
5
- if (cfg.apiKey && !cfg.token) {
6
- throw new Error('whoami 只支持个人 login 身份:museav login\n' +
7
- '(apiKey 代表接入的业务系统,不是个人账户,可用 museav jobs / balance 查看业务数据)');
2
+ const me = (await client.me());
3
+ if (me.identity === 'tenant') {
4
+ const t = me.tenant;
5
+ process.stderr.write(`身份: 租户 API Key\n`);
6
+ process.stderr.write(`租户: ${t.nickname || t.name}(${t.name})\n`);
7
+ process.stderr.write(`tenant_id: ${t.id}\n`);
8
+ if (t.logo)
9
+ process.stderr.write(`logo: ${t.logo}\n`);
10
+ }
11
+ else {
12
+ process.stderr.write(`账户: ${me.nickname || me.email}(${me.email})\n`);
13
+ process.stderr.write(me.brand ? `业务系统: ${me.brand.name}\n` : `身份: 平台账户 API Key\n`);
14
+ process.stderr.write(`出图: 累计 ${me.gen_total ?? 0} 次,成功 ${me.gen_done ?? 0} 次\n`);
15
+ if (me.credits != null)
16
+ process.stderr.write(`credits: ${me.credits}\n`);
8
17
  }
9
- const me = await client.me();
10
- process.stderr.write(`账户: ${me.nickname || me.email}(${me.email})\n`);
11
- process.stderr.write(me.brand ? `业务系统: ${me.brand.name}\n` : '业务系统: 未接入(个人用户)\n');
12
- process.stderr.write(`出图: 累计 ${me.gen_total} 次,成功 ${me.gen_done} 次\n`);
13
18
  console.log(JSON.stringify(me));
14
19
  }
package/dist/index.js CHANGED
@@ -262,6 +262,9 @@ const templatesCmd = program
262
262
  .description('查可用图片/文字模板:自己租户建的 + 平台共享的(跟技能是两套不同的机制,见 gen --template)')
263
263
  .option('--category <name>', '按分类过滤,如 电商白底图 / 演唱会')
264
264
  .option('--type <type>', '按类型过滤:image(图片) / article(文字),不传则两类都列并标注')
265
+ .option('--mine', '只看我这个人建的模板(created_by 是我,排除系统种子和租户专属)')
266
+ .option('--tenant', '只看本租户建的模板')
267
+ .option('--platform', '只看平台共享的模板')
265
268
  .action(withClient((client, opts) => templates(client, opts)));
266
269
  const videoTemplatesCmd = program
267
270
  .command('video-templates')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "museav-cli",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "MUSE AV 出图中台官方 CLI —— 命令行调中台 API 出图、出视频、读图逆向、图生模板",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.ts CHANGED
@@ -123,6 +123,10 @@ export interface TemplateOption {
123
123
  sample_cover_image?: string | null
124
124
  /** 归属:自己租户建的 vs 平台共享的(tenant_id 为空) */
125
125
  tenant_id: string | null
126
+ /** 中台下发的归属标记:mine=本租户建的 / platform=平台共享 / personal=我这个人建的 */
127
+ source?: 'mine' | 'platform' | 'personal'
128
+ /** 创建人(平台管理员个人建的模板会带邮箱;租户建的为 null) */
129
+ created_by?: string | null
126
130
  generation_configs: Array<{
127
131
  model: string
128
132
  prompt_template: string
@@ -386,9 +390,13 @@ export class StudioClient {
386
390
  }
387
391
 
388
392
  /** 可用图片/文字模板清单:自己租户建的 + 平台共享的,服务端已按调用者权限过滤。
389
- * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。 */
390
- async templates(type?: 'image' | 'article'): Promise<TemplateOption[]> {
391
- const qs = type ? `?type=${type}` : ''
393
+ * type=image|article 二选一(不传则图片+文字都返回,跟中台默认一致)。
394
+ * source=mine|platform|personal|all(默认 all;mine=本租户,platform=平台共享,personal=我这个人建的)。 */
395
+ async templates(type?: 'image' | 'article', source?: 'mine' | 'platform' | 'personal' | 'all'): Promise<TemplateOption[]> {
396
+ const params = new URLSearchParams()
397
+ if (type) params.set('type', type)
398
+ if (source && source !== 'all') params.set('source', source)
399
+ const qs = params.toString() ? `?${params}` : ''
392
400
  const r = await this.request(`templates${qs}`)
393
401
  return Array.isArray(r) ? r : []
394
402
  }
@@ -22,10 +22,15 @@ export async function projects(client: StudioClient): Promise<void> {
22
22
  process.stderr.write('还没有工作区(museav projects create --name 新建)\n')
23
23
  return
24
24
  }
25
+ // 每个项目的素材数(模板不挂项目,这里统计的是项目素材库,不是模板)
26
+ const assetCounts = await Promise.all(
27
+ list.map(async (w) => ({ id: w.id, n: (await client.workspaceAssets(w.id).catch(() => [])).length })),
28
+ )
29
+ const assetOf = Object.fromEntries(assetCounts.map((x) => [x.id, x.n]))
25
30
  process.stderr.write(`工作区(${list.length} 个):\n`)
26
31
  for (const w of list) {
27
32
  process.stderr.write(
28
- ` ${w.id} ${w.name.padEnd(16)} 出图 ${w.gen_done ?? 0}/${w.gen_total ?? 0}${w.brand ? ` brand:${w.brand}` : ''}\n`,
33
+ ` ${w.id} ${w.name.padEnd(16)} 出图 ${w.gen_done ?? 0}/${w.gen_total ?? 0} 素材 ${assetOf[w.id] ?? 0}${w.brand ? ` brand:${w.brand}` : ''}\n`,
29
34
  )
30
35
  }
31
36
  process.stderr.write(`\n素材库: museav projects assets --project <id|名>\n出图归档: museav gen --project <id|名> ...\n`)
@@ -1,9 +1,25 @@
1
- /** museav templates —— 查可用图片/文字模板(自己租户建的 + 平台共享的)
2
- * --type image|article 可过滤(中台 templates 表同时装两种,不传则都列并标注类型) */
1
+ /** museav templates —— 查可用图片/文字模板。
2
+ * --type image|article 按类型过滤
3
+ * --mine 只看本租户建的;--platform 只看平台共享的;都不传则全部列出
4
+ * --category 按分类过滤 */
3
5
  import type { StudioClient } from '../client.js'
4
6
 
5
- export async function templates(client: StudioClient, opts: { category?: string; type?: string } = {}): Promise<void> {
6
- let list = await client.templates((opts.type === 'image' || opts.type === 'article') ? opts.type : undefined)
7
+ export async function templates(client: StudioClient, opts: { category?: string; type?: string; mine?: boolean; tenant?: boolean; platform?: boolean } = {}): Promise<void> {
8
+ const type = opts.type === 'image' || opts.type === 'article' ? opts.type : undefined
9
+ // 三个归属维度都走服务端 source 参数(正式 API,不再客户端猜):
10
+ // --mine → source=personal(created_by = 当前账户邮箱,我这个人建的)
11
+ // --tenant → source=mine(本租户专属)
12
+ // --platform → source=platform(平台共享)
13
+ let list: Awaited<ReturnType<StudioClient['templates']>>
14
+ if (opts.mine) {
15
+ list = await client.templates(type, 'personal')
16
+ } else if (opts.tenant) {
17
+ list = await client.templates(type, 'mine')
18
+ } else if (opts.platform) {
19
+ list = await client.templates(type, 'platform')
20
+ } else {
21
+ list = await client.templates(type)
22
+ }
7
23
  if (opts.category) {
8
24
  const kw = opts.category.toLowerCase()
9
25
  list = list.filter((t) => (t.category || '').toLowerCase().includes(kw))
@@ -13,7 +29,11 @@ export async function templates(client: StudioClient, opts: { category?: string;
13
29
  return
14
30
  }
15
31
 
16
- const tag = (t: (typeof list)[number]) => (t.tenant_id ? '' : '[平台]')
32
+ const tag = (t: (typeof list)[number]) => {
33
+ if (t.source === 'personal') return '[个人]'
34
+ if (t.tenant_id) return '[租户]'
35
+ return '[平台]'
36
+ }
17
37
  const typeTag = (t: (typeof list)[number]) => (t.template_type === 'article' ? '[文字]' : t.template_type === 'image' ? '[图片]' : '')
18
38
 
19
39
  process.stderr.write(`可用模板(${list.length} 个):\n`)
@@ -27,7 +47,7 @@ export async function templates(client: StudioClient, opts: { category?: string;
27
47
  )
28
48
  }
29
49
  process.stderr.write(`\n出图: museav gen --template <模板id> [--fields '{"key":"值"}']\n`)
30
- process.stderr.write(`按类型过滤: museav templates --type image|article\n`)
50
+ process.stderr.write(`筛选: --mine(我建的) --tenant(本租户) --platform(平台共享) --type image|article --category <分类>\n`)
31
51
  // stdout 只出 id,便于脚本与 agent 解析
32
52
  console.log(list.map((t) => t.id).join('\n'))
33
53
  }
@@ -1,19 +1,46 @@
1
- /** museav whoami —— 查当前登录账户 + 租户归属 */
1
+ /** museav whoami —— 查当前身份 + 关联信息。
2
+ *
3
+ * apiKey 模式下也支持(2026-08-17 修复):服务端 /api/me 对两种 apiKey 都返回真实数据——
4
+ * - 平台账户 apiKey(STUDIO_API_KEY 指向 accounts.sk-*) → 走个人身份,含 credits / gen 统计
5
+ * - 租户 apiKey(指向 api_tenants.tenant_key) → 走业务身份,只下发 tenant 信息
6
+ * 个人 login(museav login)走完整 me,输出不变。
7
+ */
2
8
  import type { StudioClient } from '../client.js'
3
- import { loadConfig } from '../config.js'
9
+
10
+ /** /api/me 实际返回的形状:可能是账户身份也可能是租户身份(apiKey 模式) */
11
+ type MeResponse =
12
+ | {
13
+ identity?: 'account'
14
+ id: string
15
+ email: string
16
+ nickname?: string | null
17
+ bio?: string | null
18
+ role: string
19
+ credits?: number | null
20
+ gen_quota?: number | null
21
+ gen_total?: number
22
+ gen_done?: number
23
+ brand?: { name: string } | null
24
+ }
25
+ | {
26
+ identity: 'tenant'
27
+ tenant: { id: string; name: string; nickname?: string | null; logo?: string | null }
28
+ }
4
29
 
5
30
  export async function whoami(client: StudioClient): Promise<void> {
6
- // apiKey 是系统接入方/服务身份,不是个人账户,/api/me 不适用——直接提示,别等服务端 401
7
- const cfg = loadConfig()
8
- if (cfg.apiKey && !cfg.token) {
9
- throw new Error(
10
- 'whoami 只支持个人 login 身份:museav login\n' +
11
- '(apiKey 代表接入的业务系统,不是个人账户,可用 museav jobs / balance 查看业务数据)',
12
- )
31
+ const me = (await client.me()) as MeResponse
32
+
33
+ if (me.identity === 'tenant') {
34
+ const t = me.tenant
35
+ process.stderr.write(`身份: 租户 API Key\n`)
36
+ process.stderr.write(`租户: ${t.nickname || t.name}(${t.name})\n`)
37
+ process.stderr.write(`tenant_id: ${t.id}\n`)
38
+ if (t.logo) process.stderr.write(`logo: ${t.logo}\n`)
39
+ } else {
40
+ process.stderr.write(`账户: ${me.nickname || me.email}(${me.email})\n`)
41
+ process.stderr.write(me.brand ? `业务系统: ${me.brand.name}\n` : `身份: 平台账户 API Key\n`)
42
+ process.stderr.write(`出图: 累计 ${me.gen_total ?? 0} 次,成功 ${me.gen_done ?? 0} 次\n`)
43
+ if (me.credits != null) process.stderr.write(`credits: ${me.credits}\n`)
13
44
  }
14
- const me = await client.me()
15
- process.stderr.write(`账户: ${me.nickname || me.email}(${me.email})\n`)
16
- process.stderr.write(me.brand ? `业务系统: ${me.brand.name}\n` : '业务系统: 未接入(个人用户)\n')
17
- process.stderr.write(`出图: 累计 ${me.gen_total} 次,成功 ${me.gen_done} 次\n`)
18
45
  console.log(JSON.stringify(me))
19
46
  }
package/src/index.ts CHANGED
@@ -281,6 +281,9 @@ const templatesCmd = program
281
281
  .description('查可用图片/文字模板:自己租户建的 + 平台共享的(跟技能是两套不同的机制,见 gen --template)')
282
282
  .option('--category <name>', '按分类过滤,如 电商白底图 / 演唱会')
283
283
  .option('--type <type>', '按类型过滤:image(图片) / article(文字),不传则两类都列并标注')
284
+ .option('--mine', '只看我这个人建的模板(created_by 是我,排除系统种子和租户专属)')
285
+ .option('--tenant', '只看本租户建的模板')
286
+ .option('--platform', '只看平台共享的模板')
284
287
  .action(withClient((client: StudioClient, opts: any) => templates(client, opts)))
285
288
 
286
289
  const videoTemplatesCmd = program