@spira-lab/cli 0.0.9 → 0.0.11

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 +211 -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,83 @@ 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: "formatId",
21369
+ flag: "--format-id <id>",
21370
+ description: "A format_id from `spira media info`. Omit for the best rung that plays everywhere. Every rung is delivered with audio.",
21371
+ type: "string"
21372
+ },
21373
+ {
21374
+ name: "output",
21375
+ flag: "--output <path>",
21376
+ description: "Also save a local copy from the returned URL.",
21377
+ type: "string"
21378
+ }
21379
+ ],
21380
+ http: {
21381
+ method: "POST",
21382
+ path: "/api/tools/media/video",
21383
+ authRequired: true,
21384
+ body: [
21385
+ { name: "url", source: "argument", from: "url" },
21386
+ { name: "formatId", source: "option", from: "formatId" }
21387
+ ]
21388
+ },
21389
+ resultType: "mediaAsset",
21390
+ examples: [
21391
+ "spira media video https://www.tiktok.com/@user/video/123",
21392
+ "spira media video https://www.youtube.com/watch?v=dQw4w9WgXcQ --output ./downloads/",
21393
+ "spira media video https://www.tiktok.com/@user/video/123 --format-id bytevc1_1080p_612574-0"
21394
+ ],
21395
+ relatedCommands: ["spira media info <url>", "spira media thumbnail <url>"]
21396
+ },
21397
+ {
21398
+ path: ["media", "thumbnail"],
21399
+ purpose: "Download the cover image behind a social link.",
21400
+ arguments: [{ name: "url", description: "Public post or video URL.", required: true }],
21401
+ options: [
21402
+ {
21403
+ name: "output",
21404
+ flag: "--output <path>",
21405
+ description: "Also save a local copy from the returned URL.",
21406
+ type: "string"
21407
+ }
21408
+ ],
21409
+ http: {
21410
+ method: "POST",
21411
+ path: "/api/tools/media/thumbnail",
21412
+ authRequired: true,
21413
+ body: [{ name: "url", source: "argument", from: "url" }]
21414
+ },
21415
+ resultType: "mediaAsset",
21416
+ examples: [
21417
+ "spira media thumbnail https://x.com/user/status/123",
21418
+ "spira media thumbnail https://www.facebook.com/watch/?v=123 --output ./cover.jpg"
21419
+ ],
21420
+ relatedCommands: ["spira media info <url>", "spira media video <url>"]
21421
+ },
21343
21422
  {
21344
21423
  path: ["dataBase", "glob"],
21345
21424
  purpose: "List files and folders in your Brand database matching a glob pattern.",
@@ -21871,6 +21950,69 @@ async function executeHttpCommand(input) {
21871
21950
  return successResult(input.command, response.body, response.requestId);
21872
21951
  }
21873
21952
 
21953
+ // src/media/save.ts
21954
+ var import_node_fs = require("node:fs");
21955
+ var import_promises3 = require("node:fs/promises");
21956
+ var import_node_path3 = require("node:path");
21957
+ var import_node_stream = require("node:stream");
21958
+ var import_promises4 = require("node:stream/promises");
21959
+ function assetRecord(data) {
21960
+ if (!data || typeof data !== "object") return null;
21961
+ const asset = data.asset;
21962
+ return asset && typeof asset === "object" ? asset : null;
21963
+ }
21964
+ function fileNameFor(asset) {
21965
+ const key = typeof asset.key === "string" ? (0, import_node_path3.basename)(asset.key) : "";
21966
+ if (key) return key;
21967
+ const fromUrl = typeof asset.url === "string" ? (0, import_node_path3.basename)(new URL(asset.url).pathname) : "";
21968
+ return fromUrl || "download";
21969
+ }
21970
+ async function isDirectory(path) {
21971
+ return (0, import_promises3.stat)(path).then((entry) => entry.isDirectory()).catch(() => false);
21972
+ }
21973
+ async function resolveTargetPath(output, asset) {
21974
+ const target = (0, import_node_path3.resolve)(output);
21975
+ const looksLikeDirectory = /[\\/]$/.test(output) || await isDirectory(target);
21976
+ return looksLikeDirectory ? (0, import_node_path3.join)(target, fileNameFor(asset)) : target;
21977
+ }
21978
+ async function writeAssetToDisk(url2, targetPath) {
21979
+ const response = await fetch(url2);
21980
+ if (!response.ok || !response.body) {
21981
+ throw new Error(`Download failed with status ${response.status}.`);
21982
+ }
21983
+ await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(targetPath), { recursive: true });
21984
+ await (0, import_promises4.pipeline)(
21985
+ import_node_stream.Readable.fromWeb(response.body),
21986
+ (0, import_node_fs.createWriteStream)(targetPath)
21987
+ );
21988
+ }
21989
+ async function saveMediaAsset(result, output) {
21990
+ if (result.status !== "success") return result;
21991
+ const asset = assetRecord(result.data);
21992
+ const url2 = asset && typeof asset.url === "string" ? asset.url : null;
21993
+ if (!asset || !url2) {
21994
+ return cliResultSchema.parse({
21995
+ ...result,
21996
+ warnings: [...result.warnings, "Response contained no downloadable asset URL; nothing was saved locally."]
21997
+ });
21998
+ }
21999
+ try {
22000
+ const targetPath = await resolveTargetPath(output, asset);
22001
+ await writeAssetToDisk(url2, targetPath);
22002
+ return cliResultSchema.parse({
22003
+ ...result,
22004
+ data: { ...result.data, asset: { ...asset, savedTo: targetPath } },
22005
+ impact: [...result.impact, `Saved to ${targetPath}`]
22006
+ });
22007
+ } catch (error51) {
22008
+ const message = error51 instanceof Error && error51.message ? error51.message : "Unknown error.";
22009
+ return cliResultSchema.parse({
22010
+ ...result,
22011
+ warnings: [...result.warnings, `Could not save the file locally: ${message} The asset URL is still available.`]
22012
+ });
22013
+ }
22014
+ }
22015
+
21874
22016
  // src/auth/login.ts
21875
22017
  var import_node_crypto = require("node:crypto");
21876
22018
 
@@ -21884,19 +22026,19 @@ function openBrowser(url2) {
21884
22026
  child.unref();
21885
22027
  }
21886
22028
  function closeServer(server) {
21887
- return new Promise((resolve) => server.close(() => resolve()));
22029
+ return new Promise((resolve2) => server.close(() => resolve2()));
21888
22030
  }
21889
22031
  function startLoopbackCallbackServer(input) {
21890
22032
  const server = (0, import_node_http.createServer)();
21891
22033
  let timer = null;
21892
22034
  let resolveCallback = null;
21893
22035
  let rejectCallback = null;
21894
- const callback = new Promise((resolve, reject) => {
21895
- resolveCallback = resolve;
22036
+ const callback = new Promise((resolve2, reject) => {
22037
+ resolveCallback = resolve2;
21896
22038
  rejectCallback = reject;
21897
22039
  });
21898
22040
  const ready = new Promise(
21899
- (resolve, reject) => {
22041
+ (resolve2, reject) => {
21900
22042
  timer = setTimeout(() => {
21901
22043
  rejectCallback?.(new Error("Timed out waiting for browser callback."));
21902
22044
  void closeServer(server);
@@ -21923,7 +22065,7 @@ function startLoopbackCallbackServer(input) {
21923
22065
  reject(new Error("Failed to start local CLI callback server."));
21924
22066
  return;
21925
22067
  }
21926
- resolve({
22068
+ resolve2({
21927
22069
  redirectUri: `http://127.0.0.1:${address.port}/callback`,
21928
22070
  waitForCallback: () => callback,
21929
22071
  close: () => closeServer(server)
@@ -22387,6 +22529,56 @@ function formatAnchor(anchor) {
22387
22529
  }
22388
22530
  return parts.join(", ") || "-";
22389
22531
  }
22532
+ function formatMediaDuration(value) {
22533
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "-";
22534
+ const total = Math.round(value);
22535
+ const hours = Math.floor(total / 3600);
22536
+ const minutes = Math.floor(total % 3600 / 60);
22537
+ const seconds = total % 60;
22538
+ const padded = `${String(minutes).padStart(hours ? 2 : 1, "0")}:${String(seconds).padStart(2, "0")}`;
22539
+ return hours ? `${hours}:${padded}` : padded;
22540
+ }
22541
+ function renderMediaInfo(data) {
22542
+ const info = objectRecord2(objectRecord2(data).info);
22543
+ const options = Array.isArray(info.options) ? info.options.map(objectRecord2) : [];
22544
+ const lines = [
22545
+ `platform: ${valueToText(info.platform)}`,
22546
+ `title: ${compactText(info.title)}`,
22547
+ `uploader: ${valueToText(info.uploader ?? info.channel)}`,
22548
+ `duration: ${formatMediaDuration(info.duration)}`,
22549
+ `views: ${valueToText(info.view_count)}`,
22550
+ `likes: ${valueToText(info.like_count)}`,
22551
+ `uploadDate: ${valueToText(info.upload_date)}`,
22552
+ `url: ${valueToText(info.webpage_url ?? info.url)}`,
22553
+ `suggestedFilename: ${valueToText(info.suggested_filename)}`
22554
+ ];
22555
+ if (options.length === 0) return `${lines.join("\n")}
22556
+ `;
22557
+ const rows = options.map((option) => ({
22558
+ format_id: option.format_id,
22559
+ resolution: option.label,
22560
+ codec: option.codec,
22561
+ fps: option.fps,
22562
+ size: formatByteSize(option.filesize),
22563
+ plays: option.compatible === true ? "anywhere" : "limited",
22564
+ audio: option.audio_source
22565
+ }));
22566
+ return `${lines.join("\n")}
22567
+ downloadOptions (${rows.length}):
22568
+ ${renderRows(rows, ["format_id", "resolution", "codec", "fps", "size", "plays", "audio"])}`;
22569
+ }
22570
+ function renderMediaAsset(data) {
22571
+ const asset = objectRecord2(objectRecord2(data).asset);
22572
+ const lines = [
22573
+ `title: ${compactText(asset.title)}`,
22574
+ `size: ${formatByteSize(asset.size)}`,
22575
+ `contentType: ${valueToText(asset.contentType)}`,
22576
+ `url: ${valueToText(asset.url)}`
22577
+ ];
22578
+ if (asset.savedTo) lines.push(`savedTo: ${valueToText(asset.savedTo)}`);
22579
+ return `${lines.join("\n")}
22580
+ `;
22581
+ }
22390
22582
  function renderDataBaseGlob(data) {
22391
22583
  const record2 = objectRecord2(data);
22392
22584
  const pattern = valueToText(record2.pattern);
@@ -22531,6 +22723,10 @@ function renderSuccess(result) {
22531
22723
  "startedAt",
22532
22724
  "finishedAt"
22533
22725
  ]);
22726
+ case "mediaInfo":
22727
+ return renderMediaInfo(result.data);
22728
+ case "mediaAsset":
22729
+ return renderMediaAsset(result.data);
22534
22730
  case "dataBaseGlob":
22535
22731
  return renderDataBaseGlob(result.data);
22536
22732
  case "dataBaseRead":
@@ -22868,12 +23064,12 @@ function renderCommandIndex() {
22868
23064
  `;
22869
23065
  }
22870
23066
  async function packageVersion() {
22871
- if ("0.0.9".length > 0) {
22872
- return "0.0.9";
23067
+ if ("0.0.11".length > 0) {
23068
+ return "0.0.11";
22873
23069
  }
22874
23070
  try {
22875
23071
  const packageUrl = new URL("../package.json", import_meta.url);
22876
- const pkg = JSON.parse(await (0, import_promises3.readFile)(packageUrl, "utf8"));
23072
+ const pkg = JSON.parse(await (0, import_promises5.readFile)(packageUrl, "utf8"));
22877
23073
  return pkg.version ?? "0.0.0";
22878
23074
  } catch {
22879
23075
  return "0.0.0";
@@ -23090,6 +23286,11 @@ ${url2}
23090
23286
  poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
23091
23287
  });
23092
23288
  }
23289
+ if (commandKey(definition.path) === "media video" || commandKey(definition.path) === "media thumbnail") {
23290
+ const downloaded = await executeHttpCommand({ command: definition, args, options });
23291
+ if (typeof options.output !== "string" || options.output.length === 0) return downloaded;
23292
+ return saveMediaAsset(downloaded, options.output);
23293
+ }
23093
23294
  return executeHttpCommand({ command: definition, args, options });
23094
23295
  }
23095
23296
  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.11",
4
4
  "description": "Command line client for Spira.",
5
5
  "type": "module",
6
6
  "bin": {