@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.
- package/README.md +8 -0
- package/dist/index.cjs +221 -10
- 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
|
|
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((
|
|
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((
|
|
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((
|
|
21895
|
-
resolveCallback =
|
|
22045
|
+
const callback = new Promise((resolve2, reject) => {
|
|
22046
|
+
resolveCallback = resolve2;
|
|
21896
22047
|
rejectCallback = reject;
|
|
21897
22048
|
});
|
|
21898
22049
|
const ready = new Promise(
|
|
21899
|
-
(
|
|
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
|
-
|
|
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.
|
|
22872
|
-
return "0.0.
|
|
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,
|
|
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) {
|