@spira-lab/cli 0.0.8 → 0.0.10

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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/index.cjs +335 -107
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Command line client for Spira.
4
4
 
5
+ > ⚠️ **Database feature is deprecated.** The Brand Machine "Database" subsystem
6
+ > has been retired from the product. The `spira dataBase glob | read | grep |
7
+ > search` commands still exist for backward compatibility only — do NOT build
8
+ > new workflows against them. Use the Brand Machine web dashboard or the
9
+ > per-user cc-engine workspace (`spira workspace` / dashboard Workspace tab)
10
+ > instead. The bundled `spira-database` skill has been removed so agents are
11
+ > no longer guided toward these commands.
12
+
5
13
  ## Install
6
14
 
7
15
  ```bash
package/dist/index.cjs CHANGED
@@ -3457,7 +3457,7 @@ var require_commander = __commonJS({
3457
3457
  });
3458
3458
 
3459
3459
  // src/program.ts
3460
- var import_promises3 = require("node:fs/promises");
3460
+ var import_promises5 = require("node:fs/promises");
3461
3461
 
3462
3462
  // ../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
3463
3463
  var import_index = __toESM(require_commander(), 1);
@@ -18048,6 +18048,7 @@ var cliResultTypeSchema = external_exports.enum([
18048
18048
  "aiJobAction",
18049
18049
  "aiModelList",
18050
18050
  "aiModelDetail",
18051
+ "aiModelProviderList",
18051
18052
  "workflowList",
18052
18053
  "workflowNodeCatalog",
18053
18054
  "workflowDetail",
@@ -18059,6 +18060,8 @@ var cliResultTypeSchema = external_exports.enum([
18059
18060
  "scheduleOccurrenceList",
18060
18061
  "scheduleExecutionList",
18061
18062
  "scheduleAction",
18063
+ "mediaInfo",
18064
+ "mediaAsset",
18062
18065
  "dataBaseGlob",
18063
18066
  "dataBaseRead",
18064
18067
  "dataBaseGrep",
@@ -18083,16 +18086,13 @@ var cliResultSchema = external_exports.object({
18083
18086
  // src/ai/model-docs.ts
18084
18087
  var cliAiModelDocs = [
18085
18088
  {
18086
- "id": "higgsfield-ai/soul/standard",
18089
+ "id": "ai-influencer-image",
18087
18090
  "name": "AI Influencer",
18088
18091
  "mediaType": "image",
18089
18092
  "description": "AI Influencer image generation model.",
18090
18093
  "capabilities": [
18091
18094
  "text-to-image"
18092
18095
  ],
18093
- "providers": [
18094
- "image-higgsfield"
18095
- ],
18096
18096
  "variants": [
18097
18097
  {
18098
18098
  "key": "default",
@@ -18155,9 +18155,6 @@ var cliAiModelDocs = [
18155
18155
  "text-to-image",
18156
18156
  "image-to-image"
18157
18157
  ],
18158
- "providers": [
18159
- "socheap-media"
18160
- ],
18161
18158
  "variants": [
18162
18159
  {
18163
18160
  "key": "1k",
@@ -18248,9 +18245,6 @@ var cliAiModelDocs = [
18248
18245
  "text-to-image",
18249
18246
  "image-to-image"
18250
18247
  ],
18251
- "providers": [
18252
- "socheap-media"
18253
- ],
18254
18248
  "variants": [
18255
18249
  {
18256
18250
  "key": "standard",
@@ -18331,9 +18325,6 @@ var cliAiModelDocs = [
18331
18325
  "text-to-image",
18332
18326
  "image-to-image"
18333
18327
  ],
18334
- "providers": [
18335
- "socheap-media"
18336
- ],
18337
18328
  "variants": [
18338
18329
  {
18339
18330
  "key": "1k",
@@ -18430,9 +18421,6 @@ var cliAiModelDocs = [
18430
18421
  "text-to-image",
18431
18422
  "image-to-image"
18432
18423
  ],
18433
- "providers": [
18434
- "image-kie"
18435
- ],
18436
18424
  "variants": [
18437
18425
  {
18438
18426
  "key": "default",
@@ -18505,9 +18493,6 @@ var cliAiModelDocs = [
18505
18493
  "text-to-image",
18506
18494
  "image-to-image"
18507
18495
  ],
18508
- "providers": [
18509
- "socheap-media"
18510
- ],
18511
18496
  "variants": [
18512
18497
  {
18513
18498
  "key": "1k",
@@ -18598,9 +18583,6 @@ var cliAiModelDocs = [
18598
18583
  "text-to-video",
18599
18584
  "image-to-video"
18600
18585
  ],
18601
- "providers": [
18602
- "socheap-media"
18603
- ],
18604
18586
  "variants": [
18605
18587
  {
18606
18588
  "key": "4s-720p",
@@ -18860,9 +18842,6 @@ var cliAiModelDocs = [
18860
18842
  "text-to-video",
18861
18843
  "image-to-video"
18862
18844
  ],
18863
- "providers": [
18864
- "socheap-media"
18865
- ],
18866
18845
  "variants": [
18867
18846
  {
18868
18847
  "key": "480p",
@@ -18974,9 +18953,6 @@ var cliAiModelDocs = [
18974
18953
  "text-to-video",
18975
18954
  "image-to-video"
18976
18955
  ],
18977
- "providers": [
18978
- "hailuo-comfly"
18979
- ],
18980
18956
  "variants": [
18981
18957
  {
18982
18958
  "key": "default",
@@ -19030,8 +19006,8 @@ var cliAiModelDocs = [
19030
19006
  }
19031
19007
  ],
19032
19008
  "notes": [
19033
- "referenceVideo and generationType are not supported by the Hailuo Comfly provider.",
19034
- "duration=10 with resolution=1080P is rejected by the provider validator."
19009
+ "referenceVideo and generationType are not supported for this model.",
19010
+ "duration=10 with resolution=1080P is not supported."
19035
19011
  ]
19036
19012
  },
19037
19013
  {
@@ -19043,9 +19019,6 @@ var cliAiModelDocs = [
19043
19019
  "text-to-video",
19044
19020
  "image-to-video"
19045
19021
  ],
19046
- "providers": [
19047
- "socheap-media"
19048
- ],
19049
19022
  "variants": [
19050
19023
  {
19051
19024
  "key": "720p",
@@ -19169,9 +19142,6 @@ var cliAiModelDocs = [
19169
19142
  "capabilities": [
19170
19143
  "image-to-video"
19171
19144
  ],
19172
- "providers": [
19173
- "fal-ai"
19174
- ],
19175
19145
  "variants": [
19176
19146
  {
19177
19147
  "key": "default",
@@ -19286,9 +19256,6 @@ var cliAiModelDocs = [
19286
19256
  "text-to-video",
19287
19257
  "image-to-video"
19288
19258
  ],
19289
- "providers": [
19290
- "socheap-media"
19291
- ],
19292
19259
  "variants": [
19293
19260
  {
19294
19261
  "key": "standard-no-sound",
@@ -19431,7 +19398,7 @@ var cliAiModelDocs = [
19431
19398
  ],
19432
19399
  "notes": [
19433
19400
  "generation type is derived from referenceImage count: 0 text, 1 reference, 2 first/last frames.",
19434
- "referenceVideo, negativePrompt, and elements are not supported on the SoCheap Kling 3.0 path.",
19401
+ "referenceVideo, negativePrompt, and elements are not supported for Kling 3.0.",
19435
19402
  "Use mode=4K for 4K generation; do not pass resolution."
19436
19403
  ]
19437
19404
  },
@@ -19443,9 +19410,6 @@ var cliAiModelDocs = [
19443
19410
  "capabilities": [
19444
19411
  "image-to-video"
19445
19412
  ],
19446
- "providers": [
19447
- "socheap-media"
19448
- ],
19449
19413
  "variants": [
19450
19414
  {
19451
19415
  "key": "standard",
@@ -19566,9 +19530,6 @@ var cliAiModelDocs = [
19566
19530
  "text-to-video",
19567
19531
  "image-to-video"
19568
19532
  ],
19569
- "providers": [
19570
- "socheap-media"
19571
- ],
19572
19533
  "variants": [
19573
19534
  {
19574
19535
  "key": "standard-480p-no-sound",
@@ -19730,9 +19691,6 @@ var cliAiModelDocs = [
19730
19691
  "text-to-video",
19731
19692
  "image-to-video"
19732
19693
  ],
19733
- "providers": [
19734
- "socheap-media"
19735
- ],
19736
19694
  "variants": [
19737
19695
  {
19738
19696
  "key": "standard-480p",
@@ -20016,9 +19974,6 @@ var cliAiModelDocs = [
20016
19974
  "text-to-video",
20017
19975
  "image-to-video"
20018
19976
  ],
20019
- "providers": [
20020
- "socheap-media"
20021
- ],
20022
19977
  "variants": [
20023
19978
  {
20024
19979
  "key": "fast-720p",
@@ -20169,14 +20124,9 @@ var cliAiModelDocs = [
20169
20124
  "description": "Whether the generated video should be extendable when supported."
20170
20125
  },
20171
20126
  {
20172
- "name": "extendFromTaskId",
20127
+ "name": "extendFromJobId",
20173
20128
  "type": "string",
20174
- "description": "Provider task id of an existing Veo generation to extend."
20175
- },
20176
- {
20177
- "name": "extendProviderId",
20178
- "type": "string",
20179
- "description": "Original provider id for extension lineage. Must be socheap-media for this CLI path."
20129
+ "description": "Spira job ID of an extendable Veo generation owned by the current user."
20180
20130
  }
20181
20131
  ],
20182
20132
  "examples": [
@@ -20196,7 +20146,7 @@ var cliAiModelDocs = [
20196
20146
  "referenceVideo and referenceAudio are not supported.",
20197
20147
  "image_to_video is not a supported generationType; use reference_to_video with one or more referenceImage URLs.",
20198
20148
  "first_last_frames_to_video requires exactly 2 referenceImage URLs.",
20199
- "extendFromTaskId requires extendProviderId=socheap-media and resolution=720p."
20149
+ "extendFromJobId requires resolution=720p."
20200
20150
  ]
20201
20151
  },
20202
20152
  {
@@ -20205,7 +20155,6 @@ var cliAiModelDocs = [
20205
20155
  mediaType: "audio",
20206
20156
  description: "Suno music generation with simple prompt and custom song modes.",
20207
20157
  capabilities: ["text-to-audio"],
20208
- providers: ["socheap-media"],
20209
20158
  variants: [
20210
20159
  {
20211
20160
  key: "v5_5",
@@ -20270,7 +20219,6 @@ var cliAiModelDocs = [
20270
20219
  mediaType: "audio",
20271
20220
  description: "ElevenLabs prompt-to-music generation.",
20272
20221
  capabilities: ["text-to-audio"],
20273
- providers: ["elevenlabs-audio"],
20274
20222
  variants: [
20275
20223
  {
20276
20224
  key: "default",
@@ -20310,7 +20258,6 @@ var cliAiModelDocs = [
20310
20258
  mediaType: "audio",
20311
20259
  description: "ElevenLabs sound-effect generation from an English prompt.",
20312
20260
  capabilities: ["text-to-audio"],
20313
- providers: ["elevenlabs-audio"],
20314
20261
  variants: [
20315
20262
  {
20316
20263
  key: "default",
@@ -20350,7 +20297,6 @@ var cliAiModelDocs = [
20350
20297
  mediaType: "audio",
20351
20298
  description: "ElevenLabs text-to-speech using an existing voice ID.",
20352
20299
  capabilities: ["text-to-speech"],
20353
- providers: ["elevenlabs-audio"],
20354
20300
  variants: [
20355
20301
  {
20356
20302
  key: "multilingual-v2-v3",
@@ -20398,7 +20344,6 @@ var cliAiModelDocs = [
20398
20344
  mediaType: "audio",
20399
20345
  description: "Text-to-speech using either an existing voice ID or one reference audio URL.",
20400
20346
  capabilities: ["text-to-speech", "audio-to-speech"],
20401
- providers: ["elevenlabs-audio"],
20402
20347
  variants: [
20403
20348
  {
20404
20349
  key: "voice-clone-tts",
@@ -20462,8 +20407,9 @@ function getCliAiModelDoc(modelId) {
20462
20407
  }
20463
20408
 
20464
20409
  // src/ai/wait.ts
20410
+ var DEFAULT_WAIT_COMMAND = "spira ai jobs create";
20465
20411
  function wait(durationMs) {
20466
- return new Promise((resolve) => setTimeout(resolve, durationMs));
20412
+ return new Promise((resolve2) => setTimeout(resolve2, durationMs));
20467
20413
  }
20468
20414
  function jobStatus(result) {
20469
20415
  if (!result.data || typeof result.data !== "object") return void 0;
@@ -20481,27 +20427,27 @@ function providerError(result) {
20481
20427
  }
20482
20428
  return void 0;
20483
20429
  }
20484
- function completedResult(result) {
20430
+ function completedResult(result, command) {
20485
20431
  return cliResultSchema.parse({
20486
20432
  ...result,
20487
20433
  resultType: "aiJobStatus",
20488
- command: "spira ai jobs create",
20434
+ command,
20489
20435
  nextActions: []
20490
20436
  });
20491
20437
  }
20492
- function pollingError(result) {
20438
+ function pollingError(result, command) {
20493
20439
  return cliResultSchema.parse({
20494
20440
  ...result,
20495
- command: "spira ai jobs create"
20441
+ command
20496
20442
  });
20497
20443
  }
20498
- function terminalError(result, jobId, status) {
20444
+ function terminalError(result, jobId, status, command) {
20499
20445
  const message = providerError(result) ?? `AI job ${jobId} ended with status ${status}.`;
20500
20446
  return cliResultSchema.parse({
20501
20447
  ...result,
20502
20448
  status: "error",
20503
20449
  resultType: "aiJobStatus",
20504
- command: "spira ai jobs create",
20450
+ command,
20505
20451
  error: {
20506
20452
  code: status === "FAILED" ? "PROVIDER_FAILED" : "ASYNC_STATE",
20507
20453
  message,
@@ -20511,12 +20457,12 @@ function terminalError(result, jobId, status) {
20511
20457
  }
20512
20458
  });
20513
20459
  }
20514
- function timeoutResult(jobId, timeoutMs, lastResult) {
20460
+ function timeoutResult(jobId, timeoutMs, command, lastResult) {
20515
20461
  return cliResultSchema.parse({
20516
20462
  object: "cli.result",
20517
20463
  status: "error",
20518
20464
  resultType: "aiJobStatus",
20519
- command: "spira ai jobs create",
20465
+ command,
20520
20466
  data: lastResult?.data,
20521
20467
  ids: { ...lastResult?.ids, jobId },
20522
20468
  error: {
@@ -20524,16 +20470,19 @@ function timeoutResult(jobId, timeoutMs, lastResult) {
20524
20470
  message: `AI job ${jobId} did not finish within ${timeoutMs}ms.`,
20525
20471
  resource: { type: "aiJob", id: jobId },
20526
20472
  retryable: true,
20527
- nextActions: [{ label: "Show job status", command: `spira ai jobs status ${jobId}` }]
20473
+ nextActions: [
20474
+ { label: "Keep waiting", command: `spira ai jobs wait ${jobId}` },
20475
+ { label: "Show job status", command: `spira ai jobs status ${jobId}` }
20476
+ ]
20528
20477
  }
20529
20478
  });
20530
20479
  }
20531
- function contractError(result, jobId) {
20480
+ function contractError(result, jobId, command) {
20532
20481
  return cliResultSchema.parse({
20533
20482
  ...result,
20534
20483
  status: "error",
20535
20484
  resultType: "aiJobStatus",
20536
- command: "spira ai jobs create",
20485
+ command,
20537
20486
  error: {
20538
20487
  code: "CONTRACT_MISMATCH",
20539
20488
  message: `AI job ${jobId} status response did not contain a recognized status.`,
@@ -20546,6 +20495,7 @@ function contractError(result, jobId) {
20546
20495
  async function waitForAiJob(input) {
20547
20496
  const now = input.now ?? Date.now;
20548
20497
  const sleep = input.sleep ?? wait;
20498
+ const command = input.command ?? DEFAULT_WAIT_COMMAND;
20549
20499
  const deadline = now() + input.timeoutMs;
20550
20500
  let lastResult;
20551
20501
  let lastStatus;
@@ -20553,19 +20503,19 @@ async function waitForAiJob(input) {
20553
20503
  const result = await input.poll();
20554
20504
  lastResult = result;
20555
20505
  if (result.status === "error") {
20556
- if (!result.error?.retryable) return pollingError(result);
20506
+ if (!result.error?.retryable) return pollingError(result, command);
20557
20507
  } else {
20558
20508
  const status = jobStatus(result);
20559
20509
  if (status && status !== lastStatus) {
20560
20510
  input.onStatus?.(status);
20561
20511
  lastStatus = status;
20562
20512
  }
20563
- if (status === "COMPLETED") return completedResult(result);
20564
- if (status === "FAILED" || status === "CANCELED") return terminalError(result, input.jobId, status);
20565
- if (status !== "QUEUED" && status !== "RUNNING") return contractError(result, input.jobId);
20513
+ if (status === "COMPLETED") return completedResult(result, command);
20514
+ if (status === "FAILED" || status === "CANCELED") return terminalError(result, input.jobId, status, command);
20515
+ if (status !== "QUEUED" && status !== "RUNNING") return contractError(result, input.jobId, command);
20566
20516
  }
20567
20517
  const remainingMs = deadline - now();
20568
- if (remainingMs <= 0) return timeoutResult(input.jobId, input.timeoutMs, lastResult);
20518
+ if (remainingMs <= 0) return timeoutResult(input.jobId, input.timeoutMs, command, lastResult);
20569
20519
  await sleep(Math.min(input.intervalMs, remainingMs));
20570
20520
  }
20571
20521
  }
@@ -20895,7 +20845,33 @@ var commandDefinitions = [
20895
20845
  },
20896
20846
  resultType: "aiJobStatus",
20897
20847
  examples: ["spira ai jobs status job_123"],
20898
- relatedCommands: []
20848
+ relatedCommands: ["spira ai jobs wait <jobId>"]
20849
+ },
20850
+ {
20851
+ path: ["ai", "jobs", "wait"],
20852
+ purpose: "Block until an AI job finishes, then print the final result.",
20853
+ arguments: [{ name: "jobId", description: "AI job ID.", required: true }],
20854
+ options: [
20855
+ {
20856
+ name: "timeout",
20857
+ flag: "--timeout <duration>",
20858
+ description: "Maximum wait duration.",
20859
+ type: "duration",
20860
+ defaultValue: 6e5,
20861
+ minimum: 1e3
20862
+ },
20863
+ {
20864
+ name: "interval",
20865
+ flag: "--interval <duration>",
20866
+ description: "Polling interval while waiting.",
20867
+ type: "duration",
20868
+ defaultValue: 3e3,
20869
+ minimum: 1e3
20870
+ }
20871
+ ],
20872
+ resultType: "aiJobStatus",
20873
+ examples: ["spira ai jobs wait job_123", "spira ai jobs wait job_123 --timeout 20m --interval 5s"],
20874
+ relatedCommands: ["spira ai jobs status <jobId>", "spira ai jobs create"]
20899
20875
  },
20900
20876
  {
20901
20877
  path: ["ai", "models", "list"],
@@ -20921,6 +20897,21 @@ var commandDefinitions = [
20921
20897
  examples: ["spira ai models get nano-banana-2", "spira ai models get seedance-2.0"],
20922
20898
  relatedCommands: ["spira ai jobs create"]
20923
20899
  },
20900
+ {
20901
+ path: ["ai", "models", "providers"],
20902
+ purpose: "List provider routing IDs for AI models; requires admin.access.",
20903
+ permissions: ["admin.access"],
20904
+ arguments: [{ name: "modelId", description: "Optional AI model ID.", required: false }],
20905
+ http: {
20906
+ method: "GET",
20907
+ path: "/api/cli/v1/ai/models/providers",
20908
+ authRequired: true,
20909
+ query: [{ name: "modelId", source: "argument", from: "modelId" }]
20910
+ },
20911
+ resultType: "aiModelProviderList",
20912
+ examples: ["spira ai models providers", "spira ai models providers veo3.1 --json"],
20913
+ relatedCommands: ["spira ai models list", "spira ai jobs create"]
20914
+ },
20924
20915
  {
20925
20916
  path: ["ai", "jobs", "create"],
20926
20917
  purpose: "Create an AI image, video, or audio generation job.",
@@ -20929,9 +20920,8 @@ var commandDefinitions = [
20929
20920
  {
20930
20921
  name: "providerId",
20931
20922
  flag: "--provider-id <providerId>",
20932
- description: "Advanced routing override.",
20933
- type: "string",
20934
- hidden: true
20923
+ description: "Explicit provider routing override; requires admin.access.",
20924
+ type: "string"
20935
20925
  },
20936
20926
  {
20937
20927
  name: "inputsJson",
@@ -20978,7 +20968,12 @@ var commandDefinitions = [
20978
20968
  `spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}'`,
20979
20969
  `spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}' --wait`
20980
20970
  ],
20981
- relatedCommands: ["spira ai jobs status <jobId>", "spira ai models get <modelId>", "spira credits balance"]
20971
+ relatedCommands: [
20972
+ "spira ai jobs wait <jobId>",
20973
+ "spira ai jobs status <jobId>",
20974
+ "spira ai models get <modelId>",
20975
+ "spira credits balance"
20976
+ ]
20982
20977
  },
20983
20978
  {
20984
20979
  path: ["workflows", "list"],
@@ -21347,6 +21342,92 @@ var commandDefinitions = [
21347
21342
  examples: ["spira schedules calendar --from 1781884800000 --to 1782490000000"],
21348
21343
  relatedCommands: ["spira schedules get <scheduleId>"]
21349
21344
  },
21345
+ {
21346
+ path: ["media", "info"],
21347
+ purpose: "Resolve a TikTok / YouTube / Instagram / X / Facebook link and show its metadata.",
21348
+ arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
21349
+ http: {
21350
+ method: "POST",
21351
+ path: "/api/tools/media/info",
21352
+ authRequired: true,
21353
+ body: [{ name: "url", source: "argument", from: "url" }]
21354
+ },
21355
+ resultType: "mediaInfo",
21356
+ examples: [
21357
+ "spira media info https://www.youtube.com/watch?v=dQw4w9WgXcQ",
21358
+ "spira media info https://www.tiktok.com/@user/video/123 --json"
21359
+ ],
21360
+ relatedCommands: ["spira media video <url>", "spira media thumbnail <url>", "spira credits balance"]
21361
+ },
21362
+ {
21363
+ path: ["media", "video"],
21364
+ purpose: "Download the video file behind a social link.",
21365
+ arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
21366
+ options: [
21367
+ {
21368
+ name: "maxHeight",
21369
+ flag: "--max-height <pixels>",
21370
+ description: "Cap the selected format height. Omit for the best available.",
21371
+ type: "number",
21372
+ minimum: 144
21373
+ },
21374
+ {
21375
+ name: "quality",
21376
+ flag: "--quality <mode>",
21377
+ description: "compatible (default) grabs H.264 MP4 that any editor opens; best grabs the highest resolution, which may be WebM/AV1.",
21378
+ type: "string",
21379
+ enumValues: ["compatible", "best"]
21380
+ },
21381
+ {
21382
+ name: "output",
21383
+ flag: "--output <path>",
21384
+ description: "Also save a local copy from the returned URL.",
21385
+ type: "string"
21386
+ }
21387
+ ],
21388
+ http: {
21389
+ method: "POST",
21390
+ path: "/api/tools/media/video",
21391
+ authRequired: true,
21392
+ body: [
21393
+ { name: "url", source: "argument", from: "url" },
21394
+ { name: "maxHeight", source: "option", from: "maxHeight" },
21395
+ { name: "quality", source: "option", from: "quality" }
21396
+ ]
21397
+ },
21398
+ resultType: "mediaAsset",
21399
+ examples: [
21400
+ "spira media video https://www.tiktok.com/@user/video/123",
21401
+ "spira media video https://www.youtube.com/watch?v=dQw4w9WgXcQ --max-height 1080 --output ./downloads/",
21402
+ "spira media video https://www.youtube.com/shorts/abc123 --quality best"
21403
+ ],
21404
+ relatedCommands: ["spira media info <url>", "spira media thumbnail <url>"]
21405
+ },
21406
+ {
21407
+ path: ["media", "thumbnail"],
21408
+ purpose: "Download the cover image behind a social link.",
21409
+ arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
21410
+ options: [
21411
+ {
21412
+ name: "output",
21413
+ flag: "--output <path>",
21414
+ description: "Also save a local copy from the returned URL.",
21415
+ type: "string"
21416
+ }
21417
+ ],
21418
+ http: {
21419
+ method: "POST",
21420
+ path: "/api/tools/media/thumbnail",
21421
+ authRequired: true,
21422
+ body: [{ name: "url", source: "argument", from: "url" }]
21423
+ },
21424
+ resultType: "mediaAsset",
21425
+ examples: [
21426
+ "spira media thumbnail https://x.com/user/status/123",
21427
+ "spira media thumbnail https://www.facebook.com/watch/?v=123 --output ./cover.jpg"
21428
+ ],
21429
+ relatedCommands: ["spira media info <url>", "spira media video <url>"]
21430
+ },
21350
21431
  {
21351
21432
  path: ["dataBase", "glob"],
21352
21433
  purpose: "List files and folders in your Brand database matching a glob pattern.",
@@ -21878,6 +21959,69 @@ async function executeHttpCommand(input) {
21878
21959
  return successResult(input.command, response.body, response.requestId);
21879
21960
  }
21880
21961
 
21962
+ // src/media/save.ts
21963
+ var import_node_fs = require("node:fs");
21964
+ var import_promises3 = require("node:fs/promises");
21965
+ var import_node_path3 = require("node:path");
21966
+ var import_node_stream = require("node:stream");
21967
+ var import_promises4 = require("node:stream/promises");
21968
+ function assetRecord(data) {
21969
+ if (!data || typeof data !== "object") return null;
21970
+ const asset = data.asset;
21971
+ return asset && typeof asset === "object" ? asset : null;
21972
+ }
21973
+ function fileNameFor(asset) {
21974
+ const key = typeof asset.key === "string" ? (0, import_node_path3.basename)(asset.key) : "";
21975
+ if (key) return key;
21976
+ const fromUrl = typeof asset.url === "string" ? (0, import_node_path3.basename)(new URL(asset.url).pathname) : "";
21977
+ return fromUrl || "download";
21978
+ }
21979
+ async function isDirectory(path) {
21980
+ return (0, import_promises3.stat)(path).then((entry) => entry.isDirectory()).catch(() => false);
21981
+ }
21982
+ async function resolveTargetPath(output, asset) {
21983
+ const target = (0, import_node_path3.resolve)(output);
21984
+ const looksLikeDirectory = /[\\/]$/.test(output) || await isDirectory(target);
21985
+ return looksLikeDirectory ? (0, import_node_path3.join)(target, fileNameFor(asset)) : target;
21986
+ }
21987
+ async function writeAssetToDisk(url2, targetPath) {
21988
+ const response = await fetch(url2);
21989
+ if (!response.ok || !response.body) {
21990
+ throw new Error(`Download failed with status ${response.status}.`);
21991
+ }
21992
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(targetPath), { recursive: true });
21993
+ await (0, import_promises4.pipeline)(
21994
+ import_node_stream.Readable.fromWeb(response.body),
21995
+ (0, import_node_fs.createWriteStream)(targetPath)
21996
+ );
21997
+ }
21998
+ async function saveMediaAsset(result, output) {
21999
+ if (result.status !== "success") return result;
22000
+ const asset = assetRecord(result.data);
22001
+ const url2 = asset && typeof asset.url === "string" ? asset.url : null;
22002
+ if (!asset || !url2) {
22003
+ return cliResultSchema.parse({
22004
+ ...result,
22005
+ warnings: [...result.warnings, "Response contained no downloadable asset URL; nothing was saved locally."]
22006
+ });
22007
+ }
22008
+ try {
22009
+ const targetPath = await resolveTargetPath(output, asset);
22010
+ await writeAssetToDisk(url2, targetPath);
22011
+ return cliResultSchema.parse({
22012
+ ...result,
22013
+ data: { ...result.data, asset: { ...asset, savedTo: targetPath } },
22014
+ impact: [...result.impact, `Saved to ${targetPath}`]
22015
+ });
22016
+ } catch (error51) {
22017
+ const message = error51 instanceof Error && error51.message ? error51.message : "Unknown error.";
22018
+ return cliResultSchema.parse({
22019
+ ...result,
22020
+ warnings: [...result.warnings, `Could not save the file locally: ${message} The asset URL is still available.`]
22021
+ });
22022
+ }
22023
+ }
22024
+
21881
22025
  // src/auth/login.ts
21882
22026
  var import_node_crypto = require("node:crypto");
21883
22027
 
@@ -21891,19 +22035,19 @@ function openBrowser(url2) {
21891
22035
  child.unref();
21892
22036
  }
21893
22037
  function closeServer(server) {
21894
- return new Promise((resolve) => server.close(() => resolve()));
22038
+ return new Promise((resolve2) => server.close(() => resolve2()));
21895
22039
  }
21896
22040
  function startLoopbackCallbackServer(input) {
21897
22041
  const server = (0, import_node_http.createServer)();
21898
22042
  let timer = null;
21899
22043
  let resolveCallback = null;
21900
22044
  let rejectCallback = null;
21901
- const callback = new Promise((resolve, reject) => {
21902
- resolveCallback = resolve;
22045
+ const callback = new Promise((resolve2, reject) => {
22046
+ resolveCallback = resolve2;
21903
22047
  rejectCallback = reject;
21904
22048
  });
21905
22049
  const ready = new Promise(
21906
- (resolve, reject) => {
22050
+ (resolve2, reject) => {
21907
22051
  timer = setTimeout(() => {
21908
22052
  rejectCallback?.(new Error("Timed out waiting for browser callback."));
21909
22053
  void closeServer(server);
@@ -21930,7 +22074,7 @@ function startLoopbackCallbackServer(input) {
21930
22074
  reject(new Error("Failed to start local CLI callback server."));
21931
22075
  return;
21932
22076
  }
21933
- resolve({
22077
+ resolve2({
21934
22078
  redirectUri: `http://127.0.0.1:${address.port}/callback`,
21935
22079
  waitForCallback: () => callback,
21936
22080
  close: () => closeServer(server)
@@ -22110,20 +22254,18 @@ function dataArray(data) {
22110
22254
  }
22111
22255
  function renderRows(rows, columns) {
22112
22256
  if (rows.length === 0) return "No results.\n";
22113
- const records = rows.map((row) => row && typeof row === "object" ? row : { value: row });
22257
+ const records = rows.map(
22258
+ (row) => row && typeof row === "object" ? row : { value: row }
22259
+ );
22114
22260
  const widths = columns.map(
22115
- (column) => Math.min(
22116
- 42,
22117
- Math.max(
22118
- column.length,
22119
- ...records.map((record2) => valueToText(record2[column]).slice(0, 42).length)
22120
- )
22121
- )
22261
+ (column) => Math.min(42, Math.max(column.length, ...records.map((record2) => valueToText(record2[column]).slice(0, 42).length)))
22122
22262
  );
22123
22263
  const header = columns.map((column, index) => column.padEnd(widths[index])).join(" ");
22124
22264
  const divider = widths.map((width) => "-".repeat(width)).join(" ");
22125
22265
  const body = records.map(
22126
- (record2) => columns.map((column, index) => valueToText(record2[column]).replace(/\s+/g, " ").slice(0, widths[index]).padEnd(widths[index])).join(" ")
22266
+ (record2) => columns.map(
22267
+ (column, index) => valueToText(record2[column]).replace(/\s+/g, " ").slice(0, widths[index]).padEnd(widths[index])
22268
+ ).join(" ")
22127
22269
  ).join("\n");
22128
22270
  return `${header}
22129
22271
  ${divider}
@@ -22293,6 +22435,9 @@ function renderAiJobStatus(data) {
22293
22435
  function renderAiModelList(data) {
22294
22436
  return renderRows(dataArray(data), ["id", "mediaType", "creditOptions", "name", "capabilities"]);
22295
22437
  }
22438
+ function renderAiModelProviderList(data) {
22439
+ return renderRows(dataArray(data), ["modelId", "name", "providers", "hidden"]);
22440
+ }
22296
22441
  function renderAiModelDetail(data) {
22297
22442
  const record2 = objectRecord2(data);
22298
22443
  const fields = Array.isArray(record2.fields) ? record2.fields.map(objectRecord2) : [];
@@ -22393,6 +22538,57 @@ function formatAnchor(anchor) {
22393
22538
  }
22394
22539
  return parts.join(", ") || "-";
22395
22540
  }
22541
+ function formatMediaDuration(value) {
22542
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "-";
22543
+ const total = Math.round(value);
22544
+ const hours = Math.floor(total / 3600);
22545
+ const minutes = Math.floor(total % 3600 / 60);
22546
+ const seconds = total % 60;
22547
+ const padded = `${String(minutes).padStart(hours ? 2 : 1, "0")}:${String(seconds).padStart(2, "0")}`;
22548
+ return hours ? `${hours}:${padded}` : padded;
22549
+ }
22550
+ function renderMediaInfo(data) {
22551
+ const info = objectRecord2(objectRecord2(data).info);
22552
+ const formats = Array.isArray(info.formats) ? info.formats.map(objectRecord2) : [];
22553
+ const lines = [
22554
+ `platform: ${valueToText(info.platform)}`,
22555
+ `title: ${compactText(info.title)}`,
22556
+ `uploader: ${valueToText(info.uploader ?? info.channel)}`,
22557
+ `duration: ${formatMediaDuration(info.duration)}`,
22558
+ `views: ${valueToText(info.view_count)}`,
22559
+ `likes: ${valueToText(info.like_count)}`,
22560
+ `uploadDate: ${valueToText(info.upload_date)}`,
22561
+ `url: ${valueToText(info.webpage_url ?? info.url)}`,
22562
+ `suggestedFilename: ${valueToText(info.suggested_filename)}`
22563
+ ];
22564
+ if (formats.length === 0) return `${lines.join("\n")}
22565
+ `;
22566
+ const rows = formats.filter((format) => format.has_video === true).map((format) => ({
22567
+ format_id: format.format_id,
22568
+ ext: format.ext,
22569
+ resolution: format.resolution,
22570
+ fps: format.fps,
22571
+ size: formatByteSize(format.filesize),
22572
+ audio: format.has_audio === true ? "yes" : "no"
22573
+ }));
22574
+ if (rows.length === 0) return `${lines.join("\n")}
22575
+ `;
22576
+ return `${lines.join("\n")}
22577
+ videoFormats (${rows.length}):
22578
+ ${renderRows(rows, ["format_id", "ext", "resolution", "fps", "size", "audio"])}`;
22579
+ }
22580
+ function renderMediaAsset(data) {
22581
+ const asset = objectRecord2(objectRecord2(data).asset);
22582
+ const lines = [
22583
+ `title: ${compactText(asset.title)}`,
22584
+ `size: ${formatByteSize(asset.size)}`,
22585
+ `contentType: ${valueToText(asset.contentType)}`,
22586
+ `url: ${valueToText(asset.url)}`
22587
+ ];
22588
+ if (asset.savedTo) lines.push(`savedTo: ${valueToText(asset.savedTo)}`);
22589
+ return `${lines.join("\n")}
22590
+ `;
22591
+ }
22396
22592
  function renderDataBaseGlob(data) {
22397
22593
  const record2 = objectRecord2(data);
22398
22594
  const pattern = valueToText(record2.pattern);
@@ -22512,6 +22708,8 @@ function renderSuccess(result) {
22512
22708
  return renderAiModelList(result.data);
22513
22709
  case "aiModelDetail":
22514
22710
  return renderAiModelDetail(result.data);
22711
+ case "aiModelProviderList":
22712
+ return renderAiModelProviderList(result.data);
22515
22713
  case "workflowList":
22516
22714
  return renderRows(dataArray(result.data), ["id", "name", "creatorName", "nodeTypes", "updatedAt"]);
22517
22715
  case "workflowNodeCatalog":
@@ -22527,7 +22725,18 @@ function renderSuccess(result) {
22527
22725
  case "scheduleOccurrenceList":
22528
22726
  return renderRows(dataArray(result.data), ["id", "scheduleId", "scheduledAt", "status", "executorType"]);
22529
22727
  case "scheduleExecutionList":
22530
- return renderRows(dataArray(result.data), ["id", "scheduleId", "status", "originalScheduledAt", "startedAt", "finishedAt"]);
22728
+ return renderRows(dataArray(result.data), [
22729
+ "id",
22730
+ "scheduleId",
22731
+ "status",
22732
+ "originalScheduledAt",
22733
+ "startedAt",
22734
+ "finishedAt"
22735
+ ]);
22736
+ case "mediaInfo":
22737
+ return renderMediaInfo(result.data);
22738
+ case "mediaAsset":
22739
+ return renderMediaAsset(result.data);
22531
22740
  case "dataBaseGlob":
22532
22741
  return renderDataBaseGlob(result.data);
22533
22742
  case "dataBaseRead":
@@ -22865,12 +23074,12 @@ function renderCommandIndex() {
22865
23074
  `;
22866
23075
  }
22867
23076
  async function packageVersion() {
22868
- if ("0.0.8".length > 0) {
22869
- return "0.0.8";
23077
+ if ("0.0.10".length > 0) {
23078
+ return "0.0.10";
22870
23079
  }
22871
23080
  try {
22872
23081
  const packageUrl = new URL("../package.json", import_meta.url);
22873
- const pkg = JSON.parse(await (0, import_promises3.readFile)(packageUrl, "utf8"));
23082
+ const pkg = JSON.parse(await (0, import_promises5.readFile)(packageUrl, "utf8"));
22874
23083
  return pkg.version ?? "0.0.0";
22875
23084
  } catch {
22876
23085
  return "0.0.0";
@@ -23073,6 +23282,25 @@ ${url2}
23073
23282
  poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
23074
23283
  });
23075
23284
  }
23285
+ if (commandKey(definition.path) === "ai jobs wait") {
23286
+ const statusCommand = findCommandDefinition(["ai", "jobs", "status"]);
23287
+ if (!statusCommand) throw new Error("AI job status command is not registered.");
23288
+ const jobId = args.jobId;
23289
+ return waitForAiJob({
23290
+ jobId,
23291
+ command: "spira ai jobs wait",
23292
+ timeoutMs: options.timeout,
23293
+ intervalMs: options.interval,
23294
+ onStatus: (status) => output.error(`AI job ${jobId}: ${status}
23295
+ `),
23296
+ poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
23297
+ });
23298
+ }
23299
+ if (commandKey(definition.path) === "media video" || commandKey(definition.path) === "media thumbnail") {
23300
+ const downloaded = await executeHttpCommand({ command: definition, args, options });
23301
+ if (typeof options.output !== "string" || options.output.length === 0) return downloaded;
23302
+ return saveMediaAsset(downloaded, options.output);
23303
+ }
23076
23304
  return executeHttpCommand({ command: definition, args, options });
23077
23305
  }
23078
23306
  function addLeafCommand(root, definition, output) {
@@ -23098,7 +23326,7 @@ function addLeafCommand(root, definition, output) {
23098
23326
  output.write(
23099
23327
  renderResult(result, {
23100
23328
  json: Boolean(options.json),
23101
- compactAiJobWait: commandKey(definition.path) === "ai jobs create" && options.wait === true
23329
+ compactAiJobWait: commandKey(definition.path) === "ai jobs create" && options.wait === true || commandKey(definition.path) === "ai jobs wait"
23102
23330
  })
23103
23331
  );
23104
23332
  if (result.status === "error") process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spira-lab/cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Command line client for Spira.",
5
5
  "type": "module",
6
6
  "bin": {