@spira-lab/cli 0.0.9 → 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 +221 -10
  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);
@@ -18060,6 +18060,8 @@ var cliResultTypeSchema = external_exports.enum([
18060
18060
  "scheduleOccurrenceList",
18061
18061
  "scheduleExecutionList",
18062
18062
  "scheduleAction",
18063
+ "mediaInfo",
18064
+ "mediaAsset",
18063
18065
  "dataBaseGlob",
18064
18066
  "dataBaseRead",
18065
18067
  "dataBaseGrep",
@@ -20407,7 +20409,7 @@ function getCliAiModelDoc(modelId) {
20407
20409
  // src/ai/wait.ts
20408
20410
  var DEFAULT_WAIT_COMMAND = "spira ai jobs create";
20409
20411
  function wait(durationMs) {
20410
- return new Promise((resolve) => setTimeout(resolve, durationMs));
20412
+ return new Promise((resolve2) => setTimeout(resolve2, durationMs));
20411
20413
  }
20412
20414
  function jobStatus(result) {
20413
20415
  if (!result.data || typeof result.data !== "object") return void 0;
@@ -21340,6 +21342,92 @@ var commandDefinitions = [
21340
21342
  examples: ["spira schedules calendar --from 1781884800000 --to 1782490000000"],
21341
21343
  relatedCommands: ["spira schedules get <scheduleId>"]
21342
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
+ },
21343
21431
  {
21344
21432
  path: ["dataBase", "glob"],
21345
21433
  purpose: "List files and folders in your Brand database matching a glob pattern.",
@@ -21871,6 +21959,69 @@ async function executeHttpCommand(input) {
21871
21959
  return successResult(input.command, response.body, response.requestId);
21872
21960
  }
21873
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
+
21874
22025
  // src/auth/login.ts
21875
22026
  var import_node_crypto = require("node:crypto");
21876
22027
 
@@ -21884,19 +22035,19 @@ function openBrowser(url2) {
21884
22035
  child.unref();
21885
22036
  }
21886
22037
  function closeServer(server) {
21887
- return new Promise((resolve) => server.close(() => resolve()));
22038
+ return new Promise((resolve2) => server.close(() => resolve2()));
21888
22039
  }
21889
22040
  function startLoopbackCallbackServer(input) {
21890
22041
  const server = (0, import_node_http.createServer)();
21891
22042
  let timer = null;
21892
22043
  let resolveCallback = null;
21893
22044
  let rejectCallback = null;
21894
- const callback = new Promise((resolve, reject) => {
21895
- resolveCallback = resolve;
22045
+ const callback = new Promise((resolve2, reject) => {
22046
+ resolveCallback = resolve2;
21896
22047
  rejectCallback = reject;
21897
22048
  });
21898
22049
  const ready = new Promise(
21899
- (resolve, reject) => {
22050
+ (resolve2, reject) => {
21900
22051
  timer = setTimeout(() => {
21901
22052
  rejectCallback?.(new Error("Timed out waiting for browser callback."));
21902
22053
  void closeServer(server);
@@ -21923,7 +22074,7 @@ function startLoopbackCallbackServer(input) {
21923
22074
  reject(new Error("Failed to start local CLI callback server."));
21924
22075
  return;
21925
22076
  }
21926
- resolve({
22077
+ resolve2({
21927
22078
  redirectUri: `http://127.0.0.1:${address.port}/callback`,
21928
22079
  waitForCallback: () => callback,
21929
22080
  close: () => closeServer(server)
@@ -22387,6 +22538,57 @@ function formatAnchor(anchor) {
22387
22538
  }
22388
22539
  return parts.join(", ") || "-";
22389
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
+ }
22390
22592
  function renderDataBaseGlob(data) {
22391
22593
  const record2 = objectRecord2(data);
22392
22594
  const pattern = valueToText(record2.pattern);
@@ -22531,6 +22733,10 @@ function renderSuccess(result) {
22531
22733
  "startedAt",
22532
22734
  "finishedAt"
22533
22735
  ]);
22736
+ case "mediaInfo":
22737
+ return renderMediaInfo(result.data);
22738
+ case "mediaAsset":
22739
+ return renderMediaAsset(result.data);
22534
22740
  case "dataBaseGlob":
22535
22741
  return renderDataBaseGlob(result.data);
22536
22742
  case "dataBaseRead":
@@ -22868,12 +23074,12 @@ function renderCommandIndex() {
22868
23074
  `;
22869
23075
  }
22870
23076
  async function packageVersion() {
22871
- if ("0.0.9".length > 0) {
22872
- return "0.0.9";
23077
+ if ("0.0.10".length > 0) {
23078
+ return "0.0.10";
22873
23079
  }
22874
23080
  try {
22875
23081
  const packageUrl = new URL("../package.json", import_meta.url);
22876
- const pkg = JSON.parse(await (0, import_promises3.readFile)(packageUrl, "utf8"));
23082
+ const pkg = JSON.parse(await (0, import_promises5.readFile)(packageUrl, "utf8"));
22877
23083
  return pkg.version ?? "0.0.0";
22878
23084
  } catch {
22879
23085
  return "0.0.0";
@@ -23090,6 +23296,11 @@ ${url2}
23090
23296
  poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
23091
23297
  });
23092
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
+ }
23093
23304
  return executeHttpCommand({ command: definition, args, options });
23094
23305
  }
23095
23306
  function addLeafCommand(root, definition, output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spira-lab/cli",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Command line client for Spira.",
5
5
  "type": "module",
6
6
  "bin": {